Package-facing patch bump so 13.24.17 ships #4026/#3575 (health probe deadline cap), #3445 (desktop-bundled Codex CLI on macOS), and #4027 (register memory_session_id, do not re-register). Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
ensureMemorySessionIdRegistered writes only when the stored id is NULL. Deliberate re-identification stays on updateMemorySessionId. Polarize the 8 suites that still treated ensure as the re-key path.
Co-authored-by: Vorrawut Judasri <vorrawutjud@gmail.com>
Plan-15 already abort-bounds worker fetch at 5s. Cap each probe and retry sleep to the caller's remaining deadline so waitForHealth(short) cannot sit out the full 5s. Windows timeout still falls through to net.createServer. Remaining-deadline leftover from #3575 by @dajiaohuang. No version bump / no npm.
When the chroma-mcp subprocess dies mid-handshake, the MCP SDK sends
`notifications/initialized` after `initialize` and throws a bare
`Error: Not connected`. `connectInternal` rethrew that error verbatim, so it
was never classified as `ChromaUnavailableError` the way every other
unavailability case is. The raw error escaped `callToolUnqueued`'s
reconnect-and-retry block and reached `ChromaSync.addDocuments` as a hard
error, which logged it through `logger.error` (routed to `captureException`)
and rethrew it up through `syncUserPrompt`.
Classify every connect failure as `ChromaUnavailableError` and downgrade the
`ensureConnected` log to `warn` for that type. This puts the transient failure
on the existing reconnect-backoff and skip-the-write path, so the prompt and
observation records stay eligible for re-sync and the failure no longer floods
error tracking.
Co-Authored-By: Claude <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 53e50678-04bf-4981-af7e-5249feef2bdd
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
copyPluginToMarketplace copied the root package.json verbatim, so the
marketplace install ran npm install against ~40 dev-only tree-sitter
grammars. --omit=dev still resolves those dev edges, so npm hit a real
peer conflict (@derekstride/tree-sitter-sql wants tree-sitter@^0.21.0,
@tree-sitter-grammars/tree-sitter-lua wants tree-sitter@^0.22.4) and
aborted with ERESOLVE.
Strip devDependencies and trustedDependencies before npm sees the graph.
A consumer install needs only the two runtime deps (better-auth and
@better-auth/api-key), so npm never resolves the conflicting edges.
Fixes#3636
Generated-By: PostHog Desktop
Task-Id: d0552e48-c759-4a5f-9c6d-7a6c2219970d
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
Reset only the in-memory memory_session_id before a fresh observer spawn. Writing NULL to sdk_sessions cascaded into NOT NULL child columns and rolled back the storage transaction, so sessions with existing observations/summaries stored nothing.
Patch only — RCA-later already in the PR body. Complements #3980 (CASCADE kept on rebuilds); does not replace it.
Closes#3628
Package-facing patch bump so 13.24.15 ships #3727 / #3706 (daemon no longer inherits user project cwd). Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
The capability skip fired for every non-ghost shape, not just the one it was
written for: the branch returned whenever no owner was found under the dead
pid, which also covers a free port whose sidecar chain died with the worker,
a port held by an unrelated process, a ghost sharing the port with another
owner, and a recycled fixture pid. Each of those returned before
ensureWorkerStarted(), the orphan assertions and the health check — so the
gate could pass while testing no recovery at all (Greptile P1 on the skip
branch).
classifyPostKillState() (tests/integration/helpers/ghost-state.ts) names the
settled state from the final port-owner sample, the fixture's liveness and
the post-kill sidecar chain:
ghost -> the recovery assertions run (unchanged)
runtime-capability-skip -> the ONLY skippable state: port free AND the
pre-kill-verified chain still alive (the
measured bun >= 1.4 socket-inheritance change)
malformed -> fail, with the reason and the observed owners:
chain-died / fixture-pid-recycled /
ghost-shared-port / foreign-owner
The function is pure so the whole table is unit-tested
(helpers/ghost-state.test.ts): these environmental states cannot be produced
on demand — they are the anomalies being named.
* fix(daemon): stop the worker holding the user's project as its cwd
A process keeps an open handle on its working directory. The daemon spawns
are reached from hooks, which run in the session's project folder, and none of
the three spawn sites set a directory -- so the daemon inherited it and, on
Windows, held it for the daemon's whole life. Since the daemon outlives the
session, renaming or moving the project failed with "The process cannot access
the file because it is being used by another process" until the user found and
killed bun.exe. POSIX allows the rename but still pins the directory against
unmount.
All three now stand in claude-mem's own data directory: the Windows
Start-Process command gains -WorkingDirectory, and the two detached spawns
gain cwd. The data directory always exists by the time a daemon starts and is
never a directory the user is reorganising.
Closes#3706
* fix(daemon): create the working directory before handing it to spawn
Passing a cwd that does not exist is worse than passing none: spawn fails
with ENOENT and Start-Process refuses outright, so on a fresh install this
fix would have turned the first daemon launch into a failure.
paths.ts resolves DATA_DIR but does not create it -- ensureDir is a helper
callers must call. Today some earlier caller happens to create it before any
daemon spawns, which is why this did not show up in testing, but the spawn
sites must not depend on that ordering. All three now mkdir -p first, which
is idempotent.
Package-facing patch bump so 13.24.14 ships the commits now on main after v13.24.13. Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Pin shell-quote to 1.9.0 in the plugin manifest, lockfile, root
devDependency, and build-hooks generator so a normal build cannot
regenerate the CVE-affected ^1.8.3 range.
Rebase of #3646.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Route start/watch/poke through a single-flight coalescing scheduler so
overlapping reads cannot consume the same range twice. Reset buffered
partial-line data when a shrink is detected so a stale fragment is not
prepended to the first line of the truncated file.
Rebase of #3633. Related: #2192, #2209, #2219, #1661.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* fix(cleanup): tolerate negative statfs availability
Treat negative statfsSync().bavail as non-credible disk telemetry and
bypass only the pre-flight disk-space gate. Bun on linux-x64 can
truncate large bavail values to a signed int32, which previously
skipped v12.4.3 cleanup forever with a misleading free<0 error.
Rebase of #3572. Closes#3551.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* ci: retrigger after clean-room npm install flake
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Treat a present empty-string OpenRouter message as a valid no-op
instead of a malformed response, so reasoning models that return
content:'' with reasoning_content keep usage metadata. Init
bookkeeping still records the empty turn for providers that
forward empty message responses. Missing message/content still
fails closed.
Rebase of #3576. Closes#3569.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Enable Bun.Glob {dot:true} so configured watch patterns that pass
through a literal or wildcard dot-directory (e.g. Antigravity
.system_generated/logs) are discovered. Matching stays constrained
to the configured pattern.
Rebase of #3574. Closes#3512.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Use the existing empty-or-skeleton classification when deciding whether
to create a context file. Previously only '*No recent activity*' skipped
create-if-absent, so a fully empty formatted timeline still wrote a
43-byte stub CLAUDE.md. Existing-file cleanup is unchanged.
Rebase of #3573. Closes#3544.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Package-facing patch bump so 13.24.13 ships the commits now on main after v13.24.12. Rebuilds plugin bundles so the injected version matches.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Night-ship rebase of #3666. Delete the divergent startsWith('~') expander that rewrote ~alice/transcripts under the current home; delegate to shared expandTilde.
Refs #3666
Night-ship rebase of #3669. Normalize separators then use shared expandHome so ~backup/* is not glued onto the home directory and Windows ~\ exclusions actually exclude.
Refs #3669
Night-ship rebase of #3670. Route the three Windsurf hooks.json reads through existing parseJsonWithBom so a UTF-8 BOM no longer looks like a corrupt file.
Refs #3670
Rename the PowerShell Add-Type class from CredRead to CredApi so CS0542 no longer prevents the Windows Credential Manager P/Invoke shim from compiling. Surface shim compile failures instead of reporting a missing credential.
Fixes#3722.
Rebase of #3689 onto current main. where bun on Volta Windows only
finds bun.cmd; spawnHidden then hits EINVAL. Ask sibling volta.exe
which bun and prefer the native .exe. Fall back to the shim if the
probe fails or times out.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Wu Shuwen <mikewushuwen@outlook.com>
Codex keeps newly installed plugin hooks disabled until the user reviews them. Tell installers to trust the five claude-mem hooks and restart only sessions opened before that approval.
Closes#3771.
Remove the unused CMEM_TRIAL_ACKNOWLEDGEMENT export. The 7-day trial copy is stale (trial is 30 days) and is never rendered — billing disclosure lives on the Stripe checkout page.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Night-ship rebase of #3717 by @rodboev onto latest main.
Treats the stdin reader's malformed-EOF and incomplete-timeout diagnostics as non-blocking so UserPromptSubmit fails open.
Supersedes #3717. Refs #3699 / #3605.
Night-ship rebase of #3716 by @rodboev onto latest main.
Selection now checks each worktree HEAD against local HEAD and remote-tracking refs without fetching. Adoption stays conservative when Git cannot prove ancestry.
Supersedes #3716. Refs #3698.
* fix(openrouter): retry max_completion_tokens on GPT-5 400s (#3770)
Some OpenRouter/OpenAI-compatible GPT-5 endpoints reject max_tokens.
Share one transport fallback that retries once with max_completion_tokens
when the response names that replacement. Rebased onto buildOpenRouterRequestBody
and strip max_tokens on the retry so both fields are never sent together.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* chore: retrigger CI after unrelated sync-hub 5s timeout flake
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
When the worker port stays occupied with no PID file after the full startup
wait, surface the port and CLAUDE_MEM_WORKER_PORT/reboot remediation on the
fail-loud channel instead of a generic unreachable count.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
`extractFromBash` calls shell-quote's `parse()` on every Bash PreToolUse
command to enrich the payload with `filePaths` for read commands. `parse()`
throws on substitutions it cannot resolve, the throw was not caught here, and
it reached the generic hook handler — which logs `Hook error:` and returns
BLOCKING_ERROR. A best-effort convenience field was turning an ordinary shell
command into a blocked tool call.
Reproduced against the version actually installed here, not just the 1.8.4 in
the report:
shell-quote 1.10.0
parse("cat ${}") -> throws Bad substitution: ${}
parse("echo hi && cat ${}") -> throws Bad substitution: ${}
The trigger is an *empty* substitution. `${FOO}`, `${#FOO}`, `${FOO:?err}` and
`${arr[@]}` all parse fine on 1.10.0, which is why the form matters: the guard
is needed on current dependencies, not only on the older one.
It also matters that `plugin/scripts/worker-service.cjs` — the file that ships —
carries a bundled copy containing the same `Bad substitution` throw, so the
behaviour does not depend on what a fresh `npm install` resolves.
The parse is now guarded and yields no paths on failure. Failing to enrich is
invisible; blocking the user's command is not.
Three tests, two of which fail without the guard (7 pass / 2 fail):
- an unparseable substitution yields [] instead of throwing
- the same when it rides alongside a genuinely readable file, so the failure
is attributable to tokenisation rather than to a bad path
- a command that parses still extracts its paths — the guard must not
swallow the feature it protects
`bun test tests/cli tests/hooks` — 116 passed.
The committed bundle is left alone: rebuilding it here diverges from the
committed artifact for unrelated reasons, so it should be regenerated by the
normal release build rather than by this PR.
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
* fix(worker): make the LLM per-attempt deadline configurable and stop retrying it
The observer's per-attempt request deadline was hardcoded at 30s with no way
to reach it from configuration, and neither of the two withRetry call sites
passes an override. On a local model that truncates work that has already
been computed: a report on an Ollama backend measured a median of 21s and a
p99 of 29.8s against the 30s deadline, with every failure landing at exactly
30.000s. The workaround was editing the installed bundle after each update.
CLAUDE_MEM_LLM_TIMEOUT_MS now sets it, validated against the same 500..300000
bounds the other CLAUDE_MEM_*_TIMEOUT_MS settings use. Hosted providers keep
the 30s default and see no change.
A request aborted by that deadline is also no longer retried. The abort
surfaces with no HTTP status, so it classified as transient and ran two more
attempts against a backend that was already saturated, which is what turns a
latency problem into a congestion collapse. It now fails with a message
naming the deadline and the setting that raises it.
Refs #3794.
* fix(worker): reject suffixed CLAUDE_MEM_LLM_TIMEOUT_MS values
parseInt('90000ms') silently accepted a typo as 90000. Require a complete
integer so a bad setting warns and keeps the 30s default.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
All three server generation providers already accept maxOutputTokens, but
nothing ever populated it, so every request went out with the constructor
default of 4096. On a model that answers at length the reply is truncated
mid-structure, the observation parser rejects it, and the job settles as a
non-retryable parse_error, so the observation is lost silently.
Unset keeps 4096, so behaviour is unchanged unless the variable is set.
Non-integer and non-positive values are ignored with a warning rather than
sent as NaN.
Co-authored-by: escooterclinic <dvla5@escooterclinic.co.uk>
* fix(context): fit the session block to the hook output limit by selection
Claude Code delivers a hook's stdout verbatim up to 10,000 characters, then
writes the whole block to disk and hands the model a ~2KB preview stub
instead -- while the hook still reports success. The context block was sized
by item counts, so on an active project it crossed that line and the model
silently received no context at all.
Counts cannot fix it: an observation title runs ~150-320 characters, a
session-summary line ~250-600, and the last-session summary block 1-5K on its
own, so any count safe on a quiet project wastes the budget and any count
that fills the budget there overflows on a busy one. The shipped default of
50 observations overflows on titles alone.
Measure instead. Render, and while the result is over budget drop the most
expensive thing still in it and render again: full narratives first (their
titles stay), then the last-session summary block, then sessions, then
observations. Whole items go, never half of one, so the block stays
well-formed and the header, footer and at least one observation survive.
--full is an explicit human request and is left unbounded.
Fixes#3802
* fix(context): count the health warning and report what was delivered
Addresses the three P1 findings on this PR. All three were real; the first two
are fixed here with tests, the third by rebuilding the two bundles that
actually carry the change.
**The observer-health warning is now inside the measured block.** It was
appended to the already-fitted text, so its characters were never counted: a
block fitted to 9,998 came back at 10,585 whenever the observer was unhealthy,
and over the limit Claude Code replaces the whole thing with the ~2KB preview
stub — losing the context *and* the outage warning, which is exactly when that
warning matters most.
`readObserverHealth` is now read once rather than per render. `fitContextToBudget`
calls its render repeatedly and that function touches state; re-reading it per
reduction would let the measured length move under the loop.
**Injection stats now describe the delivered block.** A run trimmed from seven
observations to three still reported `observation_count: 7` and 9,646 tokens
injected while the model received three observations and 4,134 tokens. That is
the one direction this telemetry must not be wrong in — it read as healthy
precisely when context was being dropped. `ContextInjectStats` already promised
this ("computed from the same observation set that was rendered"); the code now
matches its own contract. Summaries use `budget.config.sessionCount`, the same
slice `buildContextOutput` takes for `displaySummaries`.
Both live in a new exported `fitContextForDelivery`, split out of
`generateContextWithStats` so the fitting and the reporting can be tested
without a database — the module previously had no in-process test path at all,
which is why neither defect was caught.
**Bundles.** `plugin/scripts/worker-service.cjs` and
`plugin/scripts/context-generator.cjs` are rebuilt: marketplace hooks execute
the checked-in bundle, so the fix does not reach installs without them.
`mcp-server.cjs` and `server-service.cjs` are deliberately NOT included, and
that is measured rather than assumed. Rebuilding them from a clean tree
produces byte-identical output to rebuilding them with this patch applied, and
both differ from what is committed on `main` — so their diff is pre-existing
drift, not this change, and carrying it here would bury the review in noise.
Five cases in `tests/context/context-budget-3802.test.ts`, driven through
`fitContextForDelivery` rather than through `fitContextToBudget`, because both
defects were in what happens *around* the fitter.
The fixture is chosen so it can tell the two apart: 47 observations render to
9,900 characters, which fits, and 9,900 plus a 600-character warning does not.
An earlier version of this test used 50 observations and **passed under the
mutation** — the fitter's reductions are coarse, so the fitted block landed far
enough below the limit that a warning appended afterwards still fit. Measured,
then fixed.
- Moving the warning back outside the fitter fails
`counts the observer-health warning against the budget`.
- Passing the original arrays to `buildInjectStats` fails two of the stats cases.
`bun test tests/context/` gives 83 pass / 4 fail; the four are
`context database ownership`, which fail identically on `main` (a child-process
stdout JSON parse, unrelated to this change). `bun run typecheck:root`,
`lint:hook-io` and `lint:spawn-env` are clean.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* fix(provider): pause the session on a reactive quota error instead of ending it
session.abortReason was only ever set to a quota category by the two
proactive sites: the pre-request rate-limit guard, and the observer-text
heuristic. A 429 that comes back from the provider is classified correctly
and then rethrown with abortReason untouched, so the .finally() in
SessionRoutes reads null, handleGeneratorExit falls past its preserve list,
and finalizeSession drops whatever the session had buffered.
The reporter's log shows the tell directly: "Generator exited — finalizing
session {reason=null}", ten times in fifteen minutes, each one a separate
observation lost to a condition that cleared by itself.
handleSessionError now maps a classified failure onto the category that
keeps the work: quota_exhausted and rate_limit to quota, auth_invalid to
auth. Both categories are already honoured by handleGeneratorExit, so
nothing downstream needed to change. Everything else keeps finalizing —
a genuinely unrecoverable session should still be cleaned up.
The reason is recorded before the rethrow, since the .finally() that reads
it runs as the throw unwinds.
* fix(provider): abort the controller when pausing on a reactive quota error
Labelling the abort reason without aborting left the controller live while
the error unwound, so the session route booked an observer failure and an
error outcome on the way out and finalization booked the aborted outcome
after — a pause recorded twice, once as a failure, with observer-health
left marked failed.
The two observer-text paths already abort for the same conditions; this
matches them. Genuinely fatal errors still leave the controller alone.
---------
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
When the spawned CLI cannot reach the provider it returns its own error
string and exits 0. That used to classify as prose and confirm (drop) the
claimed batch. Detect the CLI's transport report, reset the batch to
pending, and keep the session alive on a transport abort so the work
survives — the complete fix for #3752.
Rebased onto main: provider_switch stays on the preserve list, and
transport errors are not appended to conversation history.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* fix(observer): strip image payloads out of the observation prompt
A tool_result carrying a screenshot went into the observer prompt as raw
base64. Two things compound it. truncateObservationField keeps the head
and tail of an oversized field, so what survived a screenshot was
thousands of characters of base64 rather than the caption beside it; and
the prompt is appended to session.conversationHistory, which every later
observation in the session re-sends in full. A browser-automation session
taking a few hundred screenshots replays all of it, every time — which is
how single observations reached 400K-1M discovery_tokens against a 30K-50K
baseline.
Measured on one screenshot-shaped tool_result: the prompt goes from 15,460
characters with a 9,338-character contiguous base64 run to 1,277 characters
with none, and the text block beside the image is kept.
Stripping is generic, on the shape of the content block, rather than by
tool name. CLAUDE_MEM_SKIP_TOOLS needs every screenshot-producing tool
enumerated in advance, and it drops the whole observation rather than
keeping the part that has signal. Anthropic image blocks and OpenAI
image_url data: URLs are both elided, with the media type and byte count
reported so the observer can see something was there. A plain http image
URL is left alone: it is short and it carries signal.
* fix(observer): keep url-backed Anthropic image sources
The Anthropic branch replaced any object source, so an image block backed
by a plain https URL lost the URL and its metadata. The OpenAI branch
already made that distinction; this matches it. Only an inlined data:
payload is removed.
* fix(observer): treat DATA: URIs as inlined image payloads
URI schemes are case-insensitive. The lowercase-only data: check let an
uppercase DATA: screenshot through as thousands of characters of base64.
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
---------
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
* fix(windows): make the ghost gate's fixture handshake independent of stdout buffering
The revision landed as 155fefce (#3989) fixed the unbounded port probe, but the
gate it was meant to unblock still fails on CI. Every failing run has the same
shape: no stage output, exactly `2 expect() calls`, and the fixture's `ready`
line only visible once the fixture is killed at the 600s cap — while the
fixture's chroma chain is already alive 3s in. The handshake read a redirected
stdout, and a single small line written by a process that then idles can sit
unflushed in that buffer indefinitely.
The fixture now appends every event to an events file (a syscall per call —
nothing left to flush) and keeps the stdout copy for human debugging, plus
emits progress stages with elapsed times. The test reads readiness from the
file, prints `waiting for fixture ready: elapsed=… lastStage=…` every 15s so
the timeline survives even a bun-timeout kill, gives up at 240s (below bun's
cap) with the full events/stdout/stderr dump, and reaps an in-flight
ensureWorkerStarted() during teardown instead of leaving it running past
cleanup.
Locally the gate passes in 41.9s with the stage timeline visible: fixture ready
after 6.1s, ghost confirmed, ensureWorkerStarted 'ready' after 27.4s.
* test(windows): sweep stragglers an abandoned launcher leaves behind
The deadline race cannot cancel ensureWorkerStarted(), so teardown waits for it
— but that wait is time-boxed, and a launcher still mid-flight can spawn its
worker or reclaim the chain AFTER the kills. Fault-injection check (deadline
temporarily at 1s, which abandons the launcher mid-flight): teardown used to
leave a ghost listener on the port and a live isolated chroma chain behind
(ghost owner dead + 3 surviving sidecar processes). Teardown now repeats
"reap the pid-file worker, free the port via the production reclaim" in bounded
rounds until the port is quiet; the same injection now leaves zero listeners
and zero surviving isolated processes, and the green path still passes in 40s.
* test(windows): give the ghost gate's event channel a real contract
The events file this handshake reads was never written. `process.env.X = ...`
does not survive child_process in bun (children get the environment the
runner started with), so the fixture only ever emitted to stdout — and the
reader's stdout fallback hid that: every local run "passed" on the fallback
while CI, where redirected stdout is buffered, kept hanging. Probed:
A bash-set env -> child sees it
B process.env.X -> child does NOT see it
C { ...process.env } passed explicitly -> child sees it
- pass the events file path as argv[2] instead (no env anywhere in the
bun -> powershell -> Start-Process -> bun chain);
- stop reading stdout back: a broken channel must fail loudly, and stdout
cannot save a buffered CI run anyway;
- events carry the port, so a listener is sweepable without `ready`;
- record the detached fixture's pid the moment Start-Process returns and
tree-kill from it when readiness never arrives — previously that fixture
had no teardown handle at all and outlived the run;
- document why the readiness deadline stays far below the 600s test cap.
* test(windows): unbind the ghost gate from the runtime it runs on
Three stalls, one commit, because they only separate under the runtime CI
uses (bun 1.4.x; the local default here is 1.3.6).
- The launch call never returned. execFileSync(powershell ...) with a stdout
pipe waits for EOF, and the DETACHED fixture inherits that pipe, so it never
closes while the fixture lives: the call returns only when something kills
the fixture — which is why every red CI run showed the fixture alive for the
full 600s and the ready line "after 0ms" (that is the moment the stall ended,
not a handshake problem). The pid now travels through a file and the call
runs with stdio 'ignore' — the same shape production's spawnDaemon() uses on
Windows. Reproduced on bun 1.4.0 locally: the gate now proceeds.
- The scenario is runtime-dependent. bun >= 1.4 no longer inherits the
listening socket into spawned children: identical code on one machine —
1.3.6 leaves the port LISTENING under the dead worker, 1.4.0 releases it with
the worker (the sidecar chain still survives; it just no longer holds the
socket). Where no ghost forms and the chain was verified present, the gate
now reports that and skips the recovery assertions instead of failing on the
runtime's behaviour.
- Every child-process call in the gate is bounded now (taskkill had no
timeout), and the kill-to-ghost window logs elapsed times, so a future stall
names the step it is stuck in.
Pass Node/undici { verbose: true } from the existing worker-utils fetch wrappers when CLAUDE_MEM_FETCH_VERBOSE is 1/true/on/yes. On failure, log the serialized error cause chain. Default fetch behavior is unchanged.
Closes#3957
Package-facing security patch for the #3861 / #3985 credential-leak fixes
(unauthenticated /api/settings redaction, MCP workspace containment, host
bind tightening). Publish-ready; do not npm publish from this PR.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
Quota cooldown is an intentional pause, not a failure, so observer-health
stayed green and the session-start banner stayed silent while the worker
queued work. Mirror the breaker into observer-health.json and show a
pause notice on the existing health/banner path without changing
queue-accept or drain-on-clear.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
isPortInUse()'s Windows fast path is an HTTP probe against /api/health, and it
was the one probe the ghost-listener reclaim (#3900) left unbounded. A ghost
listener — the dead worker's inherited socket, held open by its chroma sidecar
chain (#3603) — completes the TCP handshake and never answers, so the probe
never settled. ensureWorkerStarted() calls isPortInUse() BEFORE the reclaim, so
the reclaim could never run: instead of healing the ghost, every launcher
awaited a promise that never resolved. On CI that hung the gate added by #3900
for the full 600s test timeout in every run (12/12 red on main).
The probe now carries the same 5s abort budget as every other probe in the
module; on timeout the existing fall-through runs the socket probe, which still
reports a bound port as in use, so the launcher proceeds to the reclaim.
The gate gains stage lines and a 300s deadline around ensureWorkerStarted() so a
future unbounded await fails with a named error instead of a silent bun timeout.
Unit coverage pins the probe's abortable-signal contract and the fall-through.