From ff709973e7177ab0f59dfcbab21a9470c20077ab Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 10:58:28 +0100 Subject: [PATCH 1/8] Move cli_sessions_v2 creation from backend tRPC to session-ingest RPC Replace backend tRPC calls (createV2, linkCloudAgent, delete) with direct session-ingest WorkerEntrypoint RPC methods (createSessionForCloudAgent, deleteSessionForCloudAgent). This removes the roundtrip through the Next.js backend for session record management and moves the DB write closer to the worker that owns the table. - Convert session-ingest from plain Hono export to WorkerEntrypoint class - Add createSessionForCloudAgent / deleteSessionForCloudAgent RPC methods - Add SessionIngestBinding type declarations for both workers - Remove linkKiloSessionInBackend from DO, session-service, and ingest handler - Remove cli_sessions_v2 inserts from Next.js routers - Add rollback logic in session-prepare when DO prepare() fails - Upgrade cloudflare-session-ingest vitest to v3.2.4 - Add created_on_platform to session-ingest RPC --- .../src/execution/orchestrator.ts | 1 - .../src/persistence/CloudAgentSession.ts | 36 -- cloud-agent-next/src/persistence/types.ts | 2 +- .../src/router/handlers/session-prepare.ts | 145 ++++--- .../src/session-ingest-binding.d.ts | 27 ++ cloud-agent-next/src/session-prepare.test.ts | 32 +- cloud-agent-next/src/session-service.test.ts | 123 +----- cloud-agent-next/src/session-service.ts | 172 +------- .../ingest-handlers/kilo-session-capture.ts | 9 - cloud-agent-next/src/types.ts | 2 +- cloud-agent-next/src/websocket/ingest.ts | 3 - cloudflare-session-ingest/package.json | 2 +- cloudflare-session-ingest/src/db/kysely.ts | 1 + cloudflare-session-ingest/src/index.test.ts | 40 +- cloudflare-session-ingest/src/index.ts | 107 ++++- .../src/session-ingest-binding.d.ts | 27 ++ pnpm-lock.yaml | 406 ++---------------- src/routers/cloud-agent-next-router.ts | 17 - .../organization-cloud-agent-next-router.ts | 21 - 19 files changed, 339 insertions(+), 834 deletions(-) create mode 100644 cloud-agent-next/src/session-ingest-binding.d.ts create mode 100644 cloudflare-session-ingest/src/session-ingest-binding.d.ts diff --git a/cloud-agent-next/src/execution/orchestrator.ts b/cloud-agent-next/src/execution/orchestrator.ts index f8116c6ff..dcb21511e 100644 --- a/cloud-agent-next/src/execution/orchestrator.ts +++ b/cloud-agent-next/src/execution/orchestrator.ts @@ -322,7 +322,6 @@ export class ExecutionOrchestrator { setupCommands: initContext.setupCommands, mcpServers: initContext.mcpServers, botId: initContext.botId, - skipLinking: true, githubAppType: initContext.githubAppType, // Note: existingMetadata requires CloudAgentSessionState, not our simplified type ...gitSource, diff --git a/cloud-agent-next/src/persistence/CloudAgentSession.ts b/cloud-agent-next/src/persistence/CloudAgentSession.ts index d2d529341..d602d0537 100644 --- a/cloud-agent-next/src/persistence/CloudAgentSession.ts +++ b/cloud-agent-next/src/persistence/CloudAgentSession.ts @@ -225,7 +225,6 @@ export class CloudAgentSession extends DurableObject { // Create DO context for the ingest handler to call back into the DO const doContext: IngestDOContext = { updateKiloSessionId: (id: string) => this.updateKiloSessionId(id), - linkKiloSessionInBackend: (id: string) => this.linkKiloSessionInBackend(id), updateUpstreamBranch: (branch: string) => this.updateUpstreamBranch(branch), clearActiveExecution: () => this.clearActiveExecution(), getExecution: async (executionId: string) => { @@ -574,41 +573,6 @@ export class CloudAgentSession extends DurableObject { await this.updateMetadata(updated); } - /** - * Link the kiloSessionId to the backend for analytics/tracking. - * Called when a session_created event is received from the CLI. - * - * @param kiloSessionId - The kilo CLI session ID to link - */ - async linkKiloSessionInBackend(kiloSessionId: string): Promise { - const metadata = await this.getMetadata(); - if (!metadata?.kilocodeToken) { - throw new Error('Cannot link session: missing kilocodeToken'); - } - - const backendUrl = (this.env as unknown as WorkerEnv).KILOCODE_BACKEND_BASE_URL; - if (!backendUrl) { - throw new Error('Cannot link session: KILOCODE_BACKEND_BASE_URL not configured'); - } - - const response = await fetch(`${backendUrl}/api/cloud-sessions/linkSessions`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${metadata.kilocodeToken}`, - }, - body: JSON.stringify({ - cloudSessionId: this.sessionId, - kiloSessionId: kiloSessionId, - }), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Backend link failed: ${response.status} ${text}`); - } - } - // --------------------------------------------------------------------------- // Wrapper Communication Methods // --------------------------------------------------------------------------- diff --git a/cloud-agent-next/src/persistence/types.ts b/cloud-agent-next/src/persistence/types.ts index 069cc0714..4c6e58054 100644 --- a/cloud-agent-next/src/persistence/types.ts +++ b/cloud-agent-next/src/persistence/types.ts @@ -170,7 +170,7 @@ export type PersistenceEnv = { /** Durable Object namespace for CloudAgentSession metadata (SQLite-backed) with RPC support */ CLOUD_AGENT_SESSION: DurableObjectNamespace; /** Service binding for the session ingest worker */ - SESSION_INGEST: Fetcher; + SESSION_INGEST: SessionIngestBinding; /** Shared secret for JWT token validation */ NEXTAUTH_SECRET: string; /** Comma-separated list of allowed Origins for /stream WebSocket connections */ diff --git a/cloud-agent-next/src/router/handlers/session-prepare.ts b/cloud-agent-next/src/router/handlers/session-prepare.ts index b144103e2..0a4424bbd 100644 --- a/cloud-agent-next/src/router/handlers/session-prepare.ts +++ b/cloud-agent-next/src/router/handlers/session-prepare.ts @@ -190,35 +190,6 @@ const prepareSessionHandler = internalApiProtectedProcedure } } - // NOTE: Backend session creation (createKiloSessionInBackend) is temporarily disabled. - // The kiloSessionId will now come from the kilo CLI server's POST /session API. - // This can be re-enabled later if backend analytics/tracking is needed. - // const gitUrlForBackend = input.githubRepo - // ? `https://github.com/${input.githubRepo}` - // : input.gitUrl; - // let backendKiloSessionId: string; - // try { - // backendKiloSessionId = await sessionService.createKiloSessionInBackend( - // cloudAgentSessionId, - // ctx.authToken, - // ctx.env, - // input.kilocodeOrganizationId, - // input.mode, - // input.model, - // gitUrlForBackend - // ); - // } catch (error) { - // logger - // .withFields({ error: error instanceof Error ? error.message : String(error) }) - // .error('Failed to create cliSession in backend'); - // throw new TRPCError({ - // code: 'INTERNAL_SERVER_ERROR', - // message: `Failed to create session in backend: ${ - // error instanceof Error ? error.message : String(error) - // }`, - // }); - // } - // 3. Get sandbox logger.info('Getting sandbox'); const sandbox = getSandbox(ctx.env.Sandbox, sandboxId, { sleepAfter: 900 }); @@ -327,52 +298,98 @@ const prepareSessionHandler = internalApiProtectedProcedure logger.setTags({ kiloSessionId }); logger.info('Created kilo CLI session'); - // 12. Get DO stub and store metadata + // 12. Create cli_sessions_v2 record via session-ingest RPC (blocking) + logger.info('Creating cli_sessions_v2 record via session-ingest'); + try { + await sessionService.createCliSessionViaSessionIngest( + kiloSessionId, + cloudAgentSessionId, + ctx.userId, + ctx.env, + input.kilocodeOrganizationId, + 'cloud-agent' + ); + } catch (error) { + logger + .withFields({ error: error instanceof Error ? error.message : String(error) }) + .error('Failed to create cli_sessions_v2 record'); + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: `Failed to create session record: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + + const rollbackCliSession = async () => { + await sessionService + .deleteCliSessionViaSessionIngest(kiloSessionId, ctx.userId, ctx.env) + .catch((rollbackError: unknown) => { + logger + .withFields({ + error: + rollbackError instanceof Error ? rollbackError.message : String(rollbackError), + }) + .error('Failed to rollback cli_sessions_v2 record'); + }); + }; + + // 13. Get DO stub and store metadata const doId = ctx.env.CLOUD_AGENT_SESSION.idFromName(`${ctx.userId}:${cloudAgentSessionId}`); const stub = ctx.env.CLOUD_AGENT_SESSION.get(doId); - const prepareResult = await stub.prepare({ - sessionId: cloudAgentSessionId, - userId: ctx.userId, - orgId: input.kilocodeOrganizationId, - botId: ctx.botId, - kiloSessionId, - prompt: input.prompt, - mode: input.mode, - model: input.model, - kilocodeToken: ctx.authToken, - githubRepo: input.githubRepo, - githubToken: input.githubToken, - githubInstallationId: resolvedInstallationId, - githubAppType: resolvedGithubAppType, - gitUrl: input.gitUrl, - gitToken: input.gitToken, - envVars: input.envVars, - encryptedSecrets: input.encryptedSecrets, - setupCommands: input.setupCommands, - mcpServers: input.mcpServers, - upstreamBranch: input.upstreamBranch, - autoCommit: input.autoCommit, - condenseOnComplete: input.condenseOnComplete, - appendSystemPrompt: input.appendSystemPrompt, - callbackTarget: input.callbackTarget, - images: input.images, - // Workspace metadata - workspacePath, - sessionHome, - branchName, - sandboxId, - }); + let prepareResult; + try { + prepareResult = await stub.prepare({ + sessionId: cloudAgentSessionId, + userId: ctx.userId, + orgId: input.kilocodeOrganizationId, + botId: ctx.botId, + kiloSessionId, + prompt: input.prompt, + mode: input.mode, + model: input.model, + kilocodeToken: ctx.authToken, + githubRepo: input.githubRepo, + githubToken: input.githubToken, + githubInstallationId: resolvedInstallationId, + githubAppType: resolvedGithubAppType, + gitUrl: input.gitUrl, + gitToken: input.gitToken, + envVars: input.envVars, + encryptedSecrets: input.encryptedSecrets, + setupCommands: input.setupCommands, + mcpServers: input.mcpServers, + upstreamBranch: input.upstreamBranch, + autoCommit: input.autoCommit, + condenseOnComplete: input.condenseOnComplete, + appendSystemPrompt: input.appendSystemPrompt, + callbackTarget: input.callbackTarget, + images: input.images, + // Workspace metadata + workspacePath, + sessionHome, + branchName, + sandboxId, + }); + } catch (error) { + logger + .withFields({ error: error instanceof Error ? error.message : String(error) }) + .error('DO prepare() threw, rolling back cli_sessions_v2 record'); + await rollbackCliSession(); + throw error; + } if (!prepareResult.success) { logger.withFields({ error: prepareResult.error }).error('Failed to prepare session in DO'); + await rollbackCliSession(); throw new TRPCError({ code: 'BAD_REQUEST', message: prepareResult.error ?? 'Failed to prepare session', }); } - // 13. Record kilo server activity for idle timeout tracking + // 14. Record kilo server activity for idle timeout tracking try { await withDORetry( () => ctx.env.CLOUD_AGENT_SESSION.get(doId), @@ -388,7 +405,7 @@ const prepareSessionHandler = internalApiProtectedProcedure logger.info('Session prepared successfully'); - // 14. Return both IDs + // 15. Return both IDs return { cloudAgentSessionId, kiloSessionId }; }); }); diff --git a/cloud-agent-next/src/session-ingest-binding.d.ts b/cloud-agent-next/src/session-ingest-binding.d.ts new file mode 100644 index 000000000..9584ee9da --- /dev/null +++ b/cloud-agent-next/src/session-ingest-binding.d.ts @@ -0,0 +1,27 @@ +/** + * Augment the wrangler-generated Env to give the SESSION_INGEST service + * binding its RPC method types. `wrangler types` only sees `Fetcher` for + * service bindings; the actual RPC shape comes from the session-ingest + * worker's WorkerEntrypoint and is declared here so the generated file can + * be freely regenerated. + * + * Keep in sync with: cloudflare-session-ingest/src/index.ts + */ + +type CreateSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; + cloudAgentSessionId: string; + organizationId?: string; + createdOnPlatform?: string; +}; + +type DeleteSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; +}; + +type SessionIngestBinding = Fetcher & { + createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; + deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; +}; diff --git a/cloud-agent-next/src/session-prepare.test.ts b/cloud-agent-next/src/session-prepare.test.ts index e2c487ff8..abb740dea 100644 --- a/cloud-agent-next/src/session-prepare.test.ts +++ b/cloud-agent-next/src/session-prepare.test.ts @@ -53,14 +53,15 @@ vi.mock('./kilo/server-manager.js', () => ({ // Define mocks BEFORE vi.mock() to avoid hoisting issues // vi.hoisted() ensures these are available when the mock factory runs -const { generateSessionIdMock, createKiloSessionInBackendMock, deleteKiloSessionInBackendMock } = - vi.hoisted(() => ({ - generateSessionIdMock: vi.fn(() => 'agent_12345678-1234-1234-1234-123456789abc'), - createKiloSessionInBackendMock: vi - .fn() - .mockResolvedValue('123e4567-e89b-12d3-a456-426614174000'), - deleteKiloSessionInBackendMock: vi.fn().mockResolvedValue(undefined), - })); +const { + generateSessionIdMock, + createCliSessionViaSessionIngestMock, + deleteCliSessionViaSessionIngestMock, +} = vi.hoisted(() => ({ + generateSessionIdMock: vi.fn(() => 'agent_12345678-1234-1234-1234-123456789abc'), + createCliSessionViaSessionIngestMock: vi.fn().mockResolvedValue(undefined), + deleteCliSessionViaSessionIngestMock: vi.fn().mockResolvedValue(undefined), +})); // Mock session-service to isolate router tests vi.mock('./session-service.js', () => ({ @@ -82,8 +83,8 @@ vi.mock('./session-service.js', () => ({ } }, SessionService: class SessionService { - createKiloSessionInBackend = createKiloSessionInBackendMock; - deleteKiloSessionInBackend = deleteKiloSessionInBackendMock; + createCliSessionViaSessionIngest = createCliSessionViaSessionIngestMock; + deleteCliSessionViaSessionIngest = deleteCliSessionViaSessionIngestMock; getOrCreateSession = vi.fn().mockResolvedValue(createMockExecutionSession()); buildContext = vi.fn().mockReturnValue({ sandboxId: 'test-sandbox', @@ -174,6 +175,11 @@ function createInternalApiContext(options: { idFromName: vi.fn((id: string) => ({ id })), get: vi.fn(() => doStub), } as unknown as TRPCContext['env']['CLOUD_AGENT_SESSION'], + SESSION_INGEST: { + fetch: vi.fn(), + createSessionForCloudAgent: vi.fn().mockResolvedValue(undefined), + deleteSessionForCloudAgent: vi.fn().mockResolvedValue(undefined), + } as unknown as TRPCContext['env']['SESSION_INGEST'], INTERNAL_API_SECRET: effectiveInternalApiSecret, NEXTAUTH_SECRET: 'test-secret', }, @@ -184,8 +190,8 @@ describe('prepareSession endpoint', () => { beforeEach(() => { vi.clearAllMocks(); generateSessionIdMock.mockReturnValue('agent_12345678-1234-1234-1234-123456789abc'); - createKiloSessionInBackendMock.mockResolvedValue('123e4567-e89b-12d3-a456-426614174000'); - deleteKiloSessionInBackendMock.mockResolvedValue(undefined); + createCliSessionViaSessionIngestMock.mockResolvedValue(undefined); + deleteCliSessionViaSessionIngestMock.mockResolvedValue(undefined); }); describe('authentication', () => { @@ -426,7 +432,7 @@ describe('prepareSession endpoint', () => { ).rejects.toThrow('Session already prepared'); }); - // NOTE: Backend session creation (createKiloSessionInBackend) is currently disabled. + // NOTE: CLI session creation (createCliSessionViaSessionIngest) is handled via session-ingest. // The kiloSessionId now comes from the kilo CLI server's POST /session API. // Tests for backend session creation error handling and rollback have been removed. }); diff --git a/cloud-agent-next/src/session-service.test.ts b/cloud-agent-next/src/session-service.test.ts index a83610b23..2793b2158 100644 --- a/cloud-agent-next/src/session-service.test.ts +++ b/cloud-agent-next/src/session-service.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@cloudflare/sandbox', () => ({ getSandbox: vi.fn(), @@ -2708,127 +2708,6 @@ describe('SessionService', () => { }); }); - describe('linkKiloSessionInBackend', () => { - let originalFetch: typeof global.fetch; - - beforeEach(() => { - originalFetch = global.fetch; - }); - - afterEach(() => { - global.fetch = originalFetch; - }); - - it('should use correct tRPC wire format with request body', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ result: { data: { success: true } } }), - }); - global.fetch = mockFetch; - - const envWithBackendUrl: PersistenceEnv = { - ...mockEnv, - KILOCODE_BACKEND_BASE_URL: 'https://test.kilo.ai', - }; - - const service = new SessionService(); - // Access private method - await service['linkKiloSessionInBackend']( - 'kilo-session-123', - 'agent-session-456', - 'auth-token', - envWithBackendUrl - ); - - // Verify the request uses POST with body (not query string) - expect(mockFetch).toHaveBeenCalledWith( - 'https://test.kilo.ai/api/trpc/cliSessions.linkCloudAgent', - expect.objectContaining({ - method: 'POST', - headers: expect.objectContaining({ - Authorization: 'Bearer auth-token', - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - kilo_session_id: 'kilo-session-123', - cloud_agent_session_id: 'agent-session-456', - }), - }) - ); - }); - - it('should use default backend URL when not provided', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ result: { data: { success: true } } }), - }); - global.fetch = mockFetch; - - const service = new SessionService(); - await service['linkKiloSessionInBackend']( - 'kilo-session-123', - 'agent-session-456', - 'auth-token', - mockEnv // No KILOCODE_BACKEND_URL - ); - - const calledUrl = mockFetch.mock.calls[0]?.[0] as string; - expect(calledUrl).toContain('https://api.kilo.ai'); - }); - - it('should throw error when backend returns non-200', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 404, - text: () => Promise.resolve('Not found'), - }); - - const service = new SessionService(); - await expect( - service['linkKiloSessionInBackend']( - 'kilo-session-123', - 'agent-session-456', - 'auth-token', - mockEnv - ) - ).rejects.toThrow('Failed to link sessions: 404'); - }); - - it('should throw error when backend does not confirm success', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ result: { data: { success: false } } }), - }); - - const service = new SessionService(); - await expect( - service['linkKiloSessionInBackend']( - 'kilo-session-123', - 'agent-session-456', - 'auth-token', - mockEnv - ) - ).rejects.toThrow('Backend did not confirm successful link'); - }); - - it('should throw error when response format is unexpected', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ unexpected: 'format' }), - }); - - const service = new SessionService(); - await expect( - service['linkKiloSessionInBackend']( - 'kilo-session-123', - 'agent-session-456', - 'auth-token', - mockEnv - ) - ).rejects.toThrow('Backend did not confirm successful link'); - }); - }); - describe('captureAndStoreBranch', () => { it('should capture current branch and update metadata', async () => { const updateUpstreamBranch = vi.fn().mockResolvedValue(undefined); diff --git a/cloud-agent-next/src/session-service.ts b/cloud-agent-next/src/session-service.ts index ad5bfc315..b3b6c335a 100644 --- a/cloud-agent-next/src/session-service.ts +++ b/cloud-agent-next/src/session-service.ts @@ -8,7 +8,6 @@ import type { InterruptResult, } from './types.js'; import type { ExecutionParams as _ExecutionParams } from './schema.js'; -import { DEFAULT_BACKEND_URL } from './constants.js'; import { generateSandboxId } from './sandbox-id.js'; import { checkDiskSpace, @@ -769,7 +768,6 @@ export class SessionService { let isFirstCall = true; let capturedKiloSessionId: string | undefined = undefined; - const linkKiloSessionInBackend = this.linkKiloSessionInBackend.bind(this); const captureAndStoreBranch = this.captureAndStoreBranch.bind(this); return { @@ -804,16 +802,6 @@ export class SessionService { ) { capturedKiloSessionId = String(event.payload.sessionId); logger.setTags({ kiloSessionId: capturedKiloSessionId }); - void linkKiloSessionInBackend( - capturedKiloSessionId, - sessionId, - kilocodeToken, - env - ).catch((error: unknown) => { - logger - .withFields({ error: error instanceof Error ? error.message : String(error) }) - .error('Failed to link sessions in backend'); - }); } yield event; } @@ -949,7 +937,6 @@ export class SessionService { setupCommands, mcpServers, botId, - skipLinking, githubAppType, existingMetadata, } = options; @@ -1084,20 +1071,6 @@ export class SessionService { metadataToPreserve ); - // Skip linking if requested (e.g., for prepared sessions where backend already linked) - if (!skipLinking) { - try { - await this.linkKiloSessionInBackend(kiloSessionId, sessionId, kilocodeToken, env); - logger.info('Linked cloud-agent session to kilo session in backend'); - } catch (error) { - logger - .withFields({ error: error instanceof Error ? error.message : String(error) }) - .warn('Failed to link sessions in backend'); - } - } else { - logger.debug('Skipping backend linking (prepared session mode)'); - } - const captureAndStoreBranch = this.captureAndStoreBranch.bind(this); return { @@ -1625,145 +1598,39 @@ export class SessionService { } /** - * Create a minimal cliSession in kilocode-backend. - * Uses the customer's auth token (forwarded from the original request). - * Returns the generated kiloSessionId. + * Create a cli_sessions_v2 record via session-ingest RPC. + * Called during session preparation so the DB record exists before execution. */ - async createKiloSessionInBackend( + async createCliSessionViaSessionIngest( + kiloSessionId: string, cloudAgentSessionId: string, - authToken: string, + kiloUserId: string, env: PersistenceEnv, organizationId?: string, - lastMode?: string, - lastModel?: string, - gitUrl?: string - ): Promise { - const backendUrl = env.KILOCODE_BACKEND_BASE_URL || DEFAULT_BACKEND_URL; - - const input = { - created_on_platform: 'cloud-agent', - organization_id: organizationId ?? null, - cloud_agent_session_id: cloudAgentSessionId, - version: 2, - last_mode: lastMode, - last_model: lastModel, - git_url: gitUrl, - }; - - const response = await fetch(`${backendUrl}/api/trpc/cliSessions.createV2`, { - method: 'POST', - headers: { - Authorization: `Bearer ${authToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(input), - }); - - if (!response.ok) { - const text = await response.text(); - console.error('[createKiloSessionInBackend] Backend error:', { - status: response.status, - statusText: response.statusText, - body: '[redacted]', - backendUrl, - organizationId, - cloudAgentSessionId, - }); - throw new Error( - `Failed to create kilo session: ${response.status} - ${text.substring(0, 200)}` - ); - } - - const result = await response.json(); - - type TrpcResponse = { result?: { data?: { session_id?: string } } }; - const typedResult = result as TrpcResponse; - - const sessionId = typedResult.result?.data?.session_id; - if (!sessionId) { - throw new Error('Backend did not return session_id'); - } - - return sessionId; - } - - /** - * Delete a cliSession in kilocode-backend. - * Used for rollback when DO prepare() fails after backend session was created. - */ - async deleteKiloSessionInBackend( - kiloSessionId: string, - authToken: string, - env: PersistenceEnv + createdOnPlatform?: string ): Promise { - const backendUrl = env.KILOCODE_BACKEND_BASE_URL || DEFAULT_BACKEND_URL; - - const input = { - session_id: kiloSessionId, - }; - - const response = await fetch(`${backendUrl}/api/trpc/cliSessions.delete`, { - method: 'POST', - headers: { - Authorization: `Bearer ${authToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(input), + await env.SESSION_INGEST.createSessionForCloudAgent({ + sessionId: kiloSessionId, + kiloUserId, + cloudAgentSessionId, + organizationId, + createdOnPlatform, }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Failed to delete kilo session: ${response.status} ${text}`); - } - - const result = await response.json(); - - type TrpcResponse = { result?: { data?: { success?: boolean } } }; - const typedResult = result as TrpcResponse; - - if (!typedResult.result?.data?.success) { - throw new Error('Backend did not confirm successful deletion'); - } } /** - * Helper to link sessions in backend using tRPC wire format + * Delete a cli_sessions_v2 record via session-ingest RPC. + * Used for rollback when DO prepare() fails after the record was created. */ - private async linkKiloSessionInBackend( + async deleteCliSessionViaSessionIngest( kiloSessionId: string, - cloudAgentSessionId: string, - authToken: string, + kiloUserId: string, env: PersistenceEnv ): Promise { - const backendUrl = env.KILOCODE_BACKEND_BASE_URL || DEFAULT_BACKEND_URL; - - const input = { - kilo_session_id: kiloSessionId, - cloud_agent_session_id: cloudAgentSessionId, - }; - - const response = await fetch(`${backendUrl}/api/trpc/cliSessions.linkCloudAgent`, { - method: 'POST', - headers: { - Authorization: `Bearer ${authToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(input), + await env.SESSION_INGEST.deleteSessionForCloudAgent({ + sessionId: kiloSessionId, + kiloUserId, }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Failed to link sessions: ${response.status} ${text}`); - } - - const result = await response.json(); - - type TrpcResponse = { result?: { data?: { success?: boolean } } }; - const typedResult = result as TrpcResponse; - - if (!typedResult.result?.data?.success) { - throw new Error('Backend did not confirm successful link'); - } } /** @@ -1908,7 +1775,6 @@ type InitiateFromKiloSessionBaseOptions = { setupCommands?: string[]; mcpServers?: Record; botId?: string; - skipLinking?: boolean; /** GitHub App type for selecting correct slug/bot identity */ githubAppType?: 'standard' | 'lite'; /** diff --git a/cloud-agent-next/src/session/ingest-handlers/kilo-session-capture.ts b/cloud-agent-next/src/session/ingest-handlers/kilo-session-capture.ts index 472235165..2c3d2599f 100644 --- a/cloud-agent-next/src/session/ingest-handlers/kilo-session-capture.ts +++ b/cloud-agent-next/src/session/ingest-handlers/kilo-session-capture.ts @@ -4,7 +4,6 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12 export type KiloSessionCaptureContext = { updateKiloSessionId: (id: string) => Promise; - linkToBackend: (kiloSessionId: string) => Promise; logger: { info: (msg: string, data?: object) => void; warn: (msg: string, data?: object) => void; @@ -38,13 +37,5 @@ export async function handleKilocodeEvent( await ctx.updateKiloSessionId(kiloSessionId); ctx.logger.info('Captured kiloSessionId', { kiloSessionId }); - // Backend link is async/non-blocking - void ctx.linkToBackend(kiloSessionId).catch(err => { - ctx.logger.warn('Failed to link kiloSessionId to backend', { - kiloSessionId, - error: err instanceof Error ? err.message : String(err), - }); - }); - return true; } diff --git a/cloud-agent-next/src/types.ts b/cloud-agent-next/src/types.ts index 60261fa25..635185d5a 100644 --- a/cloud-agent-next/src/types.ts +++ b/cloud-agent-next/src/types.ts @@ -93,7 +93,7 @@ export type Env = { /** Durable Object namespace for CloudAgentSession metadata (SQLite-backed) with RPC support */ CLOUD_AGENT_SESSION: DurableObjectNamespace; /** Service binding for the session ingest worker */ - SESSION_INGEST: Fetcher; + SESSION_INGEST: SessionIngestBinding; /** Queue for callback messages (optional - supports incremental rollout) */ CALLBACK_QUEUE?: Queue; /** KV namespace for caching GitHub installation tokens */ diff --git a/cloud-agent-next/src/websocket/ingest.ts b/cloud-agent-next/src/websocket/ingest.ts index 5189e5965..5882a36ea 100644 --- a/cloud-agent-next/src/websocket/ingest.ts +++ b/cloud-agent-next/src/websocket/ingest.ts @@ -110,8 +110,6 @@ export type ExecutionData = { export type IngestDOContext = { /** Persist the kiloSessionId in DO metadata */ updateKiloSessionId: (id: string) => Promise; - /** Link kiloSessionId to backend for analytics */ - linkKiloSessionInBackend: (id: string) => Promise; /** Persist the upstream branch in DO metadata */ updateUpstreamBranch: (branch: string) => Promise; /** Clear the active execution when done */ @@ -342,7 +340,6 @@ export function createIngestHandler( attachment.kiloSessionState, { updateKiloSessionId: id => doContext.updateKiloSessionId(id), - linkToBackend: id => doContext.linkKiloSessionInBackend(id), logger: console, } ); diff --git a/cloudflare-session-ingest/package.json b/cloudflare-session-ingest/package.json index 686a68d3e..84cc0c9a3 100644 --- a/cloudflare-session-ingest/package.json +++ b/cloudflare-session-ingest/package.json @@ -19,7 +19,7 @@ "@cloudflare/workers-types": "^4.20260120.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22", - "vitest": "^2.1.8", + "vitest": "^3.2.4", "wrangler": "^4.61.0" } } diff --git a/cloudflare-session-ingest/src/db/kysely.ts b/cloudflare-session-ingest/src/db/kysely.ts index 87258c768..1b1cfd1e0 100644 --- a/cloudflare-session-ingest/src/db/kysely.ts +++ b/cloudflare-session-ingest/src/db/kysely.ts @@ -14,6 +14,7 @@ types.setTypeParser(types.builtins.INT8, val => parseInt(val, 10)); export type CliSessionsV2Table = { session_id: string; kilo_user_id: string; + cloud_agent_session_id: Generated; version: ColumnType; public_id: Generated; parent_session_id: Generated; diff --git a/cloudflare-session-ingest/src/index.test.ts b/cloudflare-session-ingest/src/index.test.ts index 45c69b3ee..b13f7d8b4 100644 --- a/cloudflare-session-ingest/src/index.test.ts +++ b/cloudflare-session-ingest/src/index.test.ts @@ -4,6 +4,17 @@ vi.mock('cloudflare:workers', () => ({ DurableObject: class DurableObject { constructor(_state: unknown, _env: unknown) {} }, + WorkerEntrypoint: class WorkerEntrypoint { + env: unknown; + ctx: ExecutionContext; + constructor() { + this.env = undefined; + this.ctx = { + waitUntil: () => {}, + passThroughOnException: () => {}, + } as unknown as ExecutionContext; + } + }, })); vi.mock('./db/kysely', () => ({ @@ -17,8 +28,6 @@ vi.mock('./dos/SessionIngestDO', () => ({ import { getDb } from './db/kysely'; import { getSessionIngestDO } from './dos/SessionIngestDO'; -let app: { fetch: (req: Request, env: TestBindings) => Response | Promise }; - type TestBindings = { HYPERDRIVE: { connectionString: string }; SESSION_INGEST_DO: unknown; @@ -27,6 +36,10 @@ type TestBindings = { NEXTAUTH_SECRET_RAW?: string; }; +let WorkerClass: { + new (): { env: unknown; ctx: unknown; fetch: (req: Request) => Promise }; +}; + function makeDbFakes() { const selectExecuteTakeFirst = vi.fn<() => Promise>(async () => undefined); const select = { @@ -42,10 +55,16 @@ function makeDbFakes() { return { db, selectExecuteTakeFirst }; } +function createWorker(env: TestBindings) { + const worker = new WorkerClass(); + worker.env = env; + return worker; +} + describe('public session route', () => { beforeAll(async () => { const mod = await import('./index'); - app = mod.default; + WorkerClass = mod.default as unknown as typeof WorkerClass; }); beforeEach(() => { @@ -61,7 +80,8 @@ describe('public session route', () => { NEXTAUTH_SECRET_RAW: 'secret', }; - const res = await app.fetch(new Request('http://local/session/not-a-uuid'), env); + const worker = createWorker(env); + const res = await worker.fetch(new Request('http://local/session/not-a-uuid')); expect(res.status).toBe(400); }); @@ -78,9 +98,9 @@ describe('public session route', () => { NEXTAUTH_SECRET_RAW: 'secret', }; - const res = await app.fetch( - new Request('http://local/session/11111111-1111-4111-8111-111111111111'), - env + const worker = createWorker(env); + const res = await worker.fetch( + new Request('http://local/session/11111111-1111-4111-8111-111111111111') ); expect(res.status).toBe(404); @@ -109,9 +129,9 @@ describe('public session route', () => { NEXTAUTH_SECRET_RAW: 'secret', }; - const res = await app.fetch( - new Request('http://local/session/11111111-1111-4111-8111-111111111111'), - env + const worker = createWorker(env); + const res = await worker.fetch( + new Request('http://local/session/11111111-1111-4111-8111-111111111111') ); expect(res.status).toBe(200); diff --git a/cloudflare-session-ingest/src/index.ts b/cloudflare-session-ingest/src/index.ts index 41126dcc1..fc2c4f567 100644 --- a/cloudflare-session-ingest/src/index.ts +++ b/cloudflare-session-ingest/src/index.ts @@ -1,3 +1,4 @@ +import { WorkerEntrypoint } from 'cloudflare:workers'; import { Hono } from 'hono'; import type { Env } from './env'; import { z } from 'zod'; @@ -5,6 +6,7 @@ import { kiloJwtAuthMiddleware } from './middleware/kilo-jwt-auth'; import { api } from './routes/api'; import { getDb } from './db/kysely'; import { getSessionIngestDO } from './dos/SessionIngestDO'; +import { getSessionAccessCacheDO } from './dos/SessionAccessCacheDO'; import { withDORetry } from './util/do-retry'; export { SessionIngestDO } from './dos/SessionIngestDO'; export { SessionAccessCacheDO } from './dos/SessionAccessCacheDO'; @@ -57,4 +59,107 @@ app.get('/session/:sessionId', async c => { }); }); -export default app; +const sessionIdSchema = z.string().startsWith('ses_').length(30); + +export default class SessionIngestWorker extends WorkerEntrypoint { + async fetch(request: Request): Promise { + return app.fetch(request, this.env, this.ctx); + } + + /** + * RPC method: create a cli_sessions_v2 record for a cloud-agent-next session. + * Called via service binding from cloud-agent-next during session preparation. + * + * Uses ON CONFLICT DO UPDATE to set cloud_agent_session_id (and organization_id + * if provided), matching the behavior previously in the backend routers. + */ + async createSessionForCloudAgent(params: { + sessionId: string; + kiloUserId: string; + cloudAgentSessionId: string; + organizationId?: string; + createdOnPlatform?: string; + }): Promise { + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + cloudAgentSessionId: z.string().min(1), + organizationId: z.string().optional(), + createdOnPlatform: z.string().optional(), + }) + .parse(params); + + const db = getDb(this.env.HYPERDRIVE); + + await db + .insertInto('cli_sessions_v2') + .values({ + session_id: parsed.sessionId, + kilo_user_id: parsed.kiloUserId, + cloud_agent_session_id: parsed.cloudAgentSessionId, + organization_id: parsed.organizationId ?? null, + created_on_platform: parsed.createdOnPlatform ?? 'cloud-agent', + version: 0, + }) + .onConflict(oc => + oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ + cloud_agent_session_id: parsed.cloudAgentSessionId, + ...(parsed.organizationId !== undefined + ? { organization_id: parsed.organizationId } + : {}), + }) + ) + .execute(); + + // Warm the session cache so subsequent ingests can skip Postgres. + await withDORetry( + () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), + sessionCache => sessionCache.add(parsed.sessionId), + 'SessionAccessCacheDO.add' + ); + } + + /** + * RPC method: delete a cli_sessions_v2 record for a cloud-agent-next session. + * Called via service binding from cloud-agent-next for rollback when DO prepare() fails. + * + * Scoped to the user (composite PK: session_id + kilo_user_id). + */ + async deleteSessionForCloudAgent(params: { + sessionId: string; + kiloUserId: string; + }): Promise { + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + }) + .parse(params); + + const db = getDb(this.env.HYPERDRIVE); + + await db + .deleteFrom('cli_sessions_v2') + .where('session_id', '=', parsed.sessionId) + .where('kilo_user_id', '=', parsed.kiloUserId) + .execute(); + + // Clear caches + await withDORetry( + () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), + sessionCache => sessionCache.remove(parsed.sessionId), + 'SessionAccessCacheDO.remove' + ); + + await withDORetry( + () => + getSessionIngestDO(this.env, { + kiloUserId: parsed.kiloUserId, + sessionId: parsed.sessionId, + }), + stub => stub.clear(), + 'SessionIngestDO.clear' + ); + } +} diff --git a/cloudflare-session-ingest/src/session-ingest-binding.d.ts b/cloudflare-session-ingest/src/session-ingest-binding.d.ts new file mode 100644 index 000000000..9584ee9da --- /dev/null +++ b/cloudflare-session-ingest/src/session-ingest-binding.d.ts @@ -0,0 +1,27 @@ +/** + * Augment the wrangler-generated Env to give the SESSION_INGEST service + * binding its RPC method types. `wrangler types` only sees `Fetcher` for + * service bindings; the actual RPC shape comes from the session-ingest + * worker's WorkerEntrypoint and is declared here so the generated file can + * be freely regenerated. + * + * Keep in sync with: cloudflare-session-ingest/src/index.ts + */ + +type CreateSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; + cloudAgentSessionId: string; + organizationId?: string; + createdOnPlatform?: string; +}; + +type DeleteSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; +}; + +type SessionIngestBinding = Fetcher & { + createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; + deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12edaee4c..3f3ff5711 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,7 @@ importers: version: 15.5.11(@mdx-js/loader@3.1.1(webpack@5.102.1(@swc/core@1.12.5)(esbuild@0.25.12)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)) '@next/third-parties': specifier: ^15.5.10 - version: 15.5.11(next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + version: 15.5.11(next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) '@octokit/auth-app': specifier: ^8.1.2 version: 8.1.2 @@ -128,7 +128,7 @@ importers: version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@sentry/nextjs': specifier: ^10.29.0 - version: 10.29.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.102.1(@swc/core@1.12.5)(esbuild@0.25.12)) + version: 10.29.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.102.1(@swc/core@1.12.5)(esbuild@0.25.12)) '@sentry/opentelemetry': specifier: ^10.29.0 version: 10.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0) @@ -212,10 +212,10 @@ importers: version: 3.0.6 jotai: specifier: ^2.15.1 - version: 2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0) + version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0) jotai-minidb: specifier: ^0.0.8 - version: 0.0.8(jotai@2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)) + version: 0.0.8(jotai@2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)) js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -233,10 +233,10 @@ importers: version: 12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next: specifier: ^15.5.10 - version: 15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.24.13(next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) openai: specifier: ^6.8.1 version: 6.8.1(ws@8.18.2)(zod@4.3.4) @@ -414,7 +414,7 @@ importers: version: 4.1.17 ts-jest: specifier: ^29.4.5 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(esbuild@0.25.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -930,8 +930,8 @@ importers: specifier: ^22 version: 22.19.1 vitest: - specifier: ^2.1.8 - version: 2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) wrangler: specifier: ^4.61.0 version: 4.61.1(@cloudflare/workers-types@4.20260130.0) @@ -6007,26 +6007,12 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} - peerDependencies: - msw: ^2.4.9 - vite: '>=6.4.1' - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -6049,55 +6035,35 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} - '@vitest/ui@2.1.9': - resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==} - peerDependencies: - vitest: 2.1.9 - '@vitest/ui@3.2.4': resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} peerDependencies: vitest: 3.2.4 - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -10004,9 +9970,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -11296,10 +11259,6 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -11308,10 +11267,6 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -11705,11 +11660,6 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -11755,31 +11705,6 @@ packages: yaml: optional: true - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -12992,45 +12917,21 @@ snapshots: dependencies: '@babel/core': 7.28.5 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -13046,34 +12947,16 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -13084,89 +12967,41 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -14972,9 +14807,9 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.7': optional: true - '@next/third-parties@15.5.11(next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': + '@next/third-parties@15.5.11(next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': dependencies: - next: 15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 third-party-capital: 1.0.20 @@ -16251,7 +16086,7 @@ snapshots: '@sentry/core@10.29.0': {} - '@sentry/nextjs@10.29.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.102.1(@swc/core@1.12.5)(esbuild@0.25.12))': + '@sentry/nextjs@10.29.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.102.1(@swc/core@1.12.5)(esbuild@0.25.12))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 @@ -16264,7 +16099,7 @@ snapshots: '@sentry/react': 10.29.0(react@19.2.0) '@sentry/vercel-edge': 10.29.0 '@sentry/webpack-plugin': 4.6.1(webpack@5.102.1(@swc/core@1.12.5)(esbuild@0.25.12)) - next: 15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) resolve: 1.22.8 rollup: 4.53.3 stacktrace-parser: 0.1.11 @@ -18280,13 +18115,6 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/expect@2.1.9': - dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - tinyrainbow: 1.2.0 - '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -18304,14 +18132,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@2.1.9(vite@7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 2.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -18344,10 +18164,6 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/pretty-format@2.1.9': - dependencies: - tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -18356,11 +18172,6 @@ snapshots: dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@2.1.9': - dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 - '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 @@ -18372,12 +18183,6 @@ snapshots: '@vitest/utils': 4.0.18 pathe: 2.0.3 - '@vitest/snapshot@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.21 - pathe: 1.1.2 - '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -18390,28 +18195,12 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 - '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 '@vitest/spy@4.0.18': {} - '@vitest/ui@2.1.9(vitest@2.1.9)': - dependencies: - '@vitest/utils': 2.1.9 - fflate: 0.8.2 - flatted: 3.3.3 - pathe: 1.1.2 - sirv: 3.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - optional: true - '@vitest/ui@3.2.4(vitest@3.2.4)': dependencies: '@vitest/utils': 3.2.4 @@ -18423,12 +18212,6 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/utils@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 - '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -18836,20 +18619,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-jest@30.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@jest/transform': 30.2.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.2.0(@babel/core@7.28.4) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - optional: true - babel-jest@30.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -18925,26 +18694,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) - optional: true - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -18970,13 +18719,6 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) - babel-preset-jest@30.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - babel-plugin-jest-hoist: 30.2.0 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - optional: true - babel-preset-jest@30.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -22426,15 +22168,15 @@ snapshots: jose@6.1.3: {} - jotai-minidb@0.0.8(jotai@2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)): + jotai-minidb@0.0.8(jotai@2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)): dependencies: '@rocicorp/resolver': 1.0.2 idb-keyval: 6.2.2 - jotai: 2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0) + jotai: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0) - jotai@2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0): + jotai@2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0): optionalDependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/template': 7.27.2 '@types/react': 19.2.2 react: 19.2.0 @@ -23356,13 +23098,13 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.13(next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next-auth@4.24.13(next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.28.3 @@ -23371,31 +23113,6 @@ snapshots: react-dom: 19.2.0(react@19.2.0) uuid: 8.3.2 - next@15.5.11(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): - dependencies: - '@next/env': 15.5.11 - '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001760 - postcss: 8.4.31 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.2.0) - optionalDependencies: - '@next/swc-darwin-arm64': 15.5.7 - '@next/swc-darwin-x64': 15.5.7 - '@next/swc-linux-arm64-gnu': 15.5.7 - '@next/swc-linux-arm64-musl': 15.5.7 - '@next/swc-linux-x64-gnu': 15.5.7 - '@next/swc-linux-x64-musl': 15.5.7 - '@next/swc-win32-arm64-msvc': 15.5.7 - '@next/swc-win32-x64-msvc': 15.5.7 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.57.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@15.5.11(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@next/env': 15.5.11 @@ -23780,8 +23497,6 @@ snapshots: path-type@4.0.0: {} - pathe@1.1.2: {} - pathe@2.0.3: {} pathval@2.0.1: {} @@ -25138,13 +24853,6 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.2.0): - dependencies: - client-only: 0.0.1 - react: 19.2.0 - optionalDependencies: - '@babel/core': 7.28.4 - styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0): dependencies: client-only: 0.0.1 @@ -25259,14 +24967,10 @@ snapshots: tinypool@1.1.1: {} - tinyrainbow@1.2.0: {} - tinyrainbow@2.0.0: {} tinyrainbow@3.0.3: {} - tinyspy@3.0.2: {} - tinyspy@4.0.4: {} tmpl@1.0.5: {} @@ -25311,12 +25015,12 @@ snapshots: '@ts-graphviz/common': 2.1.5 '@ts-graphviz/core': 2.0.7 - ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(esbuild@0.25.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -25325,19 +25029,19 @@ snapshots: typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.4) + babel-jest: 30.2.0(@babel/core@7.28.5) esbuild: 0.25.12 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)) + jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.12.5)(@types/node@22.19.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -25754,27 +25458,6 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@2.1.9(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-node@3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -25869,45 +25552,6 @@ snapshots: yaml: 2.8.1 optional: true - vitest@2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 - magic-string: 0.30.21 - pathe: 1.1.2 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 1.2.0 - vite: 7.3.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 2.1.9(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 22.19.1 - '@vitest/ui': 2.1.9(vitest@2.1.9) - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 diff --git a/src/routers/cloud-agent-next-router.ts b/src/routers/cloud-agent-next-router.ts index e345e6d88..299005a59 100644 --- a/src/routers/cloud-agent-next-router.ts +++ b/src/routers/cloud-agent-next-router.ts @@ -1,6 +1,5 @@ import 'server-only'; import { TRPCError } from '@trpc/server'; -import { eq as _eq, and as _and } from 'drizzle-orm'; import { baseProcedure, createTRPCRouter } from '@/lib/trpc/init'; import { createCloudAgentNextClient, @@ -31,8 +30,6 @@ import { baseGetSessionNextSchema, baseGetSessionNextOutputSchema, } from './cloud-agent-next-schemas'; -import { db } from '@/lib/drizzle'; -import { cli_sessions_v2 } from '@/db/schema'; import * as z from 'zod'; import { PLATFORM } from '@/lib/integrations/core/constants'; @@ -106,20 +103,6 @@ export const cloudAgentNextRouter = createTRPCRouter({ setupCommands: merged.setupCommands, }); - // Insert cli_sessions_v2 with session IDs - await db - .insert(cli_sessions_v2) - .values({ - session_id: result.kiloSessionId, - kilo_user_id: ctx.user.id, - cloud_agent_session_id: result.cloudAgentSessionId, - version: 0, - }) - .onConflictDoUpdate({ - target: [cli_sessions_v2.session_id, cli_sessions_v2.kilo_user_id], - set: { cloud_agent_session_id: result.cloudAgentSessionId }, - }); - return result; } catch (error) { if (error instanceof ProfileNotFoundError) { diff --git a/src/routers/organizations/organization-cloud-agent-next-router.ts b/src/routers/organizations/organization-cloud-agent-next-router.ts index 621f00b6b..a7ef51c5c 100644 --- a/src/routers/organizations/organization-cloud-agent-next-router.ts +++ b/src/routers/organizations/organization-cloud-agent-next-router.ts @@ -1,6 +1,5 @@ import 'server-only'; import { TRPCError } from '@trpc/server'; -import { eq as _eq, and as _and } from 'drizzle-orm'; import { createTRPCRouter } from '@/lib/trpc/init'; import { createCloudAgentNextClient, @@ -32,8 +31,6 @@ import { baseGetSessionNextSchema, baseGetSessionNextOutputSchema, } from '../cloud-agent-next-schemas'; -import { db } from '@/lib/drizzle'; -import { cli_sessions_v2 } from '@/db/schema'; import * as z from 'zod'; import { PLATFORM } from '@/lib/integrations/core/constants'; @@ -148,24 +145,6 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ setupCommands: merged.setupCommands, }); - // Insert cli_sessions_v2 with session IDs - await db - .insert(cli_sessions_v2) - .values({ - session_id: result.kiloSessionId, - kilo_user_id: ctx.user.id, - cloud_agent_session_id: result.cloudAgentSessionId, - organization_id: organizationId, - version: 0, - }) - .onConflictDoUpdate({ - target: [cli_sessions_v2.session_id, cli_sessions_v2.kilo_user_id], - set: { - cloud_agent_session_id: result.cloudAgentSessionId, - organization_id: organizationId, - }, - }); - return result; } catch (error) { if (error instanceof ProfileNotFoundError) { From 14ae51efbe2892bfdd34d1009ec14b4e09669a81 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 11:11:03 +0100 Subject: [PATCH 2/8] Convert ambient .d.ts binding types to explicit .ts modules Replace session-ingest-binding.d.ts files with regular .ts files that use explicit exports/imports, consistent with the rest of the codebase. --- cloud-agent-next/src/persistence/types.ts | 1 + .../src/session-ingest-binding.d.ts | 27 ------------------- .../src/session-ingest-binding.ts | 27 +++++++++++++++++++ cloud-agent-next/src/types.ts | 1 + .../src/session-ingest-binding.d.ts | 27 ------------------- .../src/session-ingest-binding.ts | 27 +++++++++++++++++++ 6 files changed, 56 insertions(+), 54 deletions(-) delete mode 100644 cloud-agent-next/src/session-ingest-binding.d.ts create mode 100644 cloud-agent-next/src/session-ingest-binding.ts delete mode 100644 cloudflare-session-ingest/src/session-ingest-binding.d.ts create mode 100644 cloudflare-session-ingest/src/session-ingest-binding.ts diff --git a/cloud-agent-next/src/persistence/types.ts b/cloud-agent-next/src/persistence/types.ts index 4c6e58054..46a76dad7 100644 --- a/cloud-agent-next/src/persistence/types.ts +++ b/cloud-agent-next/src/persistence/types.ts @@ -4,6 +4,7 @@ import type { CloudAgentSession } from './CloudAgentSession.js'; import type { EncryptedSecrets } from '../router/schemas.js'; import type { CallbackTarget } from '../callbacks/index.js'; import type { Images } from './schemas.js'; +import type { SessionIngestBinding } from '../session-ingest-binding.js'; /** * Base configuration shared by all MCP server types diff --git a/cloud-agent-next/src/session-ingest-binding.d.ts b/cloud-agent-next/src/session-ingest-binding.d.ts deleted file mode 100644 index 9584ee9da..000000000 --- a/cloud-agent-next/src/session-ingest-binding.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Augment the wrangler-generated Env to give the SESSION_INGEST service - * binding its RPC method types. `wrangler types` only sees `Fetcher` for - * service bindings; the actual RPC shape comes from the session-ingest - * worker's WorkerEntrypoint and is declared here so the generated file can - * be freely regenerated. - * - * Keep in sync with: cloudflare-session-ingest/src/index.ts - */ - -type CreateSessionForCloudAgentParams = { - sessionId: string; - kiloUserId: string; - cloudAgentSessionId: string; - organizationId?: string; - createdOnPlatform?: string; -}; - -type DeleteSessionForCloudAgentParams = { - sessionId: string; - kiloUserId: string; -}; - -type SessionIngestBinding = Fetcher & { - createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; - deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; -}; diff --git a/cloud-agent-next/src/session-ingest-binding.ts b/cloud-agent-next/src/session-ingest-binding.ts new file mode 100644 index 000000000..f24d1ee09 --- /dev/null +++ b/cloud-agent-next/src/session-ingest-binding.ts @@ -0,0 +1,27 @@ +/** + * RPC method types for the SESSION_INGEST service binding. + * + * `wrangler types` only sees `Fetcher` for service bindings; the actual RPC + * shape comes from the session-ingest worker's WorkerEntrypoint and is + * declared here so the generated file can be freely regenerated. + * + * Keep in sync with: cloudflare-session-ingest/src/index.ts + */ + +export type CreateSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; + cloudAgentSessionId: string; + organizationId?: string; + createdOnPlatform?: string; +}; + +export type DeleteSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; +}; + +export type SessionIngestBinding = Fetcher & { + createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; + deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; +}; diff --git a/cloud-agent-next/src/types.ts b/cloud-agent-next/src/types.ts index 635185d5a..3935e543e 100644 --- a/cloud-agent-next/src/types.ts +++ b/cloud-agent-next/src/types.ts @@ -1,6 +1,7 @@ import type { getSandbox, ExecutionSession, Sandbox } from '@cloudflare/sandbox'; import type { CloudAgentSession } from './persistence/CloudAgentSession.js'; import type { CallbackJob } from './callbacks/index.js'; +import type { SessionIngestBinding } from './session-ingest-binding.js'; import * as z from 'zod'; import { Limits } from './schema.js'; diff --git a/cloudflare-session-ingest/src/session-ingest-binding.d.ts b/cloudflare-session-ingest/src/session-ingest-binding.d.ts deleted file mode 100644 index 9584ee9da..000000000 --- a/cloudflare-session-ingest/src/session-ingest-binding.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Augment the wrangler-generated Env to give the SESSION_INGEST service - * binding its RPC method types. `wrangler types` only sees `Fetcher` for - * service bindings; the actual RPC shape comes from the session-ingest - * worker's WorkerEntrypoint and is declared here so the generated file can - * be freely regenerated. - * - * Keep in sync with: cloudflare-session-ingest/src/index.ts - */ - -type CreateSessionForCloudAgentParams = { - sessionId: string; - kiloUserId: string; - cloudAgentSessionId: string; - organizationId?: string; - createdOnPlatform?: string; -}; - -type DeleteSessionForCloudAgentParams = { - sessionId: string; - kiloUserId: string; -}; - -type SessionIngestBinding = Fetcher & { - createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; - deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; -}; diff --git a/cloudflare-session-ingest/src/session-ingest-binding.ts b/cloudflare-session-ingest/src/session-ingest-binding.ts new file mode 100644 index 000000000..f24d1ee09 --- /dev/null +++ b/cloudflare-session-ingest/src/session-ingest-binding.ts @@ -0,0 +1,27 @@ +/** + * RPC method types for the SESSION_INGEST service binding. + * + * `wrangler types` only sees `Fetcher` for service bindings; the actual RPC + * shape comes from the session-ingest worker's WorkerEntrypoint and is + * declared here so the generated file can be freely regenerated. + * + * Keep in sync with: cloudflare-session-ingest/src/index.ts + */ + +export type CreateSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; + cloudAgentSessionId: string; + organizationId?: string; + createdOnPlatform?: string; +}; + +export type DeleteSessionForCloudAgentParams = { + sessionId: string; + kiloUserId: string; +}; + +export type SessionIngestBinding = Fetcher & { + createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; + deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; +}; From a1eb5f6e4f320f20e8b806748ea9d676b8f2a03a Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 11:43:52 +0100 Subject: [PATCH 3/8] Separate session-ingest RPC from Hono app (db-proxy pattern) Extract RPC methods into SessionIngestRPC WorkerEntrypoint class in its own file, reverting index.ts to export default app. Consumers reference the named entrypoint via wrangler.jsonc service binding config. Also adds try/catch + logging around RPC calls in session-service.ts. --- .../src/session-ingest-binding.ts | 2 +- cloud-agent-next/src/session-service.ts | 45 ++++-- cloud-agent-next/wrangler.jsonc | 2 + cloudflare-session-ingest/src/index.test.ts | 61 ++------ cloudflare-session-ingest/src/index.ts | 108 +------------- .../src/session-ingest-rpc.ts | 138 ++++++++++++++++++ 6 files changed, 189 insertions(+), 167 deletions(-) create mode 100644 cloudflare-session-ingest/src/session-ingest-rpc.ts diff --git a/cloud-agent-next/src/session-ingest-binding.ts b/cloud-agent-next/src/session-ingest-binding.ts index f24d1ee09..6479a83cb 100644 --- a/cloud-agent-next/src/session-ingest-binding.ts +++ b/cloud-agent-next/src/session-ingest-binding.ts @@ -5,7 +5,7 @@ * shape comes from the session-ingest worker's WorkerEntrypoint and is * declared here so the generated file can be freely regenerated. * - * Keep in sync with: cloudflare-session-ingest/src/index.ts + * Keep in sync with: cloudflare-session-ingest/src/session-ingest-rpc.ts */ export type CreateSessionForCloudAgentParams = { diff --git a/cloud-agent-next/src/session-service.ts b/cloud-agent-next/src/session-service.ts index b3b6c335a..4762f70f2 100644 --- a/cloud-agent-next/src/session-service.ts +++ b/cloud-agent-next/src/session-service.ts @@ -1609,13 +1609,25 @@ export class SessionService { organizationId?: string, createdOnPlatform?: string ): Promise { - await env.SESSION_INGEST.createSessionForCloudAgent({ - sessionId: kiloSessionId, - kiloUserId, - cloudAgentSessionId, - organizationId, - createdOnPlatform, - }); + try { + await env.SESSION_INGEST.createSessionForCloudAgent({ + sessionId: kiloSessionId, + kiloUserId, + cloudAgentSessionId, + organizationId, + createdOnPlatform, + }); + } catch (error) { + logger + .withFields({ + kiloSessionId, + cloudAgentSessionId, + kiloUserId, + error: error instanceof Error ? error.message : String(error), + }) + .error('session-ingest RPC createSessionForCloudAgent failed'); + throw error; + } } /** @@ -1627,10 +1639,21 @@ export class SessionService { kiloUserId: string, env: PersistenceEnv ): Promise { - await env.SESSION_INGEST.deleteSessionForCloudAgent({ - sessionId: kiloSessionId, - kiloUserId, - }); + try { + await env.SESSION_INGEST.deleteSessionForCloudAgent({ + sessionId: kiloSessionId, + kiloUserId, + }); + } catch (error) { + logger + .withFields({ + kiloSessionId, + kiloUserId, + error: error instanceof Error ? error.message : String(error), + }) + .error('session-ingest RPC deleteSessionForCloudAgent failed'); + throw error; + } } /** diff --git a/cloud-agent-next/wrangler.jsonc b/cloud-agent-next/wrangler.jsonc index 40e7bdccd..28748b0b9 100644 --- a/cloud-agent-next/wrangler.jsonc +++ b/cloud-agent-next/wrangler.jsonc @@ -73,6 +73,7 @@ { "binding": "SESSION_INGEST", "service": "session-ingest", + "entrypoint": "SessionIngestRPC", }, ], /** @@ -228,6 +229,7 @@ { "binding": "SESSION_INGEST", "service": "session-ingest", + "entrypoint": "SessionIngestRPC", }, ], "r2_buckets": [ diff --git a/cloudflare-session-ingest/src/index.test.ts b/cloudflare-session-ingest/src/index.test.ts index b13f7d8b4..524a03c83 100644 --- a/cloudflare-session-ingest/src/index.test.ts +++ b/cloudflare-session-ingest/src/index.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('cloudflare:workers', () => ({ DurableObject: class DurableObject { @@ -25,6 +25,7 @@ vi.mock('./dos/SessionIngestDO', () => ({ getSessionIngestDO: vi.fn(), })); +import app from './index'; import { getDb } from './db/kysely'; import { getSessionIngestDO } from './dos/SessionIngestDO'; @@ -36,10 +37,6 @@ type TestBindings = { NEXTAUTH_SECRET_RAW?: string; }; -let WorkerClass: { - new (): { env: unknown; ctx: unknown; fetch: (req: Request) => Promise }; -}; - function makeDbFakes() { const selectExecuteTakeFirst = vi.fn<() => Promise>(async () => undefined); const select = { @@ -55,33 +52,21 @@ function makeDbFakes() { return { db, selectExecuteTakeFirst }; } -function createWorker(env: TestBindings) { - const worker = new WorkerClass(); - worker.env = env; - return worker; -} +const defaultEnv: TestBindings = { + HYPERDRIVE: { connectionString: 'postgres://test' }, + SESSION_INGEST_DO: {}, + SESSION_ACCESS_CACHE_DO: {}, + NEXTAUTH_SECRET: {}, + NEXTAUTH_SECRET_RAW: 'secret', +}; describe('public session route', () => { - beforeAll(async () => { - const mod = await import('./index'); - WorkerClass = mod.default as unknown as typeof WorkerClass; - }); - beforeEach(() => { vi.resetAllMocks(); }); it('returns 400 for invalid uuid', async () => { - const env: TestBindings = { - HYPERDRIVE: { connectionString: 'postgres://test' }, - SESSION_INGEST_DO: {}, - SESSION_ACCESS_CACHE_DO: {}, - NEXTAUTH_SECRET: {}, - NEXTAUTH_SECRET_RAW: 'secret', - }; - - const worker = createWorker(env); - const res = await worker.fetch(new Request('http://local/session/not-a-uuid')); + const res = await app.request('/session/not-a-uuid', {}, defaultEnv); expect(res.status).toBe(400); }); @@ -90,18 +75,7 @@ describe('public session route', () => { vi.mocked(getDb).mockReturnValue(db as never); selectExecuteTakeFirst.mockResolvedValueOnce(undefined); - const env: TestBindings = { - HYPERDRIVE: { connectionString: 'postgres://test' }, - SESSION_INGEST_DO: {}, - SESSION_ACCESS_CACHE_DO: {}, - NEXTAUTH_SECRET: {}, - NEXTAUTH_SECRET_RAW: 'secret', - }; - - const worker = createWorker(env); - const res = await worker.fetch( - new Request('http://local/session/11111111-1111-4111-8111-111111111111') - ); + const res = await app.request('/session/11111111-1111-4111-8111-111111111111', {}, defaultEnv); expect(res.status).toBe(404); }); @@ -121,18 +95,7 @@ describe('public session route', () => { stub as unknown as ReturnType ); - const env: TestBindings = { - HYPERDRIVE: { connectionString: 'postgres://test' }, - SESSION_INGEST_DO: {}, - SESSION_ACCESS_CACHE_DO: {}, - NEXTAUTH_SECRET: {}, - NEXTAUTH_SECRET_RAW: 'secret', - }; - - const worker = createWorker(env); - const res = await worker.fetch( - new Request('http://local/session/11111111-1111-4111-8111-111111111111') - ); + const res = await app.request('/session/11111111-1111-4111-8111-111111111111', {}, defaultEnv); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toBe('application/json; charset=utf-8'); diff --git a/cloudflare-session-ingest/src/index.ts b/cloudflare-session-ingest/src/index.ts index fc2c4f567..c7593fae0 100644 --- a/cloudflare-session-ingest/src/index.ts +++ b/cloudflare-session-ingest/src/index.ts @@ -1,4 +1,3 @@ -import { WorkerEntrypoint } from 'cloudflare:workers'; import { Hono } from 'hono'; import type { Env } from './env'; import { z } from 'zod'; @@ -6,10 +5,10 @@ import { kiloJwtAuthMiddleware } from './middleware/kilo-jwt-auth'; import { api } from './routes/api'; import { getDb } from './db/kysely'; import { getSessionIngestDO } from './dos/SessionIngestDO'; -import { getSessionAccessCacheDO } from './dos/SessionAccessCacheDO'; import { withDORetry } from './util/do-retry'; export { SessionIngestDO } from './dos/SessionIngestDO'; export { SessionAccessCacheDO } from './dos/SessionAccessCacheDO'; +export { SessionIngestRPC } from './session-ingest-rpc'; const app = new Hono<{ Bindings: Env; @@ -59,107 +58,4 @@ app.get('/session/:sessionId', async c => { }); }); -const sessionIdSchema = z.string().startsWith('ses_').length(30); - -export default class SessionIngestWorker extends WorkerEntrypoint { - async fetch(request: Request): Promise { - return app.fetch(request, this.env, this.ctx); - } - - /** - * RPC method: create a cli_sessions_v2 record for a cloud-agent-next session. - * Called via service binding from cloud-agent-next during session preparation. - * - * Uses ON CONFLICT DO UPDATE to set cloud_agent_session_id (and organization_id - * if provided), matching the behavior previously in the backend routers. - */ - async createSessionForCloudAgent(params: { - sessionId: string; - kiloUserId: string; - cloudAgentSessionId: string; - organizationId?: string; - createdOnPlatform?: string; - }): Promise { - const parsed = z - .object({ - sessionId: sessionIdSchema, - kiloUserId: z.string().min(1), - cloudAgentSessionId: z.string().min(1), - organizationId: z.string().optional(), - createdOnPlatform: z.string().optional(), - }) - .parse(params); - - const db = getDb(this.env.HYPERDRIVE); - - await db - .insertInto('cli_sessions_v2') - .values({ - session_id: parsed.sessionId, - kilo_user_id: parsed.kiloUserId, - cloud_agent_session_id: parsed.cloudAgentSessionId, - organization_id: parsed.organizationId ?? null, - created_on_platform: parsed.createdOnPlatform ?? 'cloud-agent', - version: 0, - }) - .onConflict(oc => - oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ - cloud_agent_session_id: parsed.cloudAgentSessionId, - ...(parsed.organizationId !== undefined - ? { organization_id: parsed.organizationId } - : {}), - }) - ) - .execute(); - - // Warm the session cache so subsequent ingests can skip Postgres. - await withDORetry( - () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), - sessionCache => sessionCache.add(parsed.sessionId), - 'SessionAccessCacheDO.add' - ); - } - - /** - * RPC method: delete a cli_sessions_v2 record for a cloud-agent-next session. - * Called via service binding from cloud-agent-next for rollback when DO prepare() fails. - * - * Scoped to the user (composite PK: session_id + kilo_user_id). - */ - async deleteSessionForCloudAgent(params: { - sessionId: string; - kiloUserId: string; - }): Promise { - const parsed = z - .object({ - sessionId: sessionIdSchema, - kiloUserId: z.string().min(1), - }) - .parse(params); - - const db = getDb(this.env.HYPERDRIVE); - - await db - .deleteFrom('cli_sessions_v2') - .where('session_id', '=', parsed.sessionId) - .where('kilo_user_id', '=', parsed.kiloUserId) - .execute(); - - // Clear caches - await withDORetry( - () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), - sessionCache => sessionCache.remove(parsed.sessionId), - 'SessionAccessCacheDO.remove' - ); - - await withDORetry( - () => - getSessionIngestDO(this.env, { - kiloUserId: parsed.kiloUserId, - sessionId: parsed.sessionId, - }), - stub => stub.clear(), - 'SessionIngestDO.clear' - ); - } -} +export default app; diff --git a/cloudflare-session-ingest/src/session-ingest-rpc.ts b/cloudflare-session-ingest/src/session-ingest-rpc.ts new file mode 100644 index 000000000..a93cbf3ee --- /dev/null +++ b/cloudflare-session-ingest/src/session-ingest-rpc.ts @@ -0,0 +1,138 @@ +import { WorkerEntrypoint } from 'cloudflare:workers'; +import { z } from 'zod'; +import type { Env } from './env'; +import { getDb } from './db/kysely'; +import { getSessionIngestDO } from './dos/SessionIngestDO'; +import { getSessionAccessCacheDO } from './dos/SessionAccessCacheDO'; +import { withDORetry } from './util/do-retry'; + +const sessionIdSchema = z.string().startsWith('ses_').length(30); + +export class SessionIngestRPC extends WorkerEntrypoint { + /** + * RPC method: create a cli_sessions_v2 record for a cloud-agent-next session. + * Called via service binding from cloud-agent-next during session preparation. + * + * Uses ON CONFLICT DO UPDATE to set cloud_agent_session_id (and organization_id + * if provided), matching the behavior previously in the backend routers. + */ + async createSessionForCloudAgent(params: { + sessionId: string; + kiloUserId: string; + cloudAgentSessionId: string; + organizationId?: string; + createdOnPlatform?: string; + }): Promise { + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + cloudAgentSessionId: z.string().min(1), + organizationId: z.string().optional(), + createdOnPlatform: z.string().optional(), + }) + .parse(params); + + const db = getDb(this.env.HYPERDRIVE); + + await db + .insertInto('cli_sessions_v2') + .values({ + session_id: parsed.sessionId, + kilo_user_id: parsed.kiloUserId, + cloud_agent_session_id: parsed.cloudAgentSessionId, + organization_id: parsed.organizationId ?? null, + created_on_platform: parsed.createdOnPlatform ?? 'cloud-agent', + version: 0, + }) + .onConflict(oc => + oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ + cloud_agent_session_id: parsed.cloudAgentSessionId, + ...(parsed.organizationId !== undefined + ? { organization_id: parsed.organizationId } + : {}), + }) + ) + .execute(); + + // Warm the session cache so subsequent ingests can skip Postgres. + // Best-effort: cache miss is acceptable; don't fail the create if the DO is unavailable. + try { + await withDORetry( + () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), + sessionCache => sessionCache.add(parsed.sessionId), + 'SessionAccessCacheDO.add' + ); + } catch (error) { + console.error('Failed to warm session cache after create (non-fatal)', { + sessionId: parsed.sessionId, + kiloUserId: parsed.kiloUserId, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + /** + * RPC method: delete a cli_sessions_v2 record for a cloud-agent-next session. + * Called via service binding from cloud-agent-next for rollback when DO prepare() fails. + * + * Scoped to the user (composite PK: session_id + kilo_user_id). + */ + async deleteSessionForCloudAgent(params: { + sessionId: string; + kiloUserId: string; + }): Promise { + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + }) + .parse(params); + + const db = getDb(this.env.HYPERDRIVE); + + await db + .deleteFrom('cli_sessions_v2') + .where('session_id', '=', parsed.sessionId) + .where('kilo_user_id', '=', parsed.kiloUserId) + .execute(); + + // Clear caches — best-effort; don't fail the delete if DOs are unavailable. + const cacheErrors: string[] = []; + try { + await withDORetry( + () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), + sessionCache => sessionCache.remove(parsed.sessionId), + 'SessionAccessCacheDO.remove' + ); + } catch (error) { + cacheErrors.push( + `SessionAccessCacheDO.remove: ${error instanceof Error ? error.message : String(error)}` + ); + } + + try { + await withDORetry( + () => + getSessionIngestDO(this.env, { + kiloUserId: parsed.kiloUserId, + sessionId: parsed.sessionId, + }), + stub => stub.clear(), + 'SessionIngestDO.clear' + ); + } catch (error) { + cacheErrors.push( + `SessionIngestDO.clear: ${error instanceof Error ? error.message : String(error)}` + ); + } + + if (cacheErrors.length > 0) { + console.error('Failed to clear caches after delete (non-fatal)', { + sessionId: parsed.sessionId, + kiloUserId: parsed.kiloUserId, + errors: cacheErrors, + }); + } + } +} From 224adff1798b5b42b10fadb4fb0626061e08ec82 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 11:52:33 +0100 Subject: [PATCH 4/8] Remove unused session-ingest-binding.ts from cloudflare-session-ingest --- .../src/session-ingest-binding.ts | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 cloudflare-session-ingest/src/session-ingest-binding.ts diff --git a/cloudflare-session-ingest/src/session-ingest-binding.ts b/cloudflare-session-ingest/src/session-ingest-binding.ts deleted file mode 100644 index f24d1ee09..000000000 --- a/cloudflare-session-ingest/src/session-ingest-binding.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * RPC method types for the SESSION_INGEST service binding. - * - * `wrangler types` only sees `Fetcher` for service bindings; the actual RPC - * shape comes from the session-ingest worker's WorkerEntrypoint and is - * declared here so the generated file can be freely regenerated. - * - * Keep in sync with: cloudflare-session-ingest/src/index.ts - */ - -export type CreateSessionForCloudAgentParams = { - sessionId: string; - kiloUserId: string; - cloudAgentSessionId: string; - organizationId?: string; - createdOnPlatform?: string; -}; - -export type DeleteSessionForCloudAgentParams = { - sessionId: string; - kiloUserId: string; -}; - -export type SessionIngestBinding = Fetcher & { - createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; - deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; -}; From a7e79b44ecd7193f571147a670b233402a68803f Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 11:57:54 +0100 Subject: [PATCH 5/8] Make createdOnPlatform required in createSessionForCloudAgent --- cloud-agent-next/src/session-ingest-binding.ts | 2 +- cloud-agent-next/src/session-service.ts | 4 ++-- cloudflare-session-ingest/src/session-ingest-rpc.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud-agent-next/src/session-ingest-binding.ts b/cloud-agent-next/src/session-ingest-binding.ts index 6479a83cb..602219890 100644 --- a/cloud-agent-next/src/session-ingest-binding.ts +++ b/cloud-agent-next/src/session-ingest-binding.ts @@ -13,7 +13,7 @@ export type CreateSessionForCloudAgentParams = { kiloUserId: string; cloudAgentSessionId: string; organizationId?: string; - createdOnPlatform?: string; + createdOnPlatform: string; }; export type DeleteSessionForCloudAgentParams = { diff --git a/cloud-agent-next/src/session-service.ts b/cloud-agent-next/src/session-service.ts index 4762f70f2..62fa277b8 100644 --- a/cloud-agent-next/src/session-service.ts +++ b/cloud-agent-next/src/session-service.ts @@ -1606,8 +1606,8 @@ export class SessionService { cloudAgentSessionId: string, kiloUserId: string, env: PersistenceEnv, - organizationId?: string, - createdOnPlatform?: string + organizationId: string | undefined, + createdOnPlatform: string ): Promise { try { await env.SESSION_INGEST.createSessionForCloudAgent({ diff --git a/cloudflare-session-ingest/src/session-ingest-rpc.ts b/cloudflare-session-ingest/src/session-ingest-rpc.ts index a93cbf3ee..f97f6ec9b 100644 --- a/cloudflare-session-ingest/src/session-ingest-rpc.ts +++ b/cloudflare-session-ingest/src/session-ingest-rpc.ts @@ -21,7 +21,7 @@ export class SessionIngestRPC extends WorkerEntrypoint { kiloUserId: string; cloudAgentSessionId: string; organizationId?: string; - createdOnPlatform?: string; + createdOnPlatform: string; }): Promise { const parsed = z .object({ @@ -29,7 +29,7 @@ export class SessionIngestRPC extends WorkerEntrypoint { kiloUserId: z.string().min(1), cloudAgentSessionId: z.string().min(1), organizationId: z.string().optional(), - createdOnPlatform: z.string().optional(), + createdOnPlatform: z.string().min(1), }) .parse(params); @@ -42,7 +42,7 @@ export class SessionIngestRPC extends WorkerEntrypoint { kilo_user_id: parsed.kiloUserId, cloud_agent_session_id: parsed.cloudAgentSessionId, organization_id: parsed.organizationId ?? null, - created_on_platform: parsed.createdOnPlatform ?? 'cloud-agent', + created_on_platform: parsed.createdOnPlatform, version: 0, }) .onConflict(oc => From 733d13bed514f0415c13c975669ea5509f96167c Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 13:47:49 +0100 Subject: [PATCH 6/8] session-ingest --- .../src/router/handlers/session-prepare.ts | 4 + .../src/session-ingest-binding.ts | 6 + cloud-agent-next/src/session-prepare.test.ts | 1 + cloud-agent-next/src/session-service.test.ts | 109 +++++++++++----- cloud-agent-next/src/session-service.ts | 69 ++++++---- cloud-agent-next/wrangler.jsonc | 2 +- cloudflare-app-builder/start-dev.sh | 57 ++++++--- cloudflare-git-token-service/wrangler.jsonc | 2 +- .../src/middleware/kilo-jwt-auth.ts | 2 +- cloudflare-session-ingest/src/routes/api.ts | 39 +++--- .../src/services/session-export.ts | 38 ++++++ .../src/session-ingest-rpc.ts | 119 ++++++++++++------ .../worker-configuration.d.ts | 15 ++- cloudflare-session-ingest/wrangler.jsonc | 5 + 14 files changed, 336 insertions(+), 132 deletions(-) create mode 100644 cloudflare-session-ingest/src/services/session-export.ts diff --git a/cloud-agent-next/src/router/handlers/session-prepare.ts b/cloud-agent-next/src/router/handlers/session-prepare.ts index 0a4424bbd..19a2053a1 100644 --- a/cloud-agent-next/src/router/handlers/session-prepare.ts +++ b/cloud-agent-next/src/router/handlers/session-prepare.ts @@ -6,6 +6,7 @@ import { SessionService, determineBranchName, runSetupCommands, + writeAuthFile, writeMCPSettings, } from '../../session-service.js'; import { InstallationLookupService } from '../../services/installation-lookup-service.js'; @@ -281,6 +282,9 @@ const prepareSessionHandler = internalApiProtectedProcedure await writeMCPSettings(sandbox, sessionHome, input.mcpServers); } + // 9b. Write auth file for session ingest + await writeAuthFile(sandbox, sessionHome, ctx.authToken); + // 10. Start kilo server logger.info('Starting kilo server'); const kiloServerPort = await ensureKiloServer( diff --git a/cloud-agent-next/src/session-ingest-binding.ts b/cloud-agent-next/src/session-ingest-binding.ts index 602219890..5a5ff04cf 100644 --- a/cloud-agent-next/src/session-ingest-binding.ts +++ b/cloud-agent-next/src/session-ingest-binding.ts @@ -21,7 +21,13 @@ export type DeleteSessionForCloudAgentParams = { kiloUserId: string; }; +export type ExportSessionParams = { + sessionId: string; + kiloUserId: string; +}; + export type SessionIngestBinding = Fetcher & { createSessionForCloudAgent(params: CreateSessionForCloudAgentParams): Promise; deleteSessionForCloudAgent(params: DeleteSessionForCloudAgentParams): Promise; + exportSession(params: ExportSessionParams): Promise; }; diff --git a/cloud-agent-next/src/session-prepare.test.ts b/cloud-agent-next/src/session-prepare.test.ts index abb740dea..8ef2ec6e2 100644 --- a/cloud-agent-next/src/session-prepare.test.ts +++ b/cloud-agent-next/src/session-prepare.test.ts @@ -71,6 +71,7 @@ vi.mock('./session-service.js', () => ({ (sessionId: string, upstreamBranch?: string) => upstreamBranch || `session/${sessionId}` ), runSetupCommands: vi.fn().mockResolvedValue(undefined), + writeAuthFile: vi.fn().mockResolvedValue(undefined), writeMCPSettings: vi.fn().mockResolvedValue(undefined), InvalidSessionMetadataError: class InvalidSessionMetadataError extends Error { constructor( diff --git a/cloud-agent-next/src/session-service.test.ts b/cloud-agent-next/src/session-service.test.ts index 2793b2158..128478c1a 100644 --- a/cloud-agent-next/src/session-service.test.ts +++ b/cloud-agent-next/src/session-service.test.ts @@ -46,11 +46,9 @@ import type { PersistenceEnv, CloudAgentSessionState } from './persistence/types describe('SessionService', () => { beforeEach(() => { vi.clearAllMocks(); - mockEnv.SESSION_INGEST.fetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - text: vi.fn().mockResolvedValue(JSON.stringify({ info: {}, messages: [] })), - }) as unknown as PersistenceEnv['SESSION_INGEST']['fetch']; + mockEnv.SESSION_INGEST.exportSession = vi + .fn() + .mockResolvedValue(JSON.stringify({ info: {}, messages: [] })); }); const mockedSetupWorkspace = vi.mocked(mockSetupWorkspace); @@ -74,7 +72,7 @@ describe('SessionService', () => { } as unknown as PersistenceEnv['CLOUD_AGENT_SESSION'], NEXTAUTH_SECRET: 'mock-secret', SESSION_INGEST: { - fetch: vi.fn(), + exportSession: vi.fn(), } as unknown as PersistenceEnv['SESSION_INGEST'], }; @@ -117,6 +115,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_test_123'; mockedSetupWorkspace.mockResolvedValue({ @@ -170,17 +170,14 @@ describe('SessionService', () => { expect(result.streamKilocodeExec).toBeDefined(); }); - it('does not restore session snapshot during initiate (no fetch)', async () => { - const payload = JSON.stringify({ info: {}, messages: [] }); - const fetchMock = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - text: vi.fn().mockResolvedValue(payload), - }); + it('does not restore session snapshot during initiate (no exportSession call)', async () => { + const exportSessionMock = vi + .fn() + .mockResolvedValue(JSON.stringify({ info: {}, messages: [] })); const envWithIngest: PersistenceEnv = { ...mockEnv, SESSION_INGEST: { - fetch: fetchMock, + exportSession: exportSessionMock, } as unknown as PersistenceEnv['SESSION_INGEST'], }; @@ -194,6 +191,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_restore_test_skip'; mockedSetupWorkspace.mockResolvedValue({ @@ -214,7 +213,7 @@ describe('SessionService', () => { env: envWithIngest, }); - expect(fetchMock).not.toHaveBeenCalled(); + expect(exportSessionMock).not.toHaveBeenCalled(); expect(fakeSession.writeFile).not.toHaveBeenCalled(); expect(fakeSession.exec).not.toHaveBeenCalledWith( `kilo import "/tmp/kilo-session-export-${sessionId}.json"` @@ -235,6 +234,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_test_456'; mockedSetupWorkspace.mockResolvedValue({ @@ -281,6 +282,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const service = new SessionService(); const sessionId: SessionId = 'agent_test_456'; @@ -331,6 +334,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_first_call'; mockedSetupWorkspace.mockResolvedValue({ @@ -395,6 +400,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_resume_first_flag'; @@ -447,6 +454,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const service = new SessionService(); @@ -499,6 +508,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_capture_test'; mockedSetupWorkspace.mockResolvedValue({ @@ -574,6 +585,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const mockDOGetMetadata = vi.fn(); @@ -598,6 +611,7 @@ describe('SessionService', () => { timestamp: 123456789, githubRepo: 'facebook/react', githubToken: 'test-token', + kiloSessionId: 'ses_test_kilo_session_id_0001', }; mockDOGetMetadata.mockResolvedValue(metadata); @@ -642,6 +656,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const mockDOGetMetadata = vi.fn(); @@ -658,6 +674,7 @@ describe('SessionService', () => { }; // Mock: DO returns metadata with STALE token + const kiloSessionId = 'ses_test_kilo_session_id_0001'; const metadata = { version: 123456789, sessionId, @@ -665,7 +682,8 @@ describe('SessionService', () => { userId, timestamp: 123456789, githubRepo: 'facebook/react', - githubToken: 'stale-token-from-metadata', + githubToken: 'test-token', + kiloSessionId, }; mockDOGetMetadata.mockResolvedValue(metadata); @@ -709,6 +727,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const mockDOGetMetadata = vi.fn(); @@ -733,6 +753,7 @@ describe('SessionService', () => { timestamp: 123456789, githubRepo: 'facebook/react', githubToken: 'metadata-token', + kiloSessionId: 'ses_test_kilo_session_id_0001', }; mockDOGetMetadata.mockResolvedValue(metadata); @@ -769,6 +790,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const testEnv = { @@ -804,15 +827,11 @@ describe('SessionService', () => { it('restores session snapshot before reclone when workspace is missing', async () => { const mockDOGetMetadata = vi.fn(); const payload = JSON.stringify({ info: {}, messages: [] }); - const fetchMock = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - text: vi.fn().mockResolvedValue(payload), - }); + const exportSessionMock = vi.fn().mockResolvedValue(payload); const envWithIngest: PersistenceEnv = { ...mockEnv, SESSION_INGEST: { - fetch: fetchMock, + exportSession: exportSessionMock, } as unknown as PersistenceEnv['SESSION_INGEST'], CLOUD_AGENT_SESSION: { idFromName: vi.fn(() => 'mock-do-id' as unknown as DurableObjectId), @@ -835,8 +854,11 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; + const kiloSessionId = 'ses_test_kilo_session_id_0001'; const metadata = { version: 123456789, sessionId, @@ -845,6 +867,7 @@ describe('SessionService', () => { timestamp: 123456789, githubRepo: 'facebook/react', githubToken: 'test-token', + kiloSessionId, }; mockDOGetMetadata.mockResolvedValue(metadata); @@ -860,14 +883,10 @@ describe('SessionService', () => { env: envWithIngest, }); - expect(fetchMock).toHaveBeenCalledWith( - `https://session-ingest/api/session/${sessionId}/export`, - { - headers: { - Authorization: 'Bearer test-token', - }, - } - ); + expect(exportSessionMock).toHaveBeenCalledWith({ + sessionId: kiloSessionId, + kiloUserId: userId, + }); expect(fakeSession.writeFile).toHaveBeenCalledWith( `/tmp/kilo-session-export-${sessionId}.json`, payload @@ -895,6 +914,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_envtest_123'; mockedSetupWorkspace.mockResolvedValue({ @@ -952,6 +973,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_special_chars'; mockedSetupWorkspace.mockResolvedValue({ @@ -1001,6 +1024,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_no_env'; mockedSetupWorkspace.mockResolvedValue({ @@ -1051,6 +1076,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_gh_token_test'; mockedSetupWorkspace.mockResolvedValue({ @@ -1094,6 +1121,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_gh_token_override'; mockedSetupWorkspace.mockResolvedValue({ @@ -1142,6 +1171,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_no_gh_token'; mockedSetupWorkspace.mockResolvedValue({ @@ -1179,6 +1210,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_empty_gh_token'; mockedSetupWorkspace.mockResolvedValue({ @@ -1216,6 +1249,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_giturl_with_ghtoken'; mockedSetupWorkspace.mockResolvedValue({ @@ -1254,6 +1289,7 @@ describe('SessionService', () => { timestamp: 123456789, githubRepo: 'acme/repo', setupCommands: ['npm install', 'npm run build', 'npm test'], + kiloSessionId: 'ses_test_kilo_session_id_0001', }; const { env: testEnv } = createMetadataEnv({ @@ -1283,6 +1319,8 @@ describe('SessionService', () => { const sandbox = { createSession: vi.fn().mockResolvedValue(fakeSession), mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const service = new SessionService(); @@ -1337,6 +1375,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_failfast_test'; mockedSetupWorkspace.mockResolvedValue({ @@ -1382,6 +1422,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_timeout_test'; mockedSetupWorkspace.mockResolvedValue({ @@ -1420,6 +1462,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_cwd_test'; const workspacePath = `/workspace/org/user/sessions/${sessionId}`; @@ -1463,6 +1507,8 @@ describe('SessionService', () => { const sandbox = { createSession: sandboxCreateSession, mkdir: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue({ exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), } as unknown as SandboxInstance; const sessionId: SessionId = 'agent_empty_commands'; mockedSetupWorkspace.mockResolvedValue({ @@ -1886,6 +1932,7 @@ describe('SessionService', () => { timestamp: 123456789, githubRepo: 'acme/repo', setupCommands: ['npm install', 'npm run build'], + kiloSessionId: 'ses_test_kilo_session_id_0001', }; const { env: testEnv } = createMetadataEnv({ @@ -1930,6 +1977,7 @@ describe('SessionService', () => { userId: 'user', timestamp: 123456789, githubRepo: 'acme/repo', + kiloSessionId: 'ses_test_kilo_session_id_0001', mcpServers: { puppeteer: { command: 'npx', @@ -2040,6 +2088,7 @@ describe('SessionService', () => { envVars: { API_KEY: 'test' }, setupCommands: ['npm install'], mcpServers: { test: { command: 'test-server' } }, + kiloSessionId: 'ses_test_kilo_session_id_0001', }; const { env: testEnv } = createMetadataEnv({ diff --git a/cloud-agent-next/src/session-service.ts b/cloud-agent-next/src/session-service.ts index 62fa277b8..40be82a6e 100644 --- a/cloud-agent-next/src/session-service.ts +++ b/cloud-agent-next/src/session-service.ts @@ -288,6 +288,27 @@ export async function writeMCPSettings( .info('Configured MCP servers'); } +// Write Kilo auth file so the CLI's KiloSessions can call session ingest. +// The CLI reads ~/.local/share/kilo/auth.json via Auth.get("kilo") but we +// never run `kilo auth login` — credentials are injected purely via env vars +// for config (KILO_CONFIG_CONTENT). The session ingest code path ignores the +// provider config and only reads the auth file. +export async function writeAuthFile( + sandbox: SandboxInstance, + sessionHome: string, + kilocodeToken: string +): Promise { + const authDir = `${sessionHome}/.local/share/kilo`; + const authPath = `${authDir}/auth.json`; + + await sandbox.exec(`mkdir -p ${authDir}`); + + const authContent = JSON.stringify({ kilo: { type: 'api', key: kilocodeToken } }, null, 2); + await sandbox.writeFile(authPath, authContent); + + logger.info('Wrote kilo auth file for session ingest'); +} + /** * Fetch session metadata from Durable Object using RPC with retry logic. * Creates a fresh stub for each retry attempt as recommended by Cloudflare. @@ -552,6 +573,8 @@ export class SessionService { if (env.KILOCODE_BACKEND_BASE_URL) { envVars.KILOCODE_BACKEND_BASE_URL = env.KILOCODE_BACKEND_BASE_URL; + // Used by kilo server to check user auth to send to ingest + envVars.KILO_API_URL = env.KILOCODE_BACKEND_BASE_URL; } if (env.KILO_SESSION_INGEST_URL) { @@ -746,6 +769,9 @@ export class SessionService { await writeMCPSettings(sandbox, context.sessionHome, mcpServers); } + // Write auth file for session ingest + await writeAuthFile(sandbox, context.sessionHome, kilocodeToken); + // Save metadata to Durable Object const existingMetadata = await this.loadSessionMetadata(env, context); await this.saveSessionMetadata( @@ -814,38 +840,24 @@ export class SessionService { private async restoreSessionSnapshot( session: ExecutionSession, sessionId: string, - authToken: string, + kiloSessionId: string, env: PersistenceEnv, userId: string ): Promise { const tmpPath = `/tmp/kilo-session-export-${sessionId}.json`; let wroteSnapshot = false; try { - const response = await env.SESSION_INGEST.fetch( - `https://session-ingest/api/session/${sessionId}/export`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - ); - - if (!response) { - throw new Error('Session ingest fetch returned no response'); - } + const payload = await env.SESSION_INGEST.exportSession({ + sessionId: kiloSessionId, + kiloUserId: userId, + }); - if (response.status === 401 || response.status === 404) { + if (payload === null) { throw new SessionSnapshotRestoreError( - `Session snapshot restore failed with status ${response.status}`, - response.status + `Session snapshot restore failed: session not found`, + 404 ); } - - if (!response.ok) { - throw new Error(`Session ingest returned ${response.status}`); - } - - const payload = await response.text(); await session.writeFile(tmpPath, payload); wroteSnapshot = true; @@ -1050,6 +1062,9 @@ export class SessionService { await writeMCPSettings(sandbox, context.sessionHome, mcpServers); } + // Write auth file for session ingest + await writeAuthFile(sandbox, context.sessionHome, kilocodeToken); + // Fetch metadata from DO if not provided, to ensure we preserve existing fields const metadataToPreserve = existingMetadata ?? (await this.loadSessionMetadata(env, context)) ?? undefined; @@ -1273,7 +1288,12 @@ export class SessionService { } // Cold-start resume must restore snapshot or fail. - await this.restoreSessionSnapshot(session, sessionId, kilocodeToken, env, userId); + if (!metadata.kiloSessionId) { + throw new Error( + `Session ${sessionId} has no kiloSessionId in metadata. Cannot restore snapshot.` + ); + } + await this.restoreSessionSnapshot(session, sessionId, metadata.kiloSessionId, env, userId); await restoreWorkspace(session, context.workspacePath, context.branchName, { githubRepo: metadata.githubRepo, @@ -1294,6 +1314,9 @@ export class SessionService { if (metadata.mcpServers && Object.keys(metadata.mcpServers).length > 0) { await writeMCPSettings(sandbox, context.sessionHome, metadata.mcpServers); } + + // Re-write auth file (fresh clone) + await writeAuthFile(sandbox, context.sessionHome, kilocodeToken); } /** diff --git a/cloud-agent-next/wrangler.jsonc b/cloud-agent-next/wrangler.jsonc index 28748b0b9..04327fced 100644 --- a/cloud-agent-next/wrangler.jsonc +++ b/cloud-agent-next/wrangler.jsonc @@ -118,7 +118,7 @@ "image": "./Dockerfile", "instance_type": "standard-4", "image_vars": { - "KILOCODE_CLI_VERSION": "1.0.16", + "KILOCODE_CLI_VERSION": "1.0.22", }, "max_instances": 20, "rollout_active_grace_period": 120, diff --git a/cloudflare-app-builder/start-dev.sh b/cloudflare-app-builder/start-dev.sh index ed2ad78d9..273e8c66d 100755 --- a/cloudflare-app-builder/start-dev.sh +++ b/cloudflare-app-builder/start-dev.sh @@ -5,7 +5,10 @@ # # Services started: # - cloudflare-db-proxy (port 8792) +# - cloudflare-session-ingest (port 8787) # - cloud-agent (port 8788) +# - cloud-agent-next (port 8794) +# - cloudflare-git-token-service (port 8795) # - cloudflare-app-builder (port 8790) # - ngrok (forwarding to port 8790) # @@ -75,13 +78,16 @@ fi # Create new tmux session with first window for db-proxy tmux new-session -d -s "$SESSION_NAME" -n "services" -c "$PROJECT_ROOT" -# Split into 2x2 grid +# Split into grid for 7 services # First split horizontally (top/bottom) tmux split-window -v -t "$SESSION_NAME:services" -c "$PROJECT_ROOT" -# Split top pane vertically (left/right) +# Split top pane vertically into 3 tmux split-window -h -t "$SESSION_NAME:services.0" -c "$PROJECT_ROOT" -# Split bottom pane vertically (left/right) -tmux split-window -h -t "$SESSION_NAME:services.2" -c "$PROJECT_ROOT" +tmux split-window -h -t "$SESSION_NAME:services.0" -c "$PROJECT_ROOT" +# Split bottom pane vertically into 4 +tmux split-window -h -t "$SESSION_NAME:services.3" -c "$PROJECT_ROOT" +tmux split-window -h -t "$SESSION_NAME:services.3" -c "$PROJECT_ROOT" +tmux split-window -h -t "$SESSION_NAME:services.3" -c "$PROJECT_ROOT" # Arrange panes in a tiled layout tmux select-layout -t "$SESSION_NAME:services" tiled @@ -96,30 +102,45 @@ tmux select-pane -t "$SESSION_NAME:services.0" -T "db-proxy (8792)" # Using different inspector ports to avoid conflicts (default is 9229) tmux send-keys -t "$SESSION_NAME:services.0" "cd $PROJECT_ROOT/cloudflare-db-proxy && echo '🗄️ Starting cloudflare-db-proxy (port 8792)...' && pnpm exec wrangler dev --inspector-port 9230" C-m -# Pane 1 (top-right): cloud-agent -tmux select-pane -t "$SESSION_NAME:services.1" -T "cloud-agent (8788)" -tmux send-keys -t "$SESSION_NAME:services.1" "cd $PROJECT_ROOT/cloud-agent && echo '🤖 Starting cloud-agent (port 8788)...' && pnpm exec wrangler dev --inspector-port 9231" C-m +# Pane 1 (top-middle): cloudflare-session-ingest +tmux select-pane -t "$SESSION_NAME:services.1" -T "session-ingest (8787)" +tmux send-keys -t "$SESSION_NAME:services.1" "cd $PROJECT_ROOT/cloudflare-session-ingest && echo '📥 Starting cloudflare-session-ingest (port 8787)...' && pnpm exec wrangler dev --inspector-port 9233" C-m + +# Pane 2 (top-right): cloud-agent +tmux select-pane -t "$SESSION_NAME:services.2" -T "cloud-agent (8788)" +tmux send-keys -t "$SESSION_NAME:services.2" "cd $PROJECT_ROOT/cloud-agent && echo '🤖 Starting cloud-agent (port 8788)...' && pnpm exec wrangler dev --inspector-port 9231" C-m + +# Pane 3 (bottom-left): cloudflare-git-token-service +tmux select-pane -t "$SESSION_NAME:services.3" -T "git-token-service (8795)" +tmux send-keys -t "$SESSION_NAME:services.3" "cd $PROJECT_ROOT/cloudflare-git-token-service && echo '🔑 Starting cloudflare-git-token-service (port 8795)...' && pnpm exec wrangler dev --inspector-port 9235" C-m + +# Pane 4: cloudflare-app-builder +tmux select-pane -t "$SESSION_NAME:services.4" -T "app-builder (8790)" +tmux send-keys -t "$SESSION_NAME:services.4" "cd $PROJECT_ROOT/cloudflare-app-builder && echo '🏗️ Starting cloudflare-app-builder (port 8790)...' && pnpm exec wrangler dev --inspector-port 9232" C-m -# Pane 2 (bottom-left): cloudflare-app-builder -tmux select-pane -t "$SESSION_NAME:services.2" -T "app-builder (8790)" -tmux send-keys -t "$SESSION_NAME:services.2" "cd $PROJECT_ROOT/cloudflare-app-builder && echo '🏗️ Starting cloudflare-app-builder (port 8790)...' && pnpm exec wrangler dev --inspector-port 9232" C-m +# Pane 5: ngrok +tmux select-pane -t "$SESSION_NAME:services.5" -T "ngrok → 8790" +tmux send-keys -t "$SESSION_NAME:services.5" "echo '🌐 Starting ngrok (forwarding to port 8790)...' && ngrok http 8790" C-m -# Pane 3 (bottom-right): ngrok -tmux select-pane -t "$SESSION_NAME:services.3" -T "ngrok → 8790" -tmux send-keys -t "$SESSION_NAME:services.3" "echo '🌐 Starting ngrok (forwarding to port 8790)...' && ngrok http 8790" C-m +# Pane 6 (bottom-right): cloud-agent-next +tmux select-pane -t "$SESSION_NAME:services.6" -T "cloud-agent-next (8794)" +tmux send-keys -t "$SESSION_NAME:services.6" "cd $PROJECT_ROOT/cloud-agent-next && echo '☁️ Starting cloud-agent-next (port 8794)...' && pnpm exec wrangler dev --env dev --inspector-port 9234" C-m # Select the app-builder pane by default -tmux select-pane -t "$SESSION_NAME:services.2" +tmux select-pane -t "$SESSION_NAME:services.5" echo "" echo "╔══════════════════════════════════════════════════════════════════╗" echo "║ App Builder Dev Environment Started! 🚀 ║" echo "╠══════════════════════════════════════════════════════════════════╣" echo "║ Services: ║" -echo "║ • cloudflare-db-proxy → http://localhost:8792 ║" -echo "║ • cloud-agent → http://localhost:8788 ║" -echo "║ • cloudflare-app-builder → http://localhost:8790 ║" -echo "║ • ngrok → forwarding to :8790 ║" +echo "║ • cloudflare-db-proxy → http://localhost:8792 ║" +echo "║ • cloudflare-session-ingest → http://localhost:8787 ║" +echo "║ • cloud-agent → http://localhost:8788 ║" +echo "║ • cloud-agent-next → http://localhost:8794 ║" +echo "║ • git-token-service → http://localhost:8795 ║" +echo "║ • cloudflare-app-builder → http://localhost:8790 ║" +echo "║ • ngrok → forwarding to :8790 ║" echo "╠══════════════════════════════════════════════════════════════════╣" echo "║ tmux Navigation: ║" echo "║ Switch panes: Ctrl+b then arrow keys ║" diff --git a/cloudflare-git-token-service/wrangler.jsonc b/cloudflare-git-token-service/wrangler.jsonc index 91e8a2bf1..b13504d99 100644 --- a/cloudflare-git-token-service/wrangler.jsonc +++ b/cloudflare-git-token-service/wrangler.jsonc @@ -25,7 +25,7 @@ }, ], "dev": { - "port": 8794, + "port": 8795, }, "env": { "dev": { diff --git a/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts b/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts index 602c6e55b..fe157b525 100644 --- a/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts +++ b/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts @@ -37,7 +37,7 @@ export const kiloJwtAuthMiddleware = createMiddleware<{ return c.json({ success: false, error: 'Missing token' }, 401); } - const secret = await c.env.NEXTAUTH_SECRET_PROD.get(); + const secret = c.env.DEV_NEXTAUTH_SECRET ?? (await c.env.NEXTAUTH_SECRET_PROD.get()); try { const payload = jwt.verify(token, secret, { algorithms: ['HS256'] }); diff --git a/cloudflare-session-ingest/src/routes/api.ts b/cloudflare-session-ingest/src/routes/api.ts index f2903320f..76caa711a 100644 --- a/cloudflare-session-ingest/src/routes/api.ts +++ b/cloudflare-session-ingest/src/routes/api.ts @@ -9,6 +9,7 @@ import { getSessionAccessCacheDO } from '../dos/SessionAccessCacheDO'; import { SessionSyncInputSchema } from '../types/session-sync'; import { withDORetry } from '../util/do-retry'; import { splitIngestBatchForDO } from '../util/ingest-batching'; +import { getSessionExport } from '../services/session-export'; export type ApiContext = { Bindings: Env; @@ -31,6 +32,7 @@ const ingestVersionSchema = z.coerce.number().int().nonnegative().catch(0); api.post('/session', zodJsonValidator(createSessionSchema), async c => { const body = c.req.valid('json'); + console.log('POST /api/session called', { sessionId: body.sessionId, userId: c.get('user_id') }); // Persist a placeholder session row. // This is intentionally minimal; we only need a working Hyperdrive -> Postgres path. @@ -64,6 +66,10 @@ api.post('/session', zodJsonValidator(createSessionSchema), async c => { api.delete('/session/:sessionId', async c => { const rawSessionId = c.req.param('sessionId'); + console.log('DELETE /api/session/:sessionId called', { + sessionId: rawSessionId, + userId: c.get('user_id'), + }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); @@ -158,6 +164,10 @@ api.delete('/session/:sessionId', async c => { api.post('/session/:sessionId/ingest', zodJsonValidator(ingestSessionSchema), async c => { const rawSessionId = c.req.param('sessionId'); + console.log('POST /api/session/:sessionId/ingest called', { + sessionId: rawSessionId, + userId: c.get('user_id'), + }); const sessionIdParseResult = sessionIdSchema.safeParse(rawSessionId); if (!sessionIdParseResult.success) { return c.json( @@ -288,31 +298,22 @@ api.post('/session/:sessionId/ingest', zodJsonValidator(ingestSessionSchema), as api.get('/session/:sessionId/export', async c => { const rawSessionId = c.req.param('sessionId'); + console.log('GET /api/session/:sessionId/export called', { + sessionId: rawSessionId, + userId: c.get('user_id'), + }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); } - const db = getDb(c.env.HYPERDRIVE); const kiloUserId = c.get('user_id'); + const json = await getSessionExport(c.env, parsed.data, kiloUserId); - const session = await db - .selectFrom('cli_sessions_v2') - .select(['session_id']) - .where('session_id', '=', parsed.data) - .where('kilo_user_id', '=', kiloUserId) - .executeTakeFirst(); - - if (!session) { + if (json === null) { return c.json({ success: false, error: 'session_not_found' }, 404); } - const json = await withDORetry( - () => getSessionIngestDO(c.env, { kiloUserId, sessionId: parsed.data }), - stub => stub.getAll(), - 'SessionIngestDO.getAll' - ); - return c.body(json, 200, { 'content-type': 'application/json; charset=utf-8', }); @@ -320,6 +321,10 @@ api.get('/session/:sessionId/export', async c => { api.post('/session/:sessionId/share', async c => { const rawSessionId = c.req.param('sessionId'); + console.log('POST /api/session/:sessionId/share called', { + sessionId: rawSessionId, + userId: c.get('user_id'), + }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); @@ -372,6 +377,10 @@ api.post('/session/:sessionId/share', async c => { api.post('/session/:sessionId/unshare', async c => { const rawSessionId = c.req.param('sessionId'); + console.log('POST /api/session/:sessionId/unshare called', { + sessionId: rawSessionId, + userId: c.get('user_id'), + }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); diff --git a/cloudflare-session-ingest/src/services/session-export.ts b/cloudflare-session-ingest/src/services/session-export.ts new file mode 100644 index 000000000..a6f5a7432 --- /dev/null +++ b/cloudflare-session-ingest/src/services/session-export.ts @@ -0,0 +1,38 @@ +import type { Env } from '../env'; +import { getDb } from '../db/kysely'; +import { getSessionIngestDO } from '../dos/SessionIngestDO'; +import { withDORetry } from '../util/do-retry'; + +/** + * Fetch the full session export payload from the SessionIngestDO. + * + * Verifies that the session exists in `cli_sessions_v2` and belongs to the + * given user before reading the DO. + * + * @returns The raw JSON string from `SessionIngestDO.getAll()`, or `null` + * if the session does not exist or does not belong to the user. + */ +export async function getSessionExport( + env: Env, + sessionId: string, + kiloUserId: string +): Promise { + const db = getDb(env.HYPERDRIVE); + + const session = await db + .selectFrom('cli_sessions_v2') + .select(['session_id']) + .where('session_id', '=', sessionId) + .where('kilo_user_id', '=', kiloUserId) + .executeTakeFirst(); + + if (!session) { + return null; + } + + return withDORetry( + () => getSessionIngestDO(env, { kiloUserId, sessionId }), + stub => stub.getAll(), + 'SessionIngestDO.getAll' + ); +} diff --git a/cloudflare-session-ingest/src/session-ingest-rpc.ts b/cloudflare-session-ingest/src/session-ingest-rpc.ts index f97f6ec9b..ba8530a43 100644 --- a/cloudflare-session-ingest/src/session-ingest-rpc.ts +++ b/cloudflare-session-ingest/src/session-ingest-rpc.ts @@ -5,10 +5,29 @@ import { getDb } from './db/kysely'; import { getSessionIngestDO } from './dos/SessionIngestDO'; import { getSessionAccessCacheDO } from './dos/SessionAccessCacheDO'; import { withDORetry } from './util/do-retry'; +import { getSessionExport } from './services/session-export'; const sessionIdSchema = z.string().startsWith('ses_').length(30); export class SessionIngestRPC extends WorkerEntrypoint { + /** + * RPC method: export session data from the SessionIngestDO. + * Called via service binding from cloud-agent-next during session restore. + * + * Returns the raw JSON string from the DO, or null if the session + * does not exist or does not belong to the given user. + */ + async exportSession(params: { sessionId: string; kiloUserId: string }): Promise { + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + }) + .parse(params); + + return getSessionExport(this.env, parsed.sessionId, parsed.kiloUserId); + } + /** * RPC method: create a cli_sessions_v2 record for a cloud-agent-next session. * Called via service binding from cloud-agent-next during session preparation. @@ -23,52 +42,69 @@ export class SessionIngestRPC extends WorkerEntrypoint { organizationId?: string; createdOnPlatform: string; }): Promise { - const parsed = z - .object({ - sessionId: sessionIdSchema, - kiloUserId: z.string().min(1), - cloudAgentSessionId: z.string().min(1), - organizationId: z.string().optional(), - createdOnPlatform: z.string().min(1), - }) - .parse(params); + console.log('SessionIngestRPC.createSessionForCloudAgent called', { + sessionId: params.sessionId, + kiloUserId: params.kiloUserId, + cloudAgentSessionId: params.cloudAgentSessionId, + organizationId: params.organizationId, + createdOnPlatform: params.createdOnPlatform, + envKeys: Object.keys(this.env), + hasHyperdrive: !!this.env.HYPERDRIVE, + }); - const db = getDb(this.env.HYPERDRIVE); + try { + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + cloudAgentSessionId: z.string().min(1), + organizationId: z.string().optional(), + createdOnPlatform: z.string().min(1), + }) + .parse(params); - await db - .insertInto('cli_sessions_v2') - .values({ - session_id: parsed.sessionId, - kilo_user_id: parsed.kiloUserId, - cloud_agent_session_id: parsed.cloudAgentSessionId, - organization_id: parsed.organizationId ?? null, - created_on_platform: parsed.createdOnPlatform, - version: 0, - }) - .onConflict(oc => - oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ + const db = getDb(this.env.HYPERDRIVE); + + await db + .insertInto('cli_sessions_v2') + .values({ + session_id: parsed.sessionId, + kilo_user_id: parsed.kiloUserId, cloud_agent_session_id: parsed.cloudAgentSessionId, - ...(parsed.organizationId !== undefined - ? { organization_id: parsed.organizationId } - : {}), + organization_id: parsed.organizationId ?? null, + created_on_platform: parsed.createdOnPlatform, + version: 0, }) - ) - .execute(); + .onConflict(oc => + oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ + cloud_agent_session_id: parsed.cloudAgentSessionId, + ...(parsed.organizationId !== undefined + ? { organization_id: parsed.organizationId } + : {}), + }) + ) + .execute(); - // Warm the session cache so subsequent ingests can skip Postgres. - // Best-effort: cache miss is acceptable; don't fail the create if the DO is unavailable. - try { - await withDORetry( - () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), - sessionCache => sessionCache.add(parsed.sessionId), - 'SessionAccessCacheDO.add' - ); + // Warm the session cache so subsequent ingests can skip Postgres. + // Best-effort: cache miss is acceptable; don't fail the create if the DO is unavailable. + try { + await withDORetry( + () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), + sessionCache => sessionCache.add(parsed.sessionId), + 'SessionAccessCacheDO.add' + ); + } catch (cacheError) { + console.error('Failed to warm session cache after create (non-fatal)', { + sessionId: parsed.sessionId, + kiloUserId: parsed.kiloUserId, + error: cacheError instanceof Error ? cacheError.message : String(cacheError), + }); + } } catch (error) { - console.error('Failed to warm session cache after create (non-fatal)', { - sessionId: parsed.sessionId, - kiloUserId: parsed.kiloUserId, - error: error instanceof Error ? error.message : String(error), + console.error('createSessionForCloudAgent FAILED', { + error: error instanceof Error ? (error.stack ?? error.message) : String(error), }); + throw error; } } @@ -82,6 +118,11 @@ export class SessionIngestRPC extends WorkerEntrypoint { sessionId: string; kiloUserId: string; }): Promise { + console.log('SessionIngestRPC.deleteSessionForCloudAgent called', { + sessionId: params.sessionId, + kiloUserId: params.kiloUserId, + }); + const parsed = z .object({ sessionId: sessionIdSchema, diff --git a/cloudflare-session-ingest/worker-configuration.d.ts b/cloudflare-session-ingest/worker-configuration.d.ts index 193a40fda..cbf06a44c 100644 --- a/cloudflare-session-ingest/worker-configuration.d.ts +++ b/cloudflare-session-ingest/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 72efddf1a52aac7a8d27176fc17bb8a1) +// Generated by Wrangler by running `wrangler types --env-interface CloudflareBindings` (hash: c6dc1b535aa94a0ce0faca5ffba8a22a) // Runtime types generated with workerd@1.20260128.0 2026-01-27 nodejs_compat declare namespace Cloudflare { interface GlobalProps { @@ -9,12 +9,19 @@ declare namespace Cloudflare { interface Env { HYPERDRIVE: Hyperdrive; NEXTAUTH_SECRET_PROD: SecretsStoreSecret; + DEV_NEXTAUTH_SECRET: string; SESSION_INGEST_DO: DurableObjectNamespace; SESSION_ACCESS_CACHE_DO: DurableObjectNamespace; O11Y: Fetcher /* o11y */; } } -interface Env extends Cloudflare.Env {} +interface CloudflareBindings extends Cloudflare.Env {} +type StringifyValues> = { + [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; +}; +declare namespace NodeJS { + interface ProcessEnv extends StringifyValues> {} +} // Begin runtime types /*! ***************************************************************************** @@ -9277,7 +9284,7 @@ type AIGatewayHeaders = { [key: string]: string | number | boolean | object; }; type AIGatewayUniversalRequest = { - provider: AIGatewayProviders | string; // eslint-disable-line + provider: AIGatewayProviders | string; endpoint: string; headers: Partial; query: unknown; @@ -9294,7 +9301,7 @@ declare abstract class AiGateway { extraHeaders?: object; } ): Promise; - getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line + getUrl(provider?: AIGatewayProviders | string): Promise; } interface AutoRAGInternalError extends Error {} interface AutoRAGNotFoundError extends Error {} diff --git a/cloudflare-session-ingest/wrangler.jsonc b/cloudflare-session-ingest/wrangler.jsonc index 2acb6560f..e67445556 100644 --- a/cloudflare-session-ingest/wrangler.jsonc +++ b/cloudflare-session-ingest/wrangler.jsonc @@ -5,6 +5,11 @@ "main": "src/index.ts", "compatibility_date": "2026-01-27", "compatibility_flags": ["nodejs_compat"], + "dev": { + "port": 8787, + "local_protocol": "http", + "ip": "0.0.0.0", + }, "placement": { "mode": "smart", }, From 5228ca8dba99d31f4a340c2731ccd12427b3e06a Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 22:06:41 +0100 Subject: [PATCH 7/8] Remove debug logs and simplify session RPC --- cloudflare-session-ingest/src/routes/api.ts | 21 ---- .../src/session-ingest-rpc.ts | 102 +++++++----------- 2 files changed, 40 insertions(+), 83 deletions(-) diff --git a/cloudflare-session-ingest/src/routes/api.ts b/cloudflare-session-ingest/src/routes/api.ts index 76caa711a..e5240569e 100644 --- a/cloudflare-session-ingest/src/routes/api.ts +++ b/cloudflare-session-ingest/src/routes/api.ts @@ -32,7 +32,6 @@ const ingestVersionSchema = z.coerce.number().int().nonnegative().catch(0); api.post('/session', zodJsonValidator(createSessionSchema), async c => { const body = c.req.valid('json'); - console.log('POST /api/session called', { sessionId: body.sessionId, userId: c.get('user_id') }); // Persist a placeholder session row. // This is intentionally minimal; we only need a working Hyperdrive -> Postgres path. @@ -66,10 +65,6 @@ api.post('/session', zodJsonValidator(createSessionSchema), async c => { api.delete('/session/:sessionId', async c => { const rawSessionId = c.req.param('sessionId'); - console.log('DELETE /api/session/:sessionId called', { - sessionId: rawSessionId, - userId: c.get('user_id'), - }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); @@ -164,10 +159,6 @@ api.delete('/session/:sessionId', async c => { api.post('/session/:sessionId/ingest', zodJsonValidator(ingestSessionSchema), async c => { const rawSessionId = c.req.param('sessionId'); - console.log('POST /api/session/:sessionId/ingest called', { - sessionId: rawSessionId, - userId: c.get('user_id'), - }); const sessionIdParseResult = sessionIdSchema.safeParse(rawSessionId); if (!sessionIdParseResult.success) { return c.json( @@ -298,10 +289,6 @@ api.post('/session/:sessionId/ingest', zodJsonValidator(ingestSessionSchema), as api.get('/session/:sessionId/export', async c => { const rawSessionId = c.req.param('sessionId'); - console.log('GET /api/session/:sessionId/export called', { - sessionId: rawSessionId, - userId: c.get('user_id'), - }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); @@ -321,10 +308,6 @@ api.get('/session/:sessionId/export', async c => { api.post('/session/:sessionId/share', async c => { const rawSessionId = c.req.param('sessionId'); - console.log('POST /api/session/:sessionId/share called', { - sessionId: rawSessionId, - userId: c.get('user_id'), - }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); @@ -377,10 +360,6 @@ api.post('/session/:sessionId/share', async c => { api.post('/session/:sessionId/unshare', async c => { const rawSessionId = c.req.param('sessionId'); - console.log('POST /api/session/:sessionId/unshare called', { - sessionId: rawSessionId, - userId: c.get('user_id'), - }); const parsed = sessionIdSchema.safeParse(rawSessionId); if (!parsed.success) { return c.json({ success: false, error: 'Invalid sessionId', issues: parsed.error.issues }, 400); diff --git a/cloudflare-session-ingest/src/session-ingest-rpc.ts b/cloudflare-session-ingest/src/session-ingest-rpc.ts index ba8530a43..030deb65b 100644 --- a/cloudflare-session-ingest/src/session-ingest-rpc.ts +++ b/cloudflare-session-ingest/src/session-ingest-rpc.ts @@ -42,69 +42,52 @@ export class SessionIngestRPC extends WorkerEntrypoint { organizationId?: string; createdOnPlatform: string; }): Promise { - console.log('SessionIngestRPC.createSessionForCloudAgent called', { - sessionId: params.sessionId, - kiloUserId: params.kiloUserId, - cloudAgentSessionId: params.cloudAgentSessionId, - organizationId: params.organizationId, - createdOnPlatform: params.createdOnPlatform, - envKeys: Object.keys(this.env), - hasHyperdrive: !!this.env.HYPERDRIVE, - }); - - try { - const parsed = z - .object({ - sessionId: sessionIdSchema, - kiloUserId: z.string().min(1), - cloudAgentSessionId: z.string().min(1), - organizationId: z.string().optional(), - createdOnPlatform: z.string().min(1), - }) - .parse(params); + const parsed = z + .object({ + sessionId: sessionIdSchema, + kiloUserId: z.string().min(1), + cloudAgentSessionId: z.string().min(1), + organizationId: z.string().optional(), + createdOnPlatform: z.string().min(1), + }) + .parse(params); - const db = getDb(this.env.HYPERDRIVE); + const db = getDb(this.env.HYPERDRIVE); - await db - .insertInto('cli_sessions_v2') - .values({ - session_id: parsed.sessionId, - kilo_user_id: parsed.kiloUserId, + await db + .insertInto('cli_sessions_v2') + .values({ + session_id: parsed.sessionId, + kilo_user_id: parsed.kiloUserId, + cloud_agent_session_id: parsed.cloudAgentSessionId, + organization_id: parsed.organizationId ?? null, + created_on_platform: parsed.createdOnPlatform, + version: 0, + }) + .onConflict(oc => + oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ cloud_agent_session_id: parsed.cloudAgentSessionId, - organization_id: parsed.organizationId ?? null, - created_on_platform: parsed.createdOnPlatform, - version: 0, + ...(parsed.organizationId !== undefined + ? { organization_id: parsed.organizationId } + : {}), }) - .onConflict(oc => - oc.columns(['session_id', 'kilo_user_id']).doUpdateSet({ - cloud_agent_session_id: parsed.cloudAgentSessionId, - ...(parsed.organizationId !== undefined - ? { organization_id: parsed.organizationId } - : {}), - }) - ) - .execute(); + ) + .execute(); - // Warm the session cache so subsequent ingests can skip Postgres. - // Best-effort: cache miss is acceptable; don't fail the create if the DO is unavailable. - try { - await withDORetry( - () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), - sessionCache => sessionCache.add(parsed.sessionId), - 'SessionAccessCacheDO.add' - ); - } catch (cacheError) { - console.error('Failed to warm session cache after create (non-fatal)', { - sessionId: parsed.sessionId, - kiloUserId: parsed.kiloUserId, - error: cacheError instanceof Error ? cacheError.message : String(cacheError), - }); - } - } catch (error) { - console.error('createSessionForCloudAgent FAILED', { - error: error instanceof Error ? (error.stack ?? error.message) : String(error), + // Warm the session cache so subsequent ingests can skip Postgres. + // Best-effort: cache miss is acceptable; don't fail the create if the DO is unavailable. + try { + await withDORetry( + () => getSessionAccessCacheDO(this.env, { kiloUserId: parsed.kiloUserId }), + sessionCache => sessionCache.add(parsed.sessionId), + 'SessionAccessCacheDO.add' + ); + } catch (cacheError) { + console.error('Failed to warm session cache after create (non-fatal)', { + sessionId: parsed.sessionId, + kiloUserId: parsed.kiloUserId, + error: cacheError instanceof Error ? cacheError.message : String(cacheError), }); - throw error; } } @@ -118,11 +101,6 @@ export class SessionIngestRPC extends WorkerEntrypoint { sessionId: string; kiloUserId: string; }): Promise { - console.log('SessionIngestRPC.deleteSessionForCloudAgent called', { - sessionId: params.sessionId, - kiloUserId: params.kiloUserId, - }); - const parsed = z .object({ sessionId: sessionIdSchema, From 9b4d49ef4137bd4ce36adf544d30ca08ad336f91 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 17 Feb 2026 22:32:45 +0100 Subject: [PATCH 8/8] Update kilo-jwt-auth.ts --- .../src/middleware/kilo-jwt-auth.ts | 2 +- .../worker-configuration.d.ts | 15 ++++----------- cloudflare-session-ingest/wrangler.jsonc | 2 ++ 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts b/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts index fe157b525..602c6e55b 100644 --- a/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts +++ b/cloudflare-session-ingest/src/middleware/kilo-jwt-auth.ts @@ -37,7 +37,7 @@ export const kiloJwtAuthMiddleware = createMiddleware<{ return c.json({ success: false, error: 'Missing token' }, 401); } - const secret = c.env.DEV_NEXTAUTH_SECRET ?? (await c.env.NEXTAUTH_SECRET_PROD.get()); + const secret = await c.env.NEXTAUTH_SECRET_PROD.get(); try { const payload = jwt.verify(token, secret, { algorithms: ['HS256'] }); diff --git a/cloudflare-session-ingest/worker-configuration.d.ts b/cloudflare-session-ingest/worker-configuration.d.ts index cbf06a44c..193a40fda 100644 --- a/cloudflare-session-ingest/worker-configuration.d.ts +++ b/cloudflare-session-ingest/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-interface CloudflareBindings` (hash: c6dc1b535aa94a0ce0faca5ffba8a22a) +// Generated by Wrangler by running `wrangler types` (hash: 72efddf1a52aac7a8d27176fc17bb8a1) // Runtime types generated with workerd@1.20260128.0 2026-01-27 nodejs_compat declare namespace Cloudflare { interface GlobalProps { @@ -9,19 +9,12 @@ declare namespace Cloudflare { interface Env { HYPERDRIVE: Hyperdrive; NEXTAUTH_SECRET_PROD: SecretsStoreSecret; - DEV_NEXTAUTH_SECRET: string; SESSION_INGEST_DO: DurableObjectNamespace; SESSION_ACCESS_CACHE_DO: DurableObjectNamespace; O11Y: Fetcher /* o11y */; } } -interface CloudflareBindings extends Cloudflare.Env {} -type StringifyValues> = { - [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; -}; -declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} -} +interface Env extends Cloudflare.Env {} // Begin runtime types /*! ***************************************************************************** @@ -9284,7 +9277,7 @@ type AIGatewayHeaders = { [key: string]: string | number | boolean | object; }; type AIGatewayUniversalRequest = { - provider: AIGatewayProviders | string; + provider: AIGatewayProviders | string; // eslint-disable-line endpoint: string; headers: Partial; query: unknown; @@ -9301,7 +9294,7 @@ declare abstract class AiGateway { extraHeaders?: object; } ): Promise; - getUrl(provider?: AIGatewayProviders | string): Promise; + getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line } interface AutoRAGInternalError extends Error {} interface AutoRAGNotFoundError extends Error {} diff --git a/cloudflare-session-ingest/wrangler.jsonc b/cloudflare-session-ingest/wrangler.jsonc index e67445556..f7a77d93a 100644 --- a/cloudflare-session-ingest/wrangler.jsonc +++ b/cloudflare-session-ingest/wrangler.jsonc @@ -58,6 +58,8 @@ "binding": "NEXTAUTH_SECRET_PROD", "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", "secret_name": "NEXTAUTH_SECRET_PROD", + // To set: wrangler secrets-store secret create 342a86d9e3a94da698e82d0c6e2a36f0 --name NEXTAUTH_SECRET_PROD --scopes workers + // docs https://developers.cloudflare.com/workers/wrangler/commands/#secrets-store-secret }, ], }