Commit Graph

658 Commits

Author SHA1 Message Date
Alex Newman
4f1ea7241a fix(supervisor): log SDK exit stderr tail on 'close' so it is never truncated
'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>
2026-06-12 16:43:28 -07:00
Alex Newman
468b55d96e build: regenerate plugin bundles with stale-CLI resolver fix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:31:41 -07:00
Alex Newman
47277b0c34 chore: bump version to 13.5.6 2026-06-10 20:06:33 -07:00
Alex Newman
c0b96288a7 fix(restart): worker-restart single source of truth — self-replacing worker, spawn gate, verified restarts (#2894)
* 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>
2026-06-10 19:59:34 -07:00
Alex Newman
7e364a54f2 chore: bump version to 13.5.5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 02:53:35 -07:00
Alex Newman
4ce51efd28 feat(telemetry): reliability signals — retrieval quality, compression trust, worker lifecycle, hook failures (Plan 14) (#2874)
* 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>
2026-06-10 02:51:45 -07:00
Alex Newman
2152627a99 chore: bump version to 13.5.4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:21:50 -07:00
Alex Newman
245c9b4e88 feat(telemetry): enable ingest-side GeoIP for worker events
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>
2026-06-10 00:21:19 -07:00
Alex Newman
e7efcd6eaa chore: bump version to 13.5.3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:08:36 -07:00
Alex Newman
384d328976 feat(telemetry): real token/cost/model data + install-state snapshot
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>
2026-06-09 23:07:58 -07:00
Alex Newman
256f83d8da chore: bump version to 13.5.2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:49:30 -07:00
Alex Newman
f1e8481427 feat(telemetry): platform/toolchain capture for install-funnel diagnosis — os_version, WSL, install method, bun/uv/node/Claude Code versions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:49:10 -07:00
Alex Newman
a4ec46952b chore: bump version to 13.5.1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:41:32 -07:00
Alex Newman
be43fd18b8 feat(telemetry): deep instrumentation — observation types, token economics, compression metrics, IDE on lifecycle events
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>
2026-06-09 21:33:26 -07:00
Alex Newman
cdbf858a38 chore: bump version to 13.5.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:49:15 -07:00
Alex Newman
4bdea1e21c feat(telemetry): person profiles on lifecycle events to unlock retention/cohort analytics
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>
2026-06-09 18:47:42 -07:00
Alex Newman
482a6c921d feat(telemetry): real product instrumentation — install funnel, durations, volumes, failure paths
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>
2026-06-09 18:47:42 -07:00
Alex Newman
13670b25f5 feat(telemetry): default to opt-out — telemetry on for installs with no recorded decision
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>
2026-06-09 18:47:42 -07:00
Alex Newman
099279e0e6 feat(telemetry): embed publishable PostHog project token
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:47:42 -07:00
Alex Newman
b0171ed63d feat(telemetry): opt-in anonymous usage analytics via PostHog (#2863)
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.
2026-06-09 17:16:49 -07:00
Alex Newman
aedfe5b6d9 batch: dead migration-system removal + Windows EACCES pipe fix (#2845 #2725) (#2865)
* 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>
2026-06-09 15:08:22 -07:00
Alex Newman
3759d99010 batch: land five reviewed fixes from @rodboev (#2848 #2850 #2851 #2852 #2860) (#2862)
* 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>
2026-06-09 14:59:04 -07:00
Rod Boev
35fbf917e0 fix(settings): keep file defaults from inheriting stale env overrides (#2853)
* 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
2026-06-09 14:52:01 -07:00
Alex Newman
592e18e533 chore: bump version to 13.4.2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:48:06 -07:00
Alex Newman
6259b20c90 chore: bump version to 13.4.1 2026-06-07 18:45:06 -07:00
YOMXXX
04832dcfa9 fix(build): add transcript-watcher.cjs build target (closes #2450) (#2592)
* 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.
2026-06-05 23:21:25 -07:00
Ben Younes
eae20a410a fix(setup): auto-install plugin dependencies at Setup phase (2649) (#2650)
* 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>
2026-06-05 23:04:54 -07:00
Alessandro Costa
6918c87993 fix(windows): strip BOM from settings.json + exempt lifecycle subcommands from #2188 stdin guard (#2636)
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>
2026-06-05 23:02:19 -07:00
Alex Newman
69a908d017 fix(chroma): ensure uv bin dir is on the chroma-mcp child PATH (closes #2790) (#2806)
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>
2026-06-05 23:01:38 -07:00
Alex Newman
7babbf0fb5 fix(worker): wait out cold-boot worker spawn in hook path (closes #2795) (#2804)
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>
2026-06-05 21:08:54 -07:00
Alex Newman
cc0ce51108 fix(grammar): plain-JS query + custom-grammar loading (plan-13, closes #2786) (#2805)
* 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>
2026-06-05 21:07:14 -07:00
Alex Newman
530b2a0e76 fix(pipeline): don't drop observations when the prompt row is merely absent (closes #2794) (#2803)
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>
2026-06-05 21:01:58 -07:00
Alex Newman
3415d97500 fix(context): BMP-only injected context to stop astral-surrogate 400s (closes #2787) (#2802)
* 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>
2026-06-05 20:49:12 -07:00
Alex Newman
9efb04da84 fix(hooks): bun-runner.js parses on pre-ES2020 Node (closes #2791) (#2801)
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>
2026-06-05 20:36:18 -07:00
Alex Newman
cf450cec00 fix(build): enforce shipped dependency-closure boundary (plan-10, closes #2783) (#2800)
* 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>
2026-06-05 20:27:34 -07:00
Alex Newman
dc2eef8e31 chore: bump version to 13.4.0 2026-05-29 11:34:16 -07:00
Alex Newman
57612286f0 fix: two P1 review findings — commit-verification null-cwd & buffer dedup leak
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>
2026-05-29 10:55:49 -07:00
Alex Newman
d13fc437fa feat(provider): configurable OpenAI-compatible base URL (DeepSeek/LM Studio/custom)
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>
2026-05-28 18:48:39 -07:00
Alex Newman
daf6d9dc0f fix: concrete standalone bugs — project name, dot-path, path-match, CLAUDE.md denylist
- #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>
2026-05-28 18:41:06 -07:00
Alex Newman
4016a8c43a fix(worker): plan-03 lifecycle — Windows PID-reuse start-token guard
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>
2026-05-28 18:27:52 -07:00
Alex Newman
8151cd5a8f fix(spawn): plan-02 canonical ${CLAUDE_PLUGIN_ROOT} resolution + Windows spawn fixes
- 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>
2026-05-28 18:22:56 -07:00
Alex Newman
7a33685c46 fix(install): plan-04 installer failure transparency + diagnostic CLI
- 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>
2026-05-28 18:05:41 -07:00
Alex Newman
6821d85245 refactor(hooks): plan-01 hook IO discipline — typed stdout/stderr/exit channels
- 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>
2026-05-28 18:05:33 -07:00
Alex Newman
b45515c5fe feat(server): plan-07 operability — CLI subcommands, headers, viewer, schema, docker
- #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>
2026-05-28 17:38:54 -07:00
Alex Newman
33ae79ec0d feat(server): plan-07 boot correctness + auth hardening + rename
- 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>
2026-05-28 17:25:19 -07:00
Alex Newman
0b4611e890 feat(observer): plan-11 output-fidelity contract — classify, recover, verify
- 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>
2026-05-28 17:16:08 -07:00
Alex Newman
a8cf671648 fix(tests+pipeline): repair test suite (46->7 fails) + plan-09 schema self-healing
Test-suite repair:
- Fix cross-suite mock.module contamination: polluting files (worker-api-endpoints,
  hook-execution-e2e, file-context, chroma-mcp-manager-*, summarize-*) now snapshot
  and restore real modules in afterAll, making the suite order-independent (17 fails).
- Align stale tests to current contracts (parser {valid,observations[],summary} API,
  worker-spawner WorkerStartResult, welcome-card v3 key, haiku default model, .mcp.json
  removal, claude-md mock).

Product fixes:
- plan-09: SchemaRepair.ts + Database.openWithSchemaRepair() — self-heal malformed
  on-disk schema (orphaned index / dropped column) via sqlite3 .recover before PRAGMA,
  preserving data. Migrations re-establish canonical schema. (#2433 groundwork)
- Welcome-hint flag now reads env before cached settings (env always wins).
- Replace console.* with logger.* in background services (plugin-state, project-filter,
  worktree, smart-file-read/parser, CorpusRoutes).

Remaining 7 failures are openclaw Observation I/O tests, addressed in plan-08.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 16:44:37 -07:00
Alex Newman
7a6eaca565 fix(build): plan-10 artifact hygiene — green typecheck, drop better-auth from worker, CI gate
- Fix 24 TS errors (logger Component union, Express 5 query params, clack
  spinner.stop arity, React 19 ref typing, dead SdkSessionRecord import). #2538
- Externalize better-auth from worker bundle (3.0MB -> 2.29MB). #2584
- Add worker-bundle size guard in build-hooks.js + .github/workflows/ci.yml
  running typecheck/build/test as required checks. #2570 #2538
- Tighten npm files allowlist / .npmignore so maintainer CLAUDE.md never ships. #2537

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 16:15:20 -07:00
Alex Newman
e4ec654355 chore: bump version to 13.3.0 2026-05-21 03:25:35 -07:00
Alex Newman
f1b73a46da chore: bump version to 13.2.0 2026-05-11 18:41:25 -07:00