Skip to content

feat: durable candidates MVP + scoped traceable recall + Codex archive watcher#1055

Open
MarcoLissitzky wants to merge 18 commits into
rohitg00:mainfrom
MarcoLissitzky:feat/codex-windows-archive-recall
Open

feat: durable candidates MVP + scoped traceable recall + Codex archive watcher#1055
MarcoLissitzky wants to merge 18 commits into
rohitg00:mainfrom
MarcoLissitzky:feat/codex-windows-archive-recall

Conversation

@MarcoLissitzky

@MarcoLissitzky MarcoLissitzky commented Jul 13, 2026

Copy link
Copy Markdown

What

Three layered capabilities, same infrastructure:

P1: Durable Candidates

  • Archive processing extracts candidate memories from past sessions (POST /agentmemory/archive/process)
  • Explicit, idempotent promote flow (POST /agentmemory/durable-candidates/promote)
  • Dry-run backfill from existing observations
  • Source-observation provenance tracking

P2: Scoped Traceable Recall

  • Centralized recall engine (src/recall/core.ts) — all search paths route through it
  • Every recall produces a RecallTrace with selected/dropped items and structured reasons
  • Project/repo/checkout identity fingerprinting for scope-aware recall
  • Recall debug endpoints and two new invocable skills: recall-debug, why-memory
  • Dedup guard via recall injection ledger

