Skip to content

fix: evict lowest-value observations at the session cap instead of refusing writes - #1174

Open
sbynode-ux wants to merge 1 commit into
rohitg00:mainfrom
sbynode-ux:fix/session-cap-evict
Open

fix: evict lowest-value observations at the session cap instead of refusing writes#1174
sbynode-ux wants to merge 1 commit into
rohitg00:mainfrom
sbynode-ux:fix/session-cap-evict

Conversation

@sbynode-ux

@sbynode-ux sbynode-ux commented Aug 12, 2026

Copy link
Copy Markdown

Problem

Once a session reaches MAX_OBS_PER_SESSION (default 500), mem::observe returns Session observation limit reached (N) and every subsequent observation in that session is discarded. Two consequences:

  1. The dropped observations are the newest ones in the longest sessions. A session only reaches the cap because a lot happened in it, and the work at the end is usually what a later session needs to recall.
  2. Nothing is logged server-side. A capped session is indistinguishable from an idle one in the journal, so the loss is invisible until someone counts rows. On the install where this surfaced, three sessions had been pinned at exactly 500 rows for days.

Change

At the cap, evict instead of refuse:

  • sort by least valuable first — lowest importance, oldest timestamp breaking ties (uncompressed rows have no importance yet, so they are treated as 3)
  • delete just enough rows to admit the incoming observation, so the storage bound the cap expresses is unchanged
  • remove each evicted id from the BM25 index and the vector index, so search cannot rank an observation that no longer exists
  • logger.warn with the session id, the cap and the number evicted

A failed delete is swallowed deliberately: the cap simply stays tight for that write and the next observation retries the eviction, which is strictly better than failing the capture.

Verification

test/observe-session-cap.test.ts (new, 3 cases):

  • at the cap the new observation is accepted and the lowest-importance row is the one gone
  • on an importance tie the older row is evicted, and it is removed from both indexes
  • under the cap nothing is evicted and no index entry is touched

Full suite: 1599 passed, 1 skipped.

Unrelated finding while running the suite: on a machine that has a real ~/.agentmemory/.env, 14 tests fail (embedding-provider, fetch-timeout, auto-compress, context-slots, claude-bridge-path) because config loading merges that file, so the suite picks up the developer's own keys and flags. Everything passes with an isolated HOME. Happy to open that as a separate issue if it is not already known.

Summary by CodeRabbit

  • New Features

    • Sessions now continue accepting observations when the observation limit is reached.
    • Lower-importance observations are automatically removed first; ties are resolved by removing the oldest observation.
    • Removed observations are cleared from search results and storage.
  • Bug Fixes

    • Observation cleanup continues even if an individual removal fails.
    • Uncompressed observations now receive a default importance score.

…fusing writes

MAX_OBS_PER_SESSION currently makes mem::observe return
"Session observation limit reached" once a session holds its cap, so every
later observation in that session is dropped. The observations lost are the
NEWEST ones in the LONGEST sessions, which is the part a memory system is
least able to afford losing, and nothing is logged server-side, so a capped
session looks identical to a quiet one.

Evict instead: at the cap, drop the least valuable rows (lowest importance
first, oldest breaking ties) to make room, clear them from the BM25 and vector
indexes so search cannot return dead ids, and warn with the session id and the
number evicted. The bound on how much a session stores is unchanged; only the
choice of which observation is sacrificed changes.

Three sessions in a real install had been silently pinned at exactly 500 rows
for days before this was noticed.

Signed-off-by: sbynode-ux <sby.node@gmail.com>
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

@sbynode-ux 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 Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Observation capture now evicts the least-important records when a session reaches its cap. Ties use record age. Successful evictions remove records from KV, BM25, and vector indexes. Missing importance scores use a default value, and individual deletion failures do not stop processing.

Changes

Session observation-cap eviction

Layer / File(s) Summary
Eviction and index cleanup
src/functions/observe.ts, test/observe-session-cap.test.ts
The session cap now evicts the least-important observations, breaks ties by oldest timestamp, removes successful victims from both search indexes, logs eviction results, tolerates deletion failures, and verifies under-capacity behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • rohitg00/agentmemory#1157 — Related observation eviction and deletion behavior; this change does not add session-counter or graph-provenance reconciliation.

Possibly related PRs

  • rohitg00/agentmemory#636 — This change extends the index-cleanup behavior to evicted observations during session-cap enforcement.

Suggested reviewers: rohitg00

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change from rejecting writes at the session cap to evicting the lowest-value observations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ 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.

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

🧹 Nitpick comments (1)
src/functions/observe.ts (1)

19-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the explanatory comment.

