Skip to content

fix(multi): stability pass for #627 #640 #474 #638 #431 #544 #563#648

Merged
rohitg00 merged 2 commits into
mainfrom
fix/multi-stability-pass
May 25, 2026
Merged

fix(multi): stability pass for #627 #640 #474 #638 #431 #544 #563#648
rohitg00 merged 2 commits into
mainfrom
fix/multi-stability-pass

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented May 25, 2026

Summary

End-to-end stability pass over six open issues. Each fix lands with a targeted regression test; full suite is 1119/1119 (104 files) on macOS Apple Silicon.

Issue What it broke What this PR does
#627 OpenAI thinking-model fallback dropped output Read message.reasoning_content alongside message.reasoning
#640 agentmemory stop left daemon wrapper alive → duplicate workers Write worker pidfile on boot, reap it on stop
#474 agentmemory stop left engine running with stale registrations Same worker-pidfile fix — root cause is shared with #640
#638 OpenCode plugin recorded observations into a session that never existed mem::observe implicit-creates the session when project + cwd present and no record exists
#431 OpenCode required manual AGENTS.md instruction for memory lookup Cache /session/start context, inject via existing chat.system.transform hook (zero-config)
#544 /memories + /export timed out on real corpora (8K+ memories) → viewer showed 0 New ?count=true, ?limit=N&offset=M, viewer caps requests at 500 / 2000
#563 Viewer graph oscillated forever on >1000 nodes Tick-decayed damping, per-node velocity caps, raf park on quiescence, mousedown re-wakes

Per-fix detail

#627src/providers/openai.ts

DeepSeek V4 / Qwen3 / GLM / Kimi return message.reasoning_content (with underscore). Previous code only checked message.reasoning → silent compress failure, 0/700 calls succeeded, circuit breaker repeatedly opened.

- const reasoning = message?.reasoning;
+ const reasoning = message?.reasoning ?? message?.reasoning_content;

Test: test/fetch-timeout.test.ts — adds 3 thinking-model fallback cases covering DeepSeek shape, older o-series shape, and content-wins-over-both.

#640 + #474src/cli.ts + src/index.ts

Root cause: the agentmemory worker process is spawned by iii-exec inside the engine. agentmemory stop only signaled engine pids; the worker (spawned detached, signal not propagated, or kept alive by a wrapper script) survived and reconnected to the next engine. Result: duplicate worker registrations or stale function registrations from a previous code revision.

+ // src/index.ts — write worker.pid on registerWorker
+ function workerPidfilePath(): string {
+   return join(homedir(), ".agentmemory", "worker.pid");
+ }
+ writeWorkerPidfile();
+ // src/cli.ts — read worker.pid in runStop, signal alongside engine pids
+ const workerPid = readWorkerPidfile();
+ const workerCandidates = new Set<number>();
+ if (workerPid) workerCandidates.add(workerPid);
+ // ... signalAndWait each candidate not already in engine set
+ clearWorkerPidfile();

Test: test/stop-worker-pidfile.test.ts — verifies both source files use worker.pid and that runStop wires it into the kill loop.

#638src/functions/observe.ts

OpenCode plugins (and any client that skips POST /session/start) sent observations against a session that didn't exist server-side. Observations stacked up but memory_sessions listed nothing, then summarize bailed with Session not found for summarize.

Now: when mem::observe runs and no session record exists, create one inline using the HookPayload's project + cwd + timestamp (only when both are present, so older test payloads keep their no-op path).

Test: test/observe-implicit-session.test.ts — 3 cases (implicit-create happy path, no-op when project/cwd missing, existing session not overwritten).

#431plugin/opencode/agentmemory-capture.ts

POST /session/start already returns the project context. Plugin now caches it (startContextCache) and the existing experimental.chat.system.transform hook reads from the cache first, falling back to a fresh /context call if the cache missed. Result: zero-config injection without a second round-trip. Cache cleared on session.deleted.

Test: test/opencode-auto-context.test.ts — verifies cache write at session.created, read in chat.system.transform with /context fallback, cleanup on session.deleted.

#544src/triggers/api.ts + src/viewer/index.html

Unbounded /memories and /export invocations hit the iii engine's invocation timeout on a real corpus (40 sessions × 34K observations × 8K memories). The viewer's failed fetch silently fell through to "0 memories".

/memories now supports three modes:

  • ?count=true → totals only ({total, latestCount})
  • ?limit=N&offset=M → paged slice (cap 5000)
  • no query string → unchanged unbounded response (back-compat)

/export forwards ?maxSessions=N&offset=M to mem::export which already supported it.

Viewer dashboard caps fetches: 500 on the dashboard, 2000 on the memories tab.

Test: test/memories-pagination.test.ts — 5 cases covering count mode, limit/offset, cap-at-5000, export passthrough, viewer caps.

#563src/viewer/index.html

Dense graphs (>1000 nodes) oscillated forever because 0.9 damping couldn't bleed off the per-tick force pile-up. Three changes:

  1. Tick-decayed dampingcoolBoost = min(0.4, tickCount / 1500) tightens damping over time so layouts settle.
  2. Per-node velocity cap — 6 / 12 / 24 px/tick tiered by node count, prevents one-tick force spikes from launching nodes off-screen.
  3. Raf park on quiescence — when RMS velocity < 0.05 for 30 ticks, stop requesting animation frames. Mousedown re-wakes the loop so dragging still responds.

Test: test/viewer-graph-cooldown.test.ts — 5 cases covering tickCount growth, coolBoost damping, velocity cap, raf park, mousedown wake.

Deferred: #637 (Windows em-dash ByteString)

Cannot reproduce on macOS / Linux. The user-suggested defensive encoding fix is unsafe without a Windows repro that confirms the actual exception path. Will file a separate PR once we have a Windows runner or the reporter validates a candidate patch. Not included in this PR to keep blast radius tight.

Test plan

  • npm test — 1119/1119 pass (104 files), no regressions
  • npm run build — 21 files, 2438 KB, no bundle bloat
  • Manual: confirmed worker.pid written under ~/.agentmemory/ on boot, removed on graceful shutdown
  • Static check: every new behaviour has a matching test under test/
  • Each fix kept narrow — no drive-by refactors or cleanups

Summary by CodeRabbit

  • New Features

    • Pagination for memories and export endpoints (limit/offset + totals)
    • Dashboard graph improvements with adaptive damping and parked/resume behavior
    • Automatic injection of session-start context into captured system messages
    • Implicit session creation when observations include project and cwd
    • Worker PID sidecar to track detached worker processes
  • Bug Fixes

    • More robust worker stop/cleanup and stale-context eviction
  • Tests

    • Added tests covering pagination, graph cooldown, context injection, session creation, and worker pidfile handling

Review Change Stack

Six issues, one PR. Each lands with a targeted regression test;
1119/1119 vitest pass.

#627 OpenAI thinking-model fallback
  src/providers/openai.ts now reads message.reasoning_content alongside
  message.reasoning. DeepSeek V4 / Qwen3 / GLM / Kimi return the
  underscored field — previously compress silently failed (0/700 calls)
  and the circuit breaker tripped.

#640 + #474 stop reaps the worker process
  src/index.ts writes ~/.agentmemory/worker.pid on registerWorker, clears
  it on graceful shutdown. src/cli.ts runStop now reads the worker pidfile
  and signals SIGTERM alongside the engine pids. Fixes both: the daemon
  wrapper surviving stop (#640) and the iii engine retaining stale
  function registrations because the worker reconnected to the new
  engine (#474).

#638 OpenCode session implicit-create on observe
  src/functions/observe.ts now creates the session record on the first
  observation when project + cwd are present and no session exists.
  OpenCode plugins (and any caller that skips POST /session/start) no
  longer leak observations into a session memory_sessions never lists,
  and summarize stops bailing with 'Session not found'.

#431 OpenCode auto-context (zero-config injection)
  plugin/opencode/agentmemory-capture.ts captures the context returned
  by POST /session/start into a per-session cache. The existing
  experimental.chat.system.transform hook now reads from the cache
  first, falls back to /context. Cleanup on session.deleted.

#544 paginated /memories + /export
  src/triggers/api.ts adds three query modes to /memories:
    ?count=true       — totals only, viewer status badge
    ?limit=N&offset=M — paged slice, default unlimited
  /export now forwards maxSessions + offset query params to mem::export
  (which already supported them). Viewer dashboard caps the memories
  fetch at 500; the memories tab at 2000. Both stop the iii invocation
  timeout from masking real corpora as 0 memories.

#563 viewer graph cool-down on >1000 nodes
  src/viewer/index.html adds tick-decayed damping (coolBoost), per-node
  velocity caps tiered by node count, and quiescence-based raf parking.
  Mousedown wakes the parked loop. Dense graphs settle instead of
  bouncing forever; CPU returns to idle once the layout is quiet.

#637 Windows em-dash ByteString — deferred to follow-up
  Cannot reproduce on macOS / Linux. The user-suggested defensive
  encoding fix is unsafe without a Windows repro confirming the actual
  exception path. Will land separately once a Windows runner or the
  reporter can validate.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentmemory Ready Ready Preview, Comment May 25, 2026 6:37pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

Implements start-context caching in OpenCode plugin; worker pidfile lifecycle (write/clear) and CLI stop handling for worker reaping; implicit session creation in mem::observe when project+cwd present; /memories and /export pagination and count mode; OpenAI provider fallback to reasoning_content; viewer limits and graph cooldown/wake logic.

Changes

Core functionality and infrastructure

Layer / File(s) Summary
OpenCode plugin context caching
plugin/opencode/agentmemory-capture.ts, test/opencode-auto-context.test.ts
Adds startContextCache, caches POST /session/start context on session.created, clears on session.deleted, and uses cache-first injection in experimental.chat.system.transform with /context fallback.
Worker process lifecycle management
src/index.ts, src/cli.ts, test/stop-worker-pidfile.test.ts
Adds worker pidfile helpers (~/.agentmemory/worker.pid), write on worker startup, clear on shutdown, and extends agentmemory stop to read/signal/clear worker pidfile and handle orphan worker termination.
Implicit session creation on observation
src/functions/observe.ts, test/observe-implicit-session.test.ts
mem::observe now creates a KV.sessions record when missing if payload contains non-empty project and cwd, initializing timestamps, status:active, observationCount, and an optional normalized/truncated firstPrompt.
API pagination and export support
src/triggers/api.ts, test/memories-pagination.test.ts
/agentmemory/memories supports count=true and optional limit/offset pagination (limit >0, capped at 5000; offset >=0). /agentmemory/export forwards maxSessions and offset into mem::export payload.
OpenAI thinking model support
src/providers/openai.ts, test/fetch-timeout.test.ts
Provider accepts message.reasoning_content as a fallback along with message.reasoning; when message.content is absent, prefer reasoning then reasoning_content.
Viewer memory loading and graph performance
src/viewer/index.html, test/viewer-graph-cooldown.test.ts
Limits dashboard/latest memory requests (limit=500 / limit=2000), records backend total, and implements graph cooldown: tick counter, tick-adaptive damping, per-node velocity caps, kinetic energy detection to park RAF loop, and wake-on-interaction.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 Six features hop into place,
Context cached, workers reap'd clean,
Sessions bloom where none were seen,
Pagination bounds the lists,
Thinking models get their twist,
Graphs settle with a sigh—
Dance efficient, no more spy!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title succinctly describes a stability pass addressing multiple issues with concrete issue numbers, reflecting the main purpose of the changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/multi-stability-pass

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/viewer/index.html (1)

1584-1584: 💤 Low value

quietTicks is not initialized in graphSim state.

The tickCount is explicitly initialized to 0 in the graphSim object, but quietTicks is not, despite being used similarly. The code handles this with || 0 patterns, but adding it to the initial state would be more consistent.

Suggested fix
-var graphSim = { nodes: [], edges: [], running: false, canvas: null, ctx: null, raf: null, panX: 0, panY: 0, zoom: 1, dragNode: null, mouseX: 0, mouseY: 0, tickCount: 0 };
+var graphSim = { nodes: [], edges: [], running: false, canvas: null, ctx: null, raf: null, panX: 0, panY: 0, zoom: 1, dragNode: null, mouseX: 0, mouseY: 0, tickCount: 0, quietTicks: 0 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viewer/index.html` at line 1584, graphSim's initial state is missing
quietTicks; add quietTicks: 0 to the graphSim object (which already contains
tickCount: 0) so code that reads graphSim.quietTicks doesn't need fallback
patterns. Update the graphSim declaration (the object with nodes, edges,
running, canvas, ctx, raf, panX, panY, zoom, dragNode, mouseX, mouseY,
tickCount) to include quietTicks: 0.
src/functions/observe.ts (1)

239-240: ⚡ Quick win

Reuse one captured timestamp for session create fields.

The new session object calls new Date().toISOString() multiple times in the same write path (Line 239 and Line 240). Capture once and reuse for consistent metadata.

Proposed fix
+          const nowIso = new Date().toISOString();
           await kv.set(KV.sessions, payload.sessionId, {
             id: payload.sessionId,
             project: payload.project,
             cwd: payload.cwd,
-            startedAt: payload.timestamp ?? new Date().toISOString(),
-            updatedAt: new Date().toISOString(),
+            startedAt: payload.timestamp ?? nowIso,
+            updatedAt: nowIso,
             status: "active",
             observationCount: 1,

As per coding guidelines: “Capture timestamps once with new Date().toISOString() and reuse rather than calling repeatedly.”

🤖 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 239 - 240, Capture a single timestamp
once (e.g., const ts = new Date().toISOString()) and reuse it for both session
fields instead of calling new Date().toISOString() twice; set startedAt to
payload.timestamp ?? ts and updatedAt to ts so startedAt/updatedAt in the new
session object (the properties startedAt and updatedAt in the session creation
code) remain consistent.
🤖 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 `@plugin/opencode/agentmemory-capture.ts`:
- Around line 187-200: The start-context caching uses the mutable
activeSessionId after an await which can change concurrently; capture a stable
session id before calling postJson (e.g., const sessionId = activeSessionId) and
use that local sessionId for the null check and when setting startContextCache
with startCtx (referencing activeSessionId, postJson("/session/start"),
startCtx, and startContextCache) so the cached context is bound to the correct
session even if activeSessionId changes during the await.

In `@src/cli.ts`:
- Around line 2282-2284: The Docker stop paths currently return before clearing
worker.pid, so ensure clearWorkerPidfile() is always invoked in all stop
branches (native and Docker). Modify the docker stop branches in stop logic
(where the code currently returns early) to call clearWorkerPidfile() before any
return, or refactor to call clearWorkerPidfile() alongside clearEnginePidfile()
and clearEngineState() so clearWorkerPidfile() runs unconditionally regardless
of whether the Docker or native stop path is taken.
- Around line 2250-2258: The worker PID file is only read after the early-return
when the engine is down, so orphan worker processes are not reaped; call
readWorkerPidfile() (and populate workerCandidates/workerPid) before the
running/if (!running) check (or additionally call it inside that branch) so
workerCandidates is populated even when running is false, and then proceed to
merge workerCandidates with candidates and perform the reap logic using the
existing variables (readWorkerPidfile, workerPid, workerCandidates, candidates).

In `@src/viewer/index.html`:
- Around line 1763-1767: The wheel handler currently updates graphSim.zoom but
doesn't wake a parked simulation, so add the same wake logic used in the
mousedown handler: set graphSim.quietTicks = 0 and if (graphSim.running &&
!graphSim.raf) assign graphSim.raf = requestAnimationFrame(runSimulation) (or
the equivalent call path) after updating graphSim.zoom in the wheel event
handler to ensure immediate visual feedback when zooming while parked.

---

Nitpick comments:
In `@src/functions/observe.ts`:
- Around line 239-240: Capture a single timestamp once (e.g., const ts = new
Date().toISOString()) and reuse it for both session fields instead of calling
new Date().toISOString() twice; set startedAt to payload.timestamp ?? ts and
updatedAt to ts so startedAt/updatedAt in the new session object (the properties
startedAt and updatedAt in the session creation code) remain consistent.

In `@src/viewer/index.html`:
- Line 1584: graphSim's initial state is missing quietTicks; add quietTicks: 0
to the graphSim object (which already contains tickCount: 0) so code that reads
graphSim.quietTicks doesn't need fallback patterns. Update the graphSim
declaration (the object with nodes, edges, running, canvas, ctx, raf, panX,
panY, zoom, dragNode, mouseX, mouseY, tickCount) to include quietTicks: 0.
🪄 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: 289ab8c2-8ab0-4854-932c-7c980e630186

📥 Commits

Reviewing files that changed from the base of the PR and between d38a1c6 and b3cca9d.

📒 Files selected for processing (13)
  • plugin/opencode/agentmemory-capture.ts
  • src/cli.ts
  • src/functions/observe.ts
  • src/index.ts
  • src/providers/openai.ts
  • src/triggers/api.ts
  • src/viewer/index.html
  • test/fetch-timeout.test.ts
  • test/memories-pagination.test.ts
  • test/observe-implicit-session.test.ts
  • test/opencode-auto-context.test.ts
  • test/stop-worker-pidfile.test.ts
  • test/viewer-graph-cooldown.test.ts

Comment thread plugin/opencode/agentmemory-capture.ts
Comment thread src/cli.ts
Comment thread src/cli.ts
Comment thread src/viewer/index.html Outdated
Addresses inline review on PR #648 — verified each finding against
current code and fixed the still-valid ones.

opencode plugin: snapshot activeSessionId into a local 'sessionId'
before await postJson('/session/start') — a second session.created
event during the await could rebind activeSessionId and cache the
context against the wrong key. The cache write + observe call now use
the snapshotted id.

src/cli.ts: clearWorkerPidfile() now runs in every stop branch:
  - Docker engine-not-running early return
  - Docker stopDockerEngine path
  - native engine-not-running 'Nothing to stop'
  - native happy path (was already there)
The worker pid is now read up front so the engine-down branch can also
reap an orphaned worker process (previously fell through to 'preserve
for manual cleanup'). A new dedicated branch reaps the worker and
exits cleanly when only the worker is lingering.

src/viewer/index.html: wakeGraphSim() shared helper consolidates the
quietTicks reset + raf restart pattern. Wheel handler, zoomGraph(),
recenterGraph(), and mousedown all now wake the parked simulation so
zoom/pan/click feedback is immediate after the layout has settled.
graphSim object initializes quietTicks: 0 alongside tickCount: 0.

src/functions/observe.ts: dedupe new Date().toISOString() into a
single 'ts' local for the implicit-create path so startedAt and
updatedAt stay consistent.

test/opencode-auto-context.test.ts: regex updated to assert the
snapshot-then-cache pattern instead of the previous direct
activeSessionId reference.

1119/1119 vitest pass.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

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

236-247: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add an audit entry for implicit session creation.

This branch writes a new KV.sessions record but does not emit an audit record for that state change.

As per coding guidelines, "Register functions using sdk.registerFunction() with validation of inputs, work via kv.get/kv.set/kv.list, and record audit entries via recordAudit() for state-changing operations".

🤖 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 236 - 247, The new KV.sessions write
(kv.set with KV.sessions and payload.sessionId) must be accompanied by an audit
entry: after creating the session record in the branch that initializes a
session (the code that sets id, project, cwd, startedAt, updatedAt,
status:"active", observationCount:1 and optional firstPrompt), call
recordAudit(...) to log the implicit session creation; include the session id
(payload.sessionId), project, status "active", timestamp (ts or
payload.timestamp), and any firstPrompt/trimmedPrompt in the audit metadata, and
populate the actor/actorId from the request payload or execution context
consistent with other recordAudit calls in this file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/functions/observe.ts`:
- Around line 236-247: The new KV.sessions write (kv.set with KV.sessions and
payload.sessionId) must be accompanied by an audit entry: after creating the
session record in the branch that initializes a session (the code that sets id,
project, cwd, startedAt, updatedAt, status:"active", observationCount:1 and
optional firstPrompt), call recordAudit(...) to log the implicit session
creation; include the session id (payload.sessionId), project, status "active",
timestamp (ts or payload.timestamp), and any firstPrompt/trimmedPrompt in the
audit metadata, and populate the actor/actorId from the request payload or
execution context consistent with other recordAudit calls in this file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae94974a-7235-44e8-be9e-bbc9860308e7

📥 Commits

Reviewing files that changed from the base of the PR and between b3cca9d and 1cd13e4.

📒 Files selected for processing (5)
  • plugin/opencode/agentmemory-capture.ts
  • src/cli.ts
  • src/functions/observe.ts
  • src/viewer/index.html
  • test/opencode-auto-context.test.ts

@rohitg00 rohitg00 merged commit f027c20 into main May 25, 2026
7 checks passed
@rohitg00 rohitg00 deleted the fix/multi-stability-pass branch May 25, 2026 18:46
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