'exit' can fire before piped stderr has drained, so the WARN log could
miss the tail end of the CLI's dying words — the exact text the tail
exists to capture. 'close' waits for all stdio to finish. Registry
unregistration stays on 'exit' since it doesn't read the streams.
Addresses greptile review on PR #2911.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* plan-10 Phase 1: ship deterministic plugin runtime dependency closure
Approach A — commit & ship plugin/bun.lock so the plugin's runtime
node_modules install is deterministic, fixing the recurring
`Cannot find module 'zod/v3'` (#2730).
- align generated plugin zod range to root (^4.4.3) in build-hooks.js
- new scripts/gen-plugin-lockfile.cjs generates plugin/bun.lock as a
build artifact after build-hooks.js writes plugin/package.json
- track & ship plugin/bun.lock (.gitignore negation, .npmignore, files allowlist)
- install with `bun install --frozen-lockfile --ignore-scripts` at runtime
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 2: fail loud at install time on a broken dependency closure
Strengthen verifyCriticalModules to assert each dependency is actually
importable via require.resolve (not merely a directory), and assert the
worker-required zod subpaths resolve: zod/v3, zod/v4, zod/v4-mini.
A partial/stale install now fails `npx claude-mem install` immediately
instead of surfacing later as a Stop-hook `Cannot find module 'zod/v3'`.
Bin-only packages (e.g. tree-sitter-cli, which has no bare-name entry
point) fall back to resolving <dep>/package.json so a healthy install
isn't falsely rejected.
Adds tests/cli/verify-critical-modules.test.ts covering a missing zod/v3
subpath (throws), a complete zod (passes), and a bin-only dep (passes).
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 3: clean-room install + import smoke test (#2730 backstop)
Add scripts/smoke-clean-room.cjs and a `smoke:clean-room` npm script.
Against fresh temp dirs (never the repo's node_modules) it:
- copies plugin/, runs `bun install --frozen-lockfile --ignore-scripts`,
asserts zod, zod/v3, zod/v4, zod/v4-mini resolve, and boots the bundled
worker asserting no `Cannot find module` — the direct #2730 regression guard;
- `npm pack`s, installs the tarball into a second temp dir, and load-tests
the published bin entrypoint, warning loudly on any declared main/exports
target missing from the tarball (latent #2537 gap).
Exits non-zero naming the missing module on any failure; cleans up all
temp dirs and the tarball in a finally.
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 4: gate CI and publish on the clean-room dependency closure
- ci.yml: new `clean-room-deps` job (between build and the docker e2e job)
runs a frozen-lockfile drift check on the committed plugin lockfile, then
`npm run build` + `npm run smoke:clean-room`. The drift step catches a
contributor who changed plugin deps without regenerating plugin/bun.lock.
- npm-publish.yml: add setup-bun and run `npm run smoke:clean-room` between
build and `npm publish`, so a broken runtime closure cannot be published
on a tag push (ci.yml does not run on tags). Secrets block untouched.
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10: doc recluster note + Phase 0 execution slice for #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plans: backlog recluster (2026-06-04) — cross-cluster execution order + plan-13 doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10: gen-plugin-lockfile degrades gracefully when bun is absent
The Windows build CI job has no bun on PATH; regenerating the lockfile there
threw and failed the build. The committed plugin/bun.lock is already the
deterministic closure, so skip regeneration (non-fatal) when bun is missing
and a lockfile exists; fail loud only when neither is available.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebuild plugin artifacts after merging main (v13.5.1) + plan-10 work
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: rebuild plugin artifacts after merging main v13.5.5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(deps): daily upgrade pass — agent SDK 0.3.172, better-auth 1.6.16, posthog-node 5.36.15, dompurify 3.4.9
- Bump @anthropic-ai/claude-agent-sdk 0.2.141 -> 0.3.172 (tsc + full test suite green)
- Remove deprecated @types/dompurify stub (dompurify ships its own types)
- Add overrides.tmp ^0.2.7 to clear GHSA-52f5-9888-hmc6 / GHSA-ph9p-34f9-6g65
via np -> listr-input -> inquirer -> external-editor -> tmp chain
- npm audit: 0 vulnerabilities; npm outdated: clean
- package-lock.json is gitignored in this repo, so not committed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* plan: worker-restart single-source-of-truth — 7-phase fix for restart races
Phased plan from the adversarially-verified diagnosis (wf_f07f3541-b05):
kill the cache mirror, single verified restart initiator, self-replacing
restart endpoint, unified spawn gate with lockfile, PID-file demotion,
test data-dir isolation, soak verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(restart): delete sync-script cache-mirror and HTTP restart trigger
Phase 1 of plans/2026-06-10-worker-restart-single-source-of-truth.md.
The installed-version cache mirror wrote version-N code into the
version-(N-1) cache dir, manufacturing permanent version disagreement;
the HTTP POST to /api/admin/restart raced the CLI restart that follows
it in build-and-sync. Both are deleted; the CLI worker:restart in the
marketplace copy is now the single restart initiator, and the sleep 1
between the two mechanisms is gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(restart): restart proves itself or exits 1
Phase 2 of plans/2026-06-10-worker-restart-single-source-of-truth.md.
worker-service restart now captures the old worker pid, waits for the
port with the same platform-scaled 15s budget as stop, spawns the
marketplace copy of worker-service.cjs when present, then polls
/api/health until the pid changes and the version matches this build's
baked __DEFAULT_PACKAGE_VERSION__ — success is printed to stdout,
deadline (platform-scaled 30s) exits 1 with the last observed health
payload and the spawned script path. The --daemon generic start-failure
path now exits 1 instead of masquerading as success; the three
duplicate-suppression exits remain 0.
New helper src/services/restart-verify.ts (worker-service.ts bootstraps
on import, so the helper lives in an import-safe module) with 8 tests
covering pid-flip success, stale pid, wrong version, unreachable
timeout, 503-degraded acceptance, and null-oldPid version-only
verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(restart): self-replacing worker — old worker spawns its successor
Phase 3 of plans/2026-06-10-worker-restart-single-source-of-truth.md.
/api/admin/restart was kill-only: hooks that POSTed it then raced the
dying worker with their own lazy-spawn (the observed recycle ping-pong).
Now the dying worker spawns its successor itself — after a re-entrancy-
guarded, deadline-bounded (platform-scaled 10s) graceful shutdown, and
only once its port is confirmed free; stop and signal shutdowns stay
kill-only. The hook recycle path waits for that successor via
/api/health polling (HOOK_READINESS_TIMEOUT_MS budget) and lazy-spawns
only as a fallback, with a warn-only version re-check so a hook never
recycles more than once per invocation.
Shutdown sequence lives in import-safe src/services/worker-shutdown.ts
(worker-service.ts bootstraps on import); registerSignalHandlers no
longer pre-sets isShuttingDown — the supervisor's shutdownInitiated
guard owns signal dedupe, and pre-setting would no-op the new entry
guard. 13 new tests cover re-entrancy, deadline expiry/rejection,
handoff ordering, kill-only reasons, successor-wait vs lazy-spawn
fallback, and pre-graceful bookkeeping failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(restart): one spawn gate; CLI restart defers to the self-replacing worker
Phase 4 of plans/2026-06-10-worker-restart-single-source-of-truth.md.
Three uncoordinated spawn paths (hook lazy-spawn, MCP worker-spawner,
CLI) with two different bun resolvers produced 3-launcher collisions
within a single second. Now a wx-flag lockfile (<DATA_DIR>/spawn.lock,
60s mtime staleness with re-stat-before-unlink, owner-checked release)
gates every external spawn: lock losers never fail — they skip the
spawn and wait for the winner's worker. resolveBunRuntime is deleted in
favor of ProcessManager's resolveWorkerRuntimePath (adds BUN_PATH,
~/.bun/bin, brew, which fallbacks), closing the kill-then-can't-respawn
path; mcp-server prefers the marketplace worker script so stale cache
dirs stop spawning stale workers.
Integration fix surfaced by live verification: the CLI restart raced
the Phase 3 self-replacement handoff (the successor re-binds the port
in ~200ms, so waitForPortFree always timed out and restart exited 1
while the restart had actually succeeded). The CLI now verifies the
worker's self-spawned successor directly, and only spawns — gate-
wrapped, after the port frees — as the fallback when no worker was
running, the shutdown POST was rejected, or no successor appeared. The
dying worker's handoff is intentionally ungated: it spawns only after
its own port closes, and hooks wait on it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(restart): demote the PID file — health and port are the liveness oracle
Phase 5 of plans/2026-06-10-worker-restart-single-source-of-truth.md.
The dying worker's shutdown cascade deleted the PID file
unconditionally as its final act, clobbering the successor's
freshly-written file; status then required portInUse AND pidInfo, so a
healthy worker reported as "not running". Now every PID-file deletion
is owner-guarded: the supervisor cascade deletes only its own pid
(removeOwnedPidFile), and the CLI stop/restart-fallback, the restart
handoff, and the daemon start-failure cleanup go through
removePidFileIfOwner (owner-or-dead — a live successor's file always
survives; corrupt files are left for the next boot's validator).
status sources from GET /api/health alone (pid, version, uptime,
workerPath; 503-degraded counts as running and now surfaces its queue
detail), with port-in-use-but-unreachable and not-running fallbacks —
all exit 0 as before. The --daemon duplicate gate checks the port
first (ground truth) and the PID file second (advisory, for the
freed-port-but-undeleted-file window); duplicate suppression stays
exit 0. writePidFile/touchPidFile remain — the file is diagnostics,
and the worker stays its only writer.
Also fixes combined-run test pollution: spawn-gate and worker-utils
timeout tests now eagerly import paths.js before setting a temp
CLAUDE_MEM_DATA_DIR, so the import-time DATA_DIR const can't freeze on
a deleted temp dir for suites loaded later in the same bun process.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: no test ever touches the real ~/.claude-mem again
Phase 6 of plans/2026-06-10-worker-restart-single-source-of-truth.md.
process-manager and graceful-shutdown tests wrote corrupt JSON and
sentinel PIDs (2147483647) into the real ~/.claude-mem/worker.pid and
drove the real supervisor.json cascade under a snapshot-restore that a
killed run would skip — that pollution contaminated production logs and
a prior diagnosis. Both files now set a temp CLAUDE_MEM_DATA_DIR at the
top of the file before dynamically importing the code under test (ESM
hoisting makes beforeEach too late), assert their paths landed outside
the real dir, and derive PID_FILE from the same frozen paths module the
code uses so test and code can never diverge under bun's shared module
cache. The snapshot-restore scaffolding is deleted; zero assertions
changed.
tests/preload.ts gains a tripwire: when CLAUDE_MEM_DATA_DIR is unset it
fills a per-run temp dir, so no test in any file can fall through to
the real data dir. Fallout made explicit: worker-spawn child processes
get an explicit temp dir; install-error-matrix restores rather than
deletes the env var; settings-defaults-manager pins the unset-env
default it was implicitly relying on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(settings): bootstrap notices go to stderr, never stdout
CI on PR #2894 caught the latent bug: on the first boot in a fresh data
dir, SettingsDefaultsManager printed '[SETTINGS] Created settings file
with defaults: ...' to stdout before the start command's JSON hook
payload, corrupting the machine-readable contract every fresh install's
first hook invocation relies on. The Phase 6 per-run temp data dir made
the cold-dir case deterministic in CI, exposing it. Both informational
notices (creation, nested-schema migration) now use console.warn —
stderr — matching the function's existing failure-path idiom; two
regression tests pin stdout silence on both paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(restart): address PR #2894 review — dedupe script resolver, skip futile port wait
Both inline copies of the marketplace-first script-candidate list in
worker-service.ts (restart fallback + successor handoff injection) now
call the exported resolveWorkerScriptPath() ?? __filename, so the
candidate list lives in one place. verifyRestartedWorker's failure
result gains lastPollSawHealth; when the self-replacement handoff
verification timed out while a live (but unverifiable) worker was still
serving on the port, the CLI fallback now skips its port-free wait —
the port cannot free while that worker lives, so the wait only burned
its full platform-scaled budget before the same final verification ran
anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): disclose 19 reliability-signal fields and 2 new events across all surfaces
Whitelist (scrub.ts), scrub tests, public docs (telemetry.mdx), and CLI
disclosure (COLLECTED_FIELDS/EVENT_NAMES) for the Plan 14 reliability
signals: search retrieval quality, compression trust, worker lifecycle,
and hook failure keys, plus the worker_stopped and hook_failed events.
Includes the plan document.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): retrieval-quality signals on search_performed
SearchManager.search() fills an optional telemetry envelope
(result_count, search_strategy, chroma_available, fallback_reason)
across all three search paths; handlers stash it on
res.locals.searchTelemetry and the existing finish-middleware spreads it
into the search_performed capture. Zero-result searches report
result_count: 0; Chroma fallback reasons are a closed enum, never the
error message. Response shapes unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): compression trust signals on session_compressed
fabrication_detected/fabricated_count flow through compressionProps (all
three emit paths); invalid-output respawns emit a respawn-gated
session_compressed with outcome invalid_output and the classifier value;
aborted generators emit outcome aborted with abort_reason normalized to
a closed enum in the .finally where all five abort flows converge (the
.catch path can never observe a non-null abortReason).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): worker lifecycle signals — worker_stopped, crash detection, memory metrics
Clean-shutdown sentinel written before telemetry flush and consumed at
startup; worker_started gains previous_shutdown (crash/clean/unknown)
and previous_uptime_seconds derived from the stale PID file; new
worker_stopped event (uptime_seconds, shutdown_reason stop/restart/
signal) emitted before shutdownTelemetry(); the CLI restart path tags
/api/admin/shutdown?reason=restart so restarts are distinguishable;
buildLifecycleProps adds integer process_rss_mb/heap_used_mb.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): threshold-gated hook_failed distress signal via CLI transport
recordWorkerUnreachable emits hook_failed exactly when the consecutive-
failure count reaches the fail-loud threshold; the generic blocking-error
branch emits error_mode blocking_error. Both emits are awaited before
the process.exit paths so the 2s-capped CLI POST survives; hook_type is
a closed enum registered at hookCommand entry. Exit codes unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(build): regenerate plugin artifacts with Plan 14 telemetry signals
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tests): make PostHog client regression test order-independent via global preload mock
The disableGeoip regression test mocked posthog-node per-file, but
telemetry.ts is imported transitively by many test files in the shared
bun process, so the mock registered too late and the test failed in
full-suite runs — CI on main has been red since v13.5.4. The mock now
registers in a bunfig [test].preload before any module loads, which
also guarantees test runs can never construct a real PostHog client and
flush fabricated events into production analytics (consent is
default-on and the suite outlives flushInterval). telemetry.ts gains a
test-only state reset so construction is observed deterministically
regardless of suite order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(telemetry): forward shutdown reason in Windows-managed IPC message
Review follow-up: the wrapper IPC path discarded the restart tag, so an
external Windows wrapper could only ever report shutdown_reason 'stop'.
No wrapper in this repo listens for the message, but the reason now
travels with it for any that does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
posthog-node assumes server deployments and stamps $geoip_disable: true
on every event by default, which left ~98.5% of events with no location
data. The worker runs on the user's own machine, so the ingestion
request already carries their IP — passing disableGeoip: false lets
PostHog derive coarse location (country/region/city) at ingest. Raw IPs
are still never attached to events and are discarded on ingest.
Docs and the CLI consent screen now disclose the ingest-derived coarse
location. New regression test guards the client construction options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes four session_compressed data-quality bugs found via PostHog analysis:
- Claude tokens_output was an early-streaming placeholder (2-10 tokens) from
the assistant message; the event is now stashed and fired from the SDK
result message with finalized per-turn usage (empirically verified per-turn)
- cost_usd now real: Claude from cumulative total_cost_usd deltas between
results, OpenRouter from usage.cost + cost_details.upstream_inference_cost
(BYOK) with usage accounting requested from openrouter.ai only
- compression_ratio < 1 / 0.0 artifacts killed: both-or-null usage guards in
Gemini/OpenRouter providers, input > 0 ratio guard, and a new endpoint_class
property (openrouter | custom) to segment custom-gateway usage reporting
- model never silently absent: response.model stamped over the configured
string, array-typed model config normalized, 'unknown' floor in both
session_compressed emit sites, providers seed session.lastModelId
Also adds an install-state snapshot to worker_started (start + daily
heartbeat) as person properties: db_observation_count, db_session_count,
db_summary_count, db_project_count, db_size_mb, install_age_days,
obs_count_7d, obs_count_30d, days_since_last_obs — aggregate counts and
day-deltas only, never content. Epoch math normalizes legacy seconds-unit
rows. Fixes the ide lookup querying the legacy sessions table (no
platform_source column), which silently threw on every start.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context_injected now carries the full injection story computed alongside
rendering (ContextInjectStats): observation/session counts, timeline
depth, per-type buckets, tokens_injected, tokens_saved_vs_naive,
mode/provider/search_strategy. Capture moved from middleware into the
handler so stats ride the event; empty-state injections are not counted.
session_compressed gains hook (init|ingest|summarize, stamped at prompt
dispatch), compression_ms (dispatch → response), model id, ide (session
platform source), per-type buckets + dominant observation_type, and REAL
token usage: Claude from message usage (incl. cache), Gemini from
usageMetadata, OpenRouter from usage — never the 70/30 estimates;
providers leave lastUsage null when the API gives no split. provider is
now always set (currentProvider/agentName fallback chain).
worker_started moved to post-init so it can read the install's dominant
IDE from session history — person-property ide makes IDE-level
DAU/retention breakdowns non-null without reinstalling.
signal_rate_pct from the spec was NOT added: nothing in the codebase
measures injected-token relevance, and we don't fabricate metrics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PostHog cannot compute retention, stickiness, lifecycle, or cohort
insights on profile-less events — exactly the charts growth reporting
needs. Lifecycle events (install_*, uninstall_completed, worker_started;
~1-2/day/install) now build a person profile keyed to the anonymous
install UUID with $set restricted to whitelisted enums. High-volume
operational events stay $process_person_profile:false for cost.
Adds plans/2026-06-09-telemetry-metrics-spec.md mapping every event to
the growth/retention/activation/reliability metric it powers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sequential-thinking pass over what the data needs to answer (growth,
core-loop health, failure surface, feature adoption) showed the v1
events were shells: session_compressed only fired on success with a
constant outcome, duration_ms was documented but never sent, installs
and context injection were untracked.
New events: install_completed/install_failed/uninstall_completed (npx
CLI via direct-POST transport with 2s timeout — no SDK in the CLI
bundle), context_injected. Enriched: worker_started gains trigger
(start|daily heartbeat for true DAU) + startup duration_ms;
session_compressed gains duration_ms/count/has_summary and an error
capture at the generator failure path; search_performed gains endpoint
(bounded route-name enum) + duration_ms.
Whitelist extended only with bounded enums/counters (endpoint, ide,
provider, runtime_mode, trigger, count, has_summary, is_update). Docs
and CLI consent text updated to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DO_NOT_TRACK, CLAUDE_MEM_TELEMETRY=0, telemetry.json enabled:false, and
'claude-mem telemetry disable' all still force it off. The install-ID
bootstrap no longer records enabled:false as a side effect (enabled is
now optional = no decision), so the default genuinely applies to
existing installs. Install prompt and docs updated to opt-out language.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in PostHog product analytics living in the long-running worker. Consent precedence DO_NOT_TRACK > CLAUDE_MEM_TELEMETRY > telemetry.json > default OFF; whitelist-only scrubber; claude-mem telemetry CLI; install-flow consent prompt as the final step; async dependency installs with live spinner heartbeat; full privacy docs. Ships dark until the publishable key lands.
* fix(hooks): drop pipe `break` that triggers EACCES on Windows/Cygwin
Hook/MCP plugin-root resolution piped a candidate list into a `while`
loop that `break`s on the first match:
_P=$({ printf ...; ls -dt ...; printf ...; } | while read _R; do
... && { printf '%s\n' "$_Q"; break; }; done)
On Cygwin/MSYS shells (Git-Bash on Windows) the early `break` closes
the pipe's read end while the producer subshell is still writing the
remaining candidate lines. The next `printf`/`ls` then writes to a
broken pipe, which Cygwin surfaces as EACCES rather than EPIPE:
printf: write error: Permission denied
...failing the hook. It is a pipe-lifetime race, not a path/username
encoding problem — it reproduces 40/40 once CLAUDE_PLUGIN_ROOT is set
(so the first candidate matches and `break` fires immediately, leaving
the most pending producer output).
Fix: don't `break`. Drain every candidate (only a handful) and print
the FIRST match exactly once via a `_F` guard, so the producer always
completes and no broken-pipe write happens. First match still wins, so
the contractual fallback ORDER is unchanged. The change is POSIX-clean
(no bashisms), so the `mcp` host's `sh -c` launcher is fixed too.
Patched the single source of truth (src/build/hook-shell-template.ts)
and regenerated the three host-managed files; verified all 15 command
strings still match the canonical generator byte-for-byte, and that the
regenerated commands produce 0/30 EACCES under both bash and sh with
CLAUDE_PLUGIN_ROOT set.
Fixes#2707. Related #2709.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(hooks): reset _F before the resolution loop
Review follow-up. The first-match guard reads `[ -z "$_F" ]` before _F
is ever assigned. The `while` loop runs in the pipe's subshell, which
inherits the parent shell's variables, so an inherited non-empty `_F`
(e.g. an exported var in the host environment) would make the guard
false on every iteration: nothing prints, `_P` stays empty, and the
hook exits "not found".
Prefix the command substitution with `_F=;` so the guard always starts
from a known-empty state. Regenerated the three host-managed files;
canonical generator check still matches byte-for-byte, and the command
resolves correctly under bash and sh even with `_F=1` exported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(sqlite): remove dead legacy migration system (migrations.ts)
migration001..010 + the migrations[] array were orphaned by the #534
MigrationRunner refactor — zero importers. Build + full test suite
show no regression (11 pre-existing flaky fails identical on main).
* build: regenerate bundles + restore canonical hook JSONs for template fix
---------
Co-authored-by: Steven Moreno <steven.moreno@kpginc.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: wangchenguang <darion.yaphets@gmail.com>
* fix(context): suppress native Codex transcript AGENTS writes (closes#2837)
* fix(sqlite): preserve legacy NULL observation hashes during migration 29 (closes#2812)
* Keep Codex hook output within the supported schema (closes#2844)
* Honor persisted worker request timeouts in MCP calls (closes#2822)
* Limit Codex AGENTS suppression to native transcript watches
* Preserve migrated hashes through legacy FK rebuilds
* Keep settings-file defaults from inheriting stale env state
* Keep welcome-hint settings overrides request-local
* Keep Bun test state from masking settings fallbacks
* Keep sticky test mocks from bleeding into later routes
* Keep generated worker bundle aligned after baseline carry-over
* Keep settings-file defaults from inheriting stale env state
* Keep welcome-hint settings overrides request-local
* Keep Bun test state from masking settings fallbacks
* Keep sticky test mocks from bleeding into later routes
* Keep generated hooks aligned after baseline carry-over
* Keep settings-file defaults from inheriting stale env state
* Keep welcome-hint settings overrides request-local
* Keep Bun test state from masking settings fallbacks
* Keep sticky test mocks from bleeding into later routes
* Keep generated bundles aligned after baseline carry-over
* Keep settings-file defaults from inheriting stale env state
* Keep welcome-hint settings overrides request-local
* Keep Bun test state from masking settings fallbacks
* Keep sticky test mocks from bleeding into later routes
* Keep generated scripts aligned after baseline carry-over
* Keep worker recycle tests isolated from module cache
* Preserve native Codex watch filtering while narrowing AGENTS suppression
* Keep migration backfills out of user hash namespaces
* fix(sqlite): bound stored prompt payloads to stop prompt-table bloat (closes#2793)
* fix(sqlite): normalize prompt dedupe against the stored payload
* Keep settings-file defaults from inheriting stale env state
* Keep welcome-hint settings overrides request-local
* Keep Bun test state from masking settings fallbacks
* Keep sticky test mocks from bleeding into later routes
* Keep worker recycle tests isolated from module cache
* Keep baseline fallback fixes consistent with regenerated assets
* Keep parser mode tests isolated and prompt storage observable
* Keep parser mode tests isolated and prompt storage observable
* Keep parser mode tests isolated and prompt storage observable
* build: regenerate plugin bundles for combined batch
* test: restore module mocks after codex-context processor tests
The module-level mock of utils/project-name leaked into
tests/utils/project-name.test.ts when the full suite ran in one
process.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Keep settings-file defaults from inheriting stale env state
* Keep welcome-hint settings overrides request-local
* Keep Bun test state from masking settings fallbacks
* Keep sticky test mocks from bleeding into later routes
* Keep worker recycle tests isolated from module cache
* fix(build): add transcript-watcher.cjs build target (closes#2450)
src/npx-cli/commands/runtime.ts:230 hard-codes a reference to
plugin/scripts/transcript-watcher.cjs, but scripts/build-hooks.js
never compiled this entry. The fallback `spawnBunWorkerCommand`
path silently no-ops on systems where bun isn't reachable, so
`claude-mem transcript watch` is effectively dead in the published
npm bundle.
This adds:
- A thin entry (src/services/transcripts/transcript-watcher-entry.ts)
that dispatches process.argv to the existing runTranscriptCommand
function in cli.ts (which until now had no caller in the build).
- A new esbuild target in build-hooks.js modeled on context-generator
(Node 18 CJS, minified, bun:sqlite external) plus the standard
stripHardcodedDirname pass and chmod 0755.
- Updated build summary so the new artifact shows up in the
"compiled successfully" footer.
The compiled bundle is 137 KB. `node plugin/scripts/transcript-watcher.cjs`
prints the usage line and exits 1 as expected; `... validate`,
`... init`, and `... watch` all route correctly.
No behavior change on platforms that previously had a working bun
fallback - the explicit path now just wins the existsSync check.
* fix(build): externalize zod + add bundle-size guard for transcript-watcher
Addresses Greptile review on #2592:
1. Add `zod` to the external list. worker-service and
server-beta-service both externalize zod and rely on
plugin/package.json to provide it at runtime; the watcher
build was inconsistent. The current processor.ts chain
does not actually pull in zod (bundle size unchanged
at 137.64 KB), but the external entry is defensive —
any future import in src/services/transcripts/ that
touches zod will now resolve against the plugin's
declared dependency rather than getting silently
inlined (and potentially creating a duplicate-instance
hazard).
2. Add a 200 KB size guard mirroring the 600 KB ceiling
on mcp-server.cjs. The watcher is meant to be a thin
file-tail loop; if a transitive import ever drags a
heavy module (worker-service, an SDK runtime) into
the chain, this catches it at build time with an
informative error pointing at processor.ts / watcher.ts.
No functional change to the runtime artifact (size
identical, behavior identical). Both follow-ups recommended
by Greptile.
* fix(setup): auto-install plugin dependencies at Setup phase (2649)
The plugin marketplace extracts files into ~/.claude/plugins/cache/...
but does not run `bun install`. On fresh installs the worker crashes
with `Cannot find module 'zod/v3'` on the first hook invocation
(gh #2640, #2637).
PR #2644 fixed this by auto-installing dependencies inside
`bun-runner.js`, but that runs on the SessionStart / UserPromptSubmit
critical path. Review on #2644 (YOMXXX, #2649) flagged this as the
wrong architectural home: corporate proxies, offline machines,
permissions, registry timeouts — every install failure lands on the
user's first prompt instead of at install time.
Move the auto-install to `version-check.js` (Setup phase), the only
standalone hook script and the natural place to materialise plugin
runtime state. Setup has a 300s timeout (vs 60s for SessionStart),
runs once per Claude Code launch, and the node_modules guard short-
circuits subsequent invocations.
Failure modes surfaced explicitly (spawn exception, non-zero exit,
signal-killed including OOM SIGKILL — where spawnSync leaves
status=null and error=undefined).
RED → GREEN proof:
- /tmp/issue-2649-proof/RED.log (unpatched: 1 pass, 1 fail)
- /tmp/issue-2649-proof/GREEN.log (patched: 2 pass, 0 fail)
- Full-suite baseline: 1803 pass / 55 fail (unchanged after fix:
1805 pass / 55 fail — +2 = new tests, 0 regressions).
Existing `plugin-version-check.test.ts` updated to pre-create
node_modules in beforeEach so its marker-compat assertions
(`stderr === ''`) are unaffected by the new Setup-phase install path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(setup): cleanup partial node_modules + success log (#2650 review)
Address Greptile review findings on PR #2650:
P1 — failed install permanently blocks retry. `bun install` often
creates the node_modules directory before it terminates under failure
(network timeout mid-fetch, registry 5xx, OOM kill). The
existsSync(node_modules) guard would then permanently skip every
subsequent Setup run, leaving the plugin broken with no recovery short
of manual `rm -rf node_modules`. Remove the partial dir in the failure
branch so the next Setup invocation can retry automatically.
P2 — no completion confirmation. A Setup hook that can block for up to
120s needs an explicit success line so users can distinguish a hung
install from one that finished silently. Emit a success diagnostic in
the no-error else branch.
New test `cleans up partial node_modules after a failed install` uses
a `partial-then-fail` fake-bun behavior that creates node_modules then
exits non-zero (mirrors real bun under mid-fetch failure). Asserts the
failure diagnostic surfaces AND node_modules is gone after, proving
the retry path is unblocked.
RED -> GREEN proof:
- /tmp/issue-2650-proof/RED-review-fixes.log
(without fix: 1 pass / 2 fail — success diagnostic missing AND
partial node_modules NOT cleaned up)
- /tmp/issue-2650-proof/GREEN-review-fixes.log
(with fix: 3 pass / 0 fail)
Full-suite baseline preserved:
- /tmp/issue-2650-proof/full-suite-GREEN.log
(1806 pass / 19 skip / 55 fail — same 55 pre-existing failures,
+1 new test vs prior commit, 0 regressions)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two defense-in-depth fixes for Windows environments:
1. SettingsDefaultsManager: strip UTF-8 BOM (U+FEFF) before JSON.parse.
Windows tools (editors, formatters, Claude Code Edit hook) may prepend
BOM to settings.json, which Bun rejects with "Unrecognized token",
causing a silent fallback to defaults that breaks server-beta routing.
2. bun-runner.js: exempt lifecycle subcommands (start/stop/restart/status)
from the #2188 empty-stdin guard. These subcommands manage the worker
daemon and never consume stdin. Killing the child on empty stdin
prevents the daemon from starting on platforms where Claude Code
does not pipe a payload for SessionStart (e.g. Windows CC <= 2.1.145).
Co-authored-by: Henry Gimenez da Costa <rm563217@fiap.com.br>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Windows (and any host where the worker inherited a PATH that predates the
user adding uv), the worker spawns chroma-mcp via uvx with an env whose PATH
omits uv's install dir (~/.local/bin, %USERPROFILE%\.local\bin), so 'uvx' is
not recognized and the subprocess dies in ~25ms — semantic search silently
falls back to keyword. getSpawnEnv now prepends uv's known bin dirs (plus an
optional CLAUDE_MEM_CHROMA_UVX_PATH override) to the child PATH. Additive and
cross-platform: only existing dirs are added, dedup is case-aware on Windows.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the first session after a reboot, the SessionStart context hook races the
daemon's parallel auto-start. ensureWorkerRunning lazy-spawned a worker then
waited only ~0.75s (3 attempts x 250ms) for the port — far short of a cold
~7s macOS+Chroma boot — so it returned false and the hook soft-failed to empty
additionalContext (no memory injected). The same short wait also caused
session-init to drop the user_prompts row, the upstream trigger for #2794.
Extend the post-spawn port wait to ~15.5s (6 attempts, 500ms exp. backoff,
matching POST_SPAWN_WAIT). The generous waitForWorkerReadiness() that follows
is now actually reached. Soft-fail-to-empty + exit 0 is preserved if the worker
genuinely never comes up (bounded wait, no blocking).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(smart-file-read): use a JS-valid query for plain .js files
The shared "jsts" tree-sitter query references TypeScript-only node types
(type_identifier, interface_declaration, type_alias_declaration,
enum_declaration) that don't exist in the tree-sitter-javascript grammar.
With tree-sitter 0.26.x, query compilation aborts on the first unknown
node type (Invalid node type "type_identifier"), so smart_outline /
smart_search silently return nothing for every plain .js/.mjs/.cjs file.
Add a dedicated "js" query — class names are (identifier) here, not
(type_identifier), and the TS-only patterns are dropped — and route the
"javascript" language to it. typescript and tsx keep using jsts.
Fixes#2750. Realizes the fragility predicted in #1654.
* fix: load custom grammars in smart_outline and smart_unfold
smart_outline and smart_unfold called parseFile() without projectRoot,
so user grammars from .claude-mem.json were never loaded — the same
config worked in smart_search, which passes its search root through.
Add findProjectRoot() — walks up from the file to the nearest directory
containing .claude-mem.json, falling back to cwd to match smart_search's
default — and plumb it through unfoldSymbol/parseFile. Also update the
smart-explore SKILL.md custom-grammar section to document the actual
config schema ({package, extensions, query} keyed by language) and the
query capture vocabulary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: limit capture vocabulary to keys recognized on this branch
`@prop` is introduced by the C# builtin PR together with its KIND_MAP
entry; listing it here was premature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(grammar): plain-JS query + custom-grammar loading in smart_outline/unfold (closes#2786)
plan-13 (Grammar / Parser Fidelity). Incorporates two community fixes:
- #2750: tree-sitter-javascript lacks TS-only node types, so plain .js/.mjs/.cjs
aborted query compilation (0 symbols). Add a dedicated 'js' query and route
'javascript' to it.
- #2773: smart_outline/smart_unfold never resolved .claude-mem.json, so custom
grammars never loaded. Add findProjectRoot() and thread projectRoot through
parseFile/unfoldSymbol from the MCP tools; document the config schema + capture
vocabulary. Includes find-project-root test.
Co-authored-by: Letsrollamigo <Oberon999@yandex.com>
Co-authored-by: Sviatoslav Filippov <sviatoslav.filippov@chargeafter.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Letsrollamigo <Oberon999@yandex.com>
Co-authored-by: Sviatoslav Filippov <sviatoslav.filippov@chargeafter.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a fresh DB / cold boot, the UserPromptSubmit hook can race worker boot and
never persist the user_prompts row. The observation privacy gate then called
getUserPrompt(session, 0) -> null and treated EVERY observation as 'private',
silently skipping all of them (sdk_sessions still written because that insert
runs before the gate). Result: healthy worker, 0 observations, no errors.
PrivacyCheckValidator now distinguishes:
- row ABSENT (null) -> allow ingestion + visible warn (not a privacy signal)
- row PRESENT but empty/whitespace -> suppress (genuinely redacted)
Both call sites (observation ingest, summarize) updated to the decision object.
The cold-boot race that loses the prompt row is addressed separately (#2795).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(context): BMP-only injected context to stop astral-surrogate 400s (closes#2787)
Claude Code can truncate auto-loaded context (CLAUDE.md / AGENTS.md / .mdc)
at a UTF-16 code-unit boundary; if the cut splits an astral emoji's surrogate
pair it emits a lone surrogate and the Anthropic API rejects the request with
'400 no low surrogate in string' — bricking the session in a way that survives
/clear (the bytes live in the always-reloaded context file).
- Add src/utils/bmp-safe.ts: toBmpSafe() guarantees every emitted code point is
<= U+FFFF (known type markers -> distinct BMP glyphs, other astral -> bullet,
lone surrogates dropped).
- Apply it at every context-injection write boundary: injectContextIntoMarkdownFile,
writeClaudeMdToFolder, and the cursor .mdc writer — so ANY mode (incl. custom)
is safe, not just the default markers.
- Switch the default 'code' mode (plugin/modes/code.json) and TYPE_ICONS to BMP
markers so the source of truth is already clean.
- Tests in tests/bmp-safe.test.ts assert output is surrogate-free.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: cursor context now sanitizes astral emoji to BMP (#2787)
Update the unicode test to assert the post-#2787 contract: BMP scripts
(日本語, العربية) survive untouched while astral emoji are stripped and the
output contains no UTF-16 surrogate code units.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Stop hook crashes every session with 'SyntaxError: Unexpected token .'
when a host invokes bun-runner.js under a bundled pre-ES2020 Node whose ESM
loader rejects optional chaining (?.). Replace the only ?. site with explicit
guards and add a build-time guard forbidding ?. / ?? in this launcher so it
can't regress.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 1: ship deterministic plugin runtime dependency closure
Approach A — commit & ship plugin/bun.lock so the plugin's runtime
node_modules install is deterministic, fixing the recurring
`Cannot find module 'zod/v3'` (#2730).
- align generated plugin zod range to root (^4.4.3) in build-hooks.js
- new scripts/gen-plugin-lockfile.cjs generates plugin/bun.lock as a
build artifact after build-hooks.js writes plugin/package.json
- track & ship plugin/bun.lock (.gitignore negation, .npmignore, files allowlist)
- install with `bun install --frozen-lockfile --ignore-scripts` at runtime
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 2: fail loud at install time on a broken dependency closure
Strengthen verifyCriticalModules to assert each dependency is actually
importable via require.resolve (not merely a directory), and assert the
worker-required zod subpaths resolve: zod/v3, zod/v4, zod/v4-mini.
A partial/stale install now fails `npx claude-mem install` immediately
instead of surfacing later as a Stop-hook `Cannot find module 'zod/v3'`.
Bin-only packages (e.g. tree-sitter-cli, which has no bare-name entry
point) fall back to resolving <dep>/package.json so a healthy install
isn't falsely rejected.
Adds tests/cli/verify-critical-modules.test.ts covering a missing zod/v3
subpath (throws), a complete zod (passes), and a bin-only dep (passes).
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 3: clean-room install + import smoke test (#2730 backstop)
Add scripts/smoke-clean-room.cjs and a `smoke:clean-room` npm script.
Against fresh temp dirs (never the repo's node_modules) it:
- copies plugin/, runs `bun install --frozen-lockfile --ignore-scripts`,
asserts zod, zod/v3, zod/v4, zod/v4-mini resolve, and boots the bundled
worker asserting no `Cannot find module` — the direct #2730 regression guard;
- `npm pack`s, installs the tarball into a second temp dir, and load-tests
the published bin entrypoint, warning loudly on any declared main/exports
target missing from the tarball (latent #2537 gap).
Exits non-zero naming the missing module on any failure; cleans up all
temp dirs and the tarball in a finally.
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10 Phase 4: gate CI and publish on the clean-room dependency closure
- ci.yml: new `clean-room-deps` job (between build and the docker e2e job)
runs a frozen-lockfile drift check on the committed plugin lockfile, then
`npm run build` + `npm run smoke:clean-room`. The drift step catches a
contributor who changed plugin deps without regenerating plugin/bun.lock.
- npm-publish.yml: add setup-bun and run `npm run smoke:clean-room` between
build and `npm publish`, so a broken runtime closure cannot be published
on a tag push (ci.yml does not run on tags). Secrets block untouched.
Refs #2783, #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10: doc recluster note + Phase 0 execution slice for #2730
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plans: backlog recluster (2026-06-04) — cross-cluster execution order + plan-13 doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plan-10: gen-plugin-lockfile degrades gracefully when bun is absent
The Windows build CI job has no bun on PATH; regenerating the lockfile there
threw and failed the build. The committed plugin/bun.lock is already the
deterministic closure, so skip regeneration (non-fatal) when bun is missing
and a lockfile exists; fail loud only when neither is available.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. verifyCommitHashesInText: when cwd is absent (e.g. the init-response path
passes projectRoot=undefined), every 7–40 char hex substring was classified
fabricated and then stripped to "[unverified commit]" by
stripFabricatedHashesFromSummary, corrupting persisted summaries. Absence of
a repo is not evidence of fabrication — short-circuit and treat all
candidates as verified when we cannot check.
2. SessionMessageBuffer.clear() deleted buffers but not seenToolUseIds (unlike
dispose()), so a clear() not followed by dispose() leaked the dedup set and
silently dropped a later same-toolUseId observation. Mirror dispose().
Adds regression tests for both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the OpenRouter provider's base URL configurable via CLAUDE_MEM_OPENROUTER_BASE_URL
(+ OPENROUTER_BASE_URL env), turning it into a generic OpenAI-compatible client. Shared
resolver appends /chat/completions to a base or uses a full URL verbatim; default
behavior unchanged when unset. Model id passes through verbatim. Applied to both worker
and server providers.
Closes#2382#2590#2622#2393.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #2663: derive project name from git repo root (rev-parse --show-toplevel), stable
across subdirs/worktrees; fall back to basename(cwd) outside a repo.
- #2401: 'include last message' encodes cwd with both '/' and '.' -> '-' to match Claude
Code's transcript dir naming, so a '.' in a path component no longer no-ops.
- #2691: PreToolUse:Read queries observations by BOTH absolute + cwd-relative path
(value IN candidates) so context injection matches PostToolUse storage.
- #2400: CLAUDE_MEM_FOLDER_MD_SKELETON_DENYLIST suppresses empty/skeleton CLAUDE.md
injection in deny-listed folders.
- #2473: confirmed host-side (resolved upstream); added invariant test that our MCP
server/tool names stay colon/dot-free.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Most plan-03 items were already implemented (PID start-token cross-check on
linux/darwin, observer self-recursion guard, chroma-mcp tree-kill on reconnect,
health-checker dead-entry pruning) or moot after the retry-queue/RestartGuard removal.
The one genuine gap: win32 captureProcessStartToken returned null unconditionally, so
a reused-but-alive PID always passed ownership and could wedge worker spawn on Windows.
Added a PowerShell CIM CreationDate lookup (5s per-PID cache, sanitizeEnv) so PID-reuse
detection works cross-platform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Centralize Rule B absolute-path bake (src/services/integrations/install-paths.ts) used
by Cursor/Gemini/Windsurf/Mcp installers; centralize Rule A shell template
(src/build/hook-shell-template.ts).
- Build-time guardrail asserts hooks.json/codex-hooks.json/.mcp.json match the canonical
generator (prevents drift); validation matrix test executes the resolution pipeline.
- #2696: quote cmd.exe uvx args so 'protobuf<7'/'onnxruntime>=1.20' aren't parsed as
redirection on Windows (ChromaMcpManager).
- #2695: Codex CLI spawn uses shell:true + quoting on Windows so PATHEXT resolves
codex.cmd (no more spawnSync ENOENT).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Explicit failure taxonomy + single installerError() decision point replacing ad-hoc
swallow sites; 'Installation Complete' only when all ABORT-level deps satisfied,
else 'Installation Partial' with remediation.
- npm install strict-first; --legacy-peer-deps only on confirmed ERESOLVE, announced.
- Missing uv/bun = ABORT with platform-specific remediation as primary message.
- Postinstall allowlist guard (scripts/check-postinstall-allowlist.js) prevents a
re-run of the tree-sitter-swift install hang.
- 48-cell IDE x failure-mode test matrix.
- #2548: 'npx claude-mem doctor' read-only diagnostic probe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/shared/hook-io.ts: single module routing intent (DIAGNOSTIC/MODEL_CONTEXT/
USER_HINT/BLOCKING_FEEDBACK/EXIT_SIGNAL) -> correct channel; only place that calls
console.log/process.stderr.write/process.exit for the hook path.
- Handlers are pure (return HookResult); hookCommand orchestrates hook-io.
- Fix#2292: recordWorkerUnreachable now surfaces via emitBlockingError instead of
being swallowed by the stderr no-op; exit-code strategy preserved verbatim.
- Quiet-on-success: buffered library stderr dropped on graceful exit (#2694).
- lint:hook-io CI check forbids direct stream writes in handlers/** and adapters/**.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #2572: server keys/jobs/api-key migrate-scopes CLI subcommands (secrets never
printed), hand-rolled security headers (no helmet dep), wrong-runtime guard.
- #2552: mount viewer static handler + compat API on the server runtime (ServerViewerRoutes).
- #2554: fix stale Claude model (claude-3-5-sonnet-latest -> claude-sonnet-4-6);
document subscription vs API-key auth; confirm 0.0.0.0 bind avoids loopback ECONNREFUSED.
- #2558: docker-compose restart: unless-stopped on all services, REDIS_URL fallback,
credentials-file mount (config-only, not runtime-verified in sandbox).
- #2560: postgres platform_source column+indexes (idempotent), thread platform_source
end-to-end through events schema/storage/routes/compat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Boot (#2443): load+validate 'code' mode before the server accepts jobs; fail fast
if no mode loads (no more silent 'No mode loaded' job failures).
- CLI (#2444): 'start' runs foreground by default (systemd Type=simple safe); opt into
detached via 'start --daemon'.
- Auth (#2541): replace unsalted SHA-256 with salted scrypt (scrypt$N$salt$hash) +
timingSafeEqual; legacy keys still verify and are transparently upgraded.
- Scopes (#2428): default new keys to ['memories:read','memories:write'] to match the
v1 route middleware's required scopes; add scope-migration helper (#2560 seed).
- Rename (#2562): api-key-service.ts -> sqlite-api-key-service.ts (reveals backend);
4 importers updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Classify observer output (#2485): src/sdk/output-classifier.ts distinguishes
xml/idle/prose/poisoned; ResponseProcessor logs a preview on invalid parse so
dropped batches are visible, not silent.
- Recover from poison (#2485): SessionManager.respawnPoisonedSession kills+respawns
the SDK session after N consecutive invalid outputs while preserving the
SessionMessageBuffer's pending messages.
- Verify before persist (#2574): src/sdk/commit-verification.ts validates emitted
commit hashes via git cat-file -e; fabricated hashes are flagged ([unverified
commit]) not persisted, with provenance logging.
- Test isolation: new worker tests snapshot+restore mock.module'd modules in afterAll
so the full suite stays order-independent (1847 pass, 0 fail/error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>