DEFAULT_EVICTION_IMPORTANCE already describes the constant use.
As per coding guidelines, “Do not add comments that explain what code does; 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/observe.ts` around lines 19 - 20, Remove the explanatory
comment immediately above DEFAULT_EVICTION_IMPORTANCE, leaving the constant
declaration and its value unchanged.

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/observe.ts`:
- Around line 157-166: Update the eviction loop in the observation write flow to
continue attempting additional victims until enough deletions succeed to make
room for the new observation. If the required eviction count cannot be achieved,
reject or skip the write before the persistence call around line 221, preserving
the maxObservationsPerSession cap; add a test covering failed deletions and
verifying no new observation is written.
- Around line 159-162: Update the automatic eviction flow around
getSearchIndex().remove and vectorIndexRemove to collect victim IDs and issue
one batched recordAudit() delete entry with details.evicted before deleting
records via kv.delete(). Preserve the existing eviction and index-removal
behavior while ensuring the audit is recorded before the bulk KV deletion.

In `@test/observe-session-cap.test.ts`:
- Around line 1-5: Update the module-level mocks in the test setup to mock
iii-sdk and provide TriggerAction.Void, ensuring mem::observe calls do not
execute the real trigger action. Preserve the existing mockSdk() and mockKV()
collaborator mocks.

---

Nitpick comments:
In `@src/functions/observe.ts`:
- Around line 19-20: Remove the explanatory comment immediately above
DEFAULT_EVICTION_IMPORTANCE, leaving the constant declaration and its value
unchanged.
🪄 Autofix

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: a9a1a1a9-40b5-4ef8-ac98-1ba000665856

📥 Commits

Reviewing files that changed from the base of the PR and between 2973e4e and 3a943fa.

📒 Files selected for processing (2)
  • src/functions/observe.ts
  • test/observe-session-cap.test.ts

Comment thread src/functions/observe.ts
Comment on lines +157 to +166
for (const victim of victims) {
try {
await kv.delete(KV.observations(payload.sessionId), victim.id);
evicted++;
getSearchIndex().remove(victim.id);
vectorIndexRemove(victim.id);
} catch {
// A failed delete just leaves the cap tight for this write;
// the next observation retries the eviction.
}

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

Do not write when eviction did not make room.

A failed kv.delete() is swallowed, but Line 221 still writes the new observation. At the cap, this produces more than maxObservationsPerSession records. Continue through additional candidates until the required number of deletions succeeds. If that is not possible, reject the write or explicitly change the cap guarantee. Add a failed-delete test.

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

In `@src/functions/observe.ts` around lines 157 - 166, Update the eviction loop in
the observation write flow to continue attempting additional victims until
enough deletions succeed to make room for the new observation. If the required
eviction count cannot be achieved, reject or skip the write before the
persistence call around line 221, preserving the maxObservationsPerSession cap;
add a test covering failed deletions and verifying no new observation is
written.

Comment thread src/functions/observe.ts
Comment on lines +159 to +162
await kv.delete(KV.observations(payload.sessionId), victim.id);
evicted++;
getSearchIndex().remove(victim.id);
vectorIndexRemove(victim.id);

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
set -euo pipefail

rg -n -C 4 '\brecordAudit\s*\(' src test

Repository: rohitg00/agentmemory

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- observe imports and eviction path ---'
sed -n '1,35p' src/functions/observe.ts
sed -n '125,180p' src/functions/observe.ts

printf '%s\n' '--- audit implementation and guidance ---'
sed -n '1,90p' src/functions/audit.ts

printf '%s\n' '--- comparable eviction audit path ---'
sed -n '350,410p' src/functions/retention.ts

printf '%s\n' '--- observe references ---'
rg -n -C 3 'observe|retention-evict|recordAudit|kv\.delete' test src/functions/observe.ts src/functions/retention.ts

Repository: rohitg00/agentmemory

Length of output: 46881


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- audit entry type ---'
rg -n -A 28 'export interface AuditEntry|type AuditEntry' src/types.ts

printf '%s\n' '--- session-cap tests ---'
sed -n '1,175p' test/observe-session-cap.test.ts

printf '%s\n' '--- audit policy assertions ---'
rg -n -C 5 'audit coverage|details\.evicted|targetIds|BEFORE kv\.delete|bulk' src test/retention.test.ts

Repository: rohitg00/agentmemory

Length of output: 24865


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

observe = Path("src/functions/observe.ts").read_text()
audit = Path("src/functions/audit.ts").read_text()

delete_match = re.search(
    r"for \(const victim of victims\) \{(?P<body>.*?)\n\s*\}\n\n\s*logger\.warn",
    observe,
    re.S,
)
if not delete_match:
    raise SystemExit("Could not locate the session-cap eviction loop")

body = delete_match.group("body")
print("observe imports audit.ts:", bool(re.search(r'from ["\']\.\/audit\.js["\']', observe)))
print("recordAudit call in eviction loop:", bool(re.search(r"\brecordAudit\s*\(", body)))
print("kv.delete calls in eviction loop:", len(re.findall(r"\bkv\.delete\s*\(", body)))
print("evictedIds collection in observe:", "evictedIds" in observe)
print("batch details.evicted policy:", bool(re.search(r"details\.evicted", audit)))
print("policy requires audit before delete:", bool(re.search(r"recordAudit call\n.*?BEFORE kv\.delete", audit, re.S)))
PY

Repository: rohitg00/agentmemory

Length of output: 384


Audit automatic observation evictions.

Add one batched delete audit for this bulk path with the victim IDs and details.evicted. Follow the audit policy and call recordAudit() before kv.delete().

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

In `@src/functions/observe.ts` around lines 159 - 162, Update the automatic
eviction flow around getSearchIndex().remove and vectorIndexRemove to collect
victim IDs and issue one batched recordAudit() delete entry with details.evicted
before deleting records via kv.delete(). Preserve the existing eviction and
index-removal behavior while ensuring the audit is recorded before the bulk KV
deletion.

Source: Coding guidelines

Comment on lines +1 to +5
import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("../src/logger.js", () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));

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
set -euo pipefail

