Skip to content

feat: durable candidates MVP + scoped traceable recall - #1050

Closed
MarcoLissitzky wants to merge 6 commits into
rohitg00:mainfrom
MarcoLissitzky:feat/p2-recall-observability
Closed

feat: durable candidates MVP + scoped traceable recall#1050
MarcoLissitzky wants to merge 6 commits into
rohitg00:mainfrom
MarcoLissitzky:feat/p2-recall-observability

Conversation

@MarcoLissitzky

@MarcoLissitzky MarcoLissitzky commented Jul 12, 2026

Copy link
Copy Markdown

What

Two related memory capabilities built on the 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 (POST /agentmemory/durable-candidates/backfill)
  • Source-observation provenance tracking on every candidate

P2: Scoped Traceable Recall

  • Centralized recall engine (src/recall/core.ts) — all search paths route through it instead of calling hybridSearch/kv.list directly
  • Every recall produces a RecallTrace with selected/dropped items and structured reasons
  • Project/repo/checkout identity fingerprinting for scope-aware recall
  • Recall debug endpoints (GET /agentmemory/recall/debug and /recall/debug/{traceId})
  • Two new invocable skills: recall-debug and why-memory
  • Dedup guard via recall injection ledger

Docs sync

  • README/AGENTS.md/plugin.json stats refreshed to match current codebase
  • Skill reference docs regenerated via npm run skills:gen

Verification

npm run build    # clean
npm test         # all tests pass, including 6 tool-count-consistency checks
npm run skills:gen  # no drift

Known limitations (P1)

The archive corpus alignment is not yet resolved — see docs/p1-durable-candidates-handoff-2026-07-11.md. The 29 sessions / 965 observations baseline has not been reproduced against the current archive snapshot. Real backfill/promote should not be run until the gap is explained.

Files changed

  • 47 files changed, ~2,200 additions, ~60 deletions (feature commits)
  • 3 files changed, ~107 additions (docs sync commit)
  • New: src/recall/ (7 files), src/functions/durable-candidates.ts, src/functions/durable-candidate-utils.ts
  • Modified: src/config.ts, src/types.ts, src/triggers/api.ts, src/mcp/server.ts, src/mcp/tools-registry.ts, src/index.ts
  • Tests: recall-core.test.ts, durable-candidates.test.ts, durable-candidate-utils.test.ts, vector-retrieval-health.test.ts, recall-benchmark-score.test.ts, replay-import-key.test.ts

Summary by CodeRabbit

  • New Features

    • Added scoped, budget-aware memory recall with project, repository, and checkout filtering.
    • Added recall trace inspection through new debug endpoints and a Recall Traces viewer tab.
    • Added durable candidate workflows for listing, recommendations, backfill, promotion, and archive processing.
    • Added optional prompt context injection and expanded recall controls through TOML and environment settings.
    • Added new invocable skills for diagnosing recall and explaining why memories were selected.
  • Documentation

    • Updated setup guides, API references, configuration examples, and recall benchmark documentation.
  • Bug Fixes

    • Improved archive replay idempotency, stable observation handling, and retrieval fallback reporting.

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>
@vercel

vercel Bot commented Jul 12, 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 12, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds centralized scoped recall with persisted traces, configurable token budgets, identity-aware integrations, durable candidate extraction and promotion, archive replay improvements, retrieval health tracking, and recall debugging surfaces across APIs, MCP, hooks, and the viewer.

Changes

Recall and durable candidate flow