Codex Archive Watcher

  • PowerShell watcher polls ~/.codex/archived_sessions/*.jsonl every 30s
  • Detects new archived sessions, forwards them to agentmemory via POST /agentmemory/session/end
  • Tracks seen files in ~/.agentmemory/archive-watcher-state.json for crash recovery
  • Idempotent seed phase on first run, incremental afterwards
  • Session end → summarizeSession → durable candidates extraction

CodeRabbit review fixes (round-trip from PR #1050)

  • Identity path traversal guard (src/recall/identity.ts: validate cwd against allowed root)
  • Trace-store atomic updates (state::update + increment instead of get/recompute/set)
  • Viewer XSS: escaped dropped-item values
  • Observe rollback: decrementImageRef (was incorrectly reverted to deleteImage)
  • Pre-compact error isolation: context-epoch failure does not block context fetch
  • Hard budget boundaries enforced in recall core
  • CJK tokenizer broadened to cover Hangul + fullwidth forms

Verification

npm run build    # clean
npm test         # 1422 passed / 40 pre-existing failures (same on main)
npm run skills:gen  # no drift

Known limitations (P1)

Archive corpus alignment is not yet resolved — see docs/p1-durable-candidates-handoff-2026-07-11.md.

Closes #1050

Summary by CodeRabbit

  • New Features

    • Added scoped, traceable recall with configurable context budgets, search behavior, injection controls, and recall debugging.
    • Added durable candidate workflows for listing, recommendations, promotion, backfill, and archive processing.
    • Added archive watching with background/one-time modes and Windows Scheduled Task installation.
    • Added recall trace visibility in the viewer and expanded search scoping options.
    • Added build information reporting and Codex archive ingestion support.
  • Documentation

    • Updated setup, configuration, API, MCP, skills, and integration documentation.
  • Tests

    • Added coverage for recall, archive watching, durable candidates, identity safety, replay imports, and viewer security.

Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Update all documentation to reflect the 6-commit feature branch:
- AGENTS.md: stats bump (184 src files, ~42,400 LOC, 1,500+ tests, 296 functions,
  58 KV scopes), new recall code pattern section
- README.md: stat line update, skills catalog (10 invocable + 7 reference),
  scoped traceable recall section, recall TOML config, durable candidates
  env vars, stale skill counts (15→17, 8→10 invocable)
- plugin.json: refresh description with recall capabilities
- Skill reference docs regenerated via npm run skills:gen

All 6 tool-count-consistency tests pass.

Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
Signed-off-by: Jenq Shiin Haw <1343528669@qq.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@MarcoLissitzky is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This change adds durable-candidate archive workflows, scoped traceable recall, recall configuration, archive watching, deterministic replay handling, new API/MCP/hook integrations, runtime build metadata, viewer trace inspection, documentation, and extensive tests.

Changes

Durable candidates and archive ingestion

Layer / File(s) Summary
Candidate extraction and lifecycle
src/functions/durable-candidate-utils.ts, src/functions/durable-candidates.ts, src/functions/summarize.ts, src/types.ts
Durable candidates are normalized, deduplicated, summarized, listed, recommended, backfilled, promoted, and persisted with provenance metadata.
Archive watcher and replay
src/cli/archive-watcher.ts, src/functions/replay.ts, plugin/scripts/*archive-watcher*
Archive files are monitored, stabilized, health-checked, imported idempotently, retried, and forwarded to archive processing.
Archive validation
test/archive-watcher.test.ts, test/durable-candidates.test.ts, test/replay-import-key.test.ts
Tests cover watcher polling, retries, path safety, archive idempotency, zero-observation imports, and summary retry behavior.

Scoped recall and observability

Layer / File(s) Summary
Recall engine
src/recall/*, src/state/hybrid-search.ts
Recall applies scope checks, token budgets, ranking, duplicate-injection prevention, retrieval-channel health, and persisted selection/drop traces.
Identity, configuration, and storage
src/config.ts, src/state/schema.ts, src/state/kv.ts, src/types.ts
Recall configuration loads from TOML and environment overrides, while identity, trace, statistics, ledger, and recommendation storage contracts are added.
API and integrations
src/triggers/api.ts, src/functions/context.ts, src/functions/enrich.ts, src/mcp/server.ts, src/index.ts
Context, enrichment, MCP searches, session start, debug routes, durable-candidate routes, and memory writes use the centralized recall flow.
Viewer and skills
src/viewer/index.html, plugin/skills/recall-debug/*, plugin/skills/why-memory/*
Recall traces can be listed and inspected in the viewer and through new operational skills.

Replay, build, and documentation

Layer / File(s) Summary
Deterministic replay and runtime metadata
src/replay/jsonl-parser.ts, scripts/build-runtime-assets.mjs, src/build-info.ts, package.json
JSONL observations receive stable identifiers, and builds produce loadable metadata and artifact hashes.
Documentation and generated references
README.md, AGENTS.md, INSTALL_FOR_AGENTS.md, plugin/skills/*/REFERENCE.md, docs/*, P2_FINAL_ACCEPTANCE.md
Installation, configuration, API, MCP, recall, archive, continuity, and acceptance documentation are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also adds a Codex archive watcher and several unrelated hardening, benchmark, and docs updates not requested by #1050. Split the watcher and unrelated review-fix/docs-only changes into separate PRs, or add them to the issue scope if they are intended.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main additions: durable candidates, scoped traceable recall, and the archive watcher.
Linked Issues check ✅ Passed The PR covers durable candidate archive processing, idempotent promotion/backfill, scoped recall, traces, identity filtering, debug skills/endpoints, and docs sync.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (3)
src/functions/observe.ts (1)

183-194: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Image reference count leaks in the error rollback path.

incrementImageRef(kv, filePath) is called at line 158, but the catch block at lines 182-192 calls deleteImage without a corresponding decrementImageRef. This leaves a stale entry in KV.imageRefs with count > 0 for a file that no longer exists.

🛡️ Proposed fix: add decrementImageRef to the rollback path
         } catch (error) {
           if (raw.imageData) {
+            const { decrementImageRef } = await import("./image-refs.js");
+            await decrementImageRef(kv, raw.imageData);
             const { deleteImage } = await import("../utils/image-store.js");
             const { deletedBytes } = await deleteImage(raw.imageData);
             if (deletedBytes > 0) {
               sdk.trigger({
                 function_id: "mem::disk-size-delta",
                 payload: { deltaBytes: -deletedBytes },
                 action: TriggerAction.Void(),
               });
             }
           }
           throw error;
         }
🤖 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/functions/observe.ts` around lines 183 - 194, Update the error rollback
path in the observe handler to call decrementImageRef for the same file path
after incrementImageRef has been performed, before or alongside deleteImage.
Preserve the existing deletedBytes disk-size trigger and rethrow behavior.
INSTALL_FOR_AGENTS.md (1)

103-113: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix duplicate step 7 numbering.

Inserting the new step 6 shifted "Install native skills" to step 7 (line 103), but the existing "Verify a save and recall round-trip" section (line 113) was not renumbered to step 8. Both sections are now numbered "7".

📝 Proposed fix
-## 7. Verify a save and recall round-trip
+## 8. Verify a save and recall round-trip
🤖 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 `@INSTALL_FOR_AGENTS.md` around lines 103 - 113, Renumber the “Verify a save
and recall round-trip” section heading following “Install native skills” from
step 7 to step 8, leaving the installation section and its content unchanged.
plugin/scripts/pre-tool-use.mjs (1)

62-74: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Thread cwd into enrich identity resolution cwd is posted here, but api::enrich drops it and only forwards sessionId, files, terms, toolName, and project to mem::enrich. That means enrich calls never get repo/checkout scoping from the worktree path; resolve repoId/checkoutId from cwd here or remove the field.

🤖 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 `@plugin/scripts/pre-tool-use.mjs` around lines 62 - 74, The enrich request
currently sends cwd, but the downstream api::enrich flow does not use it for
identity resolution. Update the api::enrich-to-mem::enrich forwarding path to
resolve and pass repoId and checkoutId from cwd, preserving project and the
existing enrich fields; alternatively remove cwd from this request if identity
scoping cannot be supported.
🧹 Nitpick comments (12)
src/state/hybrid-search.ts (1)

128-161: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Graph mode reporting can be inconsistent on partial failures.

If searchByEntities fails but expandFromChunks succeeds (or vice versa), graphMode is overwritten to disabled even though graphResults contains entries from the successful call. This misleads trace/debug output. Consider only marking graph as disabled when no graph results were obtained.

♻️ Suggested fix: only disable graph mode when no results exist
     } catch (err) {
-      graphMode = {
-        status: "disabled",
-        attempted: true,
-        reason: err instanceof Error ? err.message : "graph retrieval failed",
-        fallback: "BM25/vector",
-      };
+      if (graphResults.length === 0) {
+        graphMode = {
+          status: "disabled",
+          attempted: true,
+          reason: err instanceof Error ? err.message : "graph retrieval failed",
+          fallback: "BM25/vector",
+        };
+      }
     }
   }
 
   const topVectorObs = vectorResults.slice(0, 5).map((r) => r.obsId);
   if (topVectorObs.length > 0) {
     try {
       const expansionResults =
         await this.graphRetrieval.expandFromChunks(topVectorObs, 1, 5);
       graphResults = [...graphResults, ...expansionResults];
     } catch (err) {
-      graphMode = {
-        status: "disabled",
-        attempted: true,
-        reason: err instanceof Error ? err.message : "graph expansion failed",
-        fallback: "BM25/vector",
-      };
+      if (graphResults.length === 0) {
+        graphMode = {
+          status: "disabled",
+          attempted: true,
+          reason: err instanceof Error ? err.message : "graph expansion failed",
+          fallback: "BM25/vector",
+        };
+      }
     }
   }
🤖 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/state/hybrid-search.ts` around lines 128 - 161, Update the error handling
in the graph retrieval calls within the hybrid search flow so graphMode is set
to disabled only when graphResults remains empty after the attempted operations.
Preserve successful results from either searchByEntities or expandFromChunks,
and ensure partial failures do not overwrite a healthy/usable graph status when
entries were obtained.
src/triggers/api.ts (1)

1285-1289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate checkoutId in the mem::remember payload spread.

checkoutId is set twice: line 1286 (body value, else identity fallback) and again line 1289 (body value if defined). Because req.body.checkoutId is already validated as a string at Line 1267, the two branches produce the same value when present, making line 1289 dead/redundant and obscuring the identity-fallback intent. Drop the second spread.

Proposed cleanup
           ...(typeof req.body.checkoutId === "string" ? { checkoutId: req.body.checkoutId } : identity ? { checkoutId: identity.checkoutId } : {}),
           ...(scope !== undefined && { scope }),
           ...(req.body.origin !== undefined && { origin: req.body.origin }),
-          ...(req.body.checkoutId !== undefined && { checkoutId: req.body.checkoutId }),
🤖 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/triggers/api.ts` around lines 1285 - 1289, Remove the redundant
checkoutId spread from the mem::remember payload construction. Keep the earlier
checkoutId selection that uses the validated request-body string with
identity.checkoutId as the fallback, and leave the other payload fields
unchanged.
src/functions/remember.ts (1)

151-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No recordAudit() for this state-changing write.

mem::remember persists a new/superseding memory (and mutates the superseded row), yet unlike mem::forget below it records no audit entry. Per the repo guideline to use recordAudit() for state-changing operations, consider recording an audit event on save. Please confirm whether auditing here is intentionally omitted or handled downstream (e.g., via mem::cascade-update).

🤖 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/functions/remember.ts` around lines 151 - 226, Add a recordAudit() call
to the mem::remember save flow after persisting the new memory and updating any
superseded memory, covering both the creation and supersession state changes.
Use the existing audit conventions and memory identifiers, and do not rely on
mem::cascade-update to audit this write.

Source: Coding guidelines

test/durable-candidates.test.ts (1)

130-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: prefer KV constants over hardcoded scope strings.

The suite mixes KV.summaries/KV.memories/KV.durableRecommendations with hardcoded equivalents like "mem:summaries", "mem:memories", and "mem:obs:<id>". Using the KV helpers consistently keeps the tests resilient if a scope prefix changes.

Also applies to: 173-174, 206-207

🤖 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/durable-candidates.test.ts` around lines 130 - 141, Replace hardcoded
memory scope strings in the durable-candidate tests, including the cases around
the recommendation setup and the referenced sections, with the corresponding KV
constants or helpers such as KV.summaries, KV.memories,
KV.durableRecommendations, and the observation-scope helper. Keep test behavior
unchanged while ensuring all key construction uses the centralized KV
definitions.
src/types.ts (1)

112-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm wouldPromote: false literal is intentional.

DurableCandidateRecommendation fixes wouldPromote to the literal false even for recommendation: "auto_promote_eligible". This aligns with the dry-run posture described in the PR objectives (real promotion deferred pending archive-baseline reconciliation), but the always-false literal means consumers can never branch to a promote action from this type. If a future eligible-and-would-promote state is expected, widen to boolean; otherwise this is fine as an intentional dry-run marker.

🤖 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/types.ts` around lines 112 - 118, Confirm that
DurableCandidateRecommendation is intentionally dry-run only: retain
wouldPromote as the literal false because promotion remains deferred, and
document or otherwise preserve this contract for consumers. If the type must
represent future eligible actions, widen wouldPromote to boolean and update
related recommendation handling accordingly.
src/recall/tokens.ts (1)

3-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

CJK range misses Hangul and astral ideographs, underestimating tokens.

CJK_CHAR only spans BMP ranges up to \u9fff/\ufaff. Korean Hangul (U+AC00–U+D7A3) and CJK Unified Ideographs Extension B+ (U+20000+, iterated as char.length === 2 via for..of) fall into the ASCII branch and are counted at roughly 1/4 token each. For an estimator named conservative-unicode that feeds recall budget enforcement, this under-counts and can let context exceed the intended budget. Consider widening the range to include Hangul and using code-point checks for astral CJK.

♻️ Suggested range extension
-const CJK_CHAR = /[\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff]/u;
+const CJK_CHAR = /[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7a3\uf900-\ufaff\u{20000}-\u{2fa1f}]/u;
🤖 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/recall/tokens.ts` around lines 3 - 25, Update CJK_CHAR and the character
classification in countTokens so Korean Hangul (U+AC00–U+D7A3) and astral CJK
ideographs are treated as individual CJK tokens rather than entering the
asciiRun path. Use code-point-aware checks compatible with for...of iteration,
while preserving the existing ASCII-run batching behavior for non-CJK text.
test/viewer-recall-xss.test.ts (1)

21-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Escaper extraction is tightly coupled to viewer HTML indentation.

The regex /function esc\(s\) \{[\s\S]*?\r?\n \}/ requires the closing brace to be preceded by exactly four spaces. Any reformatting of src/viewer/index.html (indent change, prettier, tabs) will make escSource undefined and fail the test at Line 22 rather than at a meaningful assertion — a brittle coupling to layout rather than behavior. Consider anchoring on a less layout-dependent boundary or extracting the escaper into a shared module the test can import directly.

The new Function(...) OpenGrep hint is a false positive here: the source is read from a trusted in-repo file, not attacker input.

🤖 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/viewer-recall-xss.test.ts` around lines 21 - 23, Make the escaper
extraction in the viewer recall XSS test independent of the exact closing-brace
indentation by anchoring the regex to a stable, layout-insensitive boundary, or
reuse the escaper from a shared importable module. Keep the existing behavioral
assertions and trusted-source handling unchanged.
src/recall/ledger.ts (1)

78-95: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Injection-ledger entries are never pruned.

recordInjection writes one entry per (sessionId, itemId) and nothing reaps stale keys, so KV.injectionLedger grows monotonically as sessions and items accumulate. Consider a retention/TTL sweep (similar to the recall-trace retention config) to bound storage.

🤖 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/recall/ledger.ts` around lines 78 - 95, The recordInjection flow writes
injection-ledger entries without any retention, allowing KV.injectionLedger to
grow indefinitely. Add a retention or TTL cleanup mechanism for stale
InjectionLedgerEntry records, reusing the existing recall-trace retention
configuration and cleanup patterns, while preserving current writes for active
entries.
src/recall/core.ts (2)

103-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider fingerprintId() for content-addressable dedup.

normalizeDuplicate plus a Set<string> hand-rolls content-addressable deduplication. The repo convention is to use fingerprintId() for this, which keeps dedup semantics consistent with the rest of the codebase.

As per coding guidelines: "Use fingerprintId() for content-addressable deduplication and generateId() for unique IDs."

🤖 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/recall/core.ts` around lines 103 - 105, Replace the
normalizeDuplicate-based deduplication with the repository-standard
fingerprintId() in the recall deduplication flow, including updating the Set key
type/usage as needed. Preserve the existing duplicate-detection behavior while
using fingerprintId() for content-addressable identity.

Source: Coding guidelines


375-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize per-hit KV lookups to avoid serial N+1 latency.

Each hit triggers a sequential await this.kv.get<Memory>(...) (and a session lookup), so recall latency scales linearly with the candidate count. Since the per-hit resolutions are independent, resolve them with Promise.all (the session cache still dedupes repeated session IDs).

As per coding guidelines: "Use Promise.all for independent parallel operations where possible."

🤖 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/recall/core.ts` around lines 375 - 396, Update the hit-processing flow
around the loop in the recall method to resolve each hit’s memory and fallback
session concurrently with Promise.all, while preserving the existing candidate
mapping and sessionFor cache behavior. Avoid awaiting KV and session lookups
sequentially inside the per-hit iteration; first resolve independent per-hit
data in parallel, then build candidates with the same fields and scoring.

Source: Coding guidelines

src/eval/schemas.ts (1)

106-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse DurableCandidateTypeEnum instead of re-inlining the type list.

This literal list duplicates DurableCandidateTypeEnum (Line 35-42), which SummaryOutputSchema already reuses. Keeping two copies risks drift.

♻️ Proposed change
-  type: z
-    .enum(["pattern", "preference", "architecture", "bug", "workflow", "fact"])
-    .optional(),
+  type: DurableCandidateTypeEnum.optional(),
🤖 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/eval/schemas.ts` around lines 106 - 108, Update the candidate type field
in the relevant schema to reuse DurableCandidateTypeEnum instead of defining an
inline z.enum list, matching SummaryOutputSchema and keeping the existing
optional behavior.
src/recall/trace-store.ts (1)

98-110: 🚀 Performance & Scalability | 🔵 Trivial

Trace pruning scans the full trace set on every write.

persistRecallTrace calls kv.list over all retained traces and re-sorts/prunes on each write. As recall volume grows this becomes an O(n) read + delete on the hot recall path. Consider pruning periodically (e.g., time-bucketed or every N writes) or maintaining a bounded index rather than enumerating all traces per persist.

🤖 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/recall/trace-store.ts` around lines 98 - 110, Update persistRecallTrace
so it no longer lists, sorts, and deletes the full trace set on every write.
Introduce periodic or threshold-based pruning (or a bounded trace index) while
preserving retentionDays and maxTraces enforcement when pruning runs; keep the
normal persist path limited to storing the new trace.
🤖 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.

Inline comments:
In @.env.example:
- Line 107: Update the AGENTMEMORY_RECALL_ALLOWED_ROOTS example comment to
describe the platform-specific path delimiter used by src/recall/identity.ts:
colon on Unix-like systems and semicolon on Windows, rather than documenting
only the Windows separator.

In `@src/cli/archive-watcher.ts`:
- Around line 381-387: Update classifyResponse() so non-retryable HTTP client
errors, including 401, 403, and other applicable 4xx statuses, return the
terminal archive error class instead of "transient_http". Preserve transient
classification for 5xx, 408, and 429 responses, while retaining the existing
reason-based missing and invalid classifications.

In `@src/functions/durable-candidates.ts`:
- Around line 274-296: Capture a single evaluation timestamp with new
Date().toISOString() before the for-of loop in the durable candidate evaluation
flow, then reuse that value for every recommendation’s evaluatedAt field. Keep
the per-candidate eligibility and persistence logic unchanged.
- Around line 683-775: Align the missing-session-id fallback in parseJsonlText
used by durable-candidates with mem::replay::import-jsonl, replacing
fingerprintId("sess", text) with the same deterministic generateId("sess")
behavior. Ensure both paths derive identical session IDs before the KV.sessions
lookup, while preserving embedded session IDs unchanged.

In `@src/hooks/pre-compact.ts`:
- Around line 51-67: In the pre-compact hook, isolate the context-epoch POST
before the `/agentmemory/context` request by wrapping it in its own best-effort
try/catch. Ensure failures from that epoch advance are handled without escaping
to the outer catch, so the context fetch still executes; keep the existing
context injection behavior and timeout unchanged.

In `@src/triggers/api.ts`:
- Around line 1186-1187: Update the api::remember request body type used by the
surrounding handler to declare the accessed top-level cwd and repoId fields,
alongside the existing fields. Keep the resolveRecallIdentity call using the
top-level cwd and ensure repoId is typed consistently with its usage, rather
than relying only on scope.repoId.

In `@src/types.ts`:
- Around line 386-394: Update the AuditEntry.operation union in src/types.ts to
include the lease_renew operation emitted by the lease renewal flow in
leases.ts, while preserving all existing operation values.

In `@src/viewer/index.html`:
- Line 2434: Update the trace row builder around finalContextTokenCount to
coerce or pass the value through the existing esc() helper before concatenating
it into innerHTML, while preserving the 0 fallback for missing values. Keep the
surrounding token-count rendering unchanged.

---

Outside diff comments:
In `@INSTALL_FOR_AGENTS.md`:
- Around line 103-113: Renumber the “Verify a save and recall round-trip”
section heading following “Install native skills” from step 7 to step 8, leaving
the installation section and its content unchanged.

In `@plugin/scripts/pre-tool-use.mjs`:
- Around line 62-74: The enrich request currently sends cwd, but the downstream
api::enrich flow does not use it for identity resolution. Update the
api::enrich-to-mem::enrich forwarding path to resolve and pass repoId and
checkoutId from cwd, preserving project and the existing enrich fields;
alternatively remove cwd from this request if identity scoping cannot be
supported.

In `@src/functions/observe.ts`:
- Around line 183-194: Update the error rollback path in the observe handler to
call decrementImageRef for the same file path after incrementImageRef has been
performed, before or alongside deleteImage. Preserve the existing deletedBytes
disk-size trigger and rethrow behavior.

---

Nitpick comments:
In `@src/eval/schemas.ts`:
- Around line 106-108: Update the candidate type field in the relevant schema to
reuse DurableCandidateTypeEnum instead of defining an inline z.enum list,
matching SummaryOutputSchema and keeping the existing optional behavior.

In `@src/functions/remember.ts`:
- Around line 151-226: Add a recordAudit() call to the mem::remember save flow
after persisting the new memory and updating any superseded memory, covering
both the creation and supersession state changes. Use the existing audit
conventions and memory identifiers, and do not rely on mem::cascade-update to
audit this write.

In `@src/recall/core.ts`:
- Around line 103-105: Replace the normalizeDuplicate-based deduplication with
the repository-standard fingerprintId() in the recall deduplication flow,
including updating the Set key type/usage as needed. Preserve the existing
duplicate-detection behavior while using fingerprintId() for content-addressable
identity.
- Around line 375-396: Update the hit-processing flow around the loop in the
recall method to resolve each hit’s memory and fallback session concurrently
with Promise.all, while preserving the existing candidate mapping and sessionFor
cache behavior. Avoid awaiting KV and session lookups sequentially inside the
per-hit iteration; first resolve independent per-hit data in parallel, then
build candidates with the same fields and scoring.

In `@src/recall/ledger.ts`:
- Around line 78-95: The recordInjection flow writes injection-ledger entries
without any retention, allowing KV.injectionLedger to grow indefinitely. Add a
retention or TTL cleanup mechanism for stale InjectionLedgerEntry records,
reusing the existing recall-trace retention configuration and cleanup patterns,
while preserving current writes for active entries.

In `@src/recall/tokens.ts`:
- Around line 3-25: Update CJK_CHAR and the character classification in
countTokens so Korean Hangul (U+AC00–U+D7A3) and astral CJK ideographs are
treated as individual CJK tokens rather than entering the asciiRun path. Use
code-point-aware checks compatible with for...of iteration, while preserving the
existing ASCII-run batching behavior for non-CJK text.

In `@src/recall/trace-store.ts`:
- Around line 98-110: Update persistRecallTrace so it no longer lists, sorts,
and deletes the full trace set on every write. Introduce periodic or
threshold-based pruning (or a bounded trace index) while preserving
retentionDays and maxTraces enforcement when pruning runs; keep the normal
persist path limited to storing the new trace.

In `@src/state/hybrid-search.ts`:
- Around line 128-161: Update the error handling in the graph retrieval calls
within the hybrid search flow so graphMode is set to disabled only when
graphResults remains empty after the attempted operations. Preserve successful
results from either searchByEntities or expandFromChunks, and ensure partial
failures do not overwrite a healthy/usable graph status when entries were
obtained.

In `@src/triggers/api.ts`:
- Around line 1285-1289: Remove the redundant checkoutId spread from the
mem::remember payload construction. Keep the earlier checkoutId selection that
uses the validated request-body string with identity.checkoutId as the fallback,
and leave the other payload fields unchanged.

In `@src/types.ts`:
- Around line 112-118: Confirm that DurableCandidateRecommendation is
intentionally dry-run only: retain wouldPromote as the literal false because
promotion remains deferred, and document or otherwise preserve this contract for
consumers. If the type must represent future eligible actions, widen
wouldPromote to boolean and update related recommendation handling accordingly.

In `@test/durable-candidates.test.ts`:
- Around line 130-141: Replace hardcoded memory scope strings in the
durable-candidate tests, including the cases around the recommendation setup and
the referenced sections, with the corresponding KV constants or helpers such as
KV.summaries, KV.memories, KV.durableRecommendations, and the observation-scope
helper. Keep test behavior unchanged while ensuring all key construction uses
the centralized KV definitions.

In `@test/viewer-recall-xss.test.ts`:
- Around line 21-23: Make the escaper extraction in the viewer recall XSS test
independent of the exact closing-brace indentation by anchoring the regex to a
stable, layout-insensitive boundary, or reuse the escaper from a shared
importable module. Keep the existing behavioral assertions and trusted-source
handling unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 612ab0ed-abc8-48a6-adb5-60ac071ae702

📥 Commits

Reviewing files that changed from the base of the PR and between 93ae9bc and 154c657.

📒 Files selected for processing (92)
  • .env.example
  • .gitignore
  • AGENTS.md
  • CODERABBIT_REVIEW_TRIAGE.md
  • INSTALL_FOR_AGENTS.md
  • P2_FINAL_ACCEPTANCE.md
  • README.md
  • docs/continuity-schema-note.md
  • docs/p1-durable-candidates-handoff-2026-07-11.md
  • eval/recall/README.md
  • eval/recall/fixtures/sanitized.json
  • eval/recall/runner.ts
  • eval/recall/score.ts
  • package.json
  • plugin/plugin.json
  • plugin/scripts/archive-watcher-mutex.ps1
  • plugin/scripts/install-archive-watcher.ps1
  • plugin/scripts/notification.mjs
  • plugin/scripts/post-commit.mjs
  • plugin/scripts/post-tool-failure.mjs
  • plugin/scripts/post-tool-use.mjs
  • plugin/scripts/pre-compact.mjs
  • plugin/scripts/pre-tool-use.mjs
  • plugin/scripts/prompt-submit.mjs
  • plugin/scripts/session-end.mjs
  • plugin/scripts/session-start.mjs
  • plugin/scripts/stop.mjs
  • plugin/scripts/subagent-start.mjs
  • plugin/scripts/subagent-stop.mjs
  • plugin/scripts/task-completed.mjs
  • plugin/scripts/uninstall-archive-watcher.ps1
  • plugin/scripts/watch-archived-sessions.ps1
  • plugin/skills/agentmemory-config/REFERENCE.md
  • plugin/skills/agentmemory-mcp-tools/REFERENCE.md
  • plugin/skills/agentmemory-rest-api/REFERENCE.md
  • plugin/skills/recall-debug/SKILL.md
  • plugin/skills/why-memory/SKILL.md
  • scripts/build-runtime-assets.mjs
  • src/build-info.ts
  • src/cli.ts
  • src/cli/archive-watcher.ts
  • src/cli/connect/index.ts
  • src/config.ts
  • src/eval/schemas.ts
  • src/functions/context.ts
  • src/functions/durable-candidate-utils.ts
  • src/functions/durable-candidates.ts
  • src/functions/enrich.ts
  • src/functions/image-quota-cleanup.ts
  • src/functions/observe.ts
  • src/functions/remember.ts
  • src/functions/replay.ts
  • src/functions/slots.ts
  • src/functions/summarize.ts
  • src/hooks/pre-compact.ts
  • src/hooks/pre-tool-use.ts
  • src/hooks/prompt-submit.ts
  • src/index.ts
  • src/mcp/server.ts
  • src/mcp/tools-registry.ts
  • src/prompts/summary.ts
  • src/recall/core.ts
  • src/recall/identity.ts
  • src/recall/ledger.ts
  • src/recall/scope.ts
  • src/recall/tokens.ts
  • src/recall/trace-store.ts
  • src/recall/vector-health.ts
  • src/replay/jsonl-parser.ts
  • src/state/hybrid-search.ts
  • src/state/kv.ts
  • src/state/memory-utils.ts
  • src/state/schema.ts
  • src/triggers/api.ts
  • src/types.ts
  • src/viewer/index.html
  • test/archive-watcher.test.ts
  • test/build-info.test.ts
  • test/durable-candidate-utils.test.ts
  • test/durable-candidates.test.ts
  • test/recall-benchmark-score.test.ts
  • test/recall-config.test.ts
  • test/recall-context-limit.test.ts
  • test/recall-core.test.ts
  • test/recall-identity.test.ts
  • test/recall-trace-store.test.ts
  • test/remember-project-scope.test.ts
  • test/replay-import-key.test.ts
  • test/replay.test.ts
  • test/summarize.test.ts
  • test/vector-retrieval-health.test.ts
  • test/viewer-recall-xss.test.ts

Comment thread .env.example
# AGENTMEMORY_RECALL_MAX_SEMANTIC_TOKENS=600
# AGENTMEMORY_RECALL_UNKNOWN_AUTO_INJECTION=false
# AGENTMEMORY_RECALL_UNKNOWN_EXPLICIT_SEARCH=true
# AGENTMEMORY_RECALL_ALLOWED_ROOTS=~/work;~/src # Semicolon-separated trusted roots for cwd-based repo identity on Windows; defaults to the worker cwd

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find how AGENTMEMORY_RECALL_ALLOWED_ROOTS is parsed in config.ts
rg -n -C5 'AGENTMEMORY_RECALL_ALLOWED_ROOTS|allowedRoots|ALLOWED_ROOTS' --type=ts src/

Repository: rohitg00/agentmemory

Length of output: 3480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## .env.example around line 107\n'
sed -n '100,112p' .env.example

printf '\n## Search for docs mentioning AGENTMEMORY_RECALL_ALLOWED_ROOTS or delimiter wording\n'
rg -n -C2 'AGENTMEMORY_RECALL_ALLOWED_ROOTS|semicolon|delimiter|path.delimiter|trusted roots' --glob '!**/node_modules/**' .

Repository: rohitg00/agentmemory

Length of output: 4363


Use the platform path separator here. src/recall/identity.ts splits AGENTMEMORY_RECALL_ALLOWED_ROOTS with path.delimiter, so this example should say : on Unix and ; on Windows (or just refer to the platform delimiter) instead of only mentioning Windows.

🤖 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 @.env.example at line 107, Update the AGENTMEMORY_RECALL_ALLOWED_ROOTS
example comment to describe the platform-specific path delimiter used by
src/recall/identity.ts: colon on Unix-like systems and semicolon on Windows,
rather than documenting only the Windows separator.

Comment on lines +381 to +387
function classifyResponse(status: number, reason: string): ArchiveErrorClass {
const lower = reason.toLowerCase();
if (lower.includes("missing")) return "missing_archive";
if (lower.includes("invalid") || lower.includes("outside") || lower.includes("symlink")) return "invalid_archive";
if (status >= 500 || status === 408 || status === 429 || status === 401 || status === 403) return "transient_http";
return "transient_http";
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and the types/consumers for archive error handling.
sed -n '340,460p' src/cli/archive-watcher.ts

printf '\n--- usages ---\n'
rg -n "classifyResponse|ArchiveErrorClass|invalid_archive|transient_http|markPending|pending" src/cli/archive-watcher.ts src -g '!**/node_modules/**'

Repository: rohitg00/agentmemory

Length of output: 17390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first, then inspect the surrounding implementation.
ast-grep outline src/cli/archive-watcher.ts --view expanded

printf '\n--- relevant slices ---\n'
sed -n '1,220p' src/cli/archive-watcher.ts
printf '\n--- middle ---\n'
sed -n '220,520p' src/cli/archive-watcher.ts
printf '\n--- tail ---\n'
sed -n '520,760p' src/cli/archive-watcher.ts

Repository: rohitg00/agentmemory

Length of output: 32305


Handle non-retryable HTTP failures as terminal

classifyResponse() always returns "transient_http" for the status branch, so the check is effectively redundant. More importantly, 401/403 (and other 4xx client errors) are retried via markPending() instead of becoming terminal, so a bad auth/configuration can spin forever on backoff. Treat non-retryable client errors as terminal here.

🤖 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/cli/archive-watcher.ts` around lines 381 - 387, Update classifyResponse()
so non-retryable HTTP client errors, including 401, 403, and other applicable
4xx statuses, return the terminal archive error class instead of
"transient_http". Preserve transient classification for 5xx, 408, and 429
responses, while retaining the existing reason-based missing and invalid
classifications.

Comment on lines +274 to +296
for (const candidate of all) {
const reasons: string[] = [];
if (candidate.confidence >= 0.9) reasons.push("confidence >= 0.90");
if (candidate.project) reasons.push("project scope explicit");
if (candidate.sourceObservationIds.length >= 3) reasons.push("3 evidence observations");
const hasConflict = memories.some((memory) =>
memory.isLatest !== false &&
memory.project === candidate.project &&
jaccardSimilarity(memory.content.toLowerCase(), candidate.content.toLowerCase()) > 0.7,
);
if (!hasConflict) reasons.push("no conflicting memory");
if (!isPlanOrTodo(candidate)) reasons.push("not a plan or todo");
const eligible = reasons.length === 5;
const recommendation: DurableCandidateRecommendation = {
candidateId: candidate.id,
recommendation: eligible ? "auto_promote_eligible" : "not_eligible",
reasons,
wouldPromote: false,
evaluatedAt: new Date().toISOString(),
};
await kv.set(KV.durableRecommendations, candidate.id, recommendation);
recommendations.push(recommendation);
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Capture the evaluation timestamp once. evaluatedAt: new Date().toISOString() is recomputed on every loop iteration, producing slightly different timestamps for a single evaluation run. Capture it once before the loop and reuse it.

♻️ Proposed change
       const memories = await kv.list<Memory>(KV.memories);
       const recommendations: DurableCandidateRecommendation[] = [];
+      const evaluatedAt = new Date().toISOString();
       for (const candidate of all) {
-          evaluatedAt: new Date().toISOString(),
+          evaluatedAt,

As per coding guidelines: "Capture timestamps once with new Date().toISOString() and reuse the captured value."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const candidate of all) {
const reasons: string[] = [];
if (candidate.confidence >= 0.9) reasons.push("confidence >= 0.90");
if (candidate.project) reasons.push("project scope explicit");
if (candidate.sourceObservationIds.length >= 3) reasons.push("3 evidence observations");
const hasConflict = memories.some((memory) =>
memory.isLatest !== false &&
memory.project === candidate.project &&
jaccardSimilarity(memory.content.toLowerCase(), candidate.content.toLowerCase()) > 0.7,
);
if (!hasConflict) reasons.push("no conflicting memory");
if (!isPlanOrTodo(candidate)) reasons.push("not a plan or todo");
const eligible = reasons.length === 5;
const recommendation: DurableCandidateRecommendation = {
candidateId: candidate.id,
recommendation: eligible ? "auto_promote_eligible" : "not_eligible",
reasons,
wouldPromote: false,
evaluatedAt: new Date().toISOString(),
};
await kv.set(KV.durableRecommendations, candidate.id, recommendation);
recommendations.push(recommendation);
}
const memories = await kv.list<Memory>(KV.memories);
const recommendations: DurableCandidateRecommendation[] = [];
const evaluatedAt = new Date().toISOString();
for (const candidate of all) {
const reasons: string[] = [];
if (candidate.confidence >= 0.9) reasons.push("confidence >= 0.90");
if (candidate.project) reasons.push("project scope explicit");
if (candidate.sourceObservationIds.length >= 3) reasons.push("3 evidence observations");
const hasConflict = memories.some((memory) =>
memory.isLatest !== false &&
memory.project === candidate.project &&
jaccardSimilarity(memory.content.toLowerCase(), candidate.content.toLowerCase()) > 0.7,
);
if (!hasConflict) reasons.push("no conflicting memory");
if (!isPlanOrTodo(candidate)) reasons.push("not a plan or todo");
const eligible = reasons.length === 5;
const recommendation: DurableCandidateRecommendation = {
candidateId: candidate.id,
recommendation: eligible ? "auto_promote_eligible" : "not_eligible",
reasons,
wouldPromote: false,
evaluatedAt,
};
await kv.set(KV.durableRecommendations, candidate.id, recommendation);
recommendations.push(recommendation);
}
🤖 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/functions/durable-candidates.ts` around lines 274 - 296, Capture a single
evaluation timestamp with new Date().toISOString() before the for-of loop in the
durable candidate evaluation flow, then reuse that value for every
recommendation’s evaluatedAt field. Keep the per-candidate eligibility and
persistence logic unchanged.

Source: Coding guidelines

Comment on lines +683 to +775
const parsed = parseJsonlText(text);

const fileHash = hashText(text);
const idempotencyKey = fingerprintId(
"arch",
`${parsed.sessionId}\n${fileHash}`,
);
const existingImport = await kv.get<ArchiveImportRecord>(
KV.archiveImports,
idempotencyKey,
);
if (
existingImport &&
(existingImport.status === "completed" || existingImport.summaryCreated === true)
) {
skipped.push({
archivePath: file,
sessionId: parsed.sessionId,
reason: "already_completed",
});
continue;
}

const startedAt = new Date().toISOString();
let record: ArchiveImportRecord = {
...existingImport,
id: idempotencyKey,
archivePath: file,
fileHash,
sessionId: parsed.sessionId,
status: existingImport?.status || "discovered",
createdAt: existingImport?.createdAt || startedAt,
updatedAt: startedAt,
attempts: (existingImport?.attempts || 0) + 1,
parsedObservationCount: parsed.observations.length,
importedObservationCount:
existingImport?.importedObservationCount || 0,
source: "archive-process",
};
await kv.set(KV.archiveImports, idempotencyKey, record);

const observationsAlreadyImported =
record.status === "observations_imported" ||
record.status === "summarizing" ||
(record.status === "failed" && record.failureStage === "summary");
if (!observationsAlreadyImported) {
record = {
...record,
status: "importing_observations",
failureStage: undefined,
lastError: undefined,
updatedAt: new Date().toISOString(),
};
await kv.set(KV.archiveImports, idempotencyKey, record);

const importResult = (await sdk.trigger({
function_id: "mem::replay::import-jsonl",
payload: { path: file, maxFiles: 1 },
})) as { success?: boolean; error?: string };
if (!importResult?.success) {
record = {
...record,
status: "failed",
failureStage: "observations",
lastError: importResult?.error || "import_failed",
updatedAt: new Date().toISOString(),
};
await kv.set(KV.archiveImports, idempotencyKey, record);
skipped.push({
archivePath: file,
sessionId: parsed.sessionId,
reason: record.lastError || "import_failed",
});
continue;
}

const session = await kv.get<Session>(KV.sessions, parsed.sessionId);
if (!session) {
record = {
...record,
status: "failed",
failureStage: "observations",
lastError: "session_not_found_after_import",
updatedAt: new Date().toISOString(),
};
await kv.set(KV.archiveImports, idempotencyKey, record);
skipped.push({
archivePath: file,
sessionId: parsed.sessionId,
reason: record.lastError || "session_not_found_after_import",
});
continue;
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the two call sites use different fallback strategies.
rg -nP 'parseJsonlText\(' src/functions/durable-candidates.ts src/functions/replay.ts
# Confirm parseJsonlText's default fallback is deterministic
ast-grep run --pattern 'const effectiveSessionId = $$$' --lang typescript src/replay/jsonl-parser.ts

Repository: rohitg00/agentmemory

Length of output: 459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant import path and parser fallback details with context.
sed -n '360,430p' src/functions/replay.ts
printf '\n---\n'
sed -n '120,155p' src/replay/jsonl-parser.ts
printf '\n---\n'
sed -n '683,775p' src/functions/durable-candidates.ts

Repository: rohitg00/agentmemory

Length of output: 7685


Session-id fallback should match mem::replay::import-jsonl.
durable-candidates falls back to fingerprintId("sess", text), but import-jsonl re-parses the same file with generateId("sess"). For files without an embedded session id, the import writes under a different session key, so kv.get<Session>(KV.sessions, parsed.sessionId) misses and the archive is marked session_not_found_after_import. Use the same fallback on both paths, preferably the deterministic one.

🤖 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/functions/durable-candidates.ts` around lines 683 - 775, Align the
missing-session-id fallback in parseJsonlText used by durable-candidates with
mem::replay::import-jsonl, replacing fingerprintId("sess", text) with the same
deterministic generateId("sess") behavior. Ensure both paths derive identical
session IDs before the KV.sessions lookup, while preserving embedded session IDs
unchanged.

Comment thread src/hooks/pre-compact.ts
Comment on lines +51 to 67
await fetch(`${REST_URL}/agentmemory/recall/context-epoch`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId }),
signal: AbortSignal.timeout(1000),
});
const res = await fetch(`${REST_URL}/agentmemory/context`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId, project, budget: 1500 }),
body: JSON.stringify({
sessionId,
project,
cwd: (data.cwd as string | undefined) || process.cwd(),
outputMode: "bootstrap",
}),
signal: AbortSignal.timeout(5000),
});

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Isolate the context-epoch advance so its failure doesn't skip context injection.

Both requests live in the same try. If the 1s context-epoch POST throws (timeout/transient error), control jumps straight to the outer catch and the /agentmemory/context fetch is never issued, so compaction loses its bootstrap context. Wrap the epoch advance in its own best-effort try so context injection still proceeds.

🛡️ Proposed fix
   try {
-    await fetch(`${REST_URL}/agentmemory/recall/context-epoch`, {
-      method: "POST",
-      headers: authHeaders(),
-      body: JSON.stringify({ sessionId }),
-      signal: AbortSignal.timeout(1000),
-    });
+    try {
+      await fetch(`${REST_URL}/agentmemory/recall/context-epoch`, {
+        method: "POST",
+        headers: authHeaders(),
+        body: JSON.stringify({ sessionId }),
+        signal: AbortSignal.timeout(1000),
+      });
+    } catch {
+      // best-effort epoch advance; still fetch context below
+    }
     const res = await fetch(`${REST_URL}/agentmemory/context`, {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await fetch(`${REST_URL}/agentmemory/recall/context-epoch`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId }),
signal: AbortSignal.timeout(1000),
});
const res = await fetch(`${REST_URL}/agentmemory/context`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId, project, budget: 1500 }),
body: JSON.stringify({
sessionId,
project,
cwd: (data.cwd as string | undefined) || process.cwd(),
outputMode: "bootstrap",
}),
signal: AbortSignal.timeout(5000),
});
try {
await fetch(`${REST_URL}/agentmemory/recall/context-epoch`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId }),
signal: AbortSignal.timeout(1000),
});
} catch {
// best-effort epoch advance; still fetch context below
}
const res = await fetch(`${REST_URL}/agentmemory/context`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
sessionId,
project,
cwd: (data.cwd as string | undefined) || process.cwd(),
outputMode: "bootstrap",
}),
signal: AbortSignal.timeout(5000),
});
🤖 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/hooks/pre-compact.ts` around lines 51 - 67, In the pre-compact hook,
isolate the context-epoch POST before the `/agentmemory/context` request by
wrapping it in its own best-effort try/catch. Ensure failures from that epoch
advance are handled without escaping to the outer catch, so the context fetch
still executes; keep the existing context injection behavior and timeout
unchanged.

Comment thread src/triggers/api.ts
Comment on lines +1186 to +1187
const cwd = typeof req.body.cwd === "string" && req.body.cwd.trim() ? req.body.cwd.trim() : undefined;
const identity = cwd ? await resolveRecallIdentity(cwd, req.body.project || "") : undefined;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'interface ApiRequest$_ { $$$ }' --lang typescript src/triggers/api.ts
rg -nP 'type ApiRequest|interface ApiRequest' src -A6

Repository: rohitg00/agentmemory

Length of output: 744


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the request handler and the ApiRequest type used by src/triggers/api.ts.
sed -n '1140,1305p' src/triggers/api.ts

echo '---'
rg -n "export (interface|type) ApiRequest|interface ApiRequest|type ApiRequest" . -g '!dist' -g '!build' -g '!node_modules' -A12 -B4

Repository: rohitg00/agentmemory

Length of output: 8273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for the SDK type declarations if they're vendored or referenced locally.
fd -a "iii-sdk" .
fd -a "api.d.ts" .
fd -a "index.d.ts" .

Repository: rohitg00/agentmemory

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the declared iii-sdk version and whether any local typings pin ApiRequest's body shape.
sed -n '1,220p' package.json

echo '---'
rg -n '"iii-sdk"|ApiRequest' package.json pnpm-lock.yaml package-lock.json yarn.lock src -A4 -B4

Repository: rohitg00/agentmemory

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe whether the dependency types are available in the lockfile or workspace sources.
fd -a "iii-sdk" . -t f
fd -a "*.d.ts" . -t f | head -n 50

Repository: rohitg00/agentmemory

Length of output: 444


🌐 Web query:

iii-sdk 0.11.2 ApiRequest type definition body index signature cwd repoId

💡 Result:

In the iii-sdk (version 0.11.x), the ApiRequest type is a generic TypeScript object used for HTTP trigger handlers [1]. The type definition typically includes specific fields for request metadata and data, but it does not contain index signatures for 'cwd' or 'repoId' [2][1]. Based on authoritative documentation and source code for the iii-sdk, the ApiRequest structure is defined as follows [1]: type ApiRequest<TBody = unknown> = { path_params: Record<string, string>; // Route parameters query_params: Record<string, string | string[]>; // Query string body: TBody; // Parsed JSON body headers: Record<string, string | string[]>; // Request headers method: string; // HTTP method request_body: any; // Raw body stream (sometimes referenced as ChannelReader) } The fields 'cwd' (current working directory) and 'repoId' (repository identifier) are not part of the standard ApiRequest type definition in this SDK [2][1]. These concepts are generally not exposed via the incoming HTTP request object in the iii-sdk architecture, which focuses on function registration, trigger binding, and invocation over WebSockets [3][4][1]. If you are attempting to access repository or environment-specific data, such information is typically managed by the iii Engine or external configuration, not the runtime ApiRequest object [4][5].

Citations:


🌐 Web query:

iii-sdk ApiRequest TypeScript definition generic body index signature

💡 Result:

In the iii-sdk for Node.js/TypeScript, the ApiRequest interface is used to represent incoming HTTP requests [1]. While the SDK documentation provides standard field definitions (such as path, method, headers, and query parameters) [1], the body field within ApiRequest is typically defined as a generic or an any-type to accommodate varying JSON payloads [1]. Regarding the use of an index signature to handle dynamic body content, developers often define their specific request body types when working with functions in iii-sdk [2][3]. Because the Node.js SDK does not automatically infer types from TypeScript [3], developers are encouraged to use explicit schemas (e.g., via Zod and JSON Schema) to validate and structure the request data [3]. If you need to define a body with dynamic keys, you can implement an interface with an index signature in your handler function's input type: interface CustomBody { [key: string]: any; // or define specific known keys alongside dynamic ones } // Example usage async function myHandler(req: ApiRequest): Promise { const body = req.body as CustomBody; //... } For formal definitions, the iii-sdk exports the ApiRequest type directly [1][4], and you can inspect the package's type definitions (located in the installed node_modules/iii-sdk/dist/index.d.ts or similar paths) to see the exact structure currently provided by your installed version [5]. Changes to type definitions are managed within the sdk/packages/node/iii/src/iii-types.ts file in the official repository [6].

Citations:


Add cwd and top-level repoId to the api::remember request body type req.body.cwd and req.body.repoId are accessed here, but the ApiRequest<...> generic only declares scope.repoId. Either extend the body type or read the nested field consistently.

🤖 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/triggers/api.ts` around lines 1186 - 1187, Update the api::remember
request body type used by the surrounding handler to declare the accessed
top-level cwd and repoId fields, alongside the existing fields. Keep the
resolveRecallIdentity call using the top-level cwd and ensure repoId is typed
consistently with its usage, rather than relying only on scope.repoId.

Comment thread src/types.ts
Comment on lines +386 to +394
export interface RecallItemStats {
itemId: string;
recallCount: number;
lastRecalledAt?: string;
recentQuery?: string;
averageScore: number;
scopeMismatchCount: number;
/** Scaled integer accumulator used to derive averageScore atomically. */
scoreTotalMicros?: number;

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e ts schema.ts src/state --exec cat -n {}
rg -nP 'recallStats|recallTrace|injectionLedger|archiveImport' src/state/schema.ts

Repository: rohitg00/agentmemory

Length of output: 5424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the AuditEntry type and nearby unions in src/types.ts.
fd -e ts types.ts src --exec sh -c 'echo "--- {}"; cat -n "$1" | sed -n "1,260p"' sh {} \;

# Search for audit operation literals and recordAudit calls.
rg -n "recordAudit\\(|operation:\\s*\"|operation\\s*\\|" src

Repository: rohitg00/agentmemory

Length of output: 20645


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the AuditEntry type in src/types.ts and print the surrounding lines.
rg -n "interface AuditEntry|type AuditEntry|operation:" src/types.ts
cat -n src/types.ts | sed -n '260,420p'

Repository: rohitg00/agentmemory

Length of output: 4832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n src/types.ts | sed -n '790,860p'

Repository: rohitg00/agentmemory

Length of output: 2055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re, pathlib

types = pathlib.Path("src/types.ts").read_text()
m = re.search(r"export interface AuditEntry\s*{.*?operation:\s*(.*?)\n\s*userId\?:", types, re.S)
if not m:
    raise SystemExit("AuditEntry.operation union not found")
union_block = m.group(1)
union_ops = re.findall(r'"([^"]+)"', union_block)

ops = set()
for path in pathlib.Path("src").rglob("*.ts"):
    txt = path.read_text()
    for mm in re.finditer(r'recordAudit\(\s*[^,]+,\s*"([^"]+)"', txt):
        ops.add(mm.group(1))

