Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,32 @@ _Avoid_: raw app log, transport trace
A markdown line that records lifecycle work tied to a conversation but not shown directly as a chat turn to the customer.
_Avoid_: customer message, visible reply

**Conversation session log filename**:
The markdown filename for a dev session log, formatted from the local dev-session start time as `DD-MM-HH-mm.md`.
_Avoid_: UUID log name, ISO timestamp filename

**Conversation session log timestamp**:
The human-readable local timestamp used inside the markdown log, formatted like `2026-04-21 11:22:03 AM`.
_Avoid_: ISO UTC timestamp, raw epoch time

**Conversation session log header**:
The metadata block at the top of the markdown log that contains exactly `Session ID`, `Company ID`, `Conversation ID`, and `Started At`.
_Avoid_: extra header fields, missing identity fields

**AI background section**:
A background trace section for one AI call that stores the exact system prompt text sent to the API, the grounding context snapshot when retrieval data was used, the raw API output, and only the provider name and usage from provider metadata, but only for AI calls that the current runtime already makes.
_Avoid_: prompt payload, full provider response envelope

## Relationships

- A **Customer-view log line** belongs to exactly one conversation turn.
- A **Background trace line** belongs to exactly one conversation lifecycle event.
- A conversation session log contains both **Customer-view log lines** and **Background trace lines** for the same dev session.
- A conversation session log contains both **Customer-view log lines** and **Background trace lines** for exactly one owner-number conversation during one dev session.
- A **Conversation session log filename** identifies exactly one dev session log file.
- A **Conversation session log timestamp** is rendered for the log header and every markdown entry in local time.
- A **Conversation session log header** identifies the session and the one recorded owner conversation.
- An **AI background section** belongs to exactly one AI call inside a **Background trace line**.
- A conversation session log file is created only after the first owner-number conversation entry exists.

## Example dialogue

Expand All @@ -26,3 +47,11 @@ _Avoid_: customer message, visible reply
## Flagged ambiguities

