diff --git a/apps/web/src/domains/chat/api/messages.ts b/apps/web/src/domains/chat/api/messages.ts index cfdc108027a..436badc53fe 100644 --- a/apps/web/src/domains/chat/api/messages.ts +++ b/apps/web/src/domains/chat/api/messages.ts @@ -486,10 +486,10 @@ export async function postChatMessage( attachmentIds: string[] = [], onboarding?: PreChatOnboardingContext, ): Promise { - // Daemon 0.8.5+ accepts `conversationId` on this endpoint as a direct - // internal-id lookup; older daemons only understand the legacy - // `conversationKey` (external-key path). The gate that picks between - // them lives in `lib/backwards-compat/conversation-id-wire-field.ts`. + // The wire-field gate prefers `conversationId` on daemons that mint + // ids before the first client send, falling back to `conversationKey` + // (create-or-lookup) so locally-minted draft ids still resolve. See + // `lib/backwards-compat/conversation-id-wire-field.ts`. const body: Record = { [pickConversationIdWireField()]: conversationId, content, diff --git a/apps/web/src/domains/chat/api/post-chat-message.test.ts b/apps/web/src/domains/chat/api/post-chat-message.test.ts index 19f7a0bb352..e966e524966 100644 --- a/apps/web/src/domains/chat/api/post-chat-message.test.ts +++ b/apps/web/src/domains/chat/api/post-chat-message.test.ts @@ -188,7 +188,7 @@ describe("postChatMessage wire-field bilingual cutover", () => { // Verifies the daemon-version gate selects the right wire field on // `POST /v1/messages`. See `apps/web/src/assistant/version-compat.ts` // for the cutover policy (legacy `conversationKey` for daemons older - // than 0.8.5, canonical `conversationId` for 0.8.5+). + // than 0.8.6, canonical `conversationId` for 0.8.6+). let originalFetch: typeof fetch; let originalDocument: unknown; let capturedRequests: Array<{ url: string; body: string }> = []; @@ -231,8 +231,8 @@ describe("postChatMessage wire-field bilingual cutover", () => { return JSON.parse(requests[0]!.body) as Record; } - test("uses conversationId wire field when daemon version >= 0.8.5", async () => { - useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.5"); + test("uses conversationId wire field when daemon version >= 0.8.6", async () => { + useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.6"); await postChatMessage("asst-1", "conv-internal-1", "hi"); @@ -251,8 +251,8 @@ describe("postChatMessage wire-field bilingual cutover", () => { expect(body).not.toHaveProperty("conversationKey"); }); - test("uses conversationKey wire field for daemons older than 0.8.5", async () => { - useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.4"); + test("uses conversationKey wire field for daemons older than 0.8.6", async () => { + useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.5"); await postChatMessage("asst-1", "conv-internal-3", "hi"); diff --git a/apps/web/src/domains/chat/api/stream.test.ts b/apps/web/src/domains/chat/api/stream.test.ts index 5e8272975b6..6b0e962c9c4 100644 --- a/apps/web/src/domains/chat/api/stream.test.ts +++ b/apps/web/src/domains/chat/api/stream.test.ts @@ -359,8 +359,8 @@ describe("subscribeChatEvents idle watchdog", () => { } }); - test("uses conversationId query when daemon version >= 0.8.5", async () => { - useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.5"); + test("uses conversationId query when daemon version >= 0.8.6", async () => { + useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.6"); const requestedUrls: string[] = []; globalThis.fetch = mock( @@ -396,8 +396,8 @@ describe("subscribeChatEvents idle watchdog", () => { } }); - test("uses conversationKey query when daemon version is older than 0.8.5", async () => { - useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.4"); + test("uses conversationKey query when daemon version is older than 0.8.6", async () => { + useAssistantIdentityStore.getState().setIdentity("Vel", "0.8.5"); const requestedUrls: string[] = []; globalThis.fetch = mock( diff --git a/apps/web/src/domains/chat/api/stream.ts b/apps/web/src/domains/chat/api/stream.ts index 942677242f4..1f1661d3f40 100644 --- a/apps/web/src/domains/chat/api/stream.ts +++ b/apps/web/src/domains/chat/api/stream.ts @@ -310,10 +310,10 @@ export function subscribeChatEvents( dataFramesReceivedSinceConnect = 0; let streamError: Error | null = null; try { - // Daemon 0.8.5+ accepts `conversationId` on this endpoint as a - // direct internal-id lookup; older daemons only understand the - // legacy `conversationKey` (external-key path). The gate that - // picks between them lives in + // The wire-field gate prefers `conversationId` on daemons that + // mint ids before the first client send, falling back to + // `conversationKey` (create-or-lookup) so locally-minted draft + // ids still resolve. See // `lib/backwards-compat/conversation-id-wire-field.ts`. const { stream } = await client.sse.get | string>({ ...SDK_BASE_OPTIONS, diff --git a/apps/web/src/lib/backwards-compat/conversation-id-wire-field.test.ts b/apps/web/src/lib/backwards-compat/conversation-id-wire-field.test.ts index 70cd031fba6..a7616626e40 100644 --- a/apps/web/src/lib/backwards-compat/conversation-id-wire-field.test.ts +++ b/apps/web/src/lib/backwards-compat/conversation-id-wire-field.test.ts @@ -18,23 +18,23 @@ afterEach(() => { // Exhaustive truth-table for the underlying semver gate lives in // `utils.test.ts` (covers null/empty, unparseable, pre-release, `v` // prefix, etc). Here we verify the wire-field branch on each side -// of the 0.8.5 boundary plus the conservative-on-unknown policy. +// of the 0.8.6 boundary plus the conservative-on-unknown policy. describe("pickConversationIdWireField", () => { test("returns conversationKey when version is unknown", () => { setVersion(null); expect(pickConversationIdWireField()).toBe("conversationKey"); }); - test("returns conversationKey for assistants on 0.8.4 and older", () => { + test("returns conversationKey for assistants on 0.8.5 and older", () => { + setVersion("0.8.5"); + expect(pickConversationIdWireField()).toBe("conversationKey"); setVersion("0.8.4"); expect(pickConversationIdWireField()).toBe("conversationKey"); setVersion("0.7.0"); expect(pickConversationIdWireField()).toBe("conversationKey"); }); - test("returns conversationId for assistants on 0.8.5+", () => { - setVersion("0.8.5"); - expect(pickConversationIdWireField()).toBe("conversationId"); + test("returns conversationId for assistants on 0.8.6+", () => { setVersion("0.8.6"); expect(pickConversationIdWireField()).toBe("conversationId"); setVersion("0.9.0"); @@ -44,11 +44,11 @@ describe("pickConversationIdWireField", () => { }); test("treats RC builds of the cutover patch as supporting the new field", () => { - // 0.8.5-rc.1 ships with the same bilingual handlers as 0.8.5, - // so RC testers must get the new wire field. - setVersion("0.8.5-rc.1"); + // 0.8.6-rc.1 ships with the same handlers as 0.8.6, so RC + // testers must get the new wire field. + setVersion("0.8.6-rc.1"); expect(pickConversationIdWireField()).toBe("conversationId"); - setVersion("0.8.5-beta"); + setVersion("0.8.6-beta"); expect(pickConversationIdWireField()).toBe("conversationId"); }); diff --git a/apps/web/src/lib/backwards-compat/conversation-id-wire-field.ts b/apps/web/src/lib/backwards-compat/conversation-id-wire-field.ts index 5553135c108..3a9293bfceb 100644 --- a/apps/web/src/lib/backwards-compat/conversation-id-wire-field.ts +++ b/apps/web/src/lib/backwards-compat/conversation-id-wire-field.ts @@ -2,15 +2,17 @@ * Backwards-compat gate: conversation-id wire field on POST /v1/messages * and GET /v1/events. * - * Vellum Assistant 0.8.5 (PR #31922) made the daemon bilingual on - * conversation routing: `handleSendMessage` and - * `handleSubscribeAssistantEvents` accept either `conversationKey` - * (legacy external-key lookup; materializes a row on first use) or - * `conversationId` (direct internal-id lookup; 404 on miss). The web - * client always has the assistant-minted internal id, so we prefer the - * canonical `conversationId` field when the assistant supports it. - * - * Assistants on 0.8.4 or older only understand `conversationKey`. + * The daemon's `handleSendMessage` and `handleSubscribeAssistantEvents` + * accept either `conversationKey` (external-key lookup; materializes a + * row on first use) or `conversationId` (direct internal-id lookup; + * 404 on miss). Web mints draft conversation ids locally — see + * `createDraftConversationId()` in + * `domains/chat/utils/conversation-selection.ts` — and uses them as URL + * keys before the daemon has minted anything, so the strict-lookup + * `conversationId` path is unsafe for the first message of a new chat. + * The gate stays parked above the current daemon version until the + * draft-id flow moves to "daemon mints on first send, UI navigates on + * the response"; at that point this whole module is removable. * * NOTE: this helper reads the version snapshot via * `useAssistantIdentityStore.getState()` rather than the `use.version()` @@ -22,7 +24,7 @@ import { useAssistantIdentityStore } from "@/stores/assistant-identity-store.js"; import { compareParsed, parseSemver } from "@/utils/semver.js"; -const MIN_VERSION = "0.8.5"; +const MIN_VERSION = "0.8.6"; export type ConversationIdWireField = "conversationId" | "conversationKey"; @@ -35,7 +37,7 @@ export type ConversationIdWireField = "conversationId" | "conversationKey"; * ignore `conversationId`, so falling back to the legacy field is * strictly safer than guessing. * - Pre-release suffixes on the patch version are ignored: - * `0.8.5-rc.1` counts as `0.8.5`. Testers on RCs get the new path + * `0.8.6-rc.1` counts as `0.8.6`. Testers on RCs get the new path * the moment the patch version bumps. */ export function pickConversationIdWireField(): ConversationIdWireField {