Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/web/src/domains/chat/api/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,10 +486,10 @@ export async function postChatMessage(
attachmentIds: string[] = [],
onboarding?: PreChatOnboardingContext,
): Promise<PostMessageResult> {
// 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<string, unknown> = {
[pickConversationIdWireField()]: conversationId,
content,
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/domains/chat/api/post-chat-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = [];
Expand Down Expand Up @@ -231,8 +231,8 @@ describe("postChatMessage wire-field bilingual cutover", () => {
return JSON.parse(requests[0]!.body) as Record<string, unknown>;
}

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");

Expand All @@ -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");

Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/domains/chat/api/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/domains/chat/api/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown> | string>({
...SDK_BASE_OPTIONS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
});

Expand Down
24 changes: 13 additions & 11 deletions apps/web/src/lib/backwards-compat/conversation-id-wire-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand All @@ -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";

Expand All @@ -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 {
Expand Down