- "background generated" was ambiguous between visible assistant text and invisible lifecycle work — resolved: visible turns are **Customer-view log lines**, while pending, replay, analytics, trim, and owner-notification lifecycle records are **Background trace lines**.
- "convo logs file" was ambiguous between a global dev transcript and a per-conversation artifact — resolved: a **conversation session log** is one markdown file produced for one `bun dev` session, and it records only the conversation whose phone number matches the owner number.
- "same as 21-04-11-22.md file name" was ambiguous between example-only naming and a stable contract — resolved: the **Conversation session log filename** is the required contract and uses local dev-session start time.
- "grounding context" was ambiguous between prompt scaffolding and retrieval evidence — resolved: in an **AI background section**, grounding context means the vector-search result data included for the AI call, while prompt payload is omitted.
- "same as 21-04-11-22.md contents" was ambiguous between reproducing that sample exactly and logging current behavior — resolved: the plan must only improve logging of the current runtime and must not add new AI stages just to match the sample.
- "same as 21-04-11-22.md timestamps" was ambiguous between approximate readability and exact format — resolved: use the **Conversation session log timestamp** format in local time for headers and entries.
- "one file per dev session" was ambiguous about empty sessions — resolved: no file is created when the owner-number conversation never produces a logged entry.
- "same header as the sample" was ambiguous about which fields are required — resolved: the **Conversation session log header** contains exactly `Session ID`, `Company ID`, `Conversation ID`, and `Started At`.
- "multiple owner conversations in one dev session" is intentionally out of scope for this plan and should not drive the logging design.
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"@cs/db": "*",
"@cs/shared": "*",
"@cs/storage": "*",
"@hono/node-server": "1.19.9",
"hono": "4.12.4",
"@hono/node-server": "1.19.13",
"hono": "4.12.14",
"qrcode": "^1.5.4"
},
"devDependencies": {
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ export const createApp = (options: ApiAppOptions = {}) => {
app.onError((error, c) => {
const requestLogger = getRequestLogger(c, withLogBindings(appLogger, { surface: "http" }));
const requestId = c.get("requestId");

if (error instanceof SyntaxError) {
logEvent(
requestLogger,
Expand Down
2 changes: 2 additions & 0 deletions apps/bot/src/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,12 @@ describe("startBot", () => {
const { logger, infoCalls } = createLoggerStub();
const runtimeConfig = createBotRuntimeConfig({ moduleDirectory: "/repo/apps/bot/src" });
const socketStub = createSocketStub();
const version = [2, 3001, 999999999] as [number, number, number];

const handle = await startBot({
logger,
runtimeConfig,
resolveSocketVersion: async () => version,
loadAuthState: async () => ({
state: createAuthenticationState(),
saveCreds: async () => undefined,
Expand Down
4 changes: 4 additions & 0 deletions apps/bot/src/sessionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -874,9 +874,11 @@ describe("startTenantSessionManager", () => {
const profile = createProfile("company-1");
const store = createStoreStub([profile]);
const { timer, intervals } = createIntervalTimerStub();
const { logger } = createLoggerStub();
const startCalls: string[] = [];

await startTenantSessionManager({
logger,
runtimeOwnerId: "runtime-owner-1",
store,
timer,
Expand Down Expand Up @@ -2121,7 +2123,9 @@ describe("startTenantSessionManager", () => {
test("keeps tenant outbound lookup isolated when one tenant sender fails", async () => {
const profiles = [createProfile("company-1"), createProfile("company-2")];
const store = createStoreStub(profiles);
const { logger } = createLoggerStub();
const manager = await startTenantSessionManager({
logger,
runtimeOwnerId: "runtime-owner-1",
store,
startBot: async (options) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"cs": "./src/index.ts"
},
"scripts": {
"dev": "bun --env-file=../../.env --watch src/index.ts",
"dev": "bun ../../scripts/watch-from-root.ts src/index.ts",
"build": "bun --env-file=../../.env build src/index.ts --outdir dist --target bun",
"seed": "bun --env-file=../../.env src/index.ts seed",
"backup": "bun --env-file=../../.env src/index.ts backup",
Expand Down
8 changes: 4 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dependencies": {
"@gsap/react": "^2.1.2",
"gsap": "^3.12.5",
"hono": "^4.12.9",
"hono": "4.12.14",
Comment thread
HusseinBaraja marked this conversation as resolved.
"lucide-react": "^0.470.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
Expand All @@ -24,11 +24,11 @@
"@testing-library/react": "^16.3.2",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^29.0.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vite": "^6.1.0"
"vitest": "^4.1.4",
"vite": "8.0.5"
Comment thread
HusseinBaraja marked this conversation as resolved.
}
}
24 changes: 13 additions & 11 deletions apps/web/src/components/layout/Layout.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ vi.mock('gsap', () => ({
default: {
set: vi.fn(),
to: vi.fn(),
context: vi.fn(() => ({
revert: contextRevertMock,
})),
context: vi.fn((callback?: () => void) => {
callback?.();
return {
revert: contextRevertMock,
};
}),
},
}));

Expand All @@ -21,8 +24,8 @@ let mockPath = '/';

vi.mock('../router/HonoRouter', () => ({
useLocation: () => ({ path: mockPath, navigate: mockNavigate }),
Link: ({ href, children, className, onClick }: any) => (
<a href={href} className={className} onClick={onClick}>
Link: ({ href, children, className, onClick, ...props }: any) => (
<a href={href} className={className} onClick={onClick} {...props}>
{children}
</a>
),
Expand Down Expand Up @@ -60,18 +63,17 @@ describe('Layout', () => {
});

it('keeps the custom watermark height utilities on the footer logo artwork', () => {
render(
const { container } = render(
<Layout>
<div>content</div>
</Layout>,
);

const decorativeImages = screen.getAllByRole('img', { hidden: true });
const watermarkImage = decorativeImages.find((image) => image.className.includes('h-150'));
const watermarkImage = container.querySelector('footer img.h-150.md\\:h-225');

expect(watermarkImage).toBeDefined();
expect(watermarkImage?.className).toContain('h-150');
expect(watermarkImage?.className).toContain('md:h-225');
expect(watermarkImage).not.toBeNull();
expect(watermarkImage?.getAttribute('class')).toContain('h-150');
expect(watermarkImage?.getAttribute('class')).toContain('md:h-225');
Comment thread
HusseinBaraja marked this conversation as resolved.
});

it('uses the logo link with href="/" and scrolls to top when on landing page', () => {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/router/HonoRouter.vitest.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createEvent, fireEvent, render, screen } from '@testing-library/react';
import { cleanup, createEvent, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Link, RouterProvider } from './HonoRouter';

describe('HonoRouter Link', () => {
afterEach(() => {
window.history.replaceState({}, '', '/');
vi.restoreAllMocks();
cleanup();
});

it('forwards anchor props to the rendered element', () => {
Expand Down
15 changes: 10 additions & 5 deletions apps/web/src/components/sections/SolutionSection.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('SolutionSection', () => {
value: 640,
});

const { createdTweens, gsapFromTo } = setupGsapMocks();
const { gsapFromTo } = setupGsapMocks();
const { SolutionSection } = await import('./SolutionSection');
const { unmount } = render(<SolutionSection />);

Expand All @@ -174,9 +174,14 @@ describe('SolutionSection', () => {
vi.advanceTimersByTime(160);
});

expect(createdTweens).toHaveLength(2);
expect(createdTweens[0]?.kill).toHaveBeenCalledTimes(1);
expect(createdTweens[1]?.kill).not.toHaveBeenCalled();
expect(gsapFromTo).toHaveBeenCalledTimes(2);
const firstFloatingTween = gsapFromTo.mock.results[0]?.value;
const secondFloatingTween = gsapFromTo.mock.results[1]?.value;

expect(firstFloatingTween).toBeDefined();
expect(secondFloatingTween).toBeDefined();
expect(firstFloatingTween?.kill).toHaveBeenCalledTimes(1);
expect(secondFloatingTween?.kill).not.toHaveBeenCalled();
expect(gsapFromTo).toHaveBeenNthCalledWith(
2,
'.floating-core',
Expand All @@ -188,6 +193,6 @@ describe('SolutionSection', () => {

unmount();

expect(createdTweens[1]?.kill).toHaveBeenCalledTimes(1);
expect(secondFloatingTween?.kill).toHaveBeenCalledTimes(1);
});
});
7 changes: 5 additions & 2 deletions apps/web/src/pages/ContactPage.vitest.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { setupGsapMocks } from '../test/setupGsapMocks';

Expand All @@ -15,6 +15,7 @@ describe('ContactPage', () => {
vi.unstubAllEnvs();
vi.resetModules();
vi.clearAllMocks();
cleanup();
});

it('renders env-backed contact details and resets the form after submit', async () => {
Expand All @@ -27,7 +28,7 @@ describe('ContactPage', () => {
render(<ContactPage />);

expect(screen.getByRole('heading', { name: 'جاهزين نسمع منك ونرتب معك البداية' })).toBeDefined();
expect(screen.getByText('مخصص للأعمال في اليمن التي تريد خدمة عملاء أسرع على واتساب بدون تعقيد.')).toBeDefined();
expect(screen.getByText('مخصص للشركات في اليمن التي تحتاج خدمة عملاء أسرع على واتساب بدون تعقيد.')).toBeDefined();
expect(screen.getByText('+967 784 338 919')).toBeDefined();
expect(screen.getByText('hello@reda.chat')).toBeDefined();
expect(screen.getByText('المقر الرئيسي')).toBeDefined();
Expand Down Expand Up @@ -55,6 +56,8 @@ describe('ContactPage', () => {

it('hides phone and email rows when contact env vars are missing', async () => {
setupGsapMocks();
vi.stubEnv('SITE_CONTACT_PHONE_NUMBER', '');
vi.stubEnv('SITE_CONTACT_EMAIL_ADDRESS', '');

const { ContactPage } = await import('./ContactPage');
render(<ContactPage />);
Expand Down
7 changes: 2 additions & 5 deletions convex/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import type { Doc, Id } from './_generated/dataModel';
import { internalAction, internalMutation, internalQuery, type MutationCtx } from './_generated/server';
import { v } from 'convex/values';
import type { CleanupBatchResult, CleanupCursor } from './companyCleanup';
import {
buildProductEmbeddingPayload,
type ProductEmbeddingVariantAttributes,
} from './productEmbeddingRuntime';
import { buildProductEmbeddingPayload, type ProductEmbeddingVariantAttributes } from './productEmbeddingRuntime';
import {
buildSeedCompany,
seedCategories,
Expand Down Expand Up @@ -97,7 +94,7 @@ export const runWithSeedLockHeartbeat = async <T>({
stopHeartbeat = resolve;
});

const heartbeat = (async (): Promise<void> => {
const heartbeat = (async(): Promise<void> => {
while (!stopped) {
await Promise.race([sleep(heartbeatMs), heartbeatStopSignal]);
if (stopped) {
Expand Down
Loading