ast-grep outline src/functions/observe.ts --items all
rg -n -C 2 'from "iii-sdk"|TriggerAction\.' src/functions/observe.ts
rg -n 'vi\.mock\(["'\'']iii-sdk' test/observe-session-cap.test.ts

Repository: rohitg00/agentmemory

Length of output: 1906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test file ---'
cat -n test/observe-session-cap.test.ts

printf '%s\n' '--- function registration and trigger usage ---'
sed -n '1,380p' src/functions/observe.ts | cat -n | sed -n '1,380p'

printf '%s\n' '--- related test mock patterns ---'
rg -n -C 5 'vi\.mock\(["'\'']iii-sdk|TriggerAction\.Void|mockSdk|mockKV' test src package.json vitest.config.* 2>/dev/null || true

Repository: rohitg00/agentmemory

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- observe-session-cap.test.ts ---'
cat -n test/observe-session-cap.test.ts

printf '%s\n' '--- observe.ts imports, registration, and TriggerAction calls ---'
sed -n '1,60p' src/functions/observe.ts | cat -n
sed -n '180,225p' src/functions/observe.ts | cat -n
sed -n '245,280p' src/functions/observe.ts | cat -n
sed -n '315,350p' src/functions/observe.ts | cat -n

printf '%s\n' '--- iii-sdk mocks in the target test ---'
rg -n -C 3 'iii-sdk|TriggerAction|sdk\.trigger|registerObserveFunction|mockSdk|mockKV' test/observe-session-cap.test.ts

Repository: rohitg00/agentmemory

Length of output: 16412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/functions/observe.ts").read_text()
test = Path("test/observe-session-cap.test.ts").read_text()

assert re.search(r'import\s*\{\s*TriggerAction\s*,\s*type\s+ISdk\s*\}\s*from\s*"iii-sdk"', source)
assert "action: TriggerAction.Void()" in source
assert re.search(r'function_id:\s*"stream::send"', source)
assert 'await import("../src/functions/observe.js")' in test
assert test.count('sdk.trigger("mem::observe"') == 1
assert test.count("observeOnce(sdk,") == 3
assert not re.search(r'vi\.mock\(\s*["\']iii-sdk["\']', test)

print("runtime iii-sdk import: present")
print("TriggerAction.Void call: present")
print("stream::send action: present")
print("dynamic observe module import: present")
print("mem::observe exercised by three tests")
print("module-level iii-sdk mock: absent")
PY

Repository: rohitg00/agentmemory

Length of output: 363


Mock iii-sdk at module level.

observe.ts executes TriggerAction.Void() for every mem::observe call. Add vi.mock("iii-sdk") with TriggerAction.Void. Keep mockSdk() and mockKV() for handler collaborators.

🤖 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/observe-session-cap.test.ts` around lines 1 - 5, Update the module-level
mocks in the test setup to mock iii-sdk and provide TriggerAction.Void, ensuring
mem::observe calls do not execute the real trigger action. Preserve the existing
mockSdk() and mockKV() collaborator mocks.

Sources: Coding guidelines, Learnings

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