Skip to content

[FEAT] chat-routes: surface persisted message ids (user + assistant) to turn seams #236

Description

@vutuanlinh2k2

Summary

createChatTurnRoutes inserts both the user row and the assistant row internally but never surfaces the persisted row ids to the product seams. Consumers that need to anchor product artifacts to the exact message row have to work around the gap, and the second adopter (creative) has now independently hit the same wall as the first (gtm) — which is the signal this belongs in the contract.

  • onTurnComplete (src/chat-routes/turn-routes.ts, ChatTurnCompleteInput) passes {identity, finalText, context, failed, failureReason} but not the persisted assistant message id. Creative anchors ::: render vocabulary (proposal/task/event/asset chips) and the workspace-metadata proposal's inlineApprovalKey (<assistantMessageId>::0) to that row.
  • produce (ChatTurnProduceArgs) does not receive the user row id the factory just inserted. Creative's unchanged client requires a chat.turn.started event carrying the real userMessageId (optimistic-bubble swap, retry targeting), and its stop-poll keys on that row.

Current workarounds in tangle-network/creative-agent#432 (src/lib/.server/chat/chat-vertical.ts):

  • a per-request store decorator (captureAssistantInserts) that intercepts store.appendMessage and stashes the assistant row id for onTurnComplete — with a latest assistant row ORDER BY created_at DESC LIMIT 1 fallback that is unsafe under sub-second same-thread turns (created_at is unixepoch() second precision);
  • a user-row lookup at the top of produce (match part.turnId / metadata.clientRunId, fall back to latest user row).

gtm's vertical solves the same problem with its own store wrapping / row queries. Two independent consumers re-deriving ids the factory already holds is the "graduate once ≥2 consumers exercise it" threshold (#227).

Proposed approach

  • Add userMessageId: string | null to ChatTurnProduceArgs (null when resolveChatTurn suppressed the insert and no reused row exists — though in the reuse case the factory knows the reused row's id and should pass it).
  • Add assistantMessageId: string | null to ChatTurnCompleteInput (both the top-level onTurnComplete and lifecycle.onTurnComplete), null when no assistant row was persisted (e.g. stopped turn with empty output).
  • The factory already has both values in hand (store.appendMessage return, resolveChatTurn result) — this is plumbing, not new state.
  • Backward compatible: additive fields on existing seam inputs.

Acceptance criteria

  • produce receives the persisted (or reused) user row id.
  • onTurnComplete receives the persisted assistant row id (null when none).
  • creative and gtm can delete their capture/lookup workarounds (follow-up bumps in each repo).

Context

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status
Not Started 🕧

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions