summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-http/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-07-01 03:30:42 +0900
committerAdam Malczewski <[email protected]>2026-07-01 03:30:42 +0900
commit6bca0a8b65506239b0ce72d7f86ba96f825152b1 (patch)
tree7b6b3d0edeb2239acbaa605ba898abaf1711a12c /packages/transport-http/src
parent566c64033ad79538f9208fc3ef9477cd8a58f7da (diff)
parenta2e3bad36fd236835423e3e9124b8496b6d9c795 (diff)
downloaddispatch-6bca0a8b65506239b0ce72d7f86ba96f825152b1.tar.gz
dispatch-6bca0a8b65506239b0ce72d7f86ba96f825152b1.zip
Merge branch 'feature/summon-title' into predev
Diffstat (limited to 'packages/transport-http/src')
-rw-r--r--packages/transport-http/src/app.test.ts90
-rw-r--r--packages/transport-http/src/app.ts17
-rw-r--r--packages/transport-http/src/logic.ts6
3 files changed, 35 insertions, 78 deletions
diff --git a/packages/transport-http/src/app.test.ts b/packages/transport-http/src/app.test.ts
index 9b1480d..0876cdb 100644
--- a/packages/transport-http/src/app.test.ts
+++ b/packages/transport-http/src/app.test.ts
@@ -800,17 +800,11 @@ describe("POST /chat", () => {
expect(cap.received?.cwd).toBeUndefined();
});
- it("sets the conversation title from the request before the turn", async () => {
- const calls: { conversationId: string; title: string }[] = [];
- const store: ConversationStore = {
- ...createFakeConversationStore(),
- async setConversationTitle(conversationId, title) {
- calls.push({ conversationId, title });
- },
- };
+ it("forwards the title to the orchestrator", async () => {
+ const cap = createCapturingOrchestrator();
const app = createApp({
- conversationStore: store,
- orchestrator: createFakeOrchestrator([]),
+ conversationStore: createFakeConversationStore(),
+ orchestrator: cap,
credentialStore: createFakeCredentialStore([]),
});
@@ -821,20 +815,15 @@ describe("POST /chat", () => {
});
expect(res.status).toBe(200);
- expect(calls).toEqual([{ conversationId: "conv1", title: "My Task" }]);
+ expect(cap.received).toBeDefined();
+ expect(cap.received?.title).toBe("My Task");
});
- it("forwards a trimmed title to setConversationTitle", async () => {
- const calls: { conversationId: string; title: string }[] = [];
- const store: ConversationStore = {
- ...createFakeConversationStore(),
- async setConversationTitle(conversationId, title) {
- calls.push({ conversationId, title });
- },
- };
+ it("forwards a trimmed title to the orchestrator", async () => {
+ const cap = createCapturingOrchestrator();
const app = createApp({
- conversationStore: store,
- orchestrator: createFakeOrchestrator([]),
+ conversationStore: createFakeConversationStore(),
+ orchestrator: cap,
credentialStore: createFakeCredentialStore([]),
});
@@ -845,20 +834,14 @@ describe("POST /chat", () => {
});
expect(res.status).toBe(200);
- expect(calls).toEqual([{ conversationId: "conv1", title: "spaced" }]);
+ expect(cap.received?.title).toBe("spaced");
});
- it("does not call setConversationTitle when title is omitted", async () => {
- let setTitleCalled = false;
- const store: ConversationStore = {
- ...createFakeConversationStore(),
- async setConversationTitle() {
- setTitleCalled = true;
- },
- };
+ it("does not forward a title when omitted", async () => {
+ const cap = createCapturingOrchestrator();
const app = createApp({
- conversationStore: store,
- orchestrator: createFakeOrchestrator([]),
+ conversationStore: createFakeConversationStore(),
+ orchestrator: cap,
credentialStore: createFakeCredentialStore([]),
});
@@ -869,20 +852,14 @@ describe("POST /chat", () => {
});
expect(res.status).toBe(200);
- expect(setTitleCalled).toBe(false);
+ expect(cap.received?.title).toBeUndefined();
});
- it("does not call setConversationTitle for a whitespace-only title", async () => {
- let setTitleCalled = false;
- const store: ConversationStore = {
- ...createFakeConversationStore(),
- async setConversationTitle() {
- setTitleCalled = true;
- },
- };
+ it("does not forward a title for a whitespace-only title", async () => {
+ const cap = createCapturingOrchestrator();
const app = createApp({
- conversationStore: store,
- orchestrator: createFakeOrchestrator([]),
+ conversationStore: createFakeConversationStore(),
+ orchestrator: cap,
credentialStore: createFakeCredentialStore([]),
});
@@ -893,19 +870,12 @@ describe("POST /chat", () => {
});
expect(res.status).toBe(200);
- expect(setTitleCalled).toBe(false);
+ expect(cap.received?.title).toBeUndefined();
});
it("returns 400 when title is not a string", async () => {
- let setTitleCalled = false;
- const store: ConversationStore = {
- ...createFakeConversationStore(),
- async setConversationTitle() {
- setTitleCalled = true;
- },
- };
const app = createApp({
- conversationStore: store,
+ conversationStore: createFakeConversationStore(),
orchestrator: createFakeOrchestrator([]),
credentialStore: createFakeCredentialStore([]),
});
@@ -919,21 +889,19 @@ describe("POST /chat", () => {
expect(res.status).toBe(400);
const body = (await res.json()) as { error: string };
expect(body.error).toContain("title");
- expect(setTitleCalled).toBe(false);
});
- it("proceeds with the turn even if setConversationTitle throws", async () => {
+ it("does not call setConversationTitle itself (the orchestrator owns it)", async () => {
+ let setTitleCalled = false;
const store: ConversationStore = {
...createFakeConversationStore(),
async setConversationTitle() {
- throw new Error("store unavailable");
+ setTitleCalled = true;
},
};
const app = createApp({
conversationStore: store,
- orchestrator: createFakeOrchestrator([
- { type: "done", conversationId: "conv1", turnId: "t1", reason: "stop" },
- ]),
+ orchestrator: createFakeOrchestrator([]),
credentialStore: createFakeCredentialStore([]),
});
@@ -944,8 +912,10 @@ describe("POST /chat", () => {
});
expect(res.status).toBe(200);
- const text = await res.text();
- expect(text.trim().split("\n")).toHaveLength(1);
+ // The route must NOT pre-create the meta — that would bypass the
+ // orchestrator's new-conversation workspace/system-prompt init. The
+ // orchestrator sets the title after workspace setup instead.
+ expect(setTitleCalled).toBe(false);
});
});
diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts
index 03b6ec2..6e0748b 100644
--- a/packages/transport-http/src/app.ts
+++ b/packages/transport-http/src/app.ts
@@ -469,22 +469,6 @@ export function createApp(opts: CreateServerOptions): Hono {
imageCount: images?.length ?? 0,
});
- // Persist an explicit title BEFORE the turn starts so the tab shows it
- // immediately (and before `--open` signals the frontend to open it). The
- // store creates the conversation meta if none exists yet; a subsequent
- // append preserves a non-"Untitled" title. A title-set failure is logged
- // but never blocks the turn — the title is a nicety, the answer is not.
- if (title !== undefined) {
- try {
- await opts.conversationStore.setConversationTitle(conversationId, title);
- log.info("chat: title set", { conversationId });
- } catch (err) {
- log.warn("chat: title set failure", {
- error: err instanceof Error ? err.message : String(err),
- });
- }
- }
-
const events: AgentEvent[] = [];
let controllerRef: ReadableStreamDefaultController<Uint8Array> | undefined;
let streamClosed = false;
@@ -534,6 +518,7 @@ export function createApp(opts: CreateServerOptions): Hono {
...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
...(workspaceId !== undefined ? { workspaceId } : {}),
...(images !== undefined ? { images } : {}),
+ ...(title !== undefined ? { title } : {}),
};
opts.orchestrator
diff --git a/packages/transport-http/src/logic.ts b/packages/transport-http/src/logic.ts
index 5cf96cc..c703049 100644
--- a/packages/transport-http/src/logic.ts
+++ b/packages/transport-http/src/logic.ts
@@ -59,8 +59,10 @@ export interface ChatCommand {
* A human-readable title for the conversation tab, set at creation time.
* Parsed from the `ChatRequest.title` field; trimmed server-side. A
* whitespace-only value is treated as absent (omitted) so the auto-derived
- * title applies. Forwarded to the `/chat` route which persists it via the
- * conversation store's `setConversationTitle` before the turn starts.
+ * title applies. Forwarded to the orchestrator, which persists it via the
+ * conversation store's `setConversationTitle` AFTER the new-conversation
+ * workspace setup (so workspace assignment / first-turn system-prompt
+ * construction are not skipped) and before the first message append.
*/
readonly title?: string;
/**