Skip to content

feat(assistant): make the transcript the only conversation store - #325

Merged
kl3inIT merged 3 commits into
mainfrom
feat/assistant-transcript-context
Aug 6, 2026
Merged

feat(assistant): make the transcript the only conversation store#325
kl3inIT merged 3 commits into
mainfrom
feat/assistant-transcript-context

Conversation

@kl3inIT

@kl3inIT kl3inIT commented Aug 6, 2026

Copy link
Copy Markdown
Owner

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

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 no tenant
column, no actor column and no foreign key
, while holding raw message content.
Consistency was one hand-written memory.clear in the delivery layer, outside
the 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 explained
by 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: JdbcChatMemoryRepository filters tool
messages out before storing, MessageChatMemoryAdvisor sits above
ToolCallingAdvisor so they never arrive, and grounding is rebuilt
request-locally by design. Production held USER 591, ASSISTANT 494,
SYSTEM 0, TOOL 0.

What

  • AssistantTranscriptContextAdvisor replaces MessageChatMemoryAdvisor in
    both memory client paths. Read-only: after() returns the response untouched,
    because the turn writer already persists the answer.
  • V28 drops spring_ai_chat_memory; the ChatMemory bean,
    ObservedChatMemory, the JDBC chat-memory starter and its
    initialize-schema setting are removed.
  • Conversation deletion is one owned call on the existing cascade.

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 a
cost of collapsing, never had to be reimplemented.

The read is scoped by organization. Filtering only by conversation_id
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 history-load stage survives. ObservedChatMemory published
CONVERSATION_HISTORY_LOAD. Deleting it without a replacement is the silent
telemetry degradation AssistantConfiguration's own javadoc exists to warn
about, 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:test green
before and after merging main. Web Oxlint, typecheck, 87 unit tests, and
production build green. contracts/openapi.json regenerated for the changed
delete-endpoint summary and the hey-api client re-derived from it.

New coverage: AssistantTranscriptContextIntegrationTests (ordering, in-flight
exclusion, failed-turn exclusion, U1,U2,A2,A1 pairing, 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 /
MessageChatMemoryAdvisor were read from the Gradle-cache sources before the
replacement was written.

Increment closed

verification.md added, directory moved to completed/, roadmap row set to
shipped, decision 0035 records the choice, the rejected alternative, and the
debate's conditional verdict.

🤖 Generated with Claude Code

kl3inIT and others added 2 commits August 6, 2026 19:08
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>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kl3inIT, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4678ed68-9999-4b65-a0cd-cfda186c769e

📥 Commits

Reviewing files that changed from the base of the PR and between b652c87 and fded00a.

⛔ Files ignored due to path filters (11)
  • apps/docs/generated/openapi.public.json is excluded by !**/generated/**
  • contracts/openapi.json is excluded by !contracts/openapi.json
  • docs/decisions/0035-one-transcript-is-the-only-assistant-conversation-store.md is excluded by !docs/**
  • docs/increments/completed/2026-08-06-assistant-conversation-memory-ssot/challenge-brief.md is excluded by !docs/**
  • docs/increments/completed/2026-08-06-assistant-conversation-memory-ssot/challenge-verdict.md is excluded by !docs/**
  • docs/increments/completed/2026-08-06-assistant-conversation-memory-ssot/design.md is excluded by !docs/**
  • docs/increments/completed/2026-08-06-assistant-conversation-memory-ssot/plan.md is excluded by !docs/**
  • docs/increments/completed/2026-08-06-assistant-conversation-memory-ssot/verification.md is excluded by !docs/**
  • docs/roadmap.md is excluded by !docs/**
  • docs/specs/domains/assistant-and-mcp.md is excluded by !docs/**
  • docs/tests/domains/assistant-and-mcp.md is excluded by !docs/**
📒 Files selected for processing (20)
  • .tegami/2026-08-06-assistant-one-conversation-store.md
  • apps/api/build.gradle.kts
  • apps/api/src/main/java/com/orgmemory/api/assistant/AssistantConfiguration.java
  • apps/api/src/main/java/com/orgmemory/api/assistant/AssistantController.java
  • apps/api/src/main/java/com/orgmemory/api/assistant/ObservedChatMemory.java
  • apps/api/src/main/resources/application.yml
  • apps/api/src/test/java/com/orgmemory/api/assistant/AssistantControllerStreamingTests.java
  • apps/api/src/test/java/com/orgmemory/api/assistant/AssistantTranscriptContextIntegrationTests.java
  • apps/api/src/test/java/com/orgmemory/api/assistant/ObservedChatMemoryTests.java
  • apps/docs/content/docs/reference/api-reference/assistant.mdx
  • core/src/main/java/com/orgmemory/core/assistant/AssistantContextMessage.java
  • core/src/main/java/com/orgmemory/core/assistant/AssistantConversationMessage.java
  • core/src/main/java/com/orgmemory/core/assistant/AssistantConversationMessageRepository.java
  • core/src/main/java/com/orgmemory/core/assistant/AssistantConversationService.java
  • core/src/main/java/com/orgmemory/core/assistant/AssistantTranscriptContext.java
  • core/src/main/resources/db/migration/V28__drop_spring_ai_chat_memory.sql
  • integrations/ai-model-gateways/src/main/java/com/orgmemory/integrations/ai/gateway/AssistantTranscriptContextAdvisor.java
  • integrations/ai-model-gateways/src/main/java/com/orgmemory/integrations/ai/gateway/SpringAiChatModelAdapter.java
  • integrations/ai-model-gateways/src/test/java/com/orgmemory/integrations/ai/gateway/AssistantTranscriptContextAdvisorTests.java
  • integrations/ai-model-gateways/src/test/java/com/orgmemory/integrations/ai/gateway/SpringAiChatModelAdapterTests.java

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Tegami

This repository uses Tegami to manage releases. When your changes affect published packages, add a changelog file under .tegami/ before merging.

Create a changelog → · Changelog format

Release preview

Package Bump Version
orgmemory minor 0.2.00.3.0

Changelogs in this PR

Changelog Title
2026-08-06-assistant-one-conversation-store.md Improvements

Run pnpm run tegami locally to create a changelog interactively.

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>
@kl3inIT
kl3inIT merged commit 1417147 into main Aug 6, 2026
18 checks passed
@kl3inIT
kl3inIT deleted the feat/assistant-transcript-context branch August 6, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant