Skip to content

fix: dedupe double-triggered summaries and unchanged-input re-processing on session stop - #1061

Open
yulin0629 wants to merge 6 commits into
rohitg00:mainfrom
yulin0629:fix/summarize-double-trigger
Open

fix: dedupe double-triggered summaries and unchanged-input re-processing on session stop#1061
yulin0629 wants to merge 6 commits into
rohitg00:mainfrom
yulin0629:fix/summarize-double-trigger

Conversation

@yulin0629

@yulin0629 yulin0629 commented Jul 15, 2026

Copy link
Copy Markdown

Fixes #1062. Fixes #1063.

Problem

Every session stop currently runs mem::summarize twice, and each run re-summarizes the whole session from scratch.

The stop hook fires two requests:

  1. POST /agentmemory/summarizeapi::summarizemem::summarize
  2. POST /agentmemory/session/endapi::session::end → fans out event::session::stopped, whose handler calls mem::summarize again (src/triggers/events.ts)

mem::summarize has no lock, no in-flight guard, and no unchanged-input short-circuit (unlike mem::observe, which already uses DedupMap + withKeyedLock), so both runs do the full LLM work.

Observed in production (self-hosted, v0.9.27): every session stop produced two byte-identical summarize requests at the provider (same second, same body size), growing each cycle as the session accumulated observations — e.g. a heartbeat-style session that stopped every 30 minutes reached duplicate ~230 KB (~110K input tokens) requests per cycle:

03:24:33  claude-sonnet-5  222635B   ← same request
03:24:33  claude-sonnet-5  222635B   ← twice
03:54:27  claude-sonnet-5  229115B
03:54:27  claude-sonnet-5  229115B
04:24:22  claude-sonnet-5  235636B
04:24:22  claude-sonnet-5  235636B

Fix

  • stop hook: drop the direct /agentmemory/summarize call — /agentmemory/session/end already fans out to the summarize path. One stop, one summarize.
  • mem::summarize: serialize per-session runs with withKeyedLock (same pattern as mem::observe), and short-circuit when the stored summary's observationCount already matches the current compressed count. Observations are append-only, so an unchanged count means the LLM input would be byte-identical to the previous run — re-running cannot produce new information. This also stops full re-summarization of long-lived sessions that stop repeatedly without new activity.

Relation to #572

#572 adds a time-window dedup (SUMMARIZE_DEDUP_WINDOW_MS, default 90 s). That helps, but:

  • it does not remove the duplicate trigger itself;
  • a pure time check does not close the race — the two stop-hook requests arrive in the same second and can both pass the freshness check before either writes a summary (the duplicate pairs above are exactly this case), whereas a keyed lock makes the second run wait and then hit the short-circuit;
  • after the window expires, an unchanged session is still fully re-summarized.

The two changes are complementary: count-based skip covers "nothing new" regardless of elapsed time, and a time window could still be layered on top for sessions that keep accumulating low-value observations.

Also: graph extraction has the same shape (#1063)

The session-stopped handler sent the session's full compressed observation set to mem::graph-extract on every stop with no change detection — for a 500-observation heartbeat session that was a ~50K-token LLM call every 30 minutes with identical input. The last commit reuses the same fingerprint helper: store the set's fingerprint per session (KV.graphExtractState) and trigger extraction only when it changed.

Tests

Three new cases in test/summarize.test.ts:

  • unchanged observation count → second call returns the stored summary with skipped: true, no second LLM call
  • two concurrent calls for the same session collapse into a single LLM run
  • new observations since the last summary → re-summarizes as before

npm test: no new failures (the 4 pre-existing failures in auto-compress.test.ts / embedding-provider.test.ts reproduce on a clean checkout and come from environment leakage, unrelated to this change).

Summary by CodeRabbit

  • Bug Fixes
    • Prevented duplicate session summaries and graph extraction when inputs are unchanged.
    • Updated session shutdown to trigger summarization once, avoiding duplicate processing.
    • Improved reflection scalability by using capped graph snapshots and graceful fallbacks.
    • Improved memory health severity calculations using the runtime heap limit.
  • Tests
    • Expanded coverage for summarization deduplication, concurrency, input changes, and timeouts.
    • Added graph-extraction deduplication and health-threshold regression tests.
  • Documentation
    • Added unreleased changelog entries for reflection scalability and health monitoring improvements.

Copilot AI review requested due to automatic review settings July 15, 2026 04:00
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

A single stop-hook invocation used to run mem::summarize twice: once via
the direct POST /agentmemory/summarize, and once via
/agentmemory/session/end -> event::session::stopped. Each run
re-summarized the entire session from scratch, so every session stop
cost 2x full-history LLM calls (observed: duplicate ~230KB requests per
stop, growing as the session accumulated observations).

- stop hook: drop the direct summarize call; session/end already fans
  out to the summarize path.
- mem::summarize: serialize per-session runs with withKeyedLock (same
  pattern as mem::observe) and short-circuit when the stored summary's
  observationCount already matches — observations are append-only, so an
  unchanged count means the LLM input would be identical.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Shutdown flows now rely on session-end handling for summarization. The summarize handler serializes work per session, skips unchanged observation sets, and handles lock timeouts. Graph extraction also skips unchanged inputs. Reflection uses bounded graph snapshots, and health thresholds use the V8 heap limit.

Changes

Session-stop deduplication

Layer / File(s) Summary
Route shutdown through session end
plugin/scripts/stop.mjs, src/hooks/stop.ts
Stop paths remove direct summarize requests, retain session-end handling, and shorten the exit delay.
Serialize and fingerprint summaries
src/types.ts, src/functions/input-fingerprint.ts, src/functions/summarize.ts
Summarization uses a session lock, timeout, and observation fingerprint to skip matching summaries or persist new fingerprints.
Deduplicate graph extraction
src/state/schema.ts, src/triggers/events.ts, test/session-end-triggers-graph.test.ts
Session-stop graph extraction stores observation fingerprints and skips unchanged inputs.
Validate deduplication
test/summarize.test.ts
Tests cover repeated and concurrent calls, changed observations, lock timeouts, and new observations.

Reflection scalability

Layer / File(s) Summary
Use bounded graph snapshots
src/functions/graph.ts, src/functions/reflect.ts, test/reflect.test.ts, CHANGELOG.md
Reflect reads graph snapshots, handles listing failures, limits cluster inputs to 50 records, and tests snapshot-based fallback behavior.

Heap-aware health thresholds

Layer / File(s) Summary
Report and evaluate heap limits
src/health/monitor.ts, src/health/thresholds.ts, src/types.ts, test/health-thresholds.test.ts, CHANGELOG.md
Health snapshots include the V8 heap limit, and memory severity uses it with a fallback to heapTotal.

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

Sequence Diagram(s)

sequenceDiagram
  participant StopFlow
  participant SessionEnd
  participant SummarizeHandler
  participant GraphTrigger
  participant KV
  participant LLMProvider
  StopFlow->>SessionEnd: POST /agentmemory/session/end
  SessionEnd->>SummarizeHandler: Trigger mem::summarize
  SummarizeHandler->>KV: Compare inputFingerprint
  alt Fingerprint differs
    SummarizeHandler->>LLMProvider: Generate summary
    LLMProvider-->>SummarizeHandler: Return summary
    SummarizeHandler->>KV: Persist summary and fingerprint
  else Fingerprint matches
    KV-->>SummarizeHandler: Skip LLM workflow
  end
  SessionEnd->>GraphTrigger: Handle event::session::stopped
  GraphTrigger->>KV: Compare graph extraction fingerprint
  alt Fingerprint differs
    GraphTrigger->>KV: Persist fingerprint
    GraphTrigger->>LLMProvider: Trigger mem::graph-extract
  else Fingerprint matches
    KV-->>GraphTrigger: Skip graph extraction
  end
Loading

Possibly related issues

  • rohitg00/agentmemory#1131: The issue describes the same session-stop summarization deduplication and fingerprinting behavior.

Possibly related PRs

Suggested reviewers: rohitg00

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes health memory thresholds and Reflect graph handling, which are unrelated to the linked issue objectives. Move the health and Reflect changes into separate pull requests, or link issues that explicitly require those changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 clearly describes the primary changes: preventing duplicate summaries and skipping unchanged session-stop processing.
Linked Issues check ✅ Passed The changes satisfy the deduplication, per-session locking, unchanged-input detection, and graph-extraction requirements in [#1062] and [#1063].
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/summarize-double-trigger
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses redundant, double-triggered session summarization by removing an extra stop-hook summarize call and adding a per-session in-flight guard plus an “unchanged input” short-circuit in mem::summarize, with accompanying tests to cover the race/skip behavior.

Changes:

  • Remove the direct /agentmemory/summarize call from stop hooks so stop only triggers /agentmemory/session/end (which already fans out to summarization).
  • Serialize mem::summarize per session via withKeyedLock and add a skip path when the stored summary appears up-to-date.
  • Add tests covering the skip path, concurrency collapse, and re-summarize-on-growth behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/functions/summarize.ts Adds per-session keyed lock and an “up-to-date” short-circuit to avoid duplicate LLM work.
src/hooks/stop.ts Removes the extra summarize request, relying on session-end fanout.
plugin/scripts/stop.mjs Mirrors stop hook behavior change in the packaged script output.
test/summarize.test.ts Adds tests for the new dedup/locking behavior.
Comments suppressed due to low confidence (1)

src/hooks/stop.ts:49

  • This hook now only fires a single HTTP request (/agentmemory/session/end), but the forced-exit timer is still 1500ms. Since an unawaited fetch() keeps the event loop alive, this can unnecessarily delay hook completion vs. other single-request hooks in src/hooks/ which use 500ms.
  fetch(`${REST_URL}/agentmemory/session/end`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify({ sessionId }),
    signal: AbortSignal.timeout(5000),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/functions/summarize.ts Outdated
Comment on lines +272 to +276
const existing = await kv.get<SessionSummary>(
KV.summaries,
sessionId,
});
return {
success: false,
error: "no_provider",
reason:
"No LLM provider key set; Summarize is a no-op. Set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env to enable.",
};
}
);
if (existing && existing.observationCount === compressed.length) {
Comment thread plugin/scripts/stop.mjs
Comment on lines 26 to 33
@@ -38,7 +32,7 @@ async function main() {
setTimeout(() => process.exit(0), 1500).unref();
}

@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: 1

🤖 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 `@src/functions/summarize.ts`:
- Around line 244-282: The session lock in the summarize handler can remain held
indefinitely while produceSummaryXml runs. Bound the provider call used by
produceSummaryXml with the existing timeout mechanism, covering Anthropic and
agent-sdk as well as the already bounded providers, so hung summarization
releases withKeyedLock; alternatively, move the existing observation-count
up-to-date check before lock acquisition only if it remains race-safe.
🪄 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: b7c79df3-c4c1-4e44-bfd0-4f7bd40b4182

📥 Commits

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

📒 Files selected for processing (4)
  • plugin/scripts/stop.mjs
  • src/functions/summarize.ts
  • src/hooks/stop.ts
  • test/summarize.test.ts

Comment thread src/functions/summarize.ts

@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: 1

🧹 Nitpick comments (2)
test/summarize.test.ts (1)

548-573: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage for the "permanently hung" case; consider also covering "eventually resolves after timeout".

This test only proves the lock frees when the provider never settles. It doesn't exercise a provider that resolves shortly after the lock timeout fires — which is the scenario that can produce a stale overwrite of a newer summary (see the critical-issue comment on src/functions/summarize.ts Lines 68-91,473-475). Once that's fixed, a regression test resolving the hung provider after the timeout and asserting the later summary isn't clobbered would be valuable.

🤖 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/summarize.test.ts` around lines 548 - 573, Extend the timeout coverage
around the handler setup and provider summarize flow to use a provider promise
that resolves after SUMMARIZE_LOCK_TIMEOUT_MS, then complete a newer follow-up
summary before resolving the timed-out run. Assert that the late result does not
overwrite the newer summary, while preserving the existing timeout and
lock-release assertions.
src/functions/summarize.ts (1)

306-328: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Independent kv operations can run in parallel.

kv.list(KV.observations(sessionId)) (Line 306-308) and the later kv.get(KV.summaries, sessionId) (Line 325-328) don't depend on each other and could be issued together via Promise.all. Likewise kv.set(KV.summaries, ...) (Line 434) and safeAudit(...) (Line 435-438) write to independent keys and don't need to be sequential.

♻️ Proposed parallelization
-        const observations = await kv.list<CompressedObservation>(
-          KV.observations(sessionId),
-        );
+        const [observations, existingBeforeRun] = await Promise.all([
+          kv.list<CompressedObservation>(KV.observations(sessionId)),
+          kv.get<SessionSummary>(KV.summaries, sessionId),
+        ]);
         const compressed = observations.filter((o) => o.title);
 
         if (compressed.length === 0) {
           ...
         }
 
         const inputFingerprint = computeInputFingerprint(compressed);
-        const existing = await kv.get<SessionSummary>(
-          KV.summaries,
-          sessionId,
-        );
+        const existing = existingBeforeRun;
         if (existing && existing.inputFingerprint === inputFingerprint) {
           summary.inputFingerprint = inputFingerprint;
-        await kv.set(KV.summaries, sessionId, summary);
-          await safeAudit(kv, "compress", "mem::summarize", [sessionId], {
-            title: summary.title,
-            observationCount: compressed.length,
-          });
+          await Promise.all([
+            kv.set(KV.summaries, sessionId, summary),
+            safeAudit(kv, "compress", "mem::summarize", [sessionId], {
+              title: summary.title,
+              observationCount: compressed.length,
+            }),
+          ]);

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

Also applies to: 433-438

🤖 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/summarize.ts` around lines 306 - 328, Update the summarize flow
around the observations read and existing summary lookup to issue the
independent kv.list and kv.get operations together with Promise.all, while
preserving the existing no-observations and fingerprint logic. Near the summary
persistence block, likewise run kv.set and safeAudit concurrently with
Promise.all, ensuring both operations still complete and retain their current
arguments and behavior.

Source: Coding guidelines

🤖 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 `@src/functions/summarize.ts`:
- Around line 68-91: The timeout path must prevent a still-running runSummarize
from persisting stale output after the session lock is released. Add a
supersession/timeout check immediately before the KV summary write and safeAudit
calls, using the existing session/run state, and skip persistence when the
timeout has won; ensure the check also handles providers that resolve after
timeout while preserving normal successful persistence.

---

Nitpick comments:
In `@src/functions/summarize.ts`:
- Around line 306-328: Update the summarize flow around the observations read
and existing summary lookup to issue the independent kv.list and kv.get
operations together with Promise.all, while preserving the existing
no-observations and fingerprint logic. Near the summary persistence block,
likewise run kv.set and safeAudit concurrently with Promise.all, ensuring both
operations still complete and retain their current arguments and behavior.

In `@test/summarize.test.ts`:
- Around line 548-573: Extend the timeout coverage around the handler setup and
provider summarize flow to use a provider promise that resolves after
SUMMARIZE_LOCK_TIMEOUT_MS, then complete a newer follow-up summary before
resolving the timed-out run. Assert that the late result does not overwrite the
newer summary, while preserving the existing timeout and lock-release
assertions.
🪄 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: c802e413-58f5-4fb1-8c2d-cdcabc24b2ab

📥 Commits

Reviewing files that changed from the base of the PR and between d5741ae and b531494.

📒 Files selected for processing (5)
  • plugin/scripts/stop.mjs
  • src/functions/summarize.ts
  • src/hooks/stop.ts
  • src/types.ts
  • test/summarize.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/hooks/stop.ts
  • plugin/scripts/stop.mjs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🧹 Nitpick comments (2)
test/summarize.test.ts (1)

548-573: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage for the "permanently hung" case; consider also covering "eventually resolves after timeout".

This test only proves the lock frees when the provider never settles. It doesn't exercise a provider that resolves shortly after the lock timeout fires — which is the scenario that can produce a stale overwrite of a newer summary (see the critical-issue comment on src/functions/summarize.ts Lines 68-91,473-475). Once that's fixed, a regression test resolving the hung provider after the timeout and asserting the later summary isn't clobbered would be valuable.

🤖 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/summarize.test.ts` around lines 548 - 573, Extend the timeout coverage
around the handler setup and provider summarize flow to use a provider promise
that resolves after SUMMARIZE_LOCK_TIMEOUT_MS, then complete a newer follow-up
summary before resolving the timed-out run. Assert that the late result does not
overwrite the newer summary, while preserving the existing timeout and
lock-release assertions.
src/functions/summarize.ts (1)

306-328: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Independent kv operations can run in parallel.

kv.list(KV.observations(sessionId)) (Line 306-308) and the later kv.get(KV.summaries, sessionId) (Line 325-328) don't depend on each other and could be issued together via Promise.all. Likewise kv.set(KV.summaries, ...) (Line 434) and safeAudit(...) (Line 435-438) write to independent keys and don't need to be sequential.

♻️ Proposed parallelization
-        const observations = await kv.list<CompressedObservation>(
-          KV.observations(sessionId),
-        );
+        const [observations, existingBeforeRun] = await Promise.all([
+          kv.list<CompressedObservation>(KV.observations(sessionId)),
+          kv.get<SessionSummary>(KV.summaries, sessionId),
+        ]);
         const compressed = observations.filter((o) => o.title);
 
         if (compressed.length === 0) {
           ...
         }
 
         const inputFingerprint = computeInputFingerprint(compressed);
-        const existing = await kv.get<SessionSummary>(
-          KV.summaries,
-          sessionId,
-        );
+        const existing = existingBeforeRun;
         if (existing && existing.inputFingerprint === inputFingerprint) {
           summary.inputFingerprint = inputFingerprint;
-        await kv.set(KV.summaries, sessionId, summary);
-          await safeAudit(kv, "compress", "mem::summarize", [sessionId], {
-            title: summary.title,
-            observationCount: compressed.length,
-          });
+          await Promise.all([
+            kv.set(KV.summaries, sessionId, summary),
+            safeAudit(kv, "compress", "mem::summarize", [sessionId], {
+              title: summary.title,
+              observationCount: compressed.length,
+            }),
+          ]);

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

Also applies to: 433-438

🤖 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/summarize.ts` around lines 306 - 328, Update the summarize flow
around the observations read and existing summary lookup to issue the
independent kv.list and kv.get operations together with Promise.all, while
preserving the existing no-observations and fingerprint logic. Near the summary
persistence block, likewise run kv.set and safeAudit concurrently with
Promise.all, ensuring both operations still complete and retain their current
arguments and behavior.

Source: Coding guidelines

🤖 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 `@src/functions/summarize.ts`:
- Around line 68-91: The timeout path must prevent a still-running runSummarize
from persisting stale output after the session lock is released. Add a
supersession/timeout check immediately before the KV summary write and safeAudit
calls, using the existing session/run state, and skip persistence when the
timeout has won; ensure the check also handles providers that resolve after
timeout while preserving normal successful persistence.

---

Nitpick comments:
In `@src/functions/summarize.ts`:
- Around line 306-328: Update the summarize flow around the observations read
and existing summary lookup to issue the independent kv.list and kv.get
operations together with Promise.all, while preserving the existing
no-observations and fingerprint logic. Near the summary persistence block,
likewise run kv.set and safeAudit concurrently with Promise.all, ensuring both
operations still complete and retain their current arguments and behavior.

In `@test/summarize.test.ts`:
- Around line 548-573: Extend the timeout coverage around the handler setup and
provider summarize flow to use a provider promise that resolves after
SUMMARIZE_LOCK_TIMEOUT_MS, then complete a newer follow-up summary before
resolving the timed-out run. Assert that the late result does not overwrite the
newer summary, while preserving the existing timeout and lock-release
assertions.
🪄 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: c802e413-58f5-4fb1-8c2d-cdcabc24b2ab

📥 Commits

Reviewing files that changed from the base of the PR and between d5741ae and b531494.

📒 Files selected for processing (5)
  • plugin/scripts/stop.mjs
  • src/functions/summarize.ts
  • src/hooks/stop.ts
  • src/types.ts
  • test/summarize.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/hooks/stop.ts
  • plugin/scripts/stop.mjs
🛑 Comments failed to post (1)
src/functions/summarize.ts (1)

68-91: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -B3 -A15 'withKeyedLock' src -g'*.ts'
rg -n 'Promise\.race' src -g'*.ts'

Repository: rohitg00/agentmemory

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first
ast-grep outline src/functions/summarize.ts --view expanded
printf '\n--- keyed-mutex ---\n'
ast-grep outline src/state/keyed-mutex.ts --view expanded

printf '\n--- summarize.ts key slices ---\n'
sed -n '1,220p' src/functions/summarize.ts | cat -n

printf '\n--- keyed-mutex.ts ---\n'
sed -n '1,220p' src/state/keyed-mutex.ts | cat -n

printf '\n--- summarize tests around timeout ---\n'
sed -n '520,620p' src/functions/summarize.ts | cat -n

Repository: rohitg00/agentmemory

Length of output: 10917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on summarize persistence and any freshness checks
rg -n -A6 -B6 'kv\.set\(KV\.summaries|inputFingerprint|summarize_timeout|withKeyedLock|raceLockTimeout|runSummarize' src/functions/summarize.ts

Repository: rohitg00/agentmemory

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the exact file size before reading more if needed
wc -l src/functions/summarize.ts src/state/keyed-mutex.ts

Repository: rohitg00/agentmemory

Length of output: 234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the parts that define the timeout/lock behavior
sed -n '1,220p' src/functions/summarize.ts | nl -ba | sed -n '1,220p'
printf '\n---\n'
sed -n '1,220p' src/state/keyed-mutex.ts | nl -ba | sed -n '1,220p'

Repository: rohitg00/agentmemory

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for summary writes and freshness checks in the repository
rg -n -A4 -B4 'KV\.summaries|inputFingerprint|summarize_timeout|summarize|SessionSummary' src/functions src/state src/types -g'*.ts'

Repository: rohitg00/agentmemory

Length of output: 17669


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show summarize.ts around the timeout and persistence paths only
nl -ba src/functions/summarize.ts | sed -n '1,240p'

Repository: rohitg00/agentmemory

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show keyed mutex implementation
nl -ba src/state/keyed-mutex.ts | sed -n '1,220p'

Repository: rohitg00/agentmemory

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the summarize implementation around the write path.
sed -n '220,478p' src/functions/summarize.ts | cat -n

printf '\n--- tests around summarize timeout ---\n'
rg -n -A20 -B10 'summarize_timeout|lock timeout|raceLockTimeout|mem::summarize|runSummarize' src -g'*.ts'

Repository: rohitg00/agentmemory

Length of output: 19396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the summarize implementation around the write path using cat -n.
sed -n '220,478p' src/functions/summarize.ts | cat -n

printf '\n--- nearby tests/mentions ---\n'
rg -n -A8 -B8 'summarize_timeout|raceLockTimeout|runSummarize|mem::summarize' src -g'*.ts'

Repository: rohitg00/agentmemory

Length of output: 16092


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'kv\.set|kv\.delete|recordAudit|safeAudit|metricsStore|KV\.summaries|inputFingerprint|createdAt|updatedAt|sessionId' src/functions/summarize.ts

Repository: rohitg00/agentmemory

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A20 -B20 'registerSummarizeFunction|mem::summarize|summarize\(' src -g'*.ts'

Repository: rohitg00/agentmemory

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the bottom half of summarize.ts directly.
sed -n '280,478p' src/functions/summarize.ts | cat -n

Repository: rohitg00/agentmemory

Length of output: 8703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '280,478p' src/functions/summarize.ts | cat -n

Repository: rohitg00/agentmemory

Length of output: 8703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

grep -nE 'summaries|inputFingerprint|safeAudit|metricsStore|kv\.set|kv\.get|recordAudit|SessionSummary' src/functions/summarize.ts

Repository: rohitg00/agentmemory

Length of output: 212


Prevent timed-out summarize runs from overwriting newer summaries

Promise.race only ends the caller; it doesn’t cancel runSummarize(). Once the timeout branch wins, the lock is released and the old run can still reach kv.set(KV.summaries, sessionId, summary) / safeAudit, overwriting a newer result. Add a supersession check right before persist (or abort the provider call on timeout) and cover the case where the provider resolves after the timeout.

🤖 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/summarize.ts` around lines 68 - 91, The timeout path must
prevent a still-running runSummarize from persisting stale output after the
session lock is released. Add a supersession/timeout check immediately before
the KV summary write and safeAudit calls, using the existing session/run state,
and skip persistence when the timeout has won; ensure the check also handles
providers that resolve after timeout while preserving normal successful
persistence.

… exit timer

- Replace the observationCount-based skip with an input fingerprint
  (sha256 over ordered observation ids + timestamps). Count alone is not
  safe: evict/auto-forget can delete observations, so the set can change
  while the count returns to a previous value. Old summaries have no
  fingerprint and simply re-summarize once.
- Bound how long one run may hold the per-session lock
  (SUMMARIZE_LOCK_TIMEOUT_MS, default 5 min): anthropic/agent-sdk
  providers have no request timeout, and a hung call must not block every
  later summarize for that session.
- Align the stop hook forced-exit timer with other single-request hooks
  (1500ms -> 500ms) now that it only sends one request.
…hanged

The session-stopped handler sent the session's full compressed observation
set to mem::graph-extract on every stop. Sessions that stop repeatedly
without new activity (heartbeat-style loops) re-extracted the identical
set each time — for a 500-observation session that is a ~50K-token LLM
call per stop. Fingerprint the set (same helper as the summarize skip)
and store it per session; trigger extraction only when it changed.
@yulin0629
yulin0629 force-pushed the fix/summarize-double-trigger branch from b531494 to b6a002b Compare July 15, 2026 08:07

@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: 1

🧹 Nitpick comments (1)
src/triggers/events.ts (1)

66-80: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize independent KV reads using Promise.all.

As per coding guidelines, use parallel operations where possible with Promise.all for independent kv writes/reads. You can fetch the previous fingerprint state concurrently with the observations list to save a network round trip.

⚡ Proposed refactor
-        const observations = await kv.list<CompressedObservation>(
-          KV.observations(data.sessionId),
-        );
+        const [observations, prev] = await Promise.all([
+          kv.list<CompressedObservation>(KV.observations(data.sessionId)),
+          kv.get<{ fingerprint: string }>(KV.graphExtractState, data.sessionId).catch(() => null),
+        ]);
         const compressed = observations.filter((o) => o.title);
         if (compressed.length > 0) {
           // Sessions that stop repeatedly without new observations (e.g.
           // heartbeat loops) would re-extract the identical set every time;
           // skip when the fingerprint of the set is unchanged.
           const fingerprint = computeInputFingerprint(compressed);
-          const prev = await kv
-            .get<{ fingerprint: string }>(
-              KV.graphExtractState,
-              data.sessionId,
-            )
-            .catch(() => null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/triggers/events.ts` around lines 66 - 80, Update the event handling flow
around KV.observations and KV.graphExtractState to start both independent reads
concurrently with Promise.all, then derive compressed observations and the
previous fingerprint state from the results while preserving the existing
filtering and fallback behavior.

Source: Coding guidelines

🤖 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 `@test/session-end-triggers-graph.test.ts`:
- Around line 94-118: Replace the source-text regex tests in the
“event::session::stopped graph-extract dedup” suite with runtime tests that
invoke the handler registered by registerEventTriggers using local mock sdk and
kv objects. Mock sdk.trigger, kv.get, kv.set, and kv.list, then assert that the
compressed fingerprint is persisted, unchanged fingerprints skip extraction, and
mem::graph-extract triggers only for new fingerprints.

---

Nitpick comments:
In `@src/triggers/events.ts`:
- Around line 66-80: Update the event handling flow around KV.observations and
KV.graphExtractState to start both independent reads concurrently with
Promise.all, then derive compressed observations and the previous fingerprint
state from the results while preserving the existing filtering and fallback
behavior.
🪄 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: 1db93c90-2e2b-47cf-a727-747ab884f1dc

📥 Commits

Reviewing files that changed from the base of the PR and between b531494 and b6a002b.

📒 Files selected for processing (9)
  • plugin/scripts/stop.mjs
  • src/functions/input-fingerprint.ts
  • src/functions/summarize.ts
  • src/hooks/stop.ts
  • src/state/schema.ts
  • src/triggers/events.ts
  • src/types.ts
  • test/session-end-triggers-graph.test.ts
  • test/summarize.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/types.ts
  • src/hooks/stop.ts
  • src/functions/summarize.ts
  • test/summarize.test.ts
  • plugin/scripts/stop.mjs

Comment on lines +94 to +118

// Sessions that stop repeatedly without new observations (e.g. heartbeat
// loops) used to re-extract the identical observation set on every stop.
// The handler now fingerprints the set and skips when unchanged.
describe("event::session::stopped graph-extract dedup", () => {
const events = readFileSync("src/triggers/events.ts", "utf-8");

it("fingerprints the compressed set before triggering mem::graph-extract", () => {
expect(events).toMatch(
/const fingerprint = computeInputFingerprint\(compressed\);[\s\S]*?function_id:\s*"mem::graph-extract"/,
);
});

it("skips extraction when the stored fingerprint matches", () => {
expect(events).toMatch(
/prev\.fingerprint === fingerprint[\s\S]*?Graph extraction skipped/,
);
});

it("persists the fingerprint under KV.graphExtractState before triggering", () => {
expect(events).toMatch(
/kv\.set\(KV\.graphExtractState,\s*data\.sessionId,\s*\{\s*fingerprint,[\s\S]*?\}\);[\s\S]*?function_id:\s*"mem::graph-extract"/,
);
});
});

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 | 🟠 Major | 🏗️ Heavy lift

Refactor test to execute runtime behavior using mocks.

As per coding guidelines, test files must use the mock pattern with mock implementations of sdk.trigger, kv.get, kv.set, and kv.list. Testing logic by reading the source file as a string and matching regular expressions against it is highly brittle and fails to verify actual runtime execution.

Based on learnings, since the registerEventTriggers function accepts sdk and kv directly as arguments, you can pass local mock objects (e.g., mockSdk()/mockKV()) to invoke the target handler and assert on the executed state changes and triggers instead of attempting to mock the module.

🤖 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/session-end-triggers-graph.test.ts` around lines 94 - 118, Replace the
source-text regex tests in the “event::session::stopped graph-extract dedup”
suite with runtime tests that invoke the handler registered by
registerEventTriggers using local mock sdk and kv objects. Mock sdk.trigger,
kv.get, kv.set, and kv.list, then assert that the compressed fingerprint is
persisted, unchanged fingerprints skip extraction, and mem::graph-extract
triggers only for new fingerprints.

Sources: Coding guidelines, Learnings

@yulin0629 yulin0629 changed the title fix(summarize): dedupe double-triggered session summaries fix: dedupe double-triggered summaries and unchanged-input re-processing on session stop Jul 15, 2026
yulin0629 added a commit to yulin0629/agentmemory that referenced this pull request Jul 19, 2026
`kv.list(KV.graphNodes)` stopped completing once mem:graph:nodes.bin grew
past 100 MiB (26K nodes, 3.5M sourceObservationIds). The failure was
swallowed by `.catch(() => [])`, so graph clustering silently returned no
clusters and reflect fell back to lexical Jaccard clustering, which built
a cluster matching 6497 facts and a ~400k-token prompt. Every run then
exceeded the 180s invocation timeout, disabling insight generation.

Reflect now reads the rohitg00#814 top-degree snapshot via a single kv.get, and
cluster inputs are capped at 50 items per category so the fallback path
can no longer produce an unbounded prompt or unbounded sourceMemoryIds.
The remaining kv.list calls log a warning instead of degrading silently.

Verified in production: 42s, usedFallback=false, 5 new insights, where
every prior run timed out at 180s.

AI-Agent: Claude Code
AI-Session-IDs: 8affe40e-9240-4076-96a8-c71d01a59bdc
…d heap

memPercent divided heapUsed by heapTotal, but heapTotal is the heap V8 has
committed so far, which it deliberately keeps near-full and grows on
demand. A healthy process therefore reports ~100%, and since the RSS floor
(512MB) is trivially cleared by any real workload, the server pinned itself
at status=critical and /agentmemory/health returned 503 indefinitely.

Observed on the Oracle host: 588MB used of a 603MB committed heap = 97%,
while --max-old-space-size=6144 puts real utilisation under 10%. That
makes health useless as a liveness signal — a watchdog keyed on it would
restart a perfectly healthy server forever.

The snapshot now carries heapLimit from v8.getHeapStatistics(), and the
ratio uses it when present. heapLimit is optional so snapshots persisted
before this change still evaluate against heapTotal as they did before.

The RSS floor added for issue rohitg00#158 was a band-aid over this same defect;
it is left in place as a second guard.

AI-Agent: Claude Code
AI-Session-IDs: 8affe40e-9240-4076-96a8-c71d01a59bdc
AI-Agent: Claude Code
AI-Session-IDs: 8affe40e-9240-4076-96a8-c71d01a59bdc

@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: 1

🧹 Nitpick comments (3)
src/functions/reflect.ts (1)

191-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove implementation-detail comments.

Lines 191-197 restate the graph-loading implementation. Use clear names and keep only comments that explain non-obvious constraints.

As per coding guidelines, "In TypeScript source code, avoid code comments explaining WHAT — use clear naming instead."

🤖 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/reflect.ts` around lines 191 - 197, Remove the
implementation-detail comment above the graph-node loading logic in reflect,
leaving the code behavior unchanged. Use the existing symbol names and structure
to convey the data source and fallback behavior; retain comments only if they
document a non-obvious constraint.

Source: Coding guidelines

src/health/thresholds.ts (1)

62-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shorten the explanatory comment.

heapCeiling, heapLimit, and heapTotal already identify the values. Keep only a short rationale, or remove the comment block.

Proposed change
-  // heapTotal is the heap V8 has *committed*, which it keeps near-full by design
-  // and grows on demand — measuring against it reports ~100% on a perfectly
-  // healthy process. Measure against the real ceiling (--max-old-space-size)
-  // when the snapshot carries it; older persisted snapshots do not.
+  // Use the V8 ceiling to avoid false high-memory alerts.

As per coding guidelines, src/**/*.ts must avoid code comments explaining WHAT and use clear naming instead.

🤖 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/health/thresholds.ts` around lines 62 - 65, Shorten or remove the
explanatory comment above the heap ceiling calculation, keeping at most a brief
rationale for preferring the real V8 heap limit over heapTotal. Leave the
existing heapCeiling, heapLimit, and heapTotal logic unchanged.

Source: Coding guidelines

test/health-thresholds.test.ts (1)

83-113: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for snapshots without heapLimit.

Both tests set heapLimit, so they do not exercise the fallback in src/health/thresholds.ts Lines 66-69. Add a case with heapLimit omitted and usage above the configured threshold. This protects legacy persisted snapshots.

Proposed test
+  it("falls back to heapTotal when heapLimit is absent", () => {
+    const s = snap({
+      memory: {
+        heapUsed: 95,
+        heapTotal: 100,
+        rss: 1,
+        external: 0,
+      },
+    });
+    const result = evaluateHealth(s, {
+      memoryWarnPercent: 80,
+      memoryCriticalPercent: 90,
+      memoryRssFloorBytes: 0,
+    });
+    expect(result.status).toBe("critical");
+    expect(result.alerts.some((a) => a.startsWith("memory_critical_"))).toBe(
+      true,
+    );
+  });
🤖 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/health-thresholds.test.ts` around lines 83 - 113, Add a test in the
health threshold suite that omits memory.heapLimit from the snapshot and sets
heap usage above the configured fallback threshold, then assert evaluateHealth
returns the expected critical status and memory alert. Keep the existing
heapLimit-based tests unchanged and ensure the new case covers legacy persisted
snapshots.
🤖 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 `@src/functions/reflect.ts`:
- Around line 198-205: Update the reflection data-loading flow around
Promise.all to derive graphEdges from snapshot?.topEdges ?? [] instead of
calling listOrEmpty on KV.graphEdges, while preserving the existing snapshot
node and memory loading behavior. Extend the relevant reflect test to verify
that mem:graph:edges is not enumerated.

---

Nitpick comments:
In `@src/functions/reflect.ts`:
- Around line 191-197: Remove the implementation-detail comment above the
graph-node loading logic in reflect, leaving the code behavior unchanged. Use
the existing symbol names and structure to convey the data source and fallback
behavior; retain comments only if they document a non-obvious constraint.

In `@src/health/thresholds.ts`:
- Around line 62-65: Shorten or remove the explanatory comment above the heap
ceiling calculation, keeping at most a brief rationale for preferring the real
V8 heap limit over heapTotal. Leave the existing heapCeiling, heapLimit, and
heapTotal logic unchanged.

In `@test/health-thresholds.test.ts`:
- Around line 83-113: Add a test in the health threshold suite that omits
memory.heapLimit from the snapshot and sets heap usage above the configured
fallback threshold, then assert evaluateHealth returns the expected critical
status and memory alert. Keep the existing heapLimit-based tests unchanged and
ensure the new case covers legacy persisted snapshots.
🪄 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 Plus

Run ID: 7c1d9435-6cdb-4fa3-974f-7d4636edf8cb

📥 Commits

Reviewing files that changed from the base of the PR and between b6a002b and 9475937.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • src/functions/graph.ts
  • src/functions/reflect.ts
  • src/health/monitor.ts
  • src/health/thresholds.ts
  • src/types.ts
  • test/health-thresholds.test.ts
  • test/reflect.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/types.ts

Comment thread src/functions/reflect.ts
Comment on lines +198 to 205
const [snapshot, graphEdges, semanticMemories, lessons, crystals] =
await Promise.all([
kv.list<GraphNode>(KV.graphNodes).catch(() => []),
kv.list<GraphEdge>(KV.graphEdges).catch(() => []),
kv.list<SemanticMemory>(KV.semantic).catch(() => []),
kv.list<Lesson>(KV.lessons).catch(() => []),
kv.list<Crystal>(KV.crystals).catch(() => []),
readSnapshot(kv),
listOrEmpty<GraphEdge>(KV.graphEdges),
listOrEmpty<SemanticMemory>(KV.semantic),
listOrEmpty<Lesson>(KV.lessons),
listOrEmpty<Crystal>(KV.crystals),
]);

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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Use snapshot edges for reflection.

Line 201 still enumerates every graph edge. A large KV.graphEdges scope can consume the invocation budget before listOrEmpty catches a failure. Reflection then has snapshot nodes but no edges, so it unnecessarily uses lexical clustering.

Use snapshot?.topEdges ?? []. GraphSnapshot.topEdges is the bounded edge set that corresponds to the snapshot node set. Extend test/reflect.test.ts to assert that mem:graph:edges is not enumerated.

Proposed fix
-      const [snapshot, graphEdges, semanticMemories, lessons, crystals] =
+      const [snapshot, semanticMemories, lessons, crystals] =
         await Promise.all([
           readSnapshot(kv),
-          listOrEmpty<GraphEdge>(KV.graphEdges),
           listOrEmpty<SemanticMemory>(KV.semantic),
           listOrEmpty<Lesson>(KV.lessons),
           listOrEmpty<Crystal>(KV.crystals),
         ]);
+      const graphEdges = snapshot?.topEdges ?? [];
📝 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
const [snapshot, graphEdges, semanticMemories, lessons, crystals] =
await Promise.all([
kv.list<GraphNode>(KV.graphNodes).catch(() => []),
kv.list<GraphEdge>(KV.graphEdges).catch(() => []),
kv.list<SemanticMemory>(KV.semantic).catch(() => []),
kv.list<Lesson>(KV.lessons).catch(() => []),
kv.list<Crystal>(KV.crystals).catch(() => []),
readSnapshot(kv),
listOrEmpty<GraphEdge>(KV.graphEdges),
listOrEmpty<SemanticMemory>(KV.semantic),
listOrEmpty<Lesson>(KV.lessons),
listOrEmpty<Crystal>(KV.crystals),
]);
const [snapshot, semanticMemories, lessons, crystals] =
await Promise.all([
readSnapshot(kv),
listOrEmpty<SemanticMemory>(KV.semantic),
listOrEmpty<Lesson>(KV.lessons),
listOrEmpty<Crystal>(KV.crystals),
]);
const graphEdges = snapshot?.topEdges ?? [];
🤖 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/reflect.ts` around lines 198 - 205, Update the reflection
data-loading flow around Promise.all to derive graphEdges from
snapshot?.topEdges ?? [] instead of calling listOrEmpty on KV.graphEdges, while
preserving the existing snapshot node and memory loading behavior. Extend the
relevant reflect test to verify that mem:graph:edges is not enumerated.

@devon3000

Copy link
Copy Markdown

Rebased this onto current main (2d38daf, 0.9.29) — full suite green, 1668 passed / 0 failed. Branch: https://github.com/devon3000/agentmemory/tree/pr-1061-rebased

Three conflicts, all mechanical:

  • src/hooks/stop.ts / plugin/scripts/stop.mjsmain has since dropped the direct /agentmemory/summarize call on its own (comment cites mem::summarize is dispatched twice per Stop hook #1203), so that half of this PR is already in. Kept main's conversation_id fallback and main().catch(...).
  • src/triggers/events.ts — the event::session::stopped handler was rewritten upstream (adds a local fireVoid helper plus a consolidation cooldown). Folded the graph fingerprint into the new structure, dispatching through fireVoid so it picks up that helper's failure logging.
  • test/session-end-triggers-graph.test.ts — the two dedup assertions match the trigger call site as literal source text, so they failed on the fireVoid spelling even though the ordering they assert (fingerprint computed and persisted before mem::graph-extract fires) still holds. Widened the regex to accept either spelling.

Verified the summarize guard live against a self-hosted 0.9.29 (Anthropic provider, session pinned at MAX_OBS_PER_SESSION=500):

  • repeat call → skipped: true in 0.024s, zero LLM calls
  • two concurrent calls → both return at 25.5s with exactly one real LLM run; the second waits on the keyed lock and then hits the fingerprint skip

That second case is the one a pure time-window dedup (#572) can't close, which matches your reasoning in the description — worth keeping the lock rather than folding this into a freshness window.

One behavioural note worth a changelog line: summaries written before inputFingerprint existed carry no fingerprint, so each session re-summarizes once after upgrade before skips kick in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants