summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-http/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 16:36:10 +0900
committerAdam Malczewski <[email protected]>2026-06-12 16:36:10 +0900
commit6689eb51b467d8e370f31495840d88661f978168 (patch)
treed4234bfde98754dec48d2eff8594461780e609d4 /packages/transport-http/src
parentb3d270803f95db2467e20bb742aa42faf6867f91 (diff)
downloaddispatch-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.ts46
-rw-r--r--packages/transport-http/src/app.ts9
-rw-r--r--packages/transport-http/src/extension.ts1
-rw-r--r--packages/transport-http/src/server.bun.test.ts3
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() {},
};
}