Skip to content

fix(memory): improve agent memory reliability#1892

Merged
yyhhyyyyyy merged 1 commit into
devfrom
fix/agent-memory-reliability
Jul 7, 2026
Merged

fix(memory): improve agent memory reliability#1892
yyhhyyyyyy merged 1 commit into
devfrom
fix/agent-memory-reliability

Conversation

@yyhhyyyyyy

@yyhhyyyyyy yyhhyyyyyy commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator
  • Preserve extraction cursors across disabled and disposed races
  • Keep no-model duplicate writes conservative for superseded memories
  • Record injection access only for selected runtime memories
  • Add durable maintenance failure cooldown and abort-safe watermarks
  • Index audit memory references while preserving legacy JSON fallback
  • Tighten archive candidate filtering, keyword stats caching, and health counters
  • Add targeted coverage for memory lifecycle and runtime contracts

Summary by CodeRabbit

  • New Features

    • Improved memory handling by tracking which injected memories were actually used, helping keep prompt-related memory usage more accurate.
    • Added smarter maintenance behavior that limits repeated heavy cleanup work after failures, reducing unnecessary retries.
  • Bug Fixes

    • Fixed memory write and extraction flows so they stop cleanly when memory is disabled or unavailable, avoiding stale updates.
    • Improved archive candidate selection to exclude memories that were already accessed.
    • Made memory audit records more reliable, including better handling during schema updates.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds per-turn memory injection access accounting in the runtime presenter, a keyword-stats TTL cache in retrieval invalidated via a new agent-mutation callback, structured call/failure metrics for maintenance passes with a consolidation failure cooldown, revised provenance-hit and extraction failure semantics, and a memory_ref_id column with migration/backfill in the audit table plus an access-count archive filter.

Changes

Memory Injection Access Accounting

Layer / File(s) Summary
Injection access tracking in runtime presenter
src/main/presenter/agentRuntimePresenter/index.ts
Adds per-turn TTL-bounded tracking of selected memory IDs, records access during system prompt assembly, prunes/evicts old turns, and clears tracking on session destroy.
Runtime port and presenter surface
src/main/presenter/memoryPresenter/core/injectionPort.ts, src/main/presenter/memoryPresenter/index.ts
Adds recordInjectionAccess to MemoryRuntimePort and implements it in MemoryPresenter with ownership verification and batched access recording.
Tests
test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts, test/main/presenter/memoryPresenter.test.ts, test/main/presenter/memorySessionExtractionLock.test.ts
Covers selecting-only semantics, per-session bounding/TTL expiry, ownership filtering, and cursor behavior on ok:false extraction results.

Keyword Stats Caching

Layer / File(s) Summary
Agent mutation callback wiring
src/main/presenter/memoryPresenter/context.ts, src/main/presenter/memoryPresenter/index.ts
Adds an optional mutation callback to MemoryRuntimeContext, invoked on change events, wired to invalidate retrieval keyword stats on writes.
TTL cache in RetrievalService
src/main/presenter/memoryPresenter/services/retrievalService.ts, test/main/presenter/memoryPresenter.test.ts
Adds a TTL-based keyword-stats cache, disables access-hit recording during injection recall, and invalidates cache on cleanup.

Maintenance Failure Cooldown

Layer / File(s) Summary
Structured maintenance result types
src/main/presenter/memoryPresenter/types.ts
Adds MemoryMaintenanceStepResult/MemoryMaintenanceReflectionResult/MemoryMaintenancePersonaResult; removes threshold from MemoryVectorQueryOptions.
Reflection/persona/conflict passes
src/main/presenter/memoryPresenter/services/reflectionService.ts, .../personaService.ts, .../conflictService.ts
Introduces runMaintenanceReflectionPass/runMaintenancePersonaPass and updates runChallengeResolutionPass to return call/failure metrics instead of booleans/nulls.
Consolidation cooldown
src/main/presenter/memoryPresenter/services/maintenanceService.ts, src/main/presenter/memoryPresenter/runtimeConstants.ts, test/main/presenter/memoryPresenter.test.ts
Adds a per-agent failure cooldown, aggregates step stats, writes failure/completed audits, and rolls back consolidation timestamps on total LLM failure.

Write Coordinator Semantics

Layer / File(s) Summary
Provenance-hit and extraction changes
src/main/presenter/memoryPresenter/services/writeCoordinator.ts, src/main/presenter/memoryPresenter/infra/embeddingPipeline.ts
Simplifies duplicate handling to absorbed-only revival and changes extractAndStore to return {ok:false} on disabled/aborted writes; skips empty fts_only requeue.
Tests
test/main/presenter/memoryAdd.test.ts, test/main/presenter/memoryExtraction.test.ts, test/main/presenter/memoryPresenter.test.ts, test/main/presenter/memorySessionExtractionLock.test.ts
Updates expectations for {ok:false} extraction outcomes, superseded no-op duplicates, and backfill requeue skip.

Audit memory_ref_id and Archive Filter

Layer / File(s) Summary
Schema migration and backfill
src/main/presenter/sqlitePresenter/tables/agentMemoryAudit.ts, src/main/presenter/sqlitePresenter/schemaCatalog.ts, test/main/presenter/agentMemoryTable.test.ts
Bumps schema to v38, adds memory_ref_id column with backfill SQL and derivation logic, wired into repair hook.
Forget-event queries
src/main/presenter/sqlitePresenter/tables/agentMemoryAudit.ts
Updates listForgetEventRows/hasForgetEventFromRows to use memory_ref_id with JSON fallback.
Archive access_count filter
src/main/presenter/sqlitePresenter/tables/agentMemory.ts, test/main/presenter/fakes/memoryFakes.ts, test/main/presenter/agentMemoryTable.test.ts
Adds access_count = 0 predicate to archive candidate selection and updates fakes/tests.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentRuntimePresenter
  participant MemoryRuntimePort
  participant MemoryPresenter
  participant Repository
  AgentRuntimePresenter->>AgentRuntimePresenter: assemble system prompt, select memories for turn
  AgentRuntimePresenter->>MemoryRuntimePort: recordInjectionAccess(agentId, memoryIds, accessedAt)
  MemoryRuntimePort->>MemoryPresenter: recordInjectionAccess(agentId, memoryIds, accessedAt)
  MemoryPresenter->>MemoryPresenter: check canReadAgentMemory, dedupe IDs
  MemoryPresenter->>Repository: listByIds(memoryIds)
  Repository-->>MemoryPresenter: owned rows
  MemoryPresenter->>Repository: recordAccessBatch(ownedIds, accessedAt)
Loading
sequenceDiagram
  participant MaintenanceService
  participant ReflectionService
  participant PersonaService
  participant ConflictService
  participant AuditLog

  MaintenanceService->>MaintenanceService: check lastConsolidationFailureAt cooldown
  alt within cooldown
    MaintenanceService->>MaintenanceService: run cheap maintenance only
  else not in cooldown
    MaintenanceService->>ConflictService: runChallengeResolutionPass()
    ConflictService-->>MaintenanceService: {touched, calls, failures}
    MaintenanceService->>ReflectionService: runMaintenanceReflectionPass()
    ReflectionService-->>MaintenanceService: {result, calls, failures}
    MaintenanceService->>PersonaService: runMaintenancePersonaPass()
    PersonaService-->>MaintenanceService: {result, calls, failures}
    MaintenanceService->>MaintenanceService: didAllAttemptedLlmCallsFail()
    alt all LLM calls failed
      MaintenanceService->>MaintenanceService: stamp lastConsolidationFailureAt, rollback
      MaintenanceService->>AuditLog: write memory/maintenance_llm failed
    else success
      MaintenanceService->>AuditLog: write memory/maintenance_llm completed
    end
  end
Loading

Possibly related PRs

  • ThinkInAIXYZ/deepchat#1794: Both PRs touch the memory-injection/system-prompt assembly flow in agentRuntimePresenter/index.ts, with this PR extending it via per-turn injection access accounting.
  • ThinkInAIXYZ/deepchat#1871: Both PRs touch the memory recall keyword-stats hot path, this PR adding TTL-based caching/invalidation atop related keyword recall infrastructure.
  • ThinkInAIXYZ/deepchat#1889: Both PRs modify the consolidation/maintenance control flow in maintenanceService.ts.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the PR’s broad goal of improving agent memory reliability.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-memory-reliability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
src/main/presenter/memoryPresenter/services/conflictService.ts (1)

160-206: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

runChallengeResolutionPass can silently drop partial call/failure stats on exception.