Layer / File(s) Summary
Recall contracts and configuration
src/types.ts, src/config.ts, src/state/schema.ts, package.json, scripts/build-runtime-assets.mjs
Adds recall, tracing, scope, retrieval-health, durable-candidate, archive-import, and recommendation types; loads TOML and environment overrides; adds KV namespaces and runtime build metadata.
Recall engine and retrieval health
src/recall/*, src/state/hybrid-search.ts
Implements scoped bootstrap/semantic recall, token budgeting, injection deduplication, trace persistence, query redaction, identity resolution, and vector/graph channel status reporting.
Durable candidate lifecycle
src/functions/durable-candidate-utils.ts, src/functions/durable-candidates.ts, src/functions/summarize.ts, src/functions/remember.ts, src/prompts/summary.ts
Extracts and normalizes durable candidates, persists them through summaries, supports listing, recommendation, promotion, backfill, and archive processing, and stores candidate-linked memory metadata.
Archive replay and storage behavior
src/replay/jsonl-parser.ts, src/functions/replay.ts, src/functions/observe.ts, src/functions/image-quota-cleanup.ts
Adds deterministic archive observation IDs, Codex event parsing, idempotent session imports, zero-observation handling, and direct image rollback cleanup.
Runtime integrations
src/index.ts, src/triggers/api.ts, src/mcp/*, src/functions/context.ts, src/functions/enrich.ts, src/hooks/*, plugin/scripts/*
Routes context and enrichment through RecallCore, adds recall and durable-candidate endpoints, updates MCP output modes and scopes, and adds hook context synchronization and optional prompt injection.
Validation and observability
test/*, eval/recall/*, src/viewer/index.html
Adds recall scoring fixtures, lifecycle and replay regression tests, vector-health tests, durable-candidate tests, and a Recall Traces viewer tab.

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

Possibly related PRs

Suggested reviewers: rohitg00

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: durable candidates MVP and scoped traceable recall.
✨ 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: 14

Caution

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

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

95-101: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Legacy unscoped memories are not treated as wildcards here. sameScope() requires the scope level to match, so a legacy unknown memory will not be superseded by a new project-scoped write (and vice versa). If that crossover should still dedupe existing rows, relax this guard; otherwise update the comment.

🤖 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 95 - 101, Update the supersession
guard in the remember flow around sameScope and normalizeScope so legacy
unscoped memories are treated as wildcards when deduplicating against
project-scoped writes, while preserving the protection for explicitly different
projects; alternatively, if crossover deduplication is not intended, revise the
nearby comment to accurately describe sameScope behavior.
🧹 Nitpick comments (6)
test/vector-retrieval-health.test.ts (1)

5-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use afterEach to guarantee fake-timer cleanup.

vi.useRealTimers() on line 18 won't execute if any assertion between lines 8–17 throws, leaking fake timers into the second test. Move cleanup to an afterEach hook.

♻️ Proposed refactor
+afterEach(() => vi.useRealTimers());
+
 describe("VectorRetrievalHealth", () => {
   it("opens after transient failures and allows only one half-open probe", () => {
     vi.useFakeTimers();
     const health = new VectorRetrievalHealth(2, 1000);
     expect(health.begin(true, true)).toMatchObject({ status: "healthy", attempted: true });
     health.failure(new Error("429 quota exceeded"));
     expect(health.begin(true, true)).toMatchObject({ status: "healthy", attempted: true });
     health.failure(new Error("429 quota exceeded"));
     expect(health.begin(true, true)).toMatchObject({ status: "degraded", attempted: false });
     vi.advanceTimersByTime(1000);
     expect(health.begin(true, true)).toMatchObject({ status: "healthy", attempted: true });
     expect(health.begin(true, true)).toMatchObject({ status: "degraded", attempted: false });
     health.success();
     expect(health.begin(true, true)).toMatchObject({ status: "healthy", attempted: true });
-    vi.useRealTimers();
   });
🤖 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/vector-retrieval-health.test.ts` around lines 5 - 19, Move fake-timer
cleanup from the test body into an afterEach hook for the test suite containing
“opens after transient failures and allows only one half-open probe.” Keep
vi.useFakeTimers() in the test setup, and ensure the afterEach hook always calls
vi.useRealTimers(), including when an assertion fails.
test/remember-project-scope.test.ts (1)

206-256: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Update the stale isolation comment in src/functions/remember.ts. sameScope() already treats unknown-vs-scoped memories as different scopes, so the “wildcard” note no longer matches the code.

🤖 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/remember-project-scope.test.ts` around lines 206 - 256, Update the stale
isolation comment in sameScope() within remember.ts to accurately describe that
unknown and scoped memories are treated as different scopes; remove the outdated
“wildcard” characterization without changing the existing scope-comparison logic
or tests.
src/eval/schemas.ts (1)

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

Reuse DurableCandidateTypeEnum instead of duplicating the literal list.

RememberInputSchema.type re-declares the same six candidate types already defined in DurableCandidateTypeEnum (Line 35). These can silently drift apart when the candidate taxonomy changes. Reference the shared enum.

♻️ 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 RememberInputSchema.type
to reuse DurableCandidateTypeEnum instead of declaring a separate z.enum literal
list, while preserving its optional behavior.
src/recall/tokens.ts (1)

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

CJK-only special-casing under-covers other multi-byte scripts.

CJK_CHAR covers Hiragana/Katakana/CJK ideographs but omits Hangul (\uac00-\ud7a3), CJK punctuation (\u3000-\u303f), and fullwidth forms (\uff00-\uffef). Text in those ranges falls into the ASCII-run branch and gets divided by 4, understating token cost for a "conservative-unicode" estimator that should err toward overestimating.

♻️ Broaden multi-byte detection
-const CJK_CHAR = /[\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff]/u;
+const WIDE_CHAR = /[\u3000-\u303f\u3040-\u30ff\u3400-\u9fff\uac00-\ud7a3\uf900-\ufaff\uff00-\uffef]/u;

Then replace CJK_CHAR.test(char) with WIDE_CHAR.test(char) inside countTokens.

🤖 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 - 26, Broaden the character-range
constant used by countTokens from CJK-only detection to WIDE_CHAR, including
Hangul, CJK punctuation, and fullwidth forms; then replace the
CJK_CHAR.test(char) check with WIDE_CHAR.test(char) so these characters are
counted individually rather than as part of a four-character run.
src/recall/trace-store.ts (1)

48-60: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Retention trimming runs a full scan on every recall.

persistRecallTrace lists the entire recallTraces scope and issues per-entry deletes on every single recall. As traffic grows this turns each recall into an O(n) list plus a burst of state::delete calls on the request path. Consider trimming opportunistically (e.g., only when the store exceeds maxTraces by a margin, or on a periodic job) rather than every write.

🤖 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 48 - 60, Update persistRecallTrace
around the KV.recallTraces retention logic to avoid listing and deleting entries
on every recall. Move trimming to an opportunistic threshold or periodic cleanup
path, while preserving retentionDays and maxTraces enforcement when cleanup
runs; keep the trace write on the normal request path.
src/recall/core.ts (1)

357-378: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Semantic hydration issues KV reads serially per hit.

hybridRecall is fetched with a limit of up to max(limit*5, 50), then each hit triggers a serial kv.get(memories) (and a sessionFor lookup). On a warm corpus this is dozens of sequential state::get round-trips per recall. Consider batching the memory lookups with Promise.all before building candidates.

🤖 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 357 - 378, Update hybridRecall’s hit
hydration flow to batch per-hit memory and session lookups with Promise.all
instead of awaiting kv.get and sessionFor serially inside the loop. Preserve the
existing memory-versus-observation candidate construction and field values while
pairing each hit with its resolved memory/session data before pushing
candidates.
🤖 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 `@README.md`:
- Line 1266: Reconcile the test-count statistic in the README by updating the
development section’s outdated “1,423+ tests” value to match the current “1,500+
tests” project statistic. Keep the surrounding development guidance unchanged.
- Line 1590: Update the endpoint count in the README REST API description from
136 to 125, leaving the surrounding port, binding, and authorization details
unchanged.

In `@src/config.ts`:
- Around line 183-187: Update the error-label argument in the
maxSessionSummaries configuration within the positiveConfigInt call to use the
TOML key format, recall_budget.max_session_summaries, matching envOrToml and the
actual config.toml key.

In `@src/functions/context.ts`:
- Around line 40-64: Add an optional limit?: number field to the mem::context
input type and forward data.limit in the recallCore.recall request. Update only
the registration callback’s input definition and recall payload, preserving
existing defaults and behavior when limit is omitted.

In `@src/functions/durable-candidates.ts`:
- Around line 683-759: Update the archive import flow in archive processing and
the mem::replay::import-jsonl trigger to use one session ID: pass
parsed.sessionId in the trigger payload and make the importer use that supplied
ID instead of generating a fallback, so the subsequent kv.get(KV.sessions,
parsed.sessionId) resolves the imported session.

In `@src/functions/observe.ts`:
- Around line 182-192: The rollback branch in observe should stop directly
deleting raw.imageData and instead call decrementImageRef with kv, sdk, and
raw.imageData. Preserve the existing conditional and ensure the decrement
operation handles reference counts, embeddings, and disk-size accounting
consistently.

In `@src/hooks/pre-compact.ts`:
- Around line 51-65: Isolate the `context-epoch` fetch from the primary context
retrieval error handling in the surrounding pre-compact hook. Ensure failures
from the epoch call, including timeout or connection errors, are caught or
otherwise ignored so execution always proceeds to the `context` fetch, while
preserving the existing context-fetch behavior.

In `@src/mcp/tools-registry.ts`:
- Around line 35-41: Constrain the outputMode property in the memory_recall tool
schema to accept only the supported structured and context values, matching the
handling in the server. Reject unsupported values at schema validation before
they reach the server while preserving the existing descriptions and behavior
for valid modes.

In `@src/recall/identity.ts`:
- Around line 15-23: Update normalizeRemote to remove the URL scheme, including
git://, ssh://, and https:// forms, before producing the normalized value.
Ensure scheme-based URLs and SCP-style SSH remotes for the same repository yield
the identical host/path representation while preserving credential, .git,
trailing-slash, and lowercase normalization.
- Around line 25-50: Update resolveRecallIdentity and its gitValue usage so
request-supplied cwd is validated against an allowed workspace root before any
git probe, rejecting or falling back safely for paths outside that root.
Preserve identity resolution for valid workspaces, and ensure invalid input
cannot trigger synchronous execFileSync calls on arbitrary host paths.

In `@src/recall/trace-store.ts`:
- Around line 62-80: The recall-stat updates in the selected and scope-mismatch
processing still use non-atomic get/recompute/set logic. Replace both update
paths in the trace handling flow with state::update and atomic increment
operations for recallCount and scopeMismatchCount, while preserving the
averageScore and metadata calculations through the update mechanism so
concurrent recalls cannot overwrite each other.

In `@src/state/schema.ts`:
- Line 8: Update the shared type definitions in types.ts by moving or adding
InjectionLedgerEntry from recall/ledger.ts and defining the
durableRecommendations KV record shape there, alongside ArchiveImportRecord,
RecallTrace, and RecallItemStats. Then update consumers such as the ledger and
KV schema usage to import these shared types, preserving the existing fields and
behavior.

In `@src/triggers/api.ts`:
- Around line 1250-1254: Remove the redundant checkoutId spread from the
remember payload construction near the existing repoId and checkoutId selection
logic. Keep the earlier checkoutId expression that uses req.body.checkoutId or
identity.checkoutId, and leave the other payload fields unchanged.

In `@src/viewer/index.html`:
- Around line 2424-2437: Update the droppedCountsByDecision rendering inside the
traces.map callback to coerce each dropped[k] value to a string and pass it
through esc before concatenating it into the innerHTML string, while preserving
the existing escaped-key formatting.

---

Outside diff comments:
In `@src/functions/remember.ts`:
- Around line 95-101: Update the supersession guard in the remember flow around
sameScope and normalizeScope so legacy unscoped memories are treated as
wildcards when deduplicating against project-scoped writes, while preserving the
protection for explicitly different projects; alternatively, if crossover
deduplication is not intended, revise the nearby comment to accurately describe
sameScope behavior.

---

Nitpick comments:
In `@src/eval/schemas.ts`:
- Around line 106-108: Update RememberInputSchema.type to reuse
DurableCandidateTypeEnum instead of declaring a separate z.enum literal list,
while preserving its optional behavior.

In `@src/recall/core.ts`:
- Around line 357-378: Update hybridRecall’s hit hydration flow to batch per-hit
memory and session lookups with Promise.all instead of awaiting kv.get and
sessionFor serially inside the loop. Preserve the existing
memory-versus-observation candidate construction and field values while pairing
each hit with its resolved memory/session data before pushing candidates.

In `@src/recall/tokens.ts`:
- Around line 3-26: Broaden the character-range constant used by countTokens
from CJK-only detection to WIDE_CHAR, including Hangul, CJK punctuation, and
fullwidth forms; then replace the CJK_CHAR.test(char) check with
WIDE_CHAR.test(char) so these characters are counted individually rather than as
part of a four-character run.

In `@src/recall/trace-store.ts`:
- Around line 48-60: Update persistRecallTrace around the KV.recallTraces
retention logic to avoid listing and deleting entries on every recall. Move
trimming to an opportunistic threshold or periodic cleanup path, while
preserving retentionDays and maxTraces enforcement when cleanup runs; keep the
trace write on the normal request path.

In `@test/remember-project-scope.test.ts`:
- Around line 206-256: Update the stale isolation comment in sameScope() within
remember.ts to accurately describe that unknown and scoped memories are treated
as different scopes; remove the outdated “wildcard” characterization without
changing the existing scope-comparison logic or tests.

In `@test/vector-retrieval-health.test.ts`:
- Around line 5-19: Move fake-timer cleanup from the test body into an afterEach
hook for the test suite containing “opens after transient failures and allows
only one half-open probe.” Keep vi.useFakeTimers() in the test setup, and ensure
the afterEach hook always calls vi.useRealTimers(), including when an assertion
fails.
🪄 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: 88c4ec68-facb-48f7-bb0c-a18f7e81573e

📥 Commits

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

📒 Files selected for processing (73)
  • .env.example
  • .gitignore
  • AGENTS.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/score.ts
  • package.json
  • plugin/plugin.json
  • 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/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/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/memory-utils.ts
  • src/state/schema.ts
  • src/triggers/api.ts
  • src/types.ts
  • src/viewer/index.html
  • test/durable-candidate-utils.test.ts
  • test/durable-candidates.test.ts
  • test/recall-benchmark-score.test.ts
  • test/recall-core.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

Comment thread README.md
| Custom plugin systems | `iii worker add <name>` |

**174 source files · ~37,800 LOC · 1,423+ tests · 258 functions · 44 KV scopes** — all on three primitives. No `agentmemory plugin install`. The plugin system is iii itself.
**184 source files · ~42,400 LOC · 1,500+ tests · 296 functions · 58 KV scopes** — all on three primitives. No `agentmemory plugin install`. The plugin system is iii itself.

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

Test count inconsistency between project stats and development section.

Line 1266 states "1,500+ tests" but line 1624 (development section) still says "1,423+ tests". These should be reconciled to avoid confusion.

🤖 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 `@README.md` at line 1266, Reconcile the test-count statistic in the README by
updating the development section’s outdated “1,423+ tests” value to match the
current “1,500+ tests” project statistic. Keep the surrounding development
guidance unchanged.

Comment thread README.md
<h2 id="api"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/section-api.svg"><img src="assets/tags/section-api.svg" alt="API" height="32" /></picture></h2>

128 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
136 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.

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

Endpoint count mismatch: README says 136 but auto-generated reference says 125.

The auto-generated plugin/skills/agentmemory-rest-api/REFERENCE.md (line 8) lists exactly 125 registered endpoints, generated from src/triggers/api.ts. However, this README line claims 136 endpoints. The coding guideline for src/triggers/api.ts requires endpoint counts in README.md to be updated when adding REST endpoints. The auto-generated reference is the source of truth — update this line to match the actual registered count.

🔧 Proposed fix
-136 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
+125 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
📝 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
136 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
125 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
🤖 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 `@README.md` at line 1590, Update the endpoint count in the README REST API
description from 136 to 125, leaving the surrounding port, binding, and
authorization details unchanged.

Source: Coding guidelines

Comment thread src/config.ts
Comment on lines +183 to +187
maxSessionSummaries: positiveConfigInt(
envOrToml(env, "AGENTMEMORY_RECALL_MAX_SESSION_SUMMARIES", toml, "recall_budget", "max_session_summaries"),
budget.maxSessionSummaries,
"recall_budget.maxSessionSummaries",
),

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

Error label uses camelCase and won't match the TOML key.

The validated key is max_session_summaries, but the error label is recall_budget.maxSessionSummaries. A validation failure would point users at a key that doesn't exist in their config.toml.

✏️ Fix label
       maxSessionSummaries: positiveConfigInt(
         envOrToml(env, "AGENTMEMORY_RECALL_MAX_SESSION_SUMMARIES", toml, "recall_budget", "max_session_summaries"),
         budget.maxSessionSummaries,
-        "recall_budget.maxSessionSummaries",
+        "recall_budget.max_session_summaries",
       ),
📝 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
maxSessionSummaries: positiveConfigInt(
envOrToml(env, "AGENTMEMORY_RECALL_MAX_SESSION_SUMMARIES", toml, "recall_budget", "max_session_summaries"),
budget.maxSessionSummaries,
"recall_budget.maxSessionSummaries",
),
maxSessionSummaries: positiveConfigInt(
envOrToml(env, "AGENTMEMORY_RECALL_MAX_SESSION_SUMMARIES", toml, "recall_budget", "max_session_summaries"),
budget.maxSessionSummaries,
"recall_budget.max_session_summaries",
),
🤖 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/config.ts` around lines 183 - 187, Update the error-label argument in the
maxSessionSummaries configuration within the positiveConfigInt call to use the
TOML key format, recall_budget.max_session_summaries, matching envOrToml and the
actual config.toml key.

Comment thread src/functions/context.ts
Comment on lines 40 to +64
sdk.registerFunction("mem::context",
async (data: { sessionId: string; project: string; budget?: number }) => {
async (data: {
sessionId: string;
project: string;
budget?: number;
query?: string;
projectId?: string;
repoId?: string;
checkoutId?: string;
outputMode?: "bootstrap" | "prompt_injection" | "rendered_context" | "ranked_results";
entryPoint?: "context" | "search" | "smart_search" | "memory_recall" | "prompt" | "session_start" | "enrich";
debug?: boolean;
}) => {
if (recallCore) {
const result = await recallCore.recall({
entryPoint: data.entryPoint || "context",
outputMode: data.outputMode || "rendered_context",
query: data.query,
sessionId: data.sessionId,
projectId: data.projectId || data.project,
repoId: data.repoId,
checkoutId: data.checkoutId,
...(data.budget ? { budget: { maxContextTokens: data.budget } } : {}),
debug: data.debug,
});

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== context.ts ==\n'
sed -n '1,140p' src/functions/context.ts

printf '\n== server.ts matches ==\n'
rg -n "mem::context|limit" src/mcp/server.ts src/functions/context.ts src -g '!**/dist/**' -g '!**/build/**'

printf '\n== recall core matches ==\n'
rg -n "rankedLimit|request\.limit|limit" src -g '!**/dist/**' -g '!**/build/**'

Repository: rohitg00/agentmemory

Length of output: 46650


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/mcp/server.ts around smart_search ==\n'
sed -n '280,320p' src/mcp/server.ts

printf '\n== src/recall/core.ts around ranked limit ==\n'
sed -n '190,360p' src/recall/core.ts

printf '\n== other mem::context callers ==\n'
rg -n "function_id: \"mem::context\"|function_id: 'mem::context'|registerFunction\\(\"mem::context\"" src -g '!**/dist/**' -g '!**/build/**'

Repository: rohitg00/agentmemory

Length of output: 10420


Forward limit into recallCore.recall

limit is dropped before the recall call, so ranked_results falls back to 20 even when memory_smart_search passes a higher value. Add limit?: number to the input and forward it into recallCore.recall.

🤖 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/context.ts` around lines 40 - 64, Add an optional limit?:
number field to the mem::context input type and forward data.limit in the
recallCore.recall request. Update only the registration callback’s input
definition and recall payload, preserving existing defaults and behavior when
limit is omitted.

Comment on lines +683 to +759
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);

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the fallback used by import-jsonl and whether it accepts an explicit sessionId.
rg -nP 'parseJsonlText\s*\(' src/functions/replay.ts src/functions/durable-candidates.ts
ast-grep run --pattern 'parseJsonlText($$$)' --lang typescript src

Repository: rohitg00/agentmemory

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant functions and call sites.
ast-grep outline src/functions/replay.ts --view expanded
ast-grep outline src/functions/durable-candidates.ts --view expanded

# Read the parser implementation and the import trigger path around the key lines.
grep -n "function parseJsonlText\|const parseJsonlText\|export .*parseJsonlText" -n src/functions/*.ts
sed -n '1,220p' src/functions/replay.ts
sed -n '650,780p' src/functions/durable-candidates.ts

Repository: rohitg00/agentmemory

Length of output: 1499


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the specific sections that handle JSONL parsing and session creation.
sed -n '330,430p' src/functions/replay.ts
sed -n '680,770p' src/functions/durable-candidates.ts

# Also inspect the parser helper if it lives in a shared file.
rg -n "parseJsonlText|generateId\\(\"sess\"\\)|fingerprintId\\(\"sess\"" src -S

Repository: rohitg00/agentmemory

Length of output: 7892


Share one session ID across the archive import flow
archive::process fingerprints parsed.sessionId, but mem::replay::import-jsonl re-parses the same file with a generated fallback, so archives without embedded session metadata can be stored under a different session key and the later kv.get(KV.sessions, parsed.sessionId) misses. Pass the same session id through the trigger payload or reuse the imported session id.

🤖 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 - 759, Update the
archive import flow in archive processing and the mem::replay::import-jsonl
trigger to use one session ID: pass parsed.sessionId in the trigger payload and
make the importer use that supplied ID instead of generating a fallback, so the
subsequent kv.get(KV.sessions, parsed.sessionId) resolves the imported session.

Comment thread src/recall/identity.ts
Comment on lines +25 to +50
function gitValue(cwd: string, args: string[]): string | undefined {
try {
const value = execFileSync("git", args, {
cwd,
stdio: ["ignore", "pipe", "ignore"],
timeout: 500,
}).toString().trim();
return value || undefined;
} catch {
return undefined;
}
}

export function resolveRecallIdentity(
cwd: string,
projectId: string,
): RecallIdentity {
const checkoutRoot = gitValue(cwd, ["rev-parse", "--show-toplevel"]) || cwd;
const normalizedRoot = resolve(checkoutRoot).replace(/\\/g, "/").toLowerCase();
const remote = gitValue(checkoutRoot, ["remote", "get-url", "origin"]);
return {
projectId,
repoId: remote ? fingerprint(normalizeRemote(remote)) : fingerprint(normalizedRoot),
checkoutId: fingerprint(normalizedRoot),
};
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'function isSensitive' -A 15 src/functions/replay.ts
rg -n 'resolveRecallIdentity\(' -B3 -A3 src/triggers/api.ts

Repository: rohitg00/agentmemory

Length of output: 1779


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/recall/identity.ts ---'
cat -n src/recall/identity.ts | sed -n '1,220p'

echo '--- relevant api trigger context ---'
sed -n '360,390p' src/triggers/api.ts
echo
sed -n '730,755p' src/triggers/api.ts
echo
sed -n '1140,1165p' src/triggers/api.ts

echo '--- search for path guards / cwd validation in recall paths ---'
rg -n 'isSensitive\(|sensitive path|cwd|resolveRecallIdentity\(|gitValue\(' src -g '!**/*.map'

