v1.13.0: message_parts table + dual-write at every tool_calls/tool_results site

Adds a granular message_parts table (one row per text/tool_call/tool_result
chunk) without changing any read path. Old messages.content / tool_calls /
tool_results columns remain authoritative for v1.13.0; this dispatch is
write-only mirroring so the AI SDK migration in v1.13.1 can flip read
authority without a backfill window.

Schema:
  CREATE TABLE message_parts (id, message_id FK ON DELETE CASCADE,
    sequence int, kind text CHECK (text|tool_call|tool_result|reasoning|step_start),
    payload jsonb, created_at, UNIQUE (message_id, sequence))

New module services/inference/parts.ts with two pure derive helpers
(partsFromAssistantMessage, partsFromToolMessage) and insertParts that
fan-outs a multi-row INSERT via postgres-js.

Wired dual-write at every site that writes tool_calls or tool_results:
- tool-phase.ts: assistant finalize UPDATE, executed-tool UPDATE,
  ask_user_input sentinel UPDATE
- messages.ts answer flow: DELETE pending tool_result part + INSERT
  answered one inside the existing sql.begin
- skills.ts: synthetic assistant + tool INSERTs both inside existing tx
- chats.ts fork: CTE clones parts via ROW_NUMBER pairing (source→dest
  message id mapping in one statement, no N+1)
- error-handler.ts finalizeCompletion: text part for plain text-only
  assistant turns

Deviation: tool-phase.ts finalize UPDATEs and finalizeCompletion text-part
write are not wrapped in fresh sql.begin transactions. Safe in v1.13.0
because JSON columns are authoritative for reads. v1.13.1 must wrap these
sites before flipping read authority — TODO comments added at each
unwrapped site referencing v1.13.1.

Tests: 8 new unit tests for the derive helpers in
services/__tests__/parts.test.ts. Existing 162 tests untouched. 170 total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 05:46:29 +00:00
parent ca64bf9f0a
commit 1cb6eee24c
8 changed files with 290 additions and 0 deletions

View File

@@ -576,6 +576,15 @@ export function registerMessageRoutes(
SET tool_results = ${tx.json(newToolResults as never)}
WHERE id = ${toolRow.id}
`;
// v1.13.0: replace the pending tool_result part inserted at message
// creation (tool-phase.ts) with the answered one. Delete-then-insert
// is simpler than UPDATE because parts are append-style elsewhere;
// the UNIQUE (message_id, sequence) constraint blocks plain insert.
await tx`DELETE FROM message_parts WHERE message_id = ${toolRow.id} AND kind = 'tool_result'`;
await tx`
INSERT INTO message_parts (message_id, sequence, kind, payload)
VALUES (${toolRow.id}, 0, 'tool_result', ${tx.json(newToolResults as never)})
`;
const [assistantMsg] = await tx<{ id: string }[]>`
INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
VALUES (${sessionId}, ${chat.id}, 'assistant', '', 'streaming', clock_timestamp())