Unlike runMaintenanceReflectionPass/runMaintenancePersonaPass, which wrap their entire body in try/catch to always return a structured result, here only the generateText call is guarded. If listConflicts or resolveConflict (DB transaction) throws mid-loop, the function rejects instead of returning {touched, calls, failures}. The caller in maintenanceService.ts then skips addLlmStats for this step entirely, so any calls/failures already accumulated are lost from the aggregate used by didAllAttemptedLlmCallsFail — undermining the new failure-cooldown's accuracy and potentially letting the whole consolidation pass be marked "completed" despite an actual error.

🛡️ Proposed fix: guard the whole pass like reflection/persona services
   async runChallengeResolutionPass(
     agentId: string,
     model: MemoryModelRef
   ): Promise<MemoryMaintenanceStepResult> {
     let touched = false
     let calls = 0
     let failures = 0
-    for (const pair of this.listConflicts(agentId)) {
-      const promptCandidate = normalizeMemoryCandidate({
-        kind: pair.challenger.kind === 'episodic' ? 'episodic' : 'semantic',
-        category: pair.challenger.category,
-        content: pair.challenger.content,
-        importance: pair.challenger.importance
-      })
-      if (!promptCandidate) continue
-      const prompt = buildDecisionPrompt(promptCandidate, [{ content: pair.target.content }])
-      let decision: MemoryDecision = ADD_DECISION
-      try {
-        calls += 1
-        const raw = await this.ctx.deps.generateText(model.providerId, model.modelId, prompt)
-        decision = parseDecision(raw, 1)
-      } catch (error) {
-        failures += 1
-        logger.warn(`[Memory] challenge decision failed: ${String(error)}`)
-        continue
-      }
-      if (!this.ctx.canWriteAgentMemory(agentId)) break
-      const outcome: MemoryConflictResolution =
-        decision.decision === 'NOOP'
-          ? 'keep_target'
-          : decision.decision === 'UPDATE' || decision.decision === 'SUPERSEDE'
-            ? 'keep_challenger'
-            : 'keep_both'
-      const mergedContent =
-        decision.decision === 'UPDATE' || decision.decision === 'SUPERSEDE'
-          ? decision.mergedContent
-          : null
-      if (
-        await this.resolveConflict(agentId, pair.challenger.id, outcome, 'scheduler', model, {
-          mergedContent
-        })
-      ) {
-        touched = true
-      }
+    try {
+      for (const pair of this.listConflicts(agentId)) {
+        const promptCandidate = normalizeMemoryCandidate({
+          kind: pair.challenger.kind === 'episodic' ? 'episodic' : 'semantic',
+          category: pair.challenger.category,
+          content: pair.challenger.content,
+          importance: pair.challenger.importance
+        })
+        if (!promptCandidate) continue
+        const prompt = buildDecisionPrompt(promptCandidate, [{ content: pair.target.content }])
+        let decision: MemoryDecision = ADD_DECISION
+        try {
+          calls += 1
+          const raw = await this.ctx.deps.generateText(model.providerId, model.modelId, prompt)
+          decision = parseDecision(raw, 1)
+        } catch (error) {
+          failures += 1
+          logger.warn(`[Memory] challenge decision failed: ${String(error)}`)
+          continue
+        }
+        if (!this.ctx.canWriteAgentMemory(agentId)) break
+        const outcome: MemoryConflictResolution =
+          decision.decision === 'NOOP'
+            ? 'keep_target'
+            : decision.decision === 'UPDATE' || decision.decision === 'SUPERSEDE'
+              ? 'keep_challenger'
+              : 'keep_both'
+        const mergedContent =
+          decision.decision === 'UPDATE' || decision.decision === 'SUPERSEDE'
+            ? decision.mergedContent
+            : null
+        if (
+          await this.resolveConflict(agentId, pair.challenger.id, outcome, 'scheduler', model, {
+            mergedContent
+          })
+        ) {
+          touched = true
+        }
+      }
+    } catch (error) {
+      logger.warn(`[Memory] challenge resolution pass aborted for ${agentId}: ${String(error)}`)
     }
     return { touched, calls, failures }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/memoryPresenter/services/conflictService.ts` around lines
160 - 206, The issue is that runChallengeResolutionPass only catches errors
around generateText, so exceptions from listConflicts or resolveConflict can
abort the pass and lose the accumulated calls/failures result. Update
conflictService.ts so runChallengeResolutionPass is wrapped in the same kind of
top-level try/catch used by runMaintenanceReflectionPass and
runMaintenancePersonaPass, and always return a structured
MemoryMaintenanceStepResult with the partial counts even when an unexpected
error occurs. Keep the existing per-call try/catch for generateText, but ensure
any failure in the full loop still logs and resolves to { touched, calls,
failures }.
🧹 Nitpick comments (5)
test/main/presenter/fakes/memoryFakes.ts (1)

786-791: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fake's memory_ref_id derivation diverges from the real deriveMemoryRefId/extractSingleMemoryRef contract for empty/whitespace memoryId.

The real implementation (agentMemoryAudit.ts) trims the value and treats an empty/whitespace-only string as absent, falling through to inputRefs. This fake only checks typeof === 'string', so an empty-string outputRefs.memoryId would set memory_ref_id to '' instead of falling back to inputRefs.memoryId (or null), unlike production behavior. Low risk today, but it silently diverges from the upstream contract this fake is meant to mirror.

🔧 Suggested fix to mirror the real trimming/fallback behavior
+      memory_ref_id: (() => {
+        const output = typeof input.outputRefs?.memoryId === 'string' ? input.outputRefs.memoryId.trim() : ''
+        if (output) return output
+        const inputRef = typeof input.inputRefs?.memoryId === 'string' ? input.inputRefs.memoryId.trim() : ''
+        return inputRef || null
+      })(),
-      memory_ref_id:
-        typeof input.outputRefs?.memoryId === 'string'
-          ? input.outputRefs.memoryId
-          : typeof input.inputRefs?.memoryId === 'string'
-            ? input.inputRefs.memoryId
-            : null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/fakes/memoryFakes.ts` around lines 786 - 791, The
`memory_ref_id` fallback logic in `memoryFakes.ts` does not match the real
`deriveMemoryRefId`/`extractSingleMemoryRef` behavior for empty or
whitespace-only `memoryId` values. Update the fake’s `memory_ref_id` derivation
to mirror the production contract by trimming `outputRefs.memoryId` and
`inputRefs.memoryId`, treating blank strings as absent, and only selecting a
value when the trimmed result is non-empty before falling back to the next
source or `null`.
src/main/presenter/sqlitePresenter/tables/agentMemory.ts (1)

1220-1232: 🧹 Nitpick | 🔵 Trivial

access_count = 0 filter and docstring update are consistent with listArchiveCandidateLifecycleRows/countArchiveCandidates.

Worth flagging as an operational consideration: with the PR's new per-turn injection access accounting, access_count now increments whenever a memory is selected for injection (not just direct recall), so any memory that's ever been surfaced once will permanently fail this access_count = 0 filter and become non-archivable regardless of subsequent staleness/decay. This is likely intentional (never archive something that was actually used), but combined with a long-lived agent this could let the working set grow unbounded over time since a large share of memories will accumulate at least one injection hit. Worth confirming this tradeoff is intended and that there's a separate reclamation path (e.g., decay-based demotion) for "accessed-once-then-stale" memories.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/sqlitePresenter/tables/agentMemory.ts` around lines 1220 -
1232, The archive-candidate selection in listArchiveCandidates now excludes any
row with access_count > 0, which means the new per-turn injection access
accounting can make once-used memories permanently ineligible for archiving.
Review whether this is the intended policy; if not, relax the access_count = 0
constraint or add a separate age/decay-based path so “accessed-once-then-stale”
memories can still be reclaimed while keeping the behavior in
listArchiveCandidateLifecycleRows and countArchiveCandidates consistent.
src/main/presenter/memoryPresenter/services/maintenanceService.ts (1)

422-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant duplicate counters in mergeNearDuplicates.

Local calls/touched mirror result.calls/result.touched, always updated together (e.g. lines 473-474, 518-519). The final result.touched = result.touched || touched at line 536 is a no-op since both are already equal. Consider dropping the local shadows and using result.calls/result.touched directly for the loop-bound check and mutations.

Also applies to: 473-474, 518-519, 536-536

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/memoryPresenter/services/maintenanceService.ts` around
lines 422 - 427, In mergeNearDuplicates, remove the redundant local calls and
touched counters that shadow result.calls and result.touched. Update the
loop-bound check and all mutations to use result.calls and result.touched
directly, and delete the final result.touched = result.touched || touched
assignment since it is redundant. Keep the existing logic around merged,
inputTokens, and lastScanned unchanged.
test/main/presenter/memorySessionExtractionLock.test.ts (1)

121-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test exercises a local reimplementation, not the production cursor logic.

consumeSpan here is a standalone closure that reimplements the "advance only on ok:true" rule; it doesn't call into the actual extraction/cursor code (e.g., runMemoryExtraction in agentRuntimePresenter/index.ts). A regression in the real implementation wouldn't be caught by this test. The equivalent behavior is already covered against real code in agentRuntimePresenter.test.ts ("keeps the cursor unchanged when extraction returns ok:false").

If this file has infrastructure to invoke the real per-session lock/extraction path, prefer wiring this assertion to it instead of a local reimplementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/memorySessionExtractionLock.test.ts` around lines 121 -
134, This test is only checking a local `consumeSpan` closure instead of the
real extraction/cursor path. Replace the standalone reimplementation with an
assertion against the production flow, ideally through `runMemoryExtraction` or
the existing per-session lock/extraction helper used by
`agentRuntimePresenter/index.ts`, so the test verifies the actual cursor
advancement behavior on `ok:true` and `ok:false` results.
src/main/presenter/agentRuntimePresenter/index.ts (1)

2466-2537: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Per-session prune/clear scans the entire flat map, not just the session's entries.

memoryInjectionAccessByTurn is a single flat Map for all sessions, keyed by sessionId\u0000messageId. Both pruneMemoryInjectionAccessForSession (called up to twice per recordMemoryInjectionAccess invocation) and clearMemoryInjectionAccessForSession iterate every entry in the map and filter by string-prefix match, so cost scales with the total number of tracked turns across all active sessions (up to 128 × concurrent-session-count), not just the current session. With many concurrent memory-enabled sessions this becomes a per-turn O(N) hot-path cost.

Consider nesting by session (Map<sessionId, Map<messageId, entry>>) so prune/clear only touch the current session's sub-map.

♻️ Sketch of nested-map refactor
-  private readonly memoryInjectionAccessByTurn = new Map<string, MemoryInjectionAccessTurnEntry>()
+  private readonly memoryInjectionAccessBySession = new Map<
+    string,
+    Map<string, MemoryInjectionAccessTurnEntry>
+  >()

Then pruneMemoryInjectionAccessForSession/clearMemoryInjectionAccessForSession operate only on memoryInjectionAccessBySession.get(sessionId).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/index.ts` around lines 2466 - 2537,
The per-session cleanup in recordMemoryInjectionAccess is scanning the entire
flat memoryInjectionAccessByTurn map, which makes
pruneMemoryInjectionAccessForSession and clearMemoryInjectionAccessForSession
scale with all sessions instead of just one. Refactor the storage in
AgentRuntimePresenter to a nested Map keyed by sessionId, then messageId, and
update memoryInjectionAccessKey usage so pruneMemoryInjectionAccessForSession
and clearMemoryInjectionAccessForSession only iterate the current session’s
sub-map. Keep the existing dedupe/tracking behavior in
recordMemoryInjectionAccess and the touchedAt/TTL logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/presenter/memoryPresenter/services/conflictService.ts`:
- Around line 160-206: The issue is that runChallengeResolutionPass only catches
errors around generateText, so exceptions from listConflicts or resolveConflict
can abort the pass and lose the accumulated calls/failures result. Update
conflictService.ts so runChallengeResolutionPass is wrapped in the same kind of
top-level try/catch used by runMaintenanceReflectionPass and
runMaintenancePersonaPass, and always return a structured
MemoryMaintenanceStepResult with the partial counts even when an unexpected
error occurs. Keep the existing per-call try/catch for generateText, but ensure
any failure in the full loop still logs and resolves to { touched, calls,
failures }.

---

Nitpick comments:
In `@src/main/presenter/agentRuntimePresenter/index.ts`:
- Around line 2466-2537: The per-session cleanup in recordMemoryInjectionAccess
is scanning the entire flat memoryInjectionAccessByTurn map, which makes
pruneMemoryInjectionAccessForSession and clearMemoryInjectionAccessForSession
scale with all sessions instead of just one. Refactor the storage in
AgentRuntimePresenter to a nested Map keyed by sessionId, then messageId, and
update memoryInjectionAccessKey usage so pruneMemoryInjectionAccessForSession
and clearMemoryInjectionAccessForSession only iterate the current session’s
sub-map. Keep the existing dedupe/tracking behavior in
recordMemoryInjectionAccess and the touchedAt/TTL logic unchanged.

In `@src/main/presenter/memoryPresenter/services/maintenanceService.ts`:
- Around line 422-427: In mergeNearDuplicates, remove the redundant local calls
and touched counters that shadow result.calls and result.touched. Update the
loop-bound check and all mutations to use result.calls and result.touched
directly, and delete the final result.touched = result.touched || touched
assignment since it is redundant. Keep the existing logic around merged,
inputTokens, and lastScanned unchanged.

In `@src/main/presenter/sqlitePresenter/tables/agentMemory.ts`:
- Around line 1220-1232: The archive-candidate selection in
listArchiveCandidates now excludes any row with access_count > 0, which means
the new per-turn injection access accounting can make once-used memories
permanently ineligible for archiving. Review whether this is the intended
policy; if not, relax the access_count = 0 constraint or add a separate
age/decay-based path so “accessed-once-then-stale” memories can still be
reclaimed while keeping the behavior in listArchiveCandidateLifecycleRows and
countArchiveCandidates consistent.

In `@test/main/presenter/fakes/memoryFakes.ts`:
- Around line 786-791: The `memory_ref_id` fallback logic in `memoryFakes.ts`
does not match the real `deriveMemoryRefId`/`extractSingleMemoryRef` behavior
for empty or whitespace-only `memoryId` values. Update the fake’s
`memory_ref_id` derivation to mirror the production contract by trimming
`outputRefs.memoryId` and `inputRefs.memoryId`, treating blank strings as
absent, and only selecting a value when the trimmed result is non-empty before
falling back to the next source or `null`.

In `@test/main/presenter/memorySessionExtractionLock.test.ts`:
- Around line 121-134: This test is only checking a local `consumeSpan` closure
instead of the real extraction/cursor path. Replace the standalone
reimplementation with an assertion against the production flow, ideally through
`runMemoryExtraction` or the existing per-session lock/extraction helper used by
`agentRuntimePresenter/index.ts`, so the test verifies the actual cursor
advancement behavior on `ok:true` and `ok:false` results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3be57d80-eec6-405a-acb7-df5e0f9ba500

📥 Commits

Reviewing files that changed from the base of the PR and between 0bd5fb0 and e4567fb.

📒 Files selected for processing (23)
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/memoryPresenter/context.ts
  • src/main/presenter/memoryPresenter/core/injectionPort.ts
  • src/main/presenter/memoryPresenter/index.ts
  • src/main/presenter/memoryPresenter/infra/embeddingPipeline.ts
  • src/main/presenter/memoryPresenter/runtimeConstants.ts
  • src/main/presenter/memoryPresenter/services/conflictService.ts
  • src/main/presenter/memoryPresenter/services/maintenanceService.ts
  • src/main/presenter/memoryPresenter/services/personaService.ts
  • src/main/presenter/memoryPresenter/services/reflectionService.ts
  • src/main/presenter/memoryPresenter/services/retrievalService.ts
  • src/main/presenter/memoryPresenter/services/writeCoordinator.ts
  • src/main/presenter/memoryPresenter/types.ts
  • src/main/presenter/sqlitePresenter/schemaCatalog.ts
  • src/main/presenter/sqlitePresenter/tables/agentMemory.ts
  • src/main/presenter/sqlitePresenter/tables/agentMemoryAudit.ts
  • test/main/presenter/agentMemoryTable.test.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/fakes/memoryFakes.ts
  • test/main/presenter/memoryAdd.test.ts
  • test/main/presenter/memoryExtraction.test.ts
  • test/main/presenter/memoryPresenter.test.ts
  • test/main/presenter/memorySessionExtractionLock.test.ts

@yyhhyyyyyy yyhhyyyyyy merged commit c2540c6 into dev Jul 7, 2026
3 checks passed
@yyhhyyyyyy yyhhyyyyyy deleted the fix/agent-memory-reliability branch July 7, 2026 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant