feat(assistant): make the transcript the only conversation store - #325
Conversation
V6 gave one conversation two tables. assistant_conversation_messages held the product transcript with an organization, an actor and foreign keys. spring_ai_chat_memory held the twenty-message model window with none of those, while holding raw message content, and the two were kept consistent by a second delete call in the delivery layer outside the transaction. Measured against the deployment, the second store was not drifting: 514 of 546 conversations matched LEAST(transcript, 20) exactly and none held more memory than transcript. It was a pure function of the first store plus "was the model invoked", which is what makes it unearned rather than what makes it broken. Nothing model-only lived there either: the vendor repository filters tool messages out, the tool loop runs inside the memory advisor so they never arrive, and grounding is rebuilt request-locally by design. A project-owned read-only advisor now reads prior turns from the transcript before each model call. Its unit is a completed turn rather than a message count, which discharges three requirements at once: the question of the turn in flight is excluded without being recognized, a turn that failed before answering is excluded for the same reason, and a window of whole turns can never open on an answer, so MessageWindowChatMemory's forward-snapping needs no reimplementing. The read is scoped by organization as well as conversation. Filtering only by conversation would have reproduced the missing-tenant-column flaw at the query layer, on data that now has the column. A call with no resolved organization gets no prior turns rather than an unscoped read. The advisor also republishes the CONVERSATION_HISTORY_LOAD stage that the removed ObservedChatMemory decorator carried, so losing the context read stays visible rather than silent. Deletion now relies on the existing cascade. ChatMemory.clear(String) carried no actor while the domain delete requires one, so the second call is removed rather than delegated. A no-evidence turn's static answer now enters model context, where the model window previously held nothing because the model was never invoked. That is the transcript the user actually saw; recorded in decision 0035. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 30 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (11)
📒 Files selected for processing (20)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
TegamiThis repository uses Tegami to manage releases. When your changes affect published packages, add a changelog file under Create a changelog → · Changelog format Release preview
Changelogs in this PR
Run Managed by Tegami. |
…I docs The changelog entry used a "## Changes" heading, which check-release.mjs does not accept, and omitted its subject. The public docs bundle also still carried the pre-change delete-endpoint summary, because generated/openapi.public.json is derived from contracts/openapi.json and was not regenerated with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second and final PR of the Assistant conversation memory SSOT increment. #318
added turn identity; this one collapses the two stores onto it and closes the
increment.
Why
V6gave one conversation two tables.assistant_conversation_messagesheldthe product transcript with an organization, an actor and foreign keys.
spring_ai_chat_memoryheld the twenty-message model window with no tenantcolumn, no actor column and no foreign key, while holding raw message content.
Consistency was one hand-written
memory.clearin the delivery layer, outsidethe transaction that deleted the conversation.
Measured against the deployment across 546 conversations, the second store was
not drifting: 514 matched
LEAST(transcript, 20)exactly, 31 were explainedby a failed or model-free turn, one was unexplained, and none held more memory
than transcript. It was a pure function of the first store plus "was the model
invoked" — which is what makes a second physical store unearned, not what makes
it broken. The earlier "20 orphaned conversations" drift claim was wrong and is
withdrawn in the design.
Nothing model-only lived there either:
JdbcChatMemoryRepositoryfilters toolmessages out before storing,
MessageChatMemoryAdvisorsits aboveToolCallingAdvisorso they never arrive, and grounding is rebuiltrequest-locally by design. Production held
USER591,ASSISTANT494,SYSTEM0,TOOL0.What
AssistantTranscriptContextAdvisorreplacesMessageChatMemoryAdvisorinboth memory client paths. Read-only:
after()returns the response untouched,because the turn writer already persists the answer.
V28dropsspring_ai_chat_memory; theChatMemorybean,ObservedChatMemory, the JDBC chat-memory starter and itsinitialize-schemasetting are removed.Three decisions worth reviewing
The window counts turns, not messages. One choice discharges three
requirements: the question of the turn in flight is excluded without being
recognized (its turn has one row), a failed turn is excluded for the same
reason, and a window of whole turns can never open on an answer — so
MessageWindowChatMemory's forward-snapping, which the debate recorded as acost of collapsing, never had to be reimplemented.
The read is scoped by organization. Filtering only by
conversation_idwould have reproduced the missing-tenant-column flaw at the query layer, on data
that now has the column. A call with no resolved organization gets no prior
turns rather than an unscoped read.
The history-load stage survives.
ObservedChatMemorypublishedCONVERSATION_HISTORY_LOAD. Deleting it without a replacement is the silenttelemetry degradation
AssistantConfiguration's own javadoc exists to warnabout, so the advisor republishes it, failure branch included.
Behavior change
A no-evidence turn's static answer now enters model context, where the model
window previously held nothing because the model was never invoked. That is the
transcript the user actually saw, and a follow-up about it is now answerable.
Recorded in decision
0035.Verification
:core:test,:integrations:ai-model-gateways:test,:apps:api:testgreenbefore and after merging
main. Web Oxlint, typecheck, 87 unit tests, andproduction build green.
contracts/openapi.jsonregenerated for the changeddelete-endpoint summary and the hey-api client re-derived from it.
New coverage:
AssistantTranscriptContextIntegrationTests(ordering, in-flightexclusion, failed-turn exclusion,
U1,U2,A2,A1pairing, ten-turn bound, cross-organization read, legacy null rows) and
AssistantTranscriptContextAdvisorTests(grounding stays first, never writes back, inert without a usable conversation
id, stage published, advisor order).
Spring AI 2.0.0
BaseAdvisor/BaseChatMemoryAdvisor/MessageChatMemoryAdvisorwere read from the Gradle-cache sources before thereplacement was written.
Increment closed
verification.mdadded, directory moved tocompleted/, roadmap row set toshipped, decision
0035records the choice, the rejected alternative, and thedebate's conditional verdict.
🤖 Generated with Claude Code