Repository: rohitg00/agentmemory

Length of output: 17700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/recall/identity.ts outline ---'
ast-grep outline src/recall/identity.ts --view expanded

echo '--- related recall path helpers ---'
rg -n 'hybridSearch|kv\.list|persistRecallTrace|RecallCore|resolveRecallIdentity' src/recall src -g '!**/*.map'

Repository: rohitg00/agentmemory

Length of output: 31324


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('src/recall/identity.ts')
print(p.exists(), p.stat().st_size if p.exists() else None)
PY

Repository: rohitg00/agentmemory

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- top of src/triggers/api.ts ---'
sed -n '1,140p' src/triggers/api.ts

echo '--- resolveRecallIdentity implementation ---'
sed -n '1,120p' src/recall/identity.ts

echo '--- any auth / request provenance in api trigger ---'
rg -n 'auth|token|apikey|api key|request body|express|fastify|router|endpoint' src/triggers/api.ts src/triggers -g '!**/*.map'

Repository: rohitg00/agentmemory

Length of output: 38716


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/triggers/api.ts

Repository: rohitg00/agentmemory

Length of output: 7977


Avoid sync git probes on request-supplied cwd.
resolveRecallIdentity() is called from API handlers with cwd straight from the request body, and each call does up to two synchronous execFileSync("git", ...) calls with 500ms timeouts. That can block the worker on the hot request path and lets callers point git at arbitrary host paths. Restrict cwd to an allowed workspace root, or move the git lookup off the request path.

🤖 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/identity.ts` around lines 25 - 50, Update resolveRecallIdentity
and its gitValue usage so request-supplied cwd is validated against an allowed
workspace root before any git probe, rejecting or falling back safely for paths
outside that root. Preserve identity resolution for valid workspaces, and ensure
invalid input cannot trigger synchronous execFileSync calls on arbitrary host
paths.

Comment thread src/recall/trace-store.ts
Comment on lines +62 to +80
await Promise.all(trace.selected.map(async (item) => {
const current = (await kv.get<RecallItemStats>(KV.recallStats, item.id)) || emptyStats(item.id);
const count = current.recallCount + 1;
await kv.set(KV.recallStats, item.id, {
...current,
recallCount: count,
averageScore: ((current.averageScore * current.recallCount) + item.score) / count,
lastRecalledAt: trace.timestamp,
recentQuery: trace.query,
});
}));
const mismatches = trace.dropped.filter((item) => item.decision === "scope_mismatch");
await Promise.all(mismatches.map(async (item) => {
const current = (await kv.get<RecallItemStats>(KV.recallStats, item.id)) || emptyStats(item.id);
await kv.set(KV.recallStats, item.id, {
...current,
scopeMismatchCount: current.scopeMismatchCount + 1,
});
}));

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== trace-store outline ==\n'
ast-grep outline src/recall/trace-store.ts --view expanded || true

printf '\n== kv API references ==\n'
rg -n "class .*kv|interface .*kv|type .*kv|kv\.update|kv\.get<|kv\.set<|update\(" src --glob '!**/dist/**' --glob '!**/build/**' || true

printf '\n== recall trace-store full file (limited) ==\n'
wc -l src/recall/trace-store.ts
sed -n '1,220p' src/recall/trace-store.ts

printf '\n== kv-related files ==\n'
fd -a ".*(kv|store).*\.ts$" src . || true

Repository: rohitg00/agentmemory

Length of output: 21656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search for KV implementation and atomic ops ==\n'
rg -n "export (class|function)|kv\.|update\(|atomic|mutex|lock|transaction|compareAndSwap|cas" src -g '*.ts' || true

printf '\n== search for RecallItemStats and emptyStats ==\n'
rg -n "RecallItemStats|emptyStats|recallCount|scopeMismatchCount|averageScore|lastRecalledAt|recentQuery" src -g '*.ts' || true

Repository: rohitg00/agentmemory

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== state/kv.ts ==\n'
wc -l src/state/kv.ts
sed -n '1,240p' src/state/kv.ts

printf '\n== in-memory kv ==\n'
wc -l src/mcp/in-memory-kv.ts
sed -n '1,260p' src/mcp/in-memory-kv.ts

printf '\n== trace-store callers / locks ==\n'
rg -n "persistRecallTrace\(" src -g '*.ts'
rg -n "withKeyedLock\(|recallTraces|recallStats" src -g '*.ts'

Repository: rohitg00/agentmemory

Length of output: 9874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== keyed mutex ==\n'
cat -n src/state/keyed-mutex.ts

printf '\n== relevant recall API caller paths ==\n'
sed -n '1,220p' src/functions/smart-search.ts
sed -n '1,220p' src/recall/core.ts

Repository: rohitg00/agentmemory

Length of output: 18240


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== state::update implementation ==\n'
rg -n "function_id: \"state::update\"|state::update|state::get|state::set|state::list|state::delete" src -g '*.ts'

printf '\n== trace-store surrounding caller in RecallCore ==\n'
sed -n '240,320p' src/recall/core.ts

printf '\n== API endpoints around recall stats/traces ==\n'
sed -n '2340,2385p' src/triggers/api.ts

Repository: rohitg00/agentmemory

Length of output: 6243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search for state trigger registrations ==\n'
rg -n "register.*state|state::" src -g '*.ts'

printf '\n== state schema and any comments about atomicity ==\n'
sed -n '1,180p' src/state/schema.ts

Repository: rohitg00/agentmemory

Length of output: 5568


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search for state:: handlers in repo ==\n'
rg -n "\"state::(get|set|update|delete|list)\"|state::(get|set|update|delete|list)" src -g '*.ts'

printf '\n== keyed lock used with shared KV stats? ==\n'
rg -n "recallStats|recallTraces|persistRecallTrace|withKeyedLock" src -g '*.ts'

Repository: rohitg00/agentmemory

Length of output: 9147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '260,310p' src/recall/core.ts
sed -n '1,120p' src/recall/ledger.ts

Repository: rohitg00/agentmemory

Length of output: 5079


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== kv.update usage patterns ==\n'
rg -n "kv\.update\(" src -g '*.ts' -A4 -B2

printf '\n== any docs/comments for update op types ==\n'
rg -n "path:|type: \"(inc|increment|set|unset|push|splice)\"|ops" src -g '*.ts'

Repository: rohitg00/agentmemory

Length of output: 24225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "timestamp:" src/recall/core.ts src/types.ts src -g '*.ts' | sed -n '1,120p'
sed -n '300,420p' src/recall/core.ts

Repository: rohitg00/agentmemory

Length of output: 9024


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '360,470p' src/recall/core.ts

Repository: rohitg00/agentmemory

Length of output: 2823


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== trace construction in RecallCore ==\n'
rg -n "trace =|timestamp:|persistRecallTrace\(" src/recall/core.ts -n -A40 -B20

printf '\n== state::update handler / implementation ==\n'
rg -n "state::update|function_id: 'state::update'|function_id: \"state::update\"" src -g '*.ts' -A30 -B10

printf '\n== any README/docs for state update semantics ==\n'
rg -n "state::update|atomic update|compare-and-swap|increment" README.md docs src -g '*.md' -g '*.ts' -A4 -B2

Repository: rohitg00/agentmemory

Length of output: 17565


🌐 Web query:

iii-sdk state::update atomic increment semantics StateKV get set update documentation

💡 Result:

The iii-sdk provides atomic state management through the state::update function [1][2]. This function is designed to prevent race conditions inherent in read-modify-write patterns by allowing multiple operations to be applied to a stored JSON value atomically [1][3][4]. Atomic Increment Semantics The increment operation is part of a set of atomic operations (alongside set, merge, append, decrement, and remove) executed in order on a scoped key-value pair [5][3][6]. When using the increment operation: - It targets a numeric field within a stored JSON object [5]. - You specify the path to the field and the value (by) to add [5]. - If the target field does not exist or is not a number, the operation will fail with specific errors (e.g., increment.not_number or increment.target_not_object) [5]. It is recommended to initialize the field (e.g., using set to 0) if it does not yet exist [5]. - Multiple operations can be sent in a single state::update call, and they are processed sequentially as a single atomic unit [1][4]. Usage The state::update function accepts a payload containing the scope, key, and an array of operations [1][3][4]. Example (TypeScript): await iii.trigger({ function_id: 'state::update', payload: { scope: 'users', key: 'user_123', ops: [ { type: 'increment', path: 'loginCount', by: 1 } ], }, }); Recommendations - Use state::update for partial mutations to ensure atomicity [3][6]. - Avoid read-then-set patterns (get followed by set) for modifications, as these are not atomic and may result in lost updates during concurrent execution [3][6][7]. - The update operation returns the state of the record after the operations have been applied [3][4]. Documentation Note: While early versions (e.g., 0.10.0) suggested using read-modify-write for some state interactions [7], modern iii-sdk versions (0.11.0+) explicitly prioritize the state::update function for atomic modifications [5][3][6].

Citations:


Use atomic state updates for recall stats

These counters still do get → recompute → set, so concurrent recalls can drop recallCount, averageScore, or scopeMismatchCount updates. Switch this to state::update with atomic increment ops.

🤖 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 62 - 80, The recall-stat updates in
the selected and scope-mismatch processing still use non-atomic
get/recompute/set logic. Replace both update paths in the trace handling flow
with state::update and atomic increment operations for recallCount and
scopeMismatchCount, while preserving the averageScore and metadata calculations
through the update mechanism so concurrent recalls cannot overwrite each other.

Comment thread src/state/schema.ts
observations: (sessionId: string) => `mem:obs:${sessionId}`,
memories: "mem:memories",
summaries: "mem:summaries",
archiveImports: "mem:archive-imports",

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'interface RecallItemStats|interface ArchiveImport|interface DurableRecommendation|interface InjectionLedgerEntry' src/types.ts src/recall/ledger.ts

Repository: rohitg00/agentmemory

Length of output: 330


🏁 Script executed:

#!/bin/bash
sed -n '1,120p' src/state/schema.ts
printf '\n---\n'
sed -n '150,220p' src/types.ts
printf '\n---\n'
sed -n '360,410p' src/types.ts
printf '\n---\n'
sed -n '1,80p' src/recall/ledger.ts

Repository: rohitg00/agentmemory

Length of output: 9482


🏁 Script executed:

#!/bin/bash
rg -n 'DurableRecommendation|durableRecommendations|ArchiveImportRecord|RecallTrace|RecallItemStats|InjectionLedgerEntry' src/types.ts src/recall src/state

Repository: rohitg00/agentmemory

Length of output: 1976


Add the missing shared KV types

ArchiveImportRecord, RecallTrace, and RecallItemStats already exist in src/types.ts, but InjectionLedgerEntry is only defined in src/recall/ledger.ts, and durableRecommendations still has no shared type there. Move those shapes into src/types.ts so the new KV scopes follow the schema/types convention.

🤖 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/schema.ts` at line 8, Update the shared type definitions in
types.ts by moving or adding InjectionLedgerEntry from recall/ledger.ts and
defining the durableRecommendations KV record shape there, alongside
ArchiveImportRecord, RecallTrace, and RecallItemStats. Then update consumers
such as the ledger and KV schema usage to import these shared types, preserving
the existing fields and behavior.

Source: Coding guidelines

Comment thread src/triggers/api.ts
Comment on lines +1250 to +1254
...(typeof req.body.repoId === "string" ? { repoId: req.body.repoId } : identity?.repoId ? { repoId: identity.repoId } : {}),
...(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 }),

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

Duplicate checkoutId key in the remember payload.

checkoutId is set twice in this object spread: Line 1251 (body value or identity.checkoutId fallback) and again Line 1254 (req.body.checkoutId). The later spread wins, so when a caller omits checkoutId but supplies cwd, the identity-derived fallback on Line 1251 is still preserved (Line 1254 spreads nothing) — but the redundant key is confusing and looks like an edit artifact. Drop Line 1254.

Proposed cleanup
           ...(scope !== undefined && { scope }),
           ...(req.body.origin !== undefined && { origin: req.body.origin }),
-          ...(req.body.checkoutId !== undefined && { checkoutId: req.body.checkoutId }),
📝 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
...(typeof req.body.repoId === "string" ? { repoId: req.body.repoId } : identity?.repoId ? { repoId: identity.repoId } : {}),
...(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 }),
...(typeof req.body.repoId === "string" ? { repoId: req.body.repoId } : identity?.repoId ? { repoId: identity.repoId } : {}),
...(typeof req.body.checkoutId === "string" ? { checkoutId: req.body.checkoutId } : identity ? { checkoutId: identity.checkoutId } : {}),
...(scope !== undefined && { scope }),
...(req.body.origin !== undefined && { origin: req.body.origin }),
🤖 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 1250 - 1254, Remove the redundant
checkoutId spread from the remember payload construction near the existing
repoId and checkoutId selection logic. Keep the earlier checkoutId expression
that uses req.body.checkoutId or identity.checkoutId, and leave the other
payload fields unchanged.

Comment thread src/viewer/index.html
Comment on lines +2424 to +2437
var rows = traces.map(function(trace) {
var mode = trace.retrievalMode || {};
var vector = mode.vector || {};
var selected = (trace.selected || []).length;
var dropped = trace.droppedCountsByDecision || {};
return '<tr>' +
'<td>' + esc(trace.timestamp || '') + '</td>' +
'<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) + ': ' + dropped[k]; }).join(', ') + '</td>' +
'<td>' + (trace.finalContextTokenCount || 0) + ' tokens</td>' +
'<td>' + esc(vector.status || 'disabled') + (vector.reason ? ': ' + esc(vector.reason) : '') + '</td>' +
'</tr>';
}).join('');

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

Unescaped dropped[k] values in innerHTML.

The droppedCountsByDecision object keys are escaped via esc(k), but the values dropped[k] are concatenated directly into the HTML string without escaping. If the API ever returns string values (or an attacker can influence the response), this is an XSS vector. Coerce and escape the value.

🛡️ Proposed fix
-          '<td>' + selected + ' selected, ' + Object.keys(dropped).map(function(k) { return esc(k) + ': ' + dropped[k]; }).join(', ') + '</td>' +
+          '<td>' + selected + ' selected, ' + Object.keys(dropped).map(function(k) { return esc(k) + ': ' + esc(String(dropped[k])); }).join(', ') + '</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
var rows = traces.map(function(trace) {
var mode = trace.retrievalMode || {};
var vector = mode.vector || {};
var selected = (trace.selected || []).length;
var dropped = trace.droppedCountsByDecision || {};
return '<tr>' +
'<td>' + esc(trace.timestamp || '') + '</td>' +
'<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) + ': ' + dropped[k]; }).join(', ') + '</td>' +
'<td>' + (trace.finalContextTokenCount || 0) + ' tokens</td>' +
'<td>' + esc(vector.status || 'disabled') + (vector.reason ? ': ' + esc(vector.reason) : '') + '</td>' +
'</tr>';
}).join('');
var rows = traces.map(function(trace) {
var mode = trace.retrievalMode || {};
var vector = mode.vector || {};
var selected = (trace.selected || []).length;
var dropped = trace.droppedCountsByDecision || {};
return '<tr>' +
'<td>' + esc(trace.timestamp || '') + '</td>' +
'<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>' +
'<td>' + esc(vector.status || 'disabled') + (vector.reason ? ': ' + esc(vector.reason) : '') + '</td>' +
'</tr>';
}).join('');
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 2437-2437: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: el.innerHTML = '

Recall Traces

' + rows + '
TimeEntryQueryDecisionBudgetVector
'
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 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` around lines 2424 - 2437, Update the
droppedCountsByDecision rendering inside the traces.map callback to coerce each
dropped[k] value to a string and pass it through esc before concatenating it
into the innerHTML string, while preserving the existing escaped-key formatting.

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