missing = sorted(ops - set(union_ops))
extra = sorted(set(union_ops) - ops)

print("UNION_COUNT", len(union_ops))
print("CALLSITE_COUNT", len(ops))
print("MISSING_FROM_UNION", missing)
print("UNUSED_IN_CALLS", extra)
PY

Repository: rohitg00/agentmemory

Length of output: 391


Add lease_renew to AuditEntry.operation The new KV scopes are already registered; the remaining gap is that src/functions/leases.ts emits lease_renew, but src/types.ts does not include it in the audit operation union.

🤖 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/types.ts` around lines 386 - 394, Update the AuditEntry.operation union
in src/types.ts to include the lease_renew operation emitted by the lease
renewal flow in leases.ts, while preserving all existing operation values.

Source: Path instructions

Comment thread src/viewer/index.html
'<td>' + esc(trace.entryPoint || '') + ' / ' + esc(trace.outputMode || '') + '</td>' +
'<td>' + esc(trace.query || '[no query]') + '</td>' +
'<td>' + selected + ' selected, ' + Object.keys(dropped).map(function(k) { return esc(k) + ': ' + esc(String(dropped[k])); }).join(', ') + '</td>' +
'<td>' + (trace.finalContextTokenCount || 0) + ' tokens</td>' +

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Escape finalContextTokenCount for consistency. Every other trace field is run through esc(), but this one is concatenated raw with only a || 0 fallback. If a trace ever carries a non-numeric string here it would be injected into innerHTML unescaped. Coerce/escape it to keep the row builder uniformly safe.

🛡️ Proposed change
-          '<td>' + (trace.finalContextTokenCount || 0) + ' tokens</td>' +
+          '<td>' + esc(String(trace.finalContextTokenCount || 0)) + ' tokens</td>' +
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'<td>' + (trace.finalContextTokenCount || 0) + ' tokens</td>' +
'<td>' + esc(String(trace.finalContextTokenCount || 0)) + ' tokens</td>' +
🤖 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/viewer/index.html` at line 2434, Update the trace row builder around
finalContextTokenCount to coerce or pass the value through the existing esc()
helper before concatenating it into innerHTML, while preserving the 0 fallback
for missing values. Keep the surrounding token-count rendering unchanged.

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