B1: persistence primitives — reserveMessage + updateContent pipeline ops#32225
Conversation
…llocation Writes an empty placeholder row (content: "[]") at a known id so the agent loop can stamp streaming events with stable identity before any content is produced. Mirrors addMessage's retry shape but skips Qdrant indexing + attention projection — an empty placeholder is not meaningful for either. The caller later writes final content via updateMessageContent. No callers yet; this is B1 of the assistant-anchor backend workstream (persistence primitives, no consumers).
Extends the discriminated PersistArgs/PersistResult unions with two new ops: - reserve → reserveMessage (PR 1 of this workstream) - updateContent → updateMessageContent Both flow through the default-persistence pipeline terminal so plugin middleware can observe, redirect, or short-circuit them the same way they already can with add/update/delete. No production call sites use the new ops yet; the agent-loop migration to anchor pre-allocation lands in a follow-up PR. Updated the in-memory mock store in persistence-pipeline.test.ts to cover the new variants so the existing redirect test still type-checks.
Adds two focused tests against defaultPersistenceTerminal: - reserve pre-allocates a row with content "[]" and the supplied metadata - updateContent overwrites a previously reserved row in place and matches a direct updateMessageContent call Extends the existing redirect short-circuit test so its mock store exercises both new variants too.
…k.module sites Bun's `mock.module(...)` fully replaces a module's exports, so any test that mocks `memory/conversation-crud.js` and transitively loads the new persistence default plugin (which now imports `reserveMessage`) crashes at import-time with: SyntaxError: Export named 'reserveMessage' not found in module CI caught `background-workers-disk-pressure.test.ts` on this PR because its fixture chain loads the persistence plugin. The other 80 mock.module sites don't trigger today but will the moment any of them gain a transitive import that reaches the persistence pipeline — so we patch them all up-front, the same way PR #27459 taught us to handle `resolveQdrantUrl`. Stub shape mirrors the other primitives (`mock(async () => ({ id: ... }))`) so callers that `await reserveMessage(...)` see a Promise-shaped return.
|
Fixes the Root cause. Fix. Added a Discovered + patched via |
What
First PR in the backend-anchor workstream (B1 of B1→B5). Adds two persistence primitives —
reserveMessageand anupdateContentpipeline op — with no production callers. Future PRs will adopt them in the agent loop.Why
We're moving the agent loop toward pre-allocating the assistant message id at LLM-call start, so every SSE event (deltas, tool_use, tool_result, message_complete) can be stamped with a stable, server-minted
messageIdfrom the first byte. That lets us delete the frontend's content-match reconcile path entirely and avoid an optimistic→server id swap dance.To get there without a 1000-file PR, we ship the building blocks first.
What's in this PR
Memory layer (
assistant/src/memory/conversation-crud.ts)reserveMessage(conversationId, role, metadata?)— pre-allocates a row withcontent: "[]", returns the row. MirrorsaddMessage's SQLITE_BUSY/SQLITE_IOERR retry shape, but intentionally skips Qdrant indexing and attention projection (an empty placeholder isn't meaningful for either).updateMessageContentwas already there; no change.Pipeline (
assistant/src/plugins/types.ts,assistant/src/plugins/defaults/persistence.ts)PersistArgs/PersistResultunions:reserve→reserveMessageupdateContent→updateMessageContentTests (
assistant/src/__tests__/persistence-pipeline.test.ts)reserveop produces a row withcontent: "[]"and the supplied metadata.updateContentop overwrites a previously-reserved row, with direct-call parity againstupdateMessageContent.What's NOT in this PR
assistant_turn_start) andmessageId?wire field — that's B2.Verification
Plan reference
Backend queue (B1 → B5):
assistant_turn_startevent +messageId?on streaming events. Pure additive, no emit sites.reserveMessageat every LLM call start, stamp every SSE event with the anchor id.