* fix: rebuild plugin bundles so committed artifacts match manifest 13.24.0
The 13.24.0 release commit (85ccd626) bumped the manifests and CHANGELOG
but never re-ran the build, so plugin/scripts/*.cjs kept the 13.23.1 bytes
last produced by 89ca057a. The Claude Code marketplace installs straight
from this repo (.claude-plugin/marketplace.json -> "source": "./plugin"),
so every marketplace user on 13.24.0 has been executing 13.23.1 code.
ensureWorkerRunning() compares the resolved plugin version (13.24.0, taken
from the plugin cache directory name) against the worker's baked-in
__DEFAULT_PACKAGE_VERSION__ (13.23.1, reported by /api/health). The
mismatch SIGKILLs the worker and respawns the same stale file on every
hook event, with no state that survives the hook process to bound it --
an unbounded kill/respawn loop that takes the in-flight observer
generator down with it, so no observations get written.
This is a genuine `npm run build`, not a version-string patch. The
bundles were stale in code, not merely in the constant: src/ moved 704
insertions across 12 files since 13.23.1, including the observer's
<skip_summary reason="noise" /> protocol change, the new manual-session
module, and the platform_source plumbing in SessionStore and MemoryRoutes.
No version bump: the manifests were already correct at 13.24.0. It is the
artifacts that were wrong.
Fixes#3857
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bshprs1vjut2XmaGTRESqX
* fix(tests): restore module mocks so worker-spawner stubs stop leaking
bun runs the whole suite in one process and mock.module is process-global
and sticky, so stubs installed by one test file stay installed for every
file loaded after it.
tests/services/worker-spawner.test.ts mocked
src/services/infrastructure/{ProcessManager,HealthMonitor}.js and never
restored them. tests/infrastructure/{health-monitor,process-manager}.test.ts
import the same symbols through the src/services/infrastructure/index.js
barrel, so they silently exercised those stubs instead of the real code:
isPortInUse returned false without touching net.createServer, waitForHealth
returned false without fetching, getPlatformTimeout skipped the Windows
doubling, cleanStalePidFile always reported 'dead', and spawnDaemon never
returned undefined. waitForPortFree stayed real but resolved through the
stubbed isPortInUse binding, inverting its timeout case.
This is order-dependent, not new: bun walks test files in filesystem order,
and a fresh CI checkout loads worker-spawner (59) well before health-monitor
(169) and process-manager (172), while many local checkouts load
tests/infrastructure first and pass. That ordering is what surfaced 19
failures in Actions run 33939805959.
Snapshot the real namespaces eagerly, before the mock.module calls, and
reinstall them in afterAll. The snapshot must be eager: `import * as x`
yields a live namespace object that bun re-points when the module is mocked,
so spreading it inside afterAll copies the stubs back in. That is exactly
the latent bug in tests/cli/handlers/context-session-start.test.ts, whose
restore was re-installing its own hook-settings, oauth-token, project-name
and worker-utils stubs; fixed here the same way.
Tests only. No source, plugin bundle, or version changes — the committed
artifacts still match manifest 13.24.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcjsK6QqeBcYXbJ8gqBsea
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Catch GitHub up to the already-published npm 13.24.0. Updates every version
manifest and adds the 13.24.0 changelog for PR #3842.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
The gateway fallback said "allow one normal gateway request as a probe", but
nothing made it one. shouldUseCmemFallback is a subtraction and a less-than,
and ensureGeneratorRunning gates per session, so 28-69 live sessions are 28-69
independent callers.
It is worse than the in-memory case. The marker lives on disk
(CLAUDE_MEM_PRO_FALLBACK_AT in settings.json, loaded uncached), so every
process parses the same ISO string and computes the same expiry instant -- the
herd was handed a shared clock to synchronize on. Each member's failure then
does a read-modify-write of the user's whole settings.json to re-arm the
marker; the write is atomic but the sequence is not, so concurrent settings
edits can be clobbered.
The re-probe is now claimed through the breaker's existing machinery under a
DISTINCT key, 'cmem-gateway'. Distinct because tryAdmitQuotaProbe takes the
cooldown per call: this path's 15 minutes against the provider breaker's 30
would otherwise let two callers reach contradictory answers about whether the
same breaker is armed, and provider switches (which this branch makes routine)
make the collision reachable in both directions -- a fresh funded key born into
an armed breaker and withheld for a refusal it never earned.
getSelectedProvider stays read-only for diagnostics and status; only the new
selectProviderForGenerator claims. Every path out of ensureGeneratorRunning
that does not actually send releases the claim, including the one where a
generator is already running, so a claim cannot outlive the call that took it.
Also regenerates the plugin bundles rather than hand-merging them, which is
what the five .cjs conflicts in this consolidation always needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWmJDjFM6BNmGaLJ7Xbrsy
Both from greptile review on #3820, both reproduced against a live worker.
Clickjacking: requiring a real click stops a drive-by <img>, but not an
attacker who frames the page and harvests that click through an overlay — the
code comment claimed otherwise. The route now refuses to be framed at all:
Content-Security-Policy: frame-ancestors 'none' plus X-Frame-Options: DENY.
Health poll accepted the dying worker: the page treated any ok /health as
"restarted", but runShutdownSequence waits out a graceful deadline before it
exits and nothing closes the listener first, so the outgoing worker keeps
answering well past the first 500ms poll. Measured across a real restart:
t=500ms health OK, pid=6976 <- the worker we just asked to die
t=1000ms+ no response
(successor then spawns)
/health now reports pid, the page bakes in the pid of the worker that served
it, and success requires a DIFFERENT pid. A worker too old to report one falls
back to the previous "any healthy response" behavior rather than hanging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rebase conflicted on the built bundle because both commits regenerated it.
Resolved from source rather than by picking a side, so the shipped bundle carries
the session-start briefing, the field condenser and the scoped quota probe
release together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DC1PFXGZJk7BQ2PNW2foz2
The recycle path never briefed its replacement. `loadSessionSoFar` looked up
prior observations by `session.memorySessionId`, but `ClaudeProvider.startSession`
blanks that id on every generator start (ClaudeProvider.ts:216-221) and the
lookup runs lazily inside the async generator body (:502), i.e. after the blanking.
So the guard `if (!session.memorySessionId) return []` was taken every time and
each fresh generation started blind. Instrumented live: 4 recycles, 4 empty seeds.
Use `generateContext` instead — the builder the SessionStart hook already uses.
It keys on project + cwd rather than the SDK session id, so the blanking cannot
reach it. This deletes the parallel `buildSessionSoFar` / `PriorObservation` /
`loadSessionSoFar` path rather than repairing it. Verified live at the default
400k budget: recycle at 404,297 chars, replacement briefed with 1,371 chars,
observations continuing forward across the boundary with no re-recording.
Also:
- The exhausted branch aborted without clearing `conversationHistory` and without
gating restarts, so the next tool call spawned another generator that failed on
the same budget check — one spawn and abort per captured tool call, with history
growing 21k -> 59k chars. It now drops the conversation and withholds restarts
for a cooldown, mirroring the quota breaker.
- `consecutiveContextOverflows` reset only on a valid XML parse, so two ordinary
"nothing to record" replies could trip the exhausted branch. Overflow, quota and
auth all return before that point, so reaching it proves the prompt fit — reset
there instead.
- Oversized fields are condensed by a bounded model pass rather than head/tail cut
with the middle deleted. One attempt, 30s timeout, falls back to the existing
truncation on any failure. Live: 81,521 chars -> 184, preserving the FATAL line,
the pool max, and a latency progression that only the discarded middle contained.
tests/gemini_provider.test.ts assumed the first request went out within a fixed
number of microtasks; the context lookup adds an await, so it now waits on the
condition instead of the timing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DC1PFXGZJk7BQ2PNW2foz2
Conflicts were entirely mechanical — no source-level overlap. main changed no
files under src/ since the merge base.
- Version manifests (8 files): main bumped to 13.18.1 while this branch had
already bumped to 13.19.0. Kept 13.19.0, the forward version.
- plugin/scripts/*.cjs: both sides had rebuilt the bundles, so they differed
only by the embedded version string. Regenerated from the merged tree with
`npm run build`; the output is byte-identical to this branch's, confirming
the conflict carried no behavior.
#3817's observer_role rules land in plugin/modes/*.json and the mode-authoring
reference, which are runtime data rather than bundled code, so they merged
cleanly and survive the rebuild — verified present after the merge.
Typecheck clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sc5nL8WJRdBscNu9egvDQB
The observer-outage warning told the user to check settings.json, when a
restart clears nearly every outage (a wedged or SIGKILL'd provider
subprocess). It now leads with the restart, offered two ways:
Click to restart: http://localhost:<port>/restart
Or in a terminal: npx claude-mem restart
GET /restart is deliberately INERT — it serves a page whose button POSTs to
the existing /api/admin/restart. A GET that restarted would fire from any
page that can name the URL (<img src="http://localhost:PORT/restart">), and
requireLocalhost is no defense because the browser IS localhost. Same reason
the page does not POST on load: an <iframe> would run that script.
Restarting automatically was the other option and it is worse: the worker
restarting itself needs guards against its own automation (once per outage?
per flap? two failures racing?), and every guard is a bound on a loop that
only exists because the restart was automatic. A human pressing the button
is the bound.
Placement and color, both because the warning was getting lost:
- Appended BELOW the context, not prepended. The timeline runs long, so a
warning at the top has already scrolled off by the time context finishes
printing.
- Painted red on the human render only; the agent copy is a separate
colors=false fetch where ANSI escapes are just noise. Every non-blank line
is painted rather than the block wrapped once, so reflow cannot leave the
tail unpainted.
Verified against a worker booted from source on an isolated port: 5x
GET /restart left the pid unchanged, POST /api/admin/restart took it from
11406 to 14700 and healthy again in 1s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`releaseQuotaProbe` cleared whatever probe was in flight for the provider,
regardless of which run had claimed it. Generators overlap freely, so an
earlier one could clear a later session's probe:
1. Session A starts while the provider is healthy. `tryAdmitQuotaProbe`
admits it without a breaker to claim against, so it owns no probe.
2. The breaker arms, the cooldown elapses, and session B claims the single
post-cooldown probe.
3. Session A's long-running generator exits and unconditionally releases.
4. Session C is admitted and sends a second request while B's probe is
still in flight.
The same shape reopened the breaker when a stale claim was taken over, and
when a probe outlived the breaker it was claimed against.
Admission now returns a claim id, threaded through `startGeneratorWithProvider`
to both exit paths, and a release only clears a claim that matches. A run
admitted with no breaker carries a null claim and releases nothing.
Regression tests cover all three overlaps; each fails against the unscoped
release. Rebuilt plugin/scripts/worker-service.cjs so the shipped bundle
matches source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sc5nL8WJRdBscNu9egvDQB
Appends two rules to observer_role in all four base modes:
- SILENT BY DESIGN: the observer runs invisibly in the background and the
observed session must not learn it is being watched, because an agent that
knows it is observed changes behavior in unpredictable ways — which corrupts
the record the observer exists to create.
- NO CONTACT: never contact, message, ping, or notify another agent or session
(including the observed one), never spawn subagents, never ask for input or
attempt to influence work in progress.
observer_role is interpolated into both the init and continuation prompts, so
this reaches the observer on every turn. The 32 localized and --chill variants
override only placeholders and footers, so they inherit the new text with no
per-language edits.
Also adds the rule to the mode-creator authoring reference (example plus a
prompt-quality checklist item) so newly authored modes carry it.
Claude-Session: https://claude.ai/code/session_017LYx61kB4eAX5kHSz4qqzt
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two P1 findings from Greptile's review of #3807, both reproduced with harnesses
and both real.
1. The final observation of a session was stranded after a recycle
Recycling resets the claimed batch to pending and aborts, but the documented
restart path needs a LATER ingest event. On the last tool call of a session no
later ingest arrives, so that work sat in the pending buffer and was never
recorded — the observer going quiet, which is worse than the bug this PR fixes
because it fails silently.
A recycle is the one abort that should resume on its own: unlike quota and auth,
nothing is waiting on the user, and the replacement generation fits by
construction. SessionRoutes now starts one after an `overflow:recycle` exit,
deferred a tick because `session.generatorPromise` is assigned after the promise
chain is built and resuming inline would be overwritten by that assignment.
`overflow:exhausted`, quota, and auth deliberately do not resume; the existing
recycle counter still bounds it to two attempts before the observer pauses.
2. An expired quota cooldown admitted every waiting session at once
`isQuotaCooldownActive` was a bare time check, so after the window elapsed every
concurrent session passed it and they all hit the provider together — on the
machine in #3800 that is 28-69 requests, turning "one probe per window" back
into the burst this breaker exists to prevent.
`tryAdmitQuotaProbe` now claims the single probe synchronously, so exactly one
caller wins and the rest are withheld until it resolves. Success clears the
breaker, a fresh refusal re-arms it, and `releaseQuotaProbe` runs on every
generator exit so a claim cannot outlive its request. A claim also goes stale
after 5 minutes, so a generator that dies without reaching any completion path
cannot wedge the provider shut — the inverse failure of the one being fixed.
`isQuotaCooldownActive` stays as the read-only form for logging.
The probe gate also moved below the Claude-CLI setup gate: claiming before those
early returns leaked the claim.
tests/worker/overflow-recycle-resume.test.ts drives the real SessionRoutes exit
path and asserts the replacement start, that exhausted/quota/auth do NOT resume,
and that the session is preserved rather than finalized. Eight new quota-cooldown
cases cover single admission under 28 concurrent callers, in-flight withholding,
stale reclaim, release, re-arm, and per-provider scoping.
Full suite: 2757 pass, 0 fail across 257 files. Bundles rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERvWLxiY96C7pJrx7hPjh8
Minor rather than patch: adds the user-facing setting
CLAUDE_MEM_OBSERVER_MAX_CONVERSATION_CHARS and changes observer prompt content
(the <session_so_far> seed block), on top of the cost fixes.
Rebuilds the shipped plugin/scripts bundles so the released plugin actually
carries the observer changes, matching the previous bump commit's shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERvWLxiY96C7pJrx7hPjh8
The five Windows branches each carried their own build of
plugin/scripts/*.cjs; merging them conflicted on those artifacts. Rebuilt
once from the merged src/ so the shipped scripts contain all five fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVK8iVC2dnrqE1fV4ogp2a