diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 16:36:10 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 16:36:10 +0900 |
| commit | 6689eb51b467d8e370f31495840d88661f978168 (patch) | |
| tree | d4234bfde98754dec48d2eff8594461780e609d4 /packages/transport-http/src | |
| parent | b3d270803f95db2467e20bb742aa42faf6867f91 (diff) | |
| download | dispatch-6689eb51b467d8e370f31495840d88661f978168.tar.gz dispatch-6689eb51b467d8e370f31495840d88661f978168.zip | |
feat(cache-warming): lifecycle CR-4 — default-off, fresh nextWarmAt, conversation close (+CR-1 table, CR-2 scope)
CR-4a: warming defaults OFF (opt-in per conversation); re-enabling restores
the persisted interval.
CR-4b: re-arm BEFORE surface notify so post-warm updates carry the FUTURE
nextWarmAt; turnSettled/turnStarted now also push (fresh schedule after seal,
null while generating).
CR-4c: POST /conversations/:id/close — per-turn AbortController wired to the
kernel runTurn signal (partial persist + normal seal, done.reason "aborted"),
new conversationClosed hook, cache-warming disables sync + persists OFF.
Disconnect/chat.unsubscribe semantics unchanged.
CR-4d: no change needed — initial surface echo already at HEAD (stale up2 boot
on the FE probe).
CR-1: loaded-extensions emits a single custom rendererId:"table" field
(TablePayload exported; Name|Version|Trust|Activation, all trust tiers).
CR-2: SurfaceCatalogEntry.scope?: "global"|"conversation" on both surfaces.
Contracts: ui-contract 0.1.0→0.2.0, transport-contract 0.8.0→0.9.0 (additive).
907 tests pass (+13); live-verified against bin/up (warms @5s with future
nextWarmAt; mid-turn close → abortedTurn:true + done.reason aborted).
Courier: frontend-cache-warming-lifecycle-handoff.md.
Diffstat (limited to 'packages/transport-http/src')
| -rw-r--r-- | packages/transport-http/src/app.test.ts | 46 | ||||
| -rw-r--r-- | packages/transport-http/src/app.ts | 9 | ||||
| -rw-r--r-- | packages/transport-http/src/extension.ts | 1 | ||||
| -rw-r--r-- | packages/transport-http/src/server.bun.test.ts | 3 |
4 files changed, 59 insertions, 0 deletions
diff --git a/packages/transport-http/src/app.test.ts b/packages/transport-http/src/app.test.ts index 32e9689..c26a868 100644 --- a/packages/transport-http/src/app.test.ts +++ b/packages/transport-http/src/app.test.ts @@ -115,6 +115,9 @@ function createFakeOrchestrator(events: AgentEvent[]): SessionOrchestrator { isActive() { return false; }, + closeConversation() { + return { abortedTurn: false }; + }, async handleMessage(input) { for (const event of events) { input.onEvent(event); @@ -142,6 +145,9 @@ function createCapturingOrchestrator(): SessionOrchestrator & { isActive() { return false; }, + closeConversation() { + return { abortedTurn: false }; + }, async handleMessage(input) { state.received = input; }, @@ -159,6 +165,9 @@ function createThrowingOrchestrator(error: Error): SessionOrchestrator { isActive() { return false; }, + closeConversation() { + return { abortedTurn: false }; + }, async handleMessage() { throw error; }, @@ -1088,6 +1097,43 @@ describe("throughput recording + GET /metrics/throughput", () => { }); }); +describe("POST /conversations/:id/close", () => { + it("closes via the orchestrator and returns CloseConversationResponse", async () => { + const closeCalls: string[] = []; + const orchestrator: SessionOrchestrator = { + ...createFakeOrchestrator([]), + closeConversation(conversationId) { + closeCalls.push(conversationId); + return { abortedTurn: true }; + }, + }; + const app = createApp({ + conversationStore: createFakeConversationStore(), + orchestrator, + credentialStore: createFakeCredentialStore([]), + logger: noopLogger, + }); + + const res = await app.request("/conversations/conv-9/close", { method: "POST" }); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ conversationId: "conv-9", abortedTurn: true }); + expect(closeCalls).toEqual(["conv-9"]); + }); + + it("reports abortedTurn false for an idle conversation", async () => { + const app = createApp({ + conversationStore: createFakeConversationStore(), + orchestrator: createFakeOrchestrator([]), + credentialStore: createFakeCredentialStore([]), + logger: noopLogger, + }); + + const res = await app.request("/conversations/conv-idle/close", { method: "POST" }); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ conversationId: "conv-idle", abortedTurn: false }); + }); +}); + describe("GET /conversations/:id/cwd", () => { it("returns null when unset", async () => { const app = createApp({ diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts index 7778bad..11d2850 100644 --- a/packages/transport-http/src/app.ts +++ b/packages/transport-http/src/app.ts @@ -1,5 +1,6 @@ import type { AgentEvent, Logger } from "@dispatch/kernel"; import type { + CloseConversationResponse, ConversationHistoryResponse, ConversationMetricsResponse, CwdResponse, @@ -319,6 +320,14 @@ export function createApp(opts: CreateServerOptions): Hono { } }); + app.post("/conversations/:id/close", (c) => { + const conversationId = c.req.param("id"); + const { abortedTurn } = opts.orchestrator.closeConversation(conversationId); + log.info("conversations: closed", { conversationId, abortedTurn }); + const body: CloseConversationResponse = { conversationId, abortedTurn }; + return c.json(body, 200); + }); + app.get("/conversations/:id/cwd", async (c) => { const conversationId = c.req.param("id"); try { diff --git a/packages/transport-http/src/extension.ts b/packages/transport-http/src/extension.ts index 6c988a5..33b9990 100644 --- a/packages/transport-http/src/extension.ts +++ b/packages/transport-http/src/extension.ts @@ -28,6 +28,7 @@ export const manifest: Manifest = { "/chat", "/chat/warm", "/conversations/:id", + "/conversations/:id/close", "/conversations/:id/cwd", "/conversations/:id/lsp", "/health", diff --git a/packages/transport-http/src/server.bun.test.ts b/packages/transport-http/src/server.bun.test.ts index 8a719c0..a465243 100644 --- a/packages/transport-http/src/server.bun.test.ts +++ b/packages/transport-http/src/server.bun.test.ts @@ -64,6 +64,9 @@ function fakeOrchestrator(): SessionOrchestrator { isActive() { return false; }, + closeConversation() { + return { abortedTurn: false }; + }, async handleMessage() {}, }; } |
