Skip to content

feat: Layer 3 transcript storage, TITANS observer, and build fixes#1661

Closed
thedotmack wants to merge 12 commits intomainfrom
thedotmack/mem-layer3-titans
Closed

feat: Layer 3 transcript storage, TITANS observer, and build fixes#1661
thedotmack wants to merge 12 commits intomainfrom
thedotmack/mem-layer3-titans

Conversation

@thedotmack
Copy link
Copy Markdown
Owner

Summary

  • Layer 3 transcript storage: get_transcript_segment MCP tool for retrieving original conversation context from ChromaDB, traversal-only via observation IDs
  • TITANS conversation observer: Fire-and-forget background analysis producing 7 observation types (emotional_signal, overconfidence, frustration, pattern_recognition, etc.)
  • Progressive search MCP fix: Dynamic type system for 3-layer search workflow (search → get_observations → get_transcript_segment)
  • Build fixes: Replaced glob with Bun.Glob to fix lru-cache/CJS incompatibility, added bun to esbuild externals, fixed OneShotQuery EnvManager import

Files changed

  • src/services/sync/ChromaSync.ts — transcript Chroma methods
  • src/services/transcripts/processor.ts — conversation exchange tracking, flush to Chroma
  • src/services/worker/http/routes/SessionRoutes.ts — Layer 3 HTTP handlers
  • src/services/worker/OneShotQuery.ts — TITANS one-shot LLM queries
  • src/servers/mcp-server.ts — get_transcript_segment tool registration
  • src/services/sqlite/SessionStore.ts — getContentSessionIdByMemoryId bridge
  • src/utils/tag-stripping.ts — stripTranscriptPrivacyTags, chunkText
  • scripts/build-hooks.js — added bun to externals
  • src/services/transcripts/watcher.ts — glob → Bun.Glob migration

Test plan

  • Worker starts and reports healthy (v12.1.0)
  • Layer 1: GET /api/search?query=... returns results
  • Layer 2: POST /api/observations/batch returns full observations
  • Layer 3: POST /api/transcript/segment returns {chunks, prompt_number, mode}
  • Layer 3: POST /api/transcript/store stores transcript chunks
  • TITANS: POST /api/sessions/conversation-observe returns 202 accepted
  • Build succeeds with npm run build-and-sync

🤖 Generated with Claude Code

thedotmack and others added 6 commits April 7, 2026 00:50
…onfidence detection

6-phase plan covering:
- MCP instruction fix for progressive search skill loading
- Type system prerequisites (dynamic TypeFilter, UNIQUE constraint)
- Transcript storage in separate Chroma collection via TranscriptWatcher
- TITANS conversation observer with 7 observation types including overconfidence
- Real-time overconfidence challenge injection (follow-up phase)
- Integration and verification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1: Add Skill(mem-search) references to MCP tool descriptions and
Layer 3 transcript placeholder to SKILL.md.

Phase 2: Make observation types dynamic via ModeManager instead of
hardcoded unions, add migration 25 for user_prompts UNIQUE constraint,
and add 7 conversation observation types (insight, commitment, correction,
frustration, pattern_recognition, emotional_signal, overconfidence).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…observer

Phase 3 (Transcript Storage):
- Second ChromaSync instance (cm__claude-mem-transcripts) for transcript chunks
- TranscriptEventProcessor accumulates exchanges, flushes on prompt boundary
- POST /api/transcript/store and /api/transcript/segment endpoints
- get_transcript_segment MCP tool for Layer 3 traversal
- Privacy tag stripping and text chunking utilities
- SessionStore.getContentSessionIdByMemoryId() for observation→transcript bridge

Phase 4 (TITANS Observer):
- buildConversationObservationPrompt() with 7 signal types including overconfidence
- OneShotQuery utility supporting Gemini/OpenRouter/Anthropic providers
- POST /api/sessions/conversation-observe fire-and-forget endpoint
- Processor fires conversation observation on prompt boundary
- SKILL.md updated with all 13 observation types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Updated progressive-disclosure.mdx with 3-layer model
- Updated CLAUDE.md with transcript storage and TITANS observer architecture
- Version bump 11.0.0 → 11.1.0 across package.json and plugin manifests
- Added missing Component types to logger (TRANSCRIPT, ONE_SHOT, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-titans

# Conflicts:
#	.claude-plugin/marketplace.json
#	package.json
#	plugin/.claude-plugin/plugin.json
#	plugin/package.json
#	plugin/skills/mem-search/SKILL.md
#	src/services/sqlite/SessionStore.ts
#	src/services/transcripts/processor.ts
… import

- Replace `glob` npm package with `Bun.Glob` in transcript watcher to fix
  lru-cache top-level await incompatibility with CJS esbuild output
- Add 'bun' to esbuild externals in build-hooks.js
- Fix OneShotQuery.ts: import loadClaudeMemEnv instead of nonexistent EnvManager class

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Warning

Rate limit exceeded

@ousamabenyounes has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2db6b5e3-4220-468e-8229-3d8a9495f082

📥 Commits

Reviewing files that changed from the base of the PR and between d7322d9 and 077051e.

📒 Files selected for processing (12)
  • plugin/scripts/worker-service.cjs
  • src/services/transcripts/processor.ts
  • src/services/worker-types.ts
  • src/services/worker/GeminiAgent.ts
  • src/services/worker/OpenRouterAgent.ts
  • src/services/worker/PaginationHelper.ts
  • src/services/worker/SDKAgent.ts
  • src/services/worker/SessionManager.ts
  • src/services/worker/agents/ResponseProcessor.ts
  • src/services/worker/agents/types.ts
  • src/services/worker/metadata-extractor.ts
  • src/ui/viewer/types.ts

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a 3‑layer retrieval flow (search → get_observations → get_transcript_segment), a separate Chroma collection for transcript segments, TITANS-style conversation-observer extraction and new observation types, DB migration/indexing for prompt dedupe, new worker HTTP endpoints/MCP tool, and supporting worker/processor/sync changes.

Changes

Cohort / File(s) Summary
Docs & Plans
CLAUDE.md, docs/public/progressive-disclosure.mdx, plugin/skills/mem-search/SKILL.md, docs/context/plan-mem-layer3-titans.md
Documented three-layer workflow (search → get_observations → get_transcript_segment), updated progressive-disclosure text and tool token annotations, and added the implementation plan.
Observation Types & Mode Config
plugin/modes/code.json, plugin/skills/mem-search/SKILL.md
Added seven observation types (insight, commitment, correction, frustration, pattern_recognition, emotional_signal, overconfidence) and expanded search.obs_type allowed values; updated MCP skill docs.
Chroma Transcript Sync & DB Accessor
src/services/sync/ChromaSync.ts, src/services/worker/DatabaseManager.ts
Added transcript-specific Chroma APIs (addTranscriptChunks, getTranscriptSegment, queryTranscriptSegment) and a second ChromaSync instance/accessor (getTranscriptChromaSync) using a transcripts collection.
Transcript Capture & Conversation Observation
src/services/transcripts/processor.ts, src/services/transcripts/watcher.ts, src/sdk/prompts.ts
Buffered ConversationExchange tracking with prompt-number boundaries, flush-on-init/end semantics, fire-and-forget conversation observation, TITANS-style prompt builder, watcher glob and file-offset persistence ordering tweaks.
Worker One‑Shot & Routes
src/services/worker/OneShotQuery.ts, src/services/worker/http/routes/SessionRoutes.ts, src/servers/mcp-server.ts
Added oneShotQuery multi-provider helper; new HTTP endpoints POST /api/transcript/store, POST /api/transcript/segment, POST /api/sessions/conversation-observe; MCP tool get_transcript_segment invoking worker endpoint.
SQLite Migrations & SessionStore
src/services/sqlite/SessionStore.ts, plugin/scripts/context-generator.cjs
Added addUserPromptsUniqueIndex migration (schema v25) to dedupe user_prompts, switched saveUserPrompt to INSERT OR IGNORE, added getContentSessionIdByMemoryId(...), and adjusted migration ordering/logging.
Types & Dynamic Filters
src/services/sqlite/types.ts, src/types/database.ts, src/services/worker/search/filters/TypeFilter.ts
Replaced literal observation-type unions with string in DB/types, moved type list to runtime via ModeManager (getObservationTypes()), and updated related filter signatures.
Utilities & Logger
src/utils/tag-stripping.ts, src/utils/logger.ts
Added stripTranscriptPrivacyTags() and chunkText() exports; expanded logger Component union with TRANSCRIPT, ONE_SHOT, and other categories.
Build & Scripts
scripts/build-hooks.js, plugin/scripts/context-generator.cjs
Marked 'bun' external for worker esbuild bundle; context-generator: logging formatting, schema/version gating and SQL/query refactors.
Prompts & Minor JSON
src/sdk/prompts.ts, plugin/modes/code.json
Added ConversationExchange type and buildConversationObservationPrompt(...); cleaned placeholder whitespace entries in JSON prompts.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Agent
    participant MCP as MCP Server
    participant Search as Search Service
    participant Chroma as Chroma VectorDB
    participant SessionStore as SessionStore
    participant Worker as Worker API

    rect rgba(100,150,200,0.5)
    note over Client,MCP: Layer 1 — Search
    Client->>MCP: search(query, obs_type)
    MCP->>Search: execute search
    Search->>Chroma: vector search observations
    Chroma-->>Search: observation IDs
    Search-->>MCP: results (observation_ids)
    MCP-->>Client: return results
    end

    rect rgba(150,100,200,0.5)
    note over Client,MCP: Layer 2 — Get Observations
    Client->>MCP: get_observations(observation_ids)
    MCP->>SessionStore: fetch observation records
    SessionStore-->>MCP: observation data
    MCP-->>Client: return observations
    end

    rect rgba(200,150,100,0.5)
    note over Client,Worker: Layer 3 — Get Transcript Segment
    Client->>MCP: get_transcript_segment(observation_id, query?)
    MCP->>Worker: POST /api/transcript/segment
    Worker->>SessionStore: resolve observation → contentSessionId, promptNumber
    SessionStore-->>Worker: contentSessionId + promptNumber
    Worker->>Chroma: query transcript collection (contentSessionId,promptNumber, query?)
    Chroma-->>Worker: transcript chunks
    Worker-->>MCP: return chunks, prompt_number
    MCP-->>Client: return transcript segment
    end
Loading
sequenceDiagram
    participant Processor as Transcript Processor
    participant Buffer as Exchange Buffer
    participant Store as Transcript Store (Chroma)
    participant ConvObserver as Conversation Observer
    participant LLM as One-Shot LLM
    participant DB as SessionStore

    Processor->>Buffer: session_init → flush & increment promptNumber
    loop prompt turns
    Processor->>Buffer: user_message → append userText
    Processor->>Buffer: assistant_message → populate assistantText
    end
    Processor->>Store: POST /api/transcript/store (chunks)
    Store-->>Processor: ack (chunk count)
    Processor->>ConvObserver: fireConversationObservation(exchanges) (async)
    ConvObserver->>LLM: oneShotQuery(observationPrompt)
    LLM-->>ConvObserver: observation XML
    ConvObserver->>DB: create/attach observations to memory_session_id
    DB-->>ConvObserver: confirm storage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble lines and stitch the thread,

Three layers hop where memories tread.
Chunks in a nest, a TITANS ear,
Observations whisper, clear.
A rabbit cheers — new trails appear.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main changes: Layer 3 transcript storage, TITANS observer implementation, and build fixes—all core focus areas of the changeset.
Description check ✅ Passed The description provides a comprehensive overview of the pull request objectives, file changes, and test coverage—all directly relevant to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch thedotmack/mem-layer3-titans

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

@claude
Copy link
Copy Markdown

claude Bot commented Apr 9, 2026

PR Review: feat: Layer 3 transcript storage, TITANS observer, and build fixes

This is a substantial feature addition — the 3-layer progressive search architecture is well-designed and the TITANS observer concept is compelling. The code is generally clean and follows existing patterns. A few issues worth addressing before merge.


Bugs

1. unverified_inference type is orphaned in the prompt [src/sdk/prompts.ts]

The prompt introduces two flavors — overconfidence and unverified_inference — and says "Both get recorded. Both get validated." But unverified_inference is absent from the XML output format's type list and not registered in code.json. If the LLM emits it, the observation gets dropped or stored as an unrecognized type. The prompt is internally inconsistent here.

Either add unverified_inference to code.json and the output format, or drop the flavor distinction and use overconfidence for both.


2. createSDKSession() side-effect in handleConversationObserve [SessionRoutes.ts]

const sessionDbId = store.createSDKSession(contentSessionId, project || 'unknown', '', undefined);
const session = store.getSessionById(sessionDbId);

This is a get-or-create. If the session doesn't exist yet (race between processor flush and session init completing), this creates an orphaned session record with an empty model string. Should be a read-only lookup — the session must already exist for there to be exchanges to observe:

const session = store.getSessionByContentId(contentSessionId);
if (!session?.memory_session_id) { /* log and return */ }

3. Migration version skip [SessionStore.ts]

addUserPromptsUniqueIndex() registers as migration 25, but the plan calls it migration 24. The existing addSessionPlatformSourceColumn() is version 23 and there is no version 24 in the codebase. If migration 24 lands in a future hotfix, it will apply out of order. Either use 24 here or document why 25 was intentional.


4. Misleading fallback for missing prompt_number [SessionRoutes.ts]

const promptNumber = obs.prompt_number ?? 1;

If an observation has no prompt_number, this silently queries the transcript from segment 1 — almost certainly wrong. Return 404 with a clear message instead:

if (obs.prompt_number == null) {
  res.status(404).json({ error: 'Observation has no prompt_number — transcript segment unavailable' });
  return;
}

Security

5. Gemini API key in URL query parameter [OneShotQuery.ts]

const url = `${GEMINI_API_URL}/${model}:generateContent?key=${apiKey}`;

API keys in URLs appear in web server logs, proxy logs, and error message strings. Gemini supports x-goog-api-key as a request header:

const url = `${GEMINI_API_URL}/${model}:generateContent`;
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }

Performance

6. SettingsDefaultsManager.loadFromFile() called 3× per query [OneShotQuery.ts]

Each of queryGemini, queryOpenRouter, and queryAnthropic independently reads settings from disk. loadClaudeMemEnv() in getAnthropicApiKey() adds a 4th file read. Load settings once at the top of oneShotQuery and pass them down.


7. No fetch timeout [OneShotQuery.ts]

All three provider calls have no timeout. A hung LLM request will block the background goroutine indefinitely. Add AbortSignal.timeout(30_000) (or similar) to each fetch call.


8. Hardcoded limit: 100 in getTranscriptSegment [ChromaSync.ts]

At 2000 chars/chunk, 100 chunks ≈ 200KB. Transcripts beyond that are silently truncated. At minimum, log a warning when the limit is hit:

if (documents.length === 100) {
  logger.warn('CHROMA', 'Transcript segment hit chunk limit — result may be truncated', { contentSessionId, promptNumber });
}

Code Quality

9. ConversationExchange interface defined twice

Exported from both src/services/transcripts/processor.ts and src/sdk/prompts.ts. prompts.ts is the more logical home (used to build the prompt) — processor.ts should import from there.


10. Async processing after res.json() in handleConversationObserve

The handler sends the response, then continues async work in the same wrapHandler frame. If an error occurs and wrapHandler tries to send a 500 after headers are already sent, Node.js throws. The cleaner pattern is to detach explicitly:

res.json({ status: 'accepted', exchangeCount: exchanges.length });
void this.processConversationObservation(contentSessionId, exchanges, project);

Where processConversationObservation is a separate private method with its own try/catch.


11. docs/plan-mem-layer3-titans.md — Phase 5 unchecked boxes

The doc has unchecked - [ ] items for Phase 5 (real-time overconfidence challenge), which isn't in this PR. A note like "Phase 5 is out of scope for this PR, tracked separately" would prevent confusion for future readers.


12. Version number discrepancy

PR description says worker reports v12.1.0. Phase 6 of the plan says to bump to 11.1.0. One of these is stale.


Summary

The Layer 3 architecture is sound and TITANS observer is a compelling addition. Main issues to address before merge: the unverified_inference orphaned type (#1), the createSDKSession side-effect that can create orphaned sessions (#2), and the Gemini key exposed in the URL (#5). The missing fetch timeout (#7) and migration version gap (#3) are also worth fixing before this ships to users.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/transcripts/watcher.ts (1)

67-78: ⚠️ Potential issue | 🔴 Critical

Persist the read offset only after the line has been durably handled.

readNewData() saves the file size before await this.onLine(trimmed). With Layer 3 storage now hanging off onLine, a crash or mid-processing failure will advance the saved offset past data that never made it to storage, so those transcript lines are skipped forever on restart. This needs a post-commit watermark, not a pre-processing one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/transcripts/watcher.ts` around lines 67 - 78, The code advances
this.tailState.offset and calls this.onOffset(size) before processing lines in
readNewData, which can lose data if await this.onLine(trimmed) fails; change the
logic so the offset is only updated after lines have been durably handled:
process the combined lines (using this.tailState.partial for the trailing
fragment) and await this.onLine(trimmed) for each non-empty trimmed line first,
and only when all awaits succeed update this.tailState.offset and call
this.onOffset(...) (or alternatively update the offset incrementally after each
successful onLine to provide a post-commit watermark). Ensure
this.tailState.partial is left as the trailing fragment until after successful
processing so crashes do not advance the persisted offset past uncommitted
lines.
🧹 Nitpick comments (3)
src/services/sqlite/SessionStore.ts (1)

66-68: Migration ordering note: version 25 called before version 24.

The addUserPromptsUniqueIndex() (migration 25) is called before addSessionPlatformSourceColumn() (migration 24). While both migrations are idempotent and this doesn't cause functional issues, it creates a non-sequential version history in schema_versions. Consider swapping lines 66 and 67 to maintain ascending order.

♻️ Optional: Reorder migration calls
     this.addSessionCustomTitleColumn();
-    this.addUserPromptsUniqueIndex();
     this.addSessionPlatformSourceColumn();
+    this.addUserPromptsUniqueIndex();
     this.addObservationModelColumns();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/sqlite/SessionStore.ts` around lines 66 - 68, The migration
calls are executed out of numeric order: addUserPromptsUniqueIndex() (v25) is
invoked before addSessionPlatformSourceColumn() (v24); swap the two calls so
addSessionPlatformSourceColumn() runs immediately before
addUserPromptsUniqueIndex() to preserve ascending migration version history in
schema_versions, leaving addObservationModelColumns() unchanged.
plugin/modes/code.json (1)

48-96: Keep prompts.type_guidance in sync with these new IDs.

This mode now declares seven additional observation types, but the prompt copy later in the file still says only six values are valid. That contradiction will bias prompt-driven generators away from the new types you just added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/modes/code.json` around lines 48 - 96, The prompt copy in
prompts.type_guidance is out of sync: update the guidance text and any
validation/count mention to include the seven new observation IDs (insight,
commitment, correction, frustration, pattern_recognition, emotional_signal,
overconfidence) so it no longer claims only six values are valid; ensure
prompts.type_guidance enumerates or accepts these IDs and revise any numeric
count or example list to match the new set.
src/services/worker/http/routes/SessionRoutes.ts (1)

979-1038: Fire-and-forget observation processing may silently lose data on session lookup failure.

The handler returns 202 immediately but the background processing silently returns without logging when:

  • Line 1013: session?.memory_session_id is null (new sessions without SDK response yet)

For new sessions that haven't received their first SDK response, observations will be silently dropped. Consider:

  1. Adding INFO-level logging for this case (not just DEBUG)
  2. Or queueing failed observations for retry once memory_session_id is available
💡 Suggested improvement for visibility
       if (!session?.memory_session_id) {
-        logger.debug('SESSION', 'No memory session ID for conversation observation', { contentSessionId });
+        logger.info('SESSION', 'Conversation observation skipped - memory session ID not yet captured', { 
+          contentSessionId,
+          sessionDbId,
+          exchangeCount: exchanges.length 
+        });
         return;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/worker/http/routes/SessionRoutes.ts` around lines 979 - 1038,
The background worker in handleConversationObserve currently drops observations
when session?.memory_session_id is missing; update the logic in
handleConversationObserve (around the createSDKSession/getSessionById check) to
(1) log an INFO-level message when memory_session_id is absent including
contentSessionId and project so drops are visible, and (2) implement a simple
retry/queue fallback: push the observation payload (observations,
contentSessionId, project, promptNumber) into a retry queue or persistent store
and schedule a retry that re-checks getSessionById/createSDKSession for
memory_session_id and calls store.storeObservation when available; ensure
storeObservation remains the final call and that retry entries expire or backoff
to avoid infinite retries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plan-mem-layer3-titans.md`:
- Around line 1-5: The plan file docs/plan-mem-layer3-titans.md is a
non-user-facing design doc and should be moved into the repository's contextual
docs folder; move the file from docs/ to docs/context/, update any internal
references (README, docs index, sidebar or mkdocs/nav entries) that point to
docs/plan-mem-layer3-titans.md to the new path, and ensure any links inside the
file reflecting its location are adjusted accordingly so navigation and builds
continue to work.

In `@plugin/skills/mem-search/SKILL.md`:
- Around line 133-140: The fenced code examples for get_transcript_segment are
missing blank lines before/after the triple-backtick fences and lack a language
tag, causing markdownlint MD031/MD040 failures; update both examples (the simple
call get_transcript_segment(observation_id=11131) and the scoped search
get_transcript_segment(observation_id=11131, query="why did we choose JWT")) by
adding a blank line above and below each fenced block and add a language tag
(e.g., ```text) after the opening backticks so both fences are properly
separated and annotated.

In `@src/sdk/prompts.ts`:
- Around line 261-265: The code builds exchangeXml by embedding ex.userText and
ex.assistantText raw, which can break or inject tags; add an XML-escaping helper
(e.g., escapeXml or encodeXml) that replaces &, <, >, " and ' with their entity
equivalents and use it when constructing exchangeXml (apply to ex.userText and
ex.assistantText inside the template) and any other places where conversation
text is interpolated into XML/HTML templates.

In `@src/services/transcripts/processor.ts`:
- Around line 369-390: The code calls workerHttpRequest('/api/transcript/store')
but ignores the returned Response, so non-2xx responses are treated like
successes and session.exchanges is cleared; change the try block to capture the
response (from workerHttpRequest) and check response.ok (and optionally
response.status/text) before considering the store successful; if response.ok is
false, log a warning with the status/body and do not clear session.exchanges (so
the buffered segment is retained for retry), while still calling
this.fireConversationObservation(session, exchangesToFlush) as needed.

In `@src/services/worker/OneShotQuery.ts`:
- Around line 23-128: The selected-provider branches currently return
immediately and the provider helpers can hang or reject; change oneShotQuery so
it only returns when a provider helper yields a non-null string (i.e., call
queryGemini, queryOpenRouter, queryAnthropic and if the result is null proceed
to the next provider instead of returning early), and harden each helper
(queryGemini, queryOpenRouter, queryAnthropic) to never throw by wrapping
fetch()/response.json() in try/catch, use an AbortController-based timeout
(e.g., 10s) to cancel slow requests, and on any error or timeout log a warning
and return null so failures fall through to the next provider.

---

Outside diff comments:
In `@src/services/transcripts/watcher.ts`:
- Around line 67-78: The code advances this.tailState.offset and calls
this.onOffset(size) before processing lines in readNewData, which can lose data
if await this.onLine(trimmed) fails; change the logic so the offset is only
updated after lines have been durably handled: process the combined lines (using
this.tailState.partial for the trailing fragment) and await this.onLine(trimmed)
for each non-empty trimmed line first, and only when all awaits succeed update
this.tailState.offset and call this.onOffset(...) (or alternatively update the
offset incrementally after each successful onLine to provide a post-commit
watermark). Ensure this.tailState.partial is left as the trailing fragment until
after successful processing so crashes do not advance the persisted offset past
uncommitted lines.

---

Nitpick comments:
In `@plugin/modes/code.json`:
- Around line 48-96: The prompt copy in prompts.type_guidance is out of sync:
update the guidance text and any validation/count mention to include the seven
new observation IDs (insight, commitment, correction, frustration,
pattern_recognition, emotional_signal, overconfidence) so it no longer claims
only six values are valid; ensure prompts.type_guidance enumerates or accepts
these IDs and revise any numeric count or example list to match the new set.

In `@src/services/sqlite/SessionStore.ts`:
- Around line 66-68: The migration calls are executed out of numeric order:
addUserPromptsUniqueIndex() (v25) is invoked before
addSessionPlatformSourceColumn() (v24); swap the two calls so
addSessionPlatformSourceColumn() runs immediately before
addUserPromptsUniqueIndex() to preserve ascending migration version history in
schema_versions, leaving addObservationModelColumns() unchanged.

In `@src/services/worker/http/routes/SessionRoutes.ts`:
- Around line 979-1038: The background worker in handleConversationObserve
currently drops observations when session?.memory_session_id is missing; update
the logic in handleConversationObserve (around the
createSDKSession/getSessionById check) to (1) log an INFO-level message when
memory_session_id is absent including contentSessionId and project so drops are
visible, and (2) implement a simple retry/queue fallback: push the observation
payload (observations, contentSessionId, project, promptNumber) into a retry
queue or persistent store and schedule a retry that re-checks
getSessionById/createSDKSession for memory_session_id and calls
store.storeObservation when available; ensure storeObservation remains the final
call and that retry entries expire or backoff to avoid infinite retries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44b66dfa-120b-4006-a8cb-19d6acb30b84

📥 Commits

Reviewing files that changed from the base of the PR and between cde4faa and de7931a.

📒 Files selected for processing (24)
  • CLAUDE.md
  • docs/plan-mem-layer3-titans.md
  • docs/public/progressive-disclosure.mdx
  • plugin/modes/code.json
  • plugin/scripts/context-generator.cjs
  • plugin/scripts/mcp-server.cjs
  • plugin/scripts/worker-service.cjs
  • plugin/skills/mem-search/SKILL.md
  • plugin/ui/viewer-bundle.js
  • scripts/build-hooks.js
  • src/sdk/prompts.ts
  • src/servers/mcp-server.ts
  • src/services/sqlite/SessionStore.ts
  • src/services/sqlite/types.ts
  • src/services/sync/ChromaSync.ts
  • src/services/transcripts/processor.ts
  • src/services/transcripts/watcher.ts
  • src/services/worker/DatabaseManager.ts
  • src/services/worker/OneShotQuery.ts
  • src/services/worker/http/routes/SessionRoutes.ts
  • src/services/worker/search/filters/TypeFilter.ts
  • src/types/database.ts
  • src/utils/logger.ts
  • src/utils/tag-stripping.ts

Comment on lines +1 to +5
# Implementation Plan: Memory Layer 3, TITANS Observer, and Progressive Search Fix

**Created:** 2026-04-05 | **Revised:** 2026-04-06 (post eng review)
**Branch:** thedotmack/mem-layer3-titans
**Scope:** 4 enhancements to claude-mem's memory system
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move this plan into docs/context/.

This is a planning/reference document, so keeping it at the top level of docs/ makes the docs tree inconsistent with the repo convention for non-user-facing design material.

Based on learnings, planning documents, design docs, and reference materials must be placed in /docs/context/ instead of /docs/public/.

🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Use a hyphen to join words.
Context: ...26-04-05 | Revised: 2026-04-06 (post eng review) Branch: thedotmack/mem-l...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plan-mem-layer3-titans.md` around lines 1 - 5, The plan file
docs/plan-mem-layer3-titans.md is a non-user-facing design doc and should be
moved into the repository's contextual docs folder; move the file from docs/ to
docs/context/, update any internal references (README, docs index, sidebar or
mkdocs/nav entries) that point to docs/plan-mem-layer3-titans.md to the new
path, and ensure any links inside the file reflecting its location are adjusted
accordingly so navigation and builds continue to work.

Comment thread plugin/skills/mem-search/SKILL.md
Comment thread src/sdk/prompts.ts
Comment thread src/services/transcripts/processor.ts
Comment thread src/services/worker/OneShotQuery.ts
… watermark

- Escape user/assistant text in XML conversation wrapper (prompts.ts)
- Check response.ok before clearing exchange buffer (processor.ts)
- Add 30s timeout + try/catch fallthrough to OneShotQuery providers
- Move offset persistence after line processing in watcher (post-commit watermark)
- Move plan doc to docs/context/ per repo convention
- Fix markdown fences in SKILL.md (blank lines + language tags)
- Reorder migration calls to ascending version order (SessionStore.ts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 9, 2026

PR Review: Layer 3 Transcript Storage, TITANS Observer, and Build Fixes

Good architectural work here — the 3-layer progressive search model is clean and the separation of transcript storage (Chroma) from searchable observations (SQLite) makes sense. A few issues need attention before this lands.


Critical: Offset watermark race in src/services/transcripts/watcher.ts

The read offset is persisted before onLine() completes. With Layer 3 storage now async behind that call, a crash mid-processing will permanently skip those transcript lines on restart.

// Current (unsafe):
this.tailState.offset = size;
await this.onLine(trimmed);   // crash here → line lost forever
await this.onOffset(size);

// Fix: commit watermark only after successful processing
await this.onLine(trimmed);
this.tailState.offset = size;
await this.onOffset(size);

This is data-loss territory — worth treating as a blocker.


Bug: Migration version ordering in src/services/sqlite/SessionStore.ts (~line 66)

addUserPromptsUniqueIndex() (v25) runs before addSessionPlatformSourceColumn() (v24). Non-sequential migrations can cause hard-to-debug schema state issues. Swap these two lines.


Bug: Silent observation drop in SessionRoutes.ts (~line 979)

The fire-and-forget handler silently returns when session?.memory_session_id is null:

if (!session?.memory_session_id) {
  logger.debug('...');  // silent drop
  return;
}

For new sessions that haven't received their SDK response yet, this means observations are quietly discarded. At minimum, elevate to logger.info. Better: queue these for retry once memory_session_id becomes available.


Security: XML injection in src/sdk/prompts.ts (~line 261)

ex.userText and ex.assistantText are interpolated directly into XML-structured prompts:

`<user>${ex.userText}</user>`
`<assistant>${ex.assistantText}</assistant>`

If either contains </user> or </assistant>, it will break the prompt structure and could be used to inject instructions. Escape these values before interpolation.


Bug: Unchecked HTTP response in src/services/transcripts/processor.ts (~line 369)

The result of workerHttpRequest('/api/transcript/store') isn't checked before session.exchanges is cleared. If the store request fails (worker restart, transient error), the transcript data is lost with no retry path. Check response.ok before clearing.


Hardening: src/services/worker/OneShotQuery.ts

A few gaps in the new file:

  • Provider selection can return undefined with no fallback — add a null guard or default
  • The LLM fetch() call has no timeout, so a hung provider will block indefinitely
  • Wrap the fetch in try/catch; currently an uncaught network error will crash the observer instead of logging and moving on

Minor: plugin/modes/code.json prompt guidance out of sync

prompts.type_guidance still references 6 observation types but the mode now declares 7 (overconfidence is missing from the guidance text). This will bias generators away from producing that type.


Nit: Design doc placement

docs/context/plan-mem-layer3-titans.md (820 lines) is an implementation plan, not user-facing docs. Consider whether it belongs in docs/public/ or should live under docs/context/ instead (or be referenced but not shipped).


Build fixes

The globBun.Glob migration and adding bun to esbuild externals look correct. The .cjs bundle diffs are large but appear to be generated artifacts — fine as-is.


Summary: The architecture is solid and the feature set is well thought out. The critical issue is the offset watermark in watcher.ts (data loss on crash), followed by the XML injection risk and the silent observation drops. The migration ordering bug is low-risk but easy to fix. Would recommend addressing these before merge.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/scripts/context-generator.cjs (1)

290-294: ⚠️ Potential issue | 🟠 Major

Make saveUserPrompt() idempotent now that the index is unique.

addUserPromptsUniqueIndex() makes (content_session_id, prompt_number) unique, but saveUserPrompt() still does a plain INSERT. A retried session_init now throws SQLITE_CONSTRAINT_UNIQUE instead of deduping, which is the exact path this migration is supposed to harden.

Suggested fix
 saveUserPrompt(e,t,s){
   let n=new Date,o=n.getTime();
-  return this.db.prepare(`
-      INSERT INTO user_prompts
-      (content_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
-      VALUES (?, ?, ?, ?, ?)
-    `).run(e,t,s,n.toISOString(),o).lastInsertRowid
+  let i=this.db.prepare(`
+      INSERT OR IGNORE INTO user_prompts
+      (content_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
+      VALUES (?, ?, ?, ?, ?)
+    `).run(e,t,s,n.toISOString(),o);
+  if(i.changes===0){
+    return this.db.prepare(`
+      SELECT id
+      FROM user_prompts
+      WHERE content_session_id = ? AND prompt_number = ?
+      LIMIT 1
+    `).get(e,t).id
+  }
+  return i.lastInsertRowid
 }

Based on learnings, plugin/scripts/context-generator.cjs contains its own embedded DB class with its own migration methods and must be reviewed independently from the TypeScript source path.

Also applies to: 480-484

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/context/plan-mem-layer3-titans.md`:
- Around line 162-175: The heading "Add 6 conversation observation types to code
mode" is inconsistent with the JSON payload which adds 7 entries; update the
heading to "Add 7 conversation observation types to code mode" (or remove the
extra `overconfidence` entry if intentional) and ensure the `observation_types`
change references the seven ids (insight, commitment, correction, frustration,
pattern_recognition, emotional_signal, overconfidence) so the section text and
payload match.
- Around line 113-125: Update the rollout plan to use the same schema version as
the implementation: change the migration label from "Migration 24" to "Migration
25" (or otherwise make the plan's migration number match the one passed to
applyMigration in src/services/sqlite/SessionStore.ts); ensure the UNIQUE index
description and the index name idx_user_prompts_unique_session_prompt remain
unchanged so verification steps align with the actual migration applied.

In `@src/services/transcripts/processor.ts`:
- Around line 147-149: The code increments session.promptNumber immediately
after calling flushTranscriptSegment(session), which means if the POST to
/api/transcript/store fails the retained session.exchanges will be associated
with the new promptNumber on the next successful flush; modify the logic in the
session handling (where flushTranscriptSegment and session.promptNumber are
used, and similarly in the blocks around lines 354-395) to ensure failed flushes
are not re-associated with a new promptNumber by either 1) moving the
session.promptNumber++ to occur only after a successful flush (i.e., increment
inside the success callback of flushTranscriptSegment) or 2) implement a retry
queue keyed by the original promptNumber (store a copy of session.exchanges and
its promptNumber when a flush fails and retry posting that exact pair until
success); update references to session.exchanges, flushTranscriptSegment, and
session_init accordingly so each buffered segment remains tied to its original
promptNumber.
- Around line 404-418: The background observe call treats any fulfilled
workerHttpRequest as success; update the ensureWorkerRunning() handler that
calls workerHttpRequest('/api/sessions/conversation-observe', ...) to inspect
the resolved response and log non-2xx results: after the promise resolves, check
response.ok (or response.status) and if false call logger.error (or logger.debug
as appropriate) with the endpoint, status, statusText and include the response
body/text for diagnostics; keep the existing .catch for transport failures but
ensure non-2xx responses are not ignored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6a825b8-053c-43eb-b7b4-eb3723768677

📥 Commits

Reviewing files that changed from the base of the PR and between de7931a and 744007e.

📒 Files selected for processing (9)
  • docs/context/plan-mem-layer3-titans.md
  • plugin/scripts/context-generator.cjs
  • plugin/scripts/worker-service.cjs
  • plugin/skills/mem-search/SKILL.md
  • src/sdk/prompts.ts
  • src/services/sqlite/SessionStore.ts
  • src/services/transcripts/processor.ts
  • src/services/transcripts/watcher.ts
  • src/services/worker/OneShotQuery.ts
✅ Files skipped from review due to trivial changes (2)
  • src/sdk/prompts.ts
  • plugin/skills/mem-search/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/services/transcripts/watcher.ts
  • src/services/worker/OneShotQuery.ts
  • src/services/sqlite/SessionStore.ts

Comment thread docs/context/plan-mem-layer3-titans.md Outdated
Comment thread docs/context/plan-mem-layer3-titans.md Outdated
Comment thread src/services/transcripts/processor.ts Outdated
Comment thread src/services/transcripts/processor.ts Outdated
…serve logging

- Make saveUserPrompt() use INSERT OR IGNORE for unique index compatibility
- Return boolean from flushTranscriptSegment; only increment promptNumber on success
- Log non-2xx responses from background conversation-observe call
- Fix plan doc: Migration 24→25, 6→7 observation types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 9, 2026

Code Review: Layer 3 Transcript Storage, TITANS Observer, Build Fixes

Overall this is a well-structured addition. The 3-layer progressive search model is clearly architected, the separation of the transcript Chroma collection from the main search index is the right call, and the fire-and-forget TITANS pattern is clean. A few issues worth addressing before merge:


🐛 Bugs

1. First conversation segment is silently dropped (processor.ts)

flushTranscriptSegment guards on session.promptNumber === 0, which means the first exchange of every session is never stored:

Session starts: promptNumber=0, exchanges=[]
session_init → flush called → promptNumber===0 → return false (no increment)
user_message  → exchange pushed with promptNumber=0
assistant_msg → exchange updated
session_init  → flush called → exchanges.length>0 BUT promptNumber===0 → return false again!

The first segment is lost every time. Fix: initialize promptNumber: 1 instead of 0, or change the guard to promptNumber < 0.


2. createSDKSession creates corrupted records (SessionRoutes.ts:~960)

const sessionDbId = store.createSDKSession(contentSessionId, project || 'unknown', '', undefined);

If the session doesn't already exist in the DB, this inserts a new row with an empty string memory_session_id. The downstream store.getSessionById(sessionDbId)?.memory_session_id check would then fail (empty string is falsy), but you've now written a garbage row to sdk_sessions. This should use a read-only lookup — find the session or bail out, don't create one.


⚠️ Notable Issues

3. Naive character-boundary chunking reduces RAG quality (tag-stripping.ts)

for (let i = 0; i < text.length; i += chunkSize) {
  chunks.push(text.substring(i, i + chunkSize));
}

This splits mid-word and mid-sentence. For vector search, sentence-boundary chunks retrieve significantly better. At minimum, split at \n\n paragraph breaks before falling back to character limits.


4. All observations tagged with exchanges[0].promptNumber (SessionRoutes.ts:~984)

store.storeObservation(
  session.memory_session_id,
  project || 'unknown',
  obs,
  exchanges[0]?.promptNumber ?? 1,  // always the first exchange
  0
);

When multiple exchanges are flushed (e.g., at session end), every observation gets the first exchange's prompt number regardless of which exchange triggered it. This breaks the observation→transcript traversal that Layer 3 depends on.


5. Hard limit of 100 chunks in getTranscriptSegment (ChromaSync.ts)

limit: 100,

100 × 2000 chars = ~200k chars. A long coding session could exceed this. Silently returns incomplete data with no warning. Either paginate or at least log when documents.length === 100.


6. Unused imports

  • watcher.ts: resolve is imported from 'path' but never referenced in the diff
  • OneShotQuery.ts: loadClaudeMemEnv is imported from EnvManager.js but not used

7. Settings file read on every API call (OneShotQuery.ts)

SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH) is called inside each of queryGemini, queryOpenRouter, and queryAnthropic — potentially 3 disk reads per oneShotQuery invocation. Load settings once at the top of oneShotQuery and pass values down.


✅ What's Working Well

  • FileTailer offset fix (watcher.ts) — moving offset persistence to after line processing is the right crash-safety fix; clean change
  • globBun.Glob migration — correct and necessary for the CJS incompatibility; the API mapping looks right
  • Separate Chroma collection for transcripts — keeping transcript chunks out of the main search index is the correct isolation boundary
  • INSERT OR IGNORE deduplication in storeUserPrompt — the UNIQUE index + fallback SELECT pattern is solid
  • Privacy tag stripping at the HTTP handler layer — consistent with the existing edge-processing philosophy
  • TypeFilter → dynamic ModeManager — removes the split-brain between hardcoded types and runtime mode config; clean refactor

Minor

  • Migration 25 is called before migration 24 (addSessionPlatformSourceColumn) in the initialize() sequence. This doesn't break anything since each migration is independently idempotent, but the ordering is inconsistent with the version numbering.
  • queryTranscriptSegment returns Chroma's raw nested array (results?.documents?.[0]) without filtering out null/empty entries — callers may need to handle nulls in the returned array.

🤖 Generated with Claude Code

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
plugin/scripts/context-generator.cjs (1)

290-294: ⚠️ Potential issue | 🔴 Critical

The bundled DB class still has the unsafe UNIQUE-index migration.

This embedded SessionStore also does a straight CREATE UNIQUE INDEX on user_prompts(content_session_id, prompt_number), so installs with pre-existing duplicates can still fail through the context-generator/plugin path. Fix the source migration to reconcile duplicates first, then regenerate this bundle rather than patching the minified artifact by hand.

Based on learnings, plugin/scripts/context-generator.cjs contains its own embedded DB class with its own migration methods, so this code path must be checked independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/scripts/context-generator.cjs` around lines 290 - 294, The embedded
migration addUserPromptsUniqueIndex still runs a straight CREATE UNIQUE INDEX
which fails on pre-existing duplicates; update the addUserPromptsUniqueIndex
method in the bundled DB class to first reconcile duplicates (e.g. identify
duplicate rows on (content_session_id, prompt_number), keep the canonical row
per group and delete other duplicates using their rowids or primary key), then
run CREATE UNIQUE INDEX IF NOT EXISTS idx_user_prompts_unique_session_prompt and
only after that insert the schema_versions row; regenerate the bundle instead of
hand-patching the minified artifact.
src/services/transcripts/processor.ts (1)

337-349: ⚠️ Potential issue | 🟠 Major

Don't tear down the session after a failed final flush.

handleSessionEnd() ignores the boolean from flushTranscriptSegment() and then deletes the in-memory session. Any 503/network failure here permanently drops the last transcript segment and its TITANS input.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/transcripts/processor.ts` around lines 337 - 349,
handleSessionEnd currently ignores the boolean result from
flushTranscriptSegment and always tears down the session; change it to check the
return value of flushTranscriptSegment(session) and if it returns false (flush
failed) abort teardown—do not call queueSummary, sessionCompleteHandler.execute,
updateContext, session.pendingTools.clear, or sessions.delete; instead log the
failure and return (or throw) so the session remains in memory for retry. Ensure
references to flushTranscriptSegment, queueSummary,
sessionCompleteHandler.execute, updateContext, session.pendingTools.clear,
getSessionKey, and sessions.delete are used so you modify the correct control
flow.
♻️ Duplicate comments (3)
docs/context/plan-mem-layer3-titans.md (2)

735-738: ⚠️ Potential issue | 🟡 Minor

This summary still says 6 conversation observation types.

The plan and implementation use seven types now, including overconfidence. Keeping 6 here makes the final checklist internally inconsistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/context/plan-mem-layer3-titans.md` around lines 735 - 738, The
Architecture section currently states "Conversation observation modality
(TITANS-inspired, 6 types)" but the implementation and plan use seven types
including the new `overconfidence`; update that sentence to "7 types" and ensure
the conversation observation types list explicitly includes `overconfidence`
(and any other seven names used in the plan) so the Architecture text and final
checklist are consistent with the implementation.

113-125: ⚠️ Potential issue | 🟡 Minor

The plan still names the user_prompts index as migration 24.

This change landed as schema version 25, but this section and Appendix B still say 24. That makes rollout and verification notes easy to follow incorrectly.

Also applies to: 798-799

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/context/plan-mem-layer3-titans.md` around lines 113 - 125, The docs
incorrectly label the UNIQUE index migration for user_prompts as migration 24;
update every mention in docs/context/plan-mem-layer3-titans.md (section "2.1:
Add UNIQUE constraint to user_prompts" and Appendix B references around lines
noted) to reflect the actual schema version 25, and ensure the migration
description references the same applyMigration(version, fn) pattern used for
Migration 22 and the file `src/services/sqlite/SessionStore.ts` so rollout and
verification steps point to migration 25 consistently.
src/services/transcripts/processor.ts (1)

146-149: ⚠️ Potential issue | 🟠 Major

A failed flush still collapses two prompts into one buffered segment.

When flushTranscriptSegment() returns false, session_init keeps going and the next user_message is appended to the same session.exchanges array. That merges two prompt boundaries into one stored segment, so observation → transcript traversal no longer matches the original prompt. A failed segment needs its own retry slot keyed by the original prompt number, or the buffer needs to rotate before the next prompt is accepted.

Also applies to: 156-165, 355-397

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/transcripts/processor.ts` around lines 146 - 149, The flush
failure currently lets the next prompt get appended to the same
session.exchanges and collapses prompt boundaries; modify the session_init
handling (and the other similar blocks around flushTranscriptSegment usage) so
that when flushTranscriptSegment(session) returns false you still rotate the
buffer / reserve a retry slot keyed by the current prompt number before
accepting the next prompt—e.g., if flushed === false then increment
session.promptNumber (or push a retry placeholder keyed by the original
session.promptNumber) so the next user_message is recorded under a new prompt
boundary; apply the same change to the other flush usages mentioned (the blocks
around lines 156-165 and 355-397) and ensure any retry logic references the
preserved original promptNumber.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/sqlite/SessionStore.ts`:
- Around line 894-905: The migration method addUserPromptsUniqueIndex attempts
to create a UNIQUE index but will fail if duplicate (content_session_id,
prompt_number) rows already exist; modify addUserPromptsUniqueIndex to first
deduplicate existing rows inside a transaction (e.g., delete duplicates keeping
one row per (content_session_id, prompt_number) using ROWID or a CTE that
preserves the min ROWID), then create the UNIQUE index and finally insert the
schema_versions row; target the addUserPromptsUniqueIndex function and ensure
the dedupe+CREATE INDEX+INSERT are atomic to avoid startup failures on older
databases.

In `@src/services/transcripts/processor.ts`:
- Around line 64-65: The bug is that promptNumber is initialized to 0 so the
first prompt (prompt 1) is dropped because session_init doesn't increment before
the first flush and flushTranscriptSegment refuses to store prompt 0; fix this
by initializing promptNumber to 1 (or alternatively adjust
flushTranscriptSegment to accept/promote 0 to 1) so the first prompt is
preserved. Update the initial value where promptNumber is set and ensure any
logic in session_init and flushTranscriptSegment (referenced by the symbols
promptNumber, session_init, and flushTranscriptSegment) remains consistent with
the new starting value.

---

Outside diff comments:
In `@plugin/scripts/context-generator.cjs`:
- Around line 290-294: The embedded migration addUserPromptsUniqueIndex still
runs a straight CREATE UNIQUE INDEX which fails on pre-existing duplicates;
update the addUserPromptsUniqueIndex method in the bundled DB class to first
reconcile duplicates (e.g. identify duplicate rows on (content_session_id,
prompt_number), keep the canonical row per group and delete other duplicates
using their rowids or primary key), then run CREATE UNIQUE INDEX IF NOT EXISTS
idx_user_prompts_unique_session_prompt and only after that insert the
schema_versions row; regenerate the bundle instead of hand-patching the minified
artifact.

In `@src/services/transcripts/processor.ts`:
- Around line 337-349: handleSessionEnd currently ignores the boolean result
from flushTranscriptSegment and always tears down the session; change it to
check the return value of flushTranscriptSegment(session) and if it returns
false (flush failed) abort teardown—do not call queueSummary,
sessionCompleteHandler.execute, updateContext, session.pendingTools.clear, or
sessions.delete; instead log the failure and return (or throw) so the session
remains in memory for retry. Ensure references to flushTranscriptSegment,
queueSummary, sessionCompleteHandler.execute, updateContext,
session.pendingTools.clear, getSessionKey, and sessions.delete are used so you
modify the correct control flow.

---

Duplicate comments:
In `@docs/context/plan-mem-layer3-titans.md`:
- Around line 735-738: The Architecture section currently states "Conversation
observation modality (TITANS-inspired, 6 types)" but the implementation and plan
use seven types including the new `overconfidence`; update that sentence to "7
types" and ensure the conversation observation types list explicitly includes
`overconfidence` (and any other seven names used in the plan) so the
Architecture text and final checklist are consistent with the implementation.
- Around line 113-125: The docs incorrectly label the UNIQUE index migration for
user_prompts as migration 24; update every mention in
docs/context/plan-mem-layer3-titans.md (section "2.1: Add UNIQUE constraint to
user_prompts" and Appendix B references around lines noted) to reflect the
actual schema version 25, and ensure the migration description references the
same applyMigration(version, fn) pattern used for Migration 22 and the file
`src/services/sqlite/SessionStore.ts` so rollout and verification steps point to
migration 25 consistently.

In `@src/services/transcripts/processor.ts`:
- Around line 146-149: The flush failure currently lets the next prompt get
appended to the same session.exchanges and collapses prompt boundaries; modify
the session_init handling (and the other similar blocks around
flushTranscriptSegment usage) so that when flushTranscriptSegment(session)
returns false you still rotate the buffer / reserve a retry slot keyed by the
current prompt number before accepting the next prompt—e.g., if flushed ===
false then increment session.promptNumber (or push a retry placeholder keyed by
the original session.promptNumber) so the next user_message is recorded under a
new prompt boundary; apply the same change to the other flush usages mentioned
(the blocks around lines 156-165 and 355-397) and ensure any retry logic
references the preserved original promptNumber.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 366b381b-bd37-4a7d-9417-22c95ff72efb

📥 Commits

Reviewing files that changed from the base of the PR and between 744007e and 49bae21.

📒 Files selected for processing (5)
  • docs/context/plan-mem-layer3-titans.md
  • plugin/scripts/context-generator.cjs
  • plugin/scripts/worker-service.cjs
  • src/services/sqlite/SessionStore.ts
  • src/services/transcripts/processor.ts

Comment thread src/services/sqlite/SessionStore.ts
Comment thread src/services/transcripts/processor.ts Outdated
…ssion end safety

- Reconcile pre-existing duplicate user_prompts rows before CREATE UNIQUE INDEX
- Log warning on failed final flush in handleSessionEnd instead of silently dropping
- Rebuild bundles with migration fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/transcripts/processor.ts (1)

337-353: ⚠️ Potential issue | 🟠 Major

A failed final flush is still discarded during teardown.

Lines 339-343 log the failure, but Lines 351-353 immediately clear state and delete the session anyway. Any transient 503/timeout on the last prompt permanently loses that segment, so Layer 3 retrieval can miss the conversation tail. This needs a retry path keyed to the original promptNumber, or the session state has to survive until the store succeeds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/transcripts/processor.ts` around lines 337 - 353,
handleSessionEnd currently logs a failed final flush from flushTranscriptSegment
but then proceeds to clear state and delete the session (calling queueSummary,
sessionCompleteHandler.execute, updateContext, session.pendingTools.clear and
sessions.delete via getSessionKey), which can permanently lose the last segment;
change handleSessionEnd so that when flushTranscriptSegment returns false it
does not purge the session immediately but instead retries the flush
(exponential backoff or limited retry loop) keyed by the session's promptNumber
or otherwise preserves session.exchanges and session state until
flushTranscriptSegment succeeds or a configurable maxRetry is reached; only
after a successful flush (or exhausting retries with a safe persisted fallback)
should you call queueSummary, sessionCompleteHandler.execute, updateContext,
clear pendingTools and sessions.delete.
♻️ Duplicate comments (1)
src/services/transcripts/processor.ts (1)

64-65: ⚠️ Potential issue | 🔴 Critical

Prompt 1 is still unflushable.

Line 64 starts promptNumber at 0, and Line 361 refuses to flush prompt 0. Because session_init only increments after a successful flush, the counter never advances and the first turn never reaches /api/transcript/store.

🩹 Minimal fix
      session = {
        sessionId,
-       promptNumber: 0,
+       promptNumber: 1,
        exchanges: [],
        platformSource: normalizePlatformSource(watch.name),
        pendingTools: new Map()
      };
...
-    if (session.exchanges.length === 0 || session.promptNumber === 0) return false;
+    if (session.exchanges.length === 0) return false;

Also applies to: 360-362

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/transcripts/processor.ts` around lines 64 - 65, The initial
promptNumber is set to 0 so session_init never advances past the first prompt
and prompt 0 is never flushed; change the initialization of promptNumber from 0
to 1 (where promptNumber and exchanges are initialized) so the first turn can be
flushed to /api/transcript/store, or alternatively adjust the session_init flush
logic to allow flushing promptNumber 0 by incrementing the counter before the
flush; reference the promptNumber variable and the session_init flush path to
locate and apply the minimal fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/sqlite/SessionStore.ts`:
- Around line 1699-1710: The code currently handles an INSERT OR IGNORE into
user_prompts and returns 0 if result.changes === 0 and the subsequent SELECT for
the existing row (by contentSessionId and promptNumber) returns nothing;
instead, update the branch in SessionStore.ts so that when result.changes === 0
you attempt to load the existing row id (SELECT id FROM user_prompts WHERE
content_session_id = ? AND prompt_number = ?), and if that query returns
undefined throw a descriptive error (including contentSessionId and
promptNumber) indicating a DB inconsistency rather than returning 0; keep
returning the found id when present and only throw when the existing variable is
missing.

---

Outside diff comments:
In `@src/services/transcripts/processor.ts`:
- Around line 337-353: handleSessionEnd currently logs a failed final flush from
flushTranscriptSegment but then proceeds to clear state and delete the session
(calling queueSummary, sessionCompleteHandler.execute, updateContext,
session.pendingTools.clear and sessions.delete via getSessionKey), which can
permanently lose the last segment; change handleSessionEnd so that when
flushTranscriptSegment returns false it does not purge the session immediately
but instead retries the flush (exponential backoff or limited retry loop) keyed
by the session's promptNumber or otherwise preserves session.exchanges and
session state until flushTranscriptSegment succeeds or a configurable maxRetry
is reached; only after a successful flush (or exhausting retries with a safe
persisted fallback) should you call queueSummary,
sessionCompleteHandler.execute, updateContext, clear pendingTools and
sessions.delete.

---

Duplicate comments:
In `@src/services/transcripts/processor.ts`:
- Around line 64-65: The initial promptNumber is set to 0 so session_init never
advances past the first prompt and prompt 0 is never flushed; change the
initialization of promptNumber from 0 to 1 (where promptNumber and exchanges are
initialized) so the first turn can be flushed to /api/transcript/store, or
alternatively adjust the session_init flush logic to allow flushing promptNumber
0 by incrementing the counter before the flush; reference the promptNumber
variable and the session_init flush path to locate and apply the minimal fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bedb1e8-5639-43ac-8b49-be14c9c8dc39

📥 Commits

Reviewing files that changed from the base of the PR and between 49bae21 and d7322d9.

📒 Files selected for processing (4)
  • plugin/scripts/context-generator.cjs
  • plugin/scripts/worker-service.cjs
  • src/services/sqlite/SessionStore.ts
  • src/services/transcripts/processor.ts

Comment on lines +1699 to +1710
const result = this.db.prepare(`
INSERT OR IGNORE INTO user_prompts
(content_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?)
`);
`).run(contentSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);

const result = stmt.run(contentSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);
if (result.changes === 0) {
const existing = this.db.prepare(
'SELECT id FROM user_prompts WHERE content_session_id = ? AND prompt_number = ? LIMIT 1'
).get(contentSessionId, promptNumber) as { id: number } | undefined;
return existing?.id ?? 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the SessionStore.ts file
fd -t f "SessionStore.ts" --path-separator / | head -20

Repository: thedotmack/claude-mem

Length of output: 100


🏁 Script executed:

# Check the file size and read the relevant lines
wc -l src/services/sqlite/SessionStore.ts

Repository: thedotmack/claude-mem

Length of output: 105


🏁 Script executed:

# Read the actual implementation around lines 1699-1710
sed -n '1690,1720p' src/services/sqlite/SessionStore.ts | cat -n

Repository: thedotmack/claude-mem

Length of output: 1346


🏁 Script executed:

# Search for the user_prompts table schema definition
rg "CREATE TABLE.*user_prompts|user_prompts.*CREATE TABLE" -A 15

Repository: thedotmack/claude-mem

Length of output: 6594


🏁 Script executed:

# Look for migration files that might define the schema
fd -t f -e "sql" -e "ts" "migration" | head -20

Repository: thedotmack/claude-mem

Length of output: 145


🏁 Script executed:

# Search for saveUserPrompt usage to understand how the return value is used
rg "saveUserPrompt" -B 2 -A 2

Repository: thedotmack/claude-mem

Length of output: 36646


🏁 Script executed:

# Check for unique constraints on user_prompts table
rg "unique.*content_session_id|prompt_number|UNIQUE" -B 2 -A 2

Repository: thedotmack/claude-mem

Length of output: 50378


🏁 Script executed:

# Search for how saveUserPrompt's return value is used
rg "saveUserPrompt.*\(" -A 2 | head -40

Repository: thedotmack/claude-mem

Length of output: 2519


🏁 Script executed:

# Look at the unique index creation more carefully
rg "addUserPromptsUniqueIndex|idx_user_prompts_unique" -B 2 -A 5

Repository: thedotmack/claude-mem

Length of output: 27232


🏁 Script executed:

# Check if there are any other constraints on user_prompts table
rg "CREATE TABLE.*user_prompts|FOREIGN KEY.*user_prompts|UNIQUE.*user_prompts" -B 1 -A 10 | head -60

Repository: thedotmack/claude-mem

Length of output: 5226


🏁 Script executed:

# Search for tests that verify duplicate prompt handling
rg "saveUserPrompt.*duplicate|duplicate.*prompt|saveUserPrompt.*existing" -B 3 -A 3

Repository: thedotmack/claude-mem

Length of output: 28392


🏁 Script executed:

# Check if the return value 0 is handled anywhere
rg "saveUserPrompt" -A 3 | rg "=== 0|== 0|!== 0|!= 0" -B 2 -A 1

Repository: thedotmack/claude-mem

Length of output: 47


Return a valid row ID or throw; do not silently return 0.

When INSERT OR IGNORE succeeds (duplicate is skipped), the follow-up lookup should always find the existing row. If it misses, that signals a database inconsistency and should raise an error rather than return 0. Throwing makes failures visible instead of masked by a sentinel value.

🩹 Fail explicitly when the existing row is not found after insert conflict
     if (result.changes === 0) {
       const existing = this.db.prepare(
         'SELECT id FROM user_prompts WHERE content_session_id = ? AND prompt_number = ? LIMIT 1'
       ).get(contentSessionId, promptNumber) as { id: number } | undefined;
-      return existing?.id ?? 0;
+      if (!existing) {
+        throw new Error(`Failed to save user prompt for ${contentSessionId}:${promptNumber}`);
+      }
+      return existing.id;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/sqlite/SessionStore.ts` around lines 1699 - 1710, The code
currently handles an INSERT OR IGNORE into user_prompts and returns 0 if
result.changes === 0 and the subsequent SELECT for the existing row (by
contentSessionId and promptNumber) returns nothing; instead, update the branch
in SessionStore.ts so that when result.changes === 0 you attempt to load the
existing row id (SELECT id FROM user_prompts WHERE content_session_id = ? AND
prompt_number = ?), and if that query returns undefined throw a descriptive
error (including contentSessionId and promptNumber) indicating a DB
inconsistency rather than returning 0; keep returning the found id when present
and only throw when the existing variable is missing.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 9, 2026

Code Review: Layer 3 Transcript Storage, TITANS Observer, Build Fixes

Good architectural scope — the 3-layer progressive search is a solid design. The build fixes (glob → Bun.Glob, esbuild externals) are clean. Here are my findings, prioritized by severity.


🔴 Critical

1. promptNumber logic traps exchanges in a never-flush loop (src/services/transcripts/processor.ts)

flushTranscriptSegment returns false when session.promptNumber === 0, and promptNumber only increments when flush returns true. This is a chicken-and-egg deadlock:

Initial:          promptNumber=0, exchanges=[]
1st session_init: flush called → promptNumber===0 → skip → promptNumber stays 0
user/assistant:   exchanges accumulate with promptNumber=0
2nd session_init: flush called → promptNumber===0 → skip again!

Prompt 1 exchanges are never stored. Fix: unconditionally increment on session_init boundary, flush if there are exchanges:

case 'session_init': {
  await this.flushTranscriptSegment(session);  // returns bool but always runs
  session.promptNumber++;                       // always increment
  ...

And remove the || session.promptNumber === 0 guard from flushTranscriptSegment.


2. handleConversationObserve creates phantom sessions (src/services/worker/http/routes/SessionRoutes.ts:~970)

const sessionDbId = store.createSDKSession(contentSessionId, project || 'unknown', '', undefined);
const session = store.getSessionById(sessionDbId);

createSDKSession creates a new session row if none exists. Calling it as a lookup will silently create ghost sessions for any contentSessionId that isn't in the DB yet (e.g. race condition, out-of-order delivery). Use a read-only lookup instead:

const contentId = store.getContentSessionIdByMemoryId(...); // already added
// or add: store.getSDKSessionByContentId(contentSessionId)

This is a data integrity issue on a critical path — the observations stored will be linked to a phantom session with no real context.


🟠 High

3. No AbortController on fetch — timeout leaks (src/services/worker/OneShotQuery.ts)

withTimeout resolves to null after 30s, but the underlying fetch continues running indefinitely. For a fire-and-forget call that runs on every session boundary, this can accumulate many orphaned HTTP requests. Fix:

const withTimeout = <T>(p: Promise<T>, ms: number): Promise<T | null> => {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  return p.finally(() => clearTimeout(timer)).catch(() => null);
};
// Pass signal to each fetch({ ..., signal: controller.signal })

4. Settings file read on every one-shot query (src/services/worker/OneShotQuery.ts:~62, ~87, ~109)

queryGemini, queryOpenRouter, and queryAnthropic each call SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH) — three file-system reads per invocation. Since OneShotQuery is called from a fire-and-forget background handler that may run concurrently, this could hit the file frequently. Load settings once at the top of oneShotQuery() and pass through.


5. getTranscriptSegment hard-limited to 100 chunks (src/services/sync/ChromaSync.ts:~876)

limit: 100,

A 200k-char conversation produces 100 chunks at 2000 chars each. This silently truncates any transcript over 200k chars. Either document this as a known limit, make it configurable, or increase it substantially. Silent truncation on full mode especially is confusing.


6. No concurrency control on conversation observation (src/services/worker/http/routes/SessionRoutes.ts)

Every session boundary fires a one-shot LLM call in the background. There's no queuing, rate limiting, or max-concurrency guard. A user with many active windows could trigger dozens of simultaneous API calls. Consider a simple semaphore or queue (e.g. p-limit) for the background calls.


🟡 Medium

7. ConversationExchange interface duplicated in two files

Defined identically in both src/services/transcripts/processor.ts and src/sdk/prompts.ts. They'll diverge. Pick one location (likely src/sdk/prompts.ts since it's the canonical type for the prompt contract) and import from there.


8. chunkText splits mid-word (src/utils/tag-stripping.ts:~106)

chunks.push(text.substring(i, i + chunkSize));

This cuts at byte boundary, not word boundary. For vector embeddings this reduces chunk quality — a chunk beginning with "...ndException was throw" has weak semantic coherence. Consider splitting on the nearest whitespace or sentence boundary within a tolerance range.


9. Migration version gap: 24 → 25 skipped (src/services/sqlite/SessionStore.ts)

The new migration registers as version 25, but the previous migration in the constructor (addSessionPlatformSourceColumn) uses version 24. The constructor call order is: version 24 is added, then version 25. This is fine numerically but the plan doc says "Migration 25" while commenting "Add migration 24" — inconsistency in the plan doc vs code. The code itself looks correct.


10. TypeFilter.getObservationTypes() called per operation — potential empty-array edge case

If ModeManager hasn't loaded its mode config yet (e.g. early in startup), getObservationTypes() returns an empty array, causing all type filter calls to silently drop valid observations (since no type matches the empty valid set). Worth adding a guard or lazy-loading the default types.


⚪ Minor

11. handleTranscriptStore doesn't coerce promptNumber to integer

promptNumber from req.body is a JSON number, but calling code could send a float or string. Since it's used as a Chroma metadata field and a URL key, parseInt(promptNumber, 10) or Number(promptNumber) would be safer.

12. wrapHandler wraps fire-and-forget — response already sent before error surface

In handleConversationObserve, res.json() is called first, then the background async runs inside wrapHandler. If wrapHandler catches an exception from the background code and tries to respond, headers are already sent. The inner try-catch prevents this but it's a subtle layering issue worth noting.


✅ Things that look good

  • FileTailer offset fix (watcher.ts): Moving the offset save to AFTER line processing is a correct crash-safety improvement.
  • Migration 25 deduplication: Deleting duplicate rows before creating the UNIQUE index is the right approach for existing installs.
  • stripTranscriptPrivacyTags wrapper: Clean reuse of existing stripping logic.
  • XML escaping in buildConversationObservationPrompt: Proper &amp;, &lt;, &gt; escaping before embedding in XML.
  • INSERT OR IGNORE + return-existing-id: The storeUserPrompt deduplication handling is correct.
  • Bun.Glob migration: Clean substitution; scanSync({ absolute: true, onlyFiles: true }) matches the old globSync({ nodir: true, absolute: true }) semantics.

Tests

The test plan in the PR description has no checked items. Are tests expected in this PR or a follow-up? The behaviors most worth testing given the issues above:

  • First prompt segment stored (exercises the promptNumber increment path)
  • createSDKSession is NOT called from conversation-observe (regression guard)
  • Timeout doesn't leak pending requests

🤖 Generated with Claude Code

…n session end

- Start promptNumber at 1 (not 0) so first prompt is flushable
- Remove dead promptNumber === 0 guard from flushTranscriptSegment
- Add 3-attempt retry with exponential backoff for final flush on session end

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 9, 2026

Code Review: feat: Layer 3 transcript storage, TITANS observer, and build fixes

The architecture concept is solid — a 3-layer progressive search with TITANS-inspired conversation observation is a meaningful improvement to the memory pipeline. The build fix (Bun.Glob, esbuild externals) is clean and necessary. Below are issues worth addressing before merge, organized by severity.


CRITICAL

src/services/worker/OneShotQuery.ts — API key exposed in URL query parameter

const url = `${GEMINI_API_URL}/${model}:generateContent?key=${apiKey}`;

API keys in URLs are logged by servers, proxies, and error reporting tools. Use x-goog-api-key header instead:

headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }

src/services/worker/http/routes/SessionRoutes.ts — Session creation race condition

createSDKSession() + getSessionById() are not atomic. Concurrent requests with the same contentSessionId can create duplicate sessions, scattering observations across them. Worth wrapping in a DB transaction or adding a unique constraint guard at the application layer.

src/services/worker/OneShotQuery.ts — Unhandled .json() rejection

All three provider fetch functions call response.json() without try/catch. A malformed response body throws an uncaught exception through the timeout wrapper. Wrap in try/catch:

let data: any;
try { data = await response.json(); } catch { return null; }

HIGH

src/services/worker/OneShotQuery.ts — Fetch not cancelled on timeout

const withTimeout = <T>(p: Promise<T>): Promise<T | null> =>
  Promise.race([p, new Promise<null>(resolve => setTimeout(() => resolve(null), ONE_SHOT_TIMEOUT_MS))]);

After timeout resolves, the original fetch continues running in the background. Each timed-out TITANS query leaks a connection. Use AbortController:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ONE_SHOT_TIMEOUT_MS);
// pass signal: controller.signal to fetch

src/services/sqlite/SessionStore.tsINSERT OR IGNORE silently returns 0

return existing?.id ?? 0;

If the constraint fires but the fallback SELECT also finds nothing (shouldn't happen, but edge cases exist), this returns 0 and the caller has no way to distinguish failure from a legitimate ID. Should throw or log here rather than return a sentinel that looks valid.

src/services/sync/ChromaSync.ts — Hard-coded limit of 100 chunks silently truncates

Long sessions will be cut off without any warning. At minimum, log a warning when documents.length === 100. Consider pagination or raising the limit substantially.

src/services/sync/ChromaSync.ts — Assumes documents and metadatas arrays are same length

If Chroma returns mismatched arrays, the metadatas[i] fallback to i as chunk_index breaks sort order. Add a length assertion before the loop.

src/services/transcripts/processor.ts — Fire-and-forget error handling

ensureWorkerRunning().then(ready => { ... }).catch(...);

The top-level .then() has no .catch(). If anything throws synchronously inside that callback, it's an unhandled rejection. Chain .catch() at the outer level.

src/services/transcripts/watcher.ts — Offset persisted before all lines confirmed processed

If onLine() fails midway through a batch, the file offset is still advanced, permanently skipping the failed lines on restart. Consider persisting offset only after all lines in the batch complete, or storing a per-line watermark.

src/services/worker/http/routes/SessionRoutes.ts — Exchange array contents not validated

if (!contentSessionId || !exchanges || !Array.isArray(exchanges)) { ... }

This passes for an empty array and for arrays containing malformed objects. Downstream code at line ~3597 accesses exchange.promptNumber, exchange.userText, etc. without guards. Validate structure before processing.


MEDIUM

src/utils/tag-stripping.tschunkSize <= 0 produces infinite loop

for (let i = 0; i < text.length; i += chunkSize)

chunkSize = 0 loops forever; chunkSize < 0 does too. Guard:

if (chunkSize <= 0) throw new Error('chunkSize must be positive');

src/services/sqlite/SessionStore.ts — Migration runs outside transaction

addUserPromptsUniqueIndex() runs DELETE followed by CREATE UNIQUE INDEX without wrapping in a transaction. Two concurrent DatabaseManager instances (possible during restarts) could interleave these operations. Wrap in this.db.transaction(...).

src/services/worker/http/routes/SessionRoutes.tsobservation_id not type-validated

observation_id is used as a DB lookup key but only presence-checked, not validated as a positive integer. Passing "0" or "-1" could produce unexpected behavior.

src/services/worker/search/filters/TypeFilter.ts — Runtime-only type validation

The move from ObservationType[] to string[] is intentional for dynamic modes, but it removes compile-time safety. Add a runtime guard: if getObservationTypes() returns empty, throw rather than silently accepting all types.


Minor

  • docs/context/plan-mem-layer3-titans.md (820 lines) — planning documents committed to the repo. These are useful during development but tend to become stale. Consider moving to a wiki or PR description instead.
  • plugin/scripts/worker-service.cjs — large compiled bundle changes (+280/-222) make the diff hard to audit. If the build output is checked in, worth confirming it matches a clean build from the source changes.

Summary

Severity Count
CRITICAL 3
HIGH 6
MEDIUM 4

The API key URL exposure and session race condition are the most urgent to address before merge. The rest are solid improvements but could be follow-up PRs if needed.

🤖 Reviewed with Claude Code

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/transcripts/processor.ts`:
- Around line 343-345: The retry wait is using a linear backoff (1000 * (attempt
+ 1)) producing 1s,2s,3s; change it to exponential backoff by computing the
delay as 1000 * (2 ** attempt) so retries wait 1s,2s,4s etc.; update the
setTimeout call in the block that references attempt (the Promise wrapping
setTimeout) to use the new exponential formula and keep the existing await/new
Promise structure intact.
- Around line 419-438: The outer fire-and-forget promise starting with
ensureWorkerRunning() can reject and cause an unhandled rejection; wrap the
whole chain in a terminal catch (or convert to an async IIFE with try/catch) so
any rejection from ensureWorkerRunning() is handled. Specifically, after the
ensureWorkerRunning().then(... ) chain attach a .catch(error => {
logger.debug('TRANSCRIPT', 'Conversation observation failed', { error: error
instanceof Error ? error.message : String(error) }); }) or wrap the logic in
(async () => { const ready = await ensureWorkerRunning(); if (!ready) return;
... })().catch(...), ensuring you reference ensureWorkerRunning and
workerHttpRequest/Conversation observation logging in the handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d9dc503-4594-491b-b55a-13b131e6c448

📥 Commits

Reviewing files that changed from the base of the PR and between d7322d9 and c75d543.

📒 Files selected for processing (2)
  • plugin/scripts/worker-service.cjs
  • src/services/transcripts/processor.ts

Comment thread src/services/transcripts/processor.ts
Comment thread src/services/transcripts/processor.ts Outdated
…ction guard

- Change retry backoff from linear to exponential (1s, 2s, 4s)
- Wrap fire-and-forget observe chain in async IIFE with terminal catch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 9, 2026

Code Review

Solid architecture overall — the 3-layer progressive search design is clean, the fire-and-forget pattern for TITANS is correct, and the FileTailer crash-safety fix (offset after processing) is a meaningful correctness improvement. A few issues worth addressing:


Bugs / Correctness

1. createSDKSession used as a lookup — has side effects

In SessionRoutes.ts, handleConversationObserve calls:

const sessionDbId = store.createSDKSession(contentSessionId, project || 'unknown', '', undefined);

This is semantically wrong — the goal is to find an existing session, but createSDKSession may create a new record with an empty memory_session_id. If the session doesn't already exist in the DB, you'll silently get a new orphaned row and then immediately bail out on the !session?.memory_session_id check. A dedicated getSessionByContentSessionId(contentSessionId) lookup would be safer here.

2. unverified_inference type referenced in prompt but not registered

The TITANS prompt template mentions:

TWO FLAVORS, SAME PRIORITY:
- overconfidence: ...
- unverified_inference: ...

...but only overconfidence is added to code.json. Any LLM that emits type: unverified_inference will produce observations that get stored but are invisible to TypeFilter and may confuse the UI. Either add it to code.json or remove the reference from the prompt.

3. !promptNumber validation rejects promptNumber = 0

In handleTranscriptStore:

if (!contentSessionId || !promptNumber || !text) {

promptNumber = 0 is falsy, so it'd get a 400. Since prompt numbers start at 1 this is currently harmless, but using promptNumber === undefined || promptNumber === null (or a Zod schema) is more explicit.


Code Quality

4. ConversationExchange interface is duplicated

It's defined identically in both src/sdk/prompts.ts and src/services/transcripts/processor.ts. The processor imports from sdk/prompts.ts for buildConversationObservationPrompt, so it could simply re-export the type from there. Duplicate interface definitions will drift.

5. chunkText splits on exact character boundaries

chunks.push(text.substring(i, i + chunkSize));

This cuts mid-word and mid-sentence. For semantic vector search, this noticeably degrades recall — a key phrase like "authentication token" split as "authenticatio" / "n token" won't embed well. A simple improvement: scan backward from the boundary for a newline or space before committing the split.

6. stripTranscriptPrivacyTags is an unexplained alias

It just calls stripTagsInternal — same as stripMemoryTagsFromPrompt. This is fine if intentional (reusing the same tag stripping rules), but a comment explaining why a new export is needed rather than reusing the existing one would prevent future confusion. If the rules are truly identical, just re-export stripMemoryTagsFromPrompt under the new name.

7. resolve imported in watcher.ts but unused

import { basename, join, resolve } from 'path';

resolve doesn't appear to be used after the Bun.Glob migration. Minor, but adds noise.


Architecture / Performance

8. handleConversationObserve processes in-handler after sending 202

The background processing runs inside the route handler after res.json(). If wrapHandler catches exceptions, errors that occur after the response is sent may be swallowed silently or cause double-response issues depending on the wrapper's implementation. Consider wrapping the async background work in a dedicated try/catch with a setImmediate / queueMicrotask boundary, or move it to an internal queue/worker pattern consistent with how SDKAgent processes pending_messages.

9. Bun.Glob locks the watcher to Bun

This is intentional per the requirements (Bun is mandatory), but the migration removes Node.js compatibility for watcher.ts entirely. The existing globSync import was a node_modules package that works in both runtimes. Worth documenting this explicitly in the file header so future contributors don't accidentally pull the watcher into a Node-only context.


Positives worth keeping

  • Migration 25 defensively removes duplicates before creating the UNIQUE index — correct for upgrades on existing installs.
  • INSERT OR IGNORE + existing-row lookup in storeUserPrompt is clean.
  • The OneShotQuery provider fallback chain with a 30s timeout is well-structured and doesn't block the main session flow.
  • Separate Chroma collection for transcripts (cm__claude-mem-transcripts) is the right call — keeps the main search index fast and avoids polluting observation search with raw transcript noise.
  • FileTailer offset-after-processing fix closes a real data loss window.

🤖 Generated with Claude Code

@thedotmack thedotmack dismissed stale reviews from coderabbitai[bot], coderabbitai[bot], coderabbitai[bot], coderabbitai[bot], and coderabbitai[bot] April 9, 2026 09:49

All requested changes addressed — CodeRabbit CI passed clean on final push

The metadata column was stored correctly in the database but never
delivered to the viewer through either the SSE streaming path or the
REST pagination path. This adds metadata to ObservationSSEPayload,
includes it in the broadcastObservation call in ResponseProcessor,
and adds o.metadata to the PaginationHelper SELECT query.

Also includes bug fixes from adversarial review:
- GeminiAgent/OpenRouterAgent: push to processingMessageMeta (was missing)
- SessionSearch: guard metadata filters to observations table only
- SessionStore: add missing indexes to migration 27
- MigrationRunner: restart-safe migration via PRAGMA table_info check
- ObservationCard: HTTP/HTTPS protocol allowlist for source URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 10, 2026

PR Review: Layer 3 Transcript Storage, TITANS Observer, and Build Fixes

Good architectural direction overall — the 3-layer progressive search model is clean, and reusing existing TranscriptWatcher/ChromaSync infrastructure is the right call. Here are issues found across the source files, ordered by severity.


🔴 Critical

1. Missing getSessionByContentId() in SessionStore

File: src/services/worker/http/routes/SessionRoutes.ts (handleConversationObserve)

The handler calls store.getSessionByContentId(contentSessionId) but this method doesn't exist in SessionStore.ts. The available methods are getSessionById(id) and getContentSessionIdByMemoryId(memorySessionId) — neither does this reverse lookup.

This will crash at runtime: store.getSessionByContentId is not a function.

Fix: Either add getSessionByContentId(contentSessionId: string) to SessionStore, or restructure using createSDKSession() which returns a DB ID you can pass to getSessionById().


🟠 High

2. Fire-and-forget with silent failure modes

File: src/services/transcripts/processor.tsfireConversationObservation()

Errors are caught at debug level. If ensureWorkerRunning() rejects unexpectedly (vs returning false), or workerHttpRequest() throws a non-response error, these are logged but invisible in production. There's also no visibility into how often observations silently fail.

Suggest upgrading failures to warn level and including the session ID in every log line so failures are traceable.

3. Provider fallthrough masks real failures in OneShotQuery

File: src/services/worker/OneShotQuery.ts

All three provider attempts catch every error at debug level. A timed-out request and an auth failure look identical. When all three fall through, there's no summary log of what was attempted and why each failed.

Suggest logging at warn on failure and distinguishing timeout vs error in the message.


🟡 Medium

4. Missing input validation on handleConversationObserve

File: src/services/worker/http/routes/SessionRoutes.ts

The endpoint validates exchanges is an array but doesn't check:

  • Exchange shape (userText/assistantText exist and are strings)
  • Text length bounds (unbounded text can cause memory pressure in the worker)
  • promptNumber is a valid positive integer
  • Empty array is rejected (currently { status: 'accepted', exchangeCount: 0 })

5. Unvalidated query parameter in queryTranscriptSegment()

File: src/services/sync/ChromaSync.ts

Empty, whitespace-only, or extremely long query strings are passed directly to Chroma without validation. Also, nResults has no upper bound — a caller could pass nResults: 10000.

if (!query || query.trim().length === 0) return [];
nResults = Math.max(1, Math.min(10, nResults));

6. Null safety after getSessionById() call

File: src/services/worker/http/routes/SessionRoutes.ts (line ~1018)

The code checks if (!session?.memory_session_id) then returns, but TypeScript still sees memory_session_id as possibly null/undefined in the storeObservation call below. Use a proper null check (if (!session || !session.memory_session_id)) to narrow the type.

7. Retry logic doesn't distinguish retryable vs permanent failures

File: src/services/transcripts/processor.tshandleSessionEnd() retry loop

The retry loop retries on all failures including permanent ones (bad request, invalid contentSessionId). Exponential backoff on a 400 error wastes time and delays session teardown. Consider returning a status enum ('success' | 'retryable' | 'permanent') from flushTranscriptSegment().

8. Migration 25 not wrapped in a transaction

File: src/services/sqlite/SessionStore.ts

The deduplication DELETE and the CREATE UNIQUE INDEX are separate statements. A crash between them leaves the DB in a partially-migrated state. Wrap both in BEGIN/COMMIT and log the count of deleted rows before deletion (the current deduplication is silent, which makes diagnosing data loss harder).


🟢 Low / Code Quality

9. Incomplete XML escaping in buildConversationObservationPrompt()

File: src/sdk/prompts.ts

escapeXml() handles &, <, > but not " or '. More importantly, ${ex.promptNumber} is interpolated directly into an XML attribute without validation — if promptNumber is somehow a string containing ", it could break the XML structure. Coerce to integer: Number.isInteger(ex.promptNumber) ? ex.promptNumber : 1.

10. chunkText() splits mid-word with no filtering

File: src/utils/tag-stripping.ts

The naive substring(i, i + chunkSize) chunking can split mid-sentence and mid-JSON object, which degrades vector embedding quality. At minimum, filter out whitespace-only chunks. A boundary-aware split (on \n\n paragraphs first) would improve embedding coherence.

11. plugin/scripts/*.cjs and plugin/ui/viewer-bundle.js in diff

These built artifacts are 2000+ lines of minified output in the diff, making review difficult. If these are committed outputs (not gitignored), consider adding a note to the PR description about what build step produces them so reviewers know they're derived.


Summary

# Severity Issue
1 🔴 Critical getSessionByContentId() missing — runtime crash
2 🟠 High Fire-and-forget silently loses conversation observations
3 🟠 High OneShotQuery provider fallthrough hides real errors
4 🟡 Medium No validation on exchange structure/length
5 🟡 Medium Unvalidated Chroma query parameter
6 🟡 Medium Null safety gap after session lookup
7 🟡 Medium Retry loop doesn't skip permanent failures
8 🟡 Medium Migration not transactional, silent deduplication
9 🟢 Low Incomplete XML escaping in prompt builder
10 🟢 Low chunkText() splits without word boundaries
11 🟢 Low Built artifacts in diff make review hard

Issue #1 is a blocker — the conversation observe endpoint will crash on every call. Issues #2-3 mean failures in the TITANS observer will be invisible in production. Recommend fixing those before merging.

🤖 Generated with Claude Code

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thedotmack
Copy link
Copy Markdown
Owner Author

Closing during backlog consolidation. The work from this PR is re-filed as issue #2025 for proper prioritization.

@thedotmack thedotmack closed this Apr 15, 2026
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.

2 participants