25 Commits

Author SHA1 Message Date
Alex Newman
108c5c65a1 Merge remote-tracking branch 'origin/main' into mutual-aardvark
# Conflicts:
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/transcript-watcher.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/server/routes/v1/ServerV1PostgresRoutes.ts
#	src/server/runtime/ServerService.ts
#	src/server/runtime/create-server-service.ts
#	src/server/runtime/types.ts
#	tests/server/server-service.test.ts
2026-06-29 15:10:51 -07:00
Alex Newman
4557920565 Merge remote-tracking branch 'origin/main' into mutual-aardvark
# Conflicts:
#	package.json
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/server/runtime/create-server-service.ts
#	tests/sdk/parse-summary.test.ts
#	tests/sdk/parser.test.ts
2026-06-27 21:20:51 -07:00
Alex Newman
3fe0725a97 chore: bump version to 13.8.1 2026-06-24 16:34:28 -07:00
Alex Newman
ebe6133089 feat(telemetry): carry observation volume on rollups so cache-value survives migration (#3017)
* feat(telemetry): carry observation volume on rollups so cache-value survives migration

The context-cache-value, per-user-savings, and observation-type-by-model
metrics were derivable only from the legacy per-occurrence events
(context_injected, session_compressed), which decay to zero as the fleet
upgrades to 13.7.0 and switches to the rollups. The rollups already received
the underlying records — they just didn't aggregate the observation fields.

- observer_turn_rollup: add observations_created (Σ per-turn observation count,
  distinct from the rollup's turn `count`) + summed obs_type_* buckets, so
  cost-per-observation (total_cost_usd / observations_created) and
  observation-type-by-top_model are derivable from the rollup alone.
- context_injected_rollup: add total_observations_injected (cache-reuse count)
  + total_tokens_saved_vs_naive (windowed savings sum).
- scrub.ts: whitelist the three new emitted keys (obs_type_* already allowed;
  deny-by-default whitelist would drop them otherwise).
- docs: correct the rollup field tables — the prior context_injected_rollup row
  documented fields the code never actually emitted.
- tests: assert both new aggregations (167 telemetry tests pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEZpnYz9z4TjKcG19qHFrJ

* build(telemetry): regenerate plugin bundles for rollup observation fields

worker-service.cjs and transcript-watcher.cjs rebuilt via `npm run build` to
bundle the new observation aggregation. Incidental, telemetry-unrelated churn
in the other service/UI bundles was left out to keep the diff meaningful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEZpnYz9z4TjKcG19qHFrJ

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:31:35 -07:00
Alex Newman
edc5cf7d59 Ponytail audit (−10.4k lines) + worker-restart hardening + deterministic dependency closure (#3021)
* 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>

* fix(sdk): stale Claude CLI can no longer silently kill every observation

findClaudeExecutable validated candidates with --version only, so an
abandoned old install shadowing a current CLI in PATH (e.g. npm-global
2.0.42 next to the auto-updating native installer) passed validation,
then died at every Observer spawn: the SDK passes --permission-mode
dontAsk (hardened-options) which old CLIs reject with exit 1. Result:
healthy worker, zero observations, no visible error (#2782 family;
previously #1857/#2049/#1866/#2142 in the same class).

- Probe every candidate (which -a + known install paths) with
  `--permission-mode dontAsk --version`: one spawn proves both flag
  compatibility and version, no API call (~150ms)
- Prefer the newest capable version; PATH order only breaks ties
- Explicit CLAUDE_CODE_PATH still wins but fails loud with version and
  remedy when too old, instead of dying silently at spawn
- All-too-old throws an error naming each candidate, its version, and
  how to fix; resolution success logs at INFO with the chosen version
- Cache successful resolution 15 min (resolver runs per SDK query);
  never cache failure so a CLI update is picked up without restart
- SDK child keeps a 2KB stderr tail and includes it in the exit WARN,
  so "unknown option" deaths are diagnosable at default log level

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(audit): add ponytail audit master plan + SQLite removal companion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-0): remove commit-hash fabrication verification

Removes the commit-verification system whose second-system effects (greedy hex
regex false-positives + compensating scaffolding) cost more than the edge case
it caught. Deletes src/sdk/commit-verification.ts + its 2 tests, the
verify-before-persist block + stripFabricatedHashesFromSummary in
ResponseProcessor, and the now-dead fabricated_count/fabrication_* telemetry
in buffer.ts/scrub.ts/npx-cli telemetry.ts.

Consequence: PostHog Fabrication Rate tile goes dark (no other producer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-1): remove duplicate SQLite stack, test the shipping path

Two parallel SQLite systems lived in src/services/sqlite/. The worker only ever
used SessionStore (which runs its own inline migrations); the second stack —
ClaudeMemDatabase/MigrationRunner + a free-function CRUD API — was reachable only
from tests. This removes the dead duplicate and rewrites its tests against the
real SessionStore path.

Deleted: Database.ts, migrations/runner.ts, index.ts, SchemaRepair.ts, all 6 CRUD
barrels, transactions.ts, and the sessions/summaries/prompts/timeline/import
submodules (18 files). Trimmed the 5 survivor leaf functions to their single live
export. Removed dead SessionStore methods getSessionSummaryById (broken) and
storeObservationsAndMarkComplete (0 callers).

Replaced 9 old test files (coupled to the dead stack) with 7 focused
session-store-*.test.ts suites + a rewritten cleanup-v12_4_3 test, all exercising
the worker's real path.

Net ~-3,262 src lines. tsc clean; full suite 2253 pass / 0 fail; dead-reference
sweep zero. No production behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-6): drop dead storage methods, dedupe EnvManager, stdlib swaps

Dead-code deletes (all zero-caller verified): sqlite ProjectsRepository.upsert +
getByRootPath, ServerSessionsRepository.getByMemorySessionId,
MemoryItemsRepository.getByLegacyObservationId, pg AgentEventsRepository.createMany
+ TeamsRepository.getMember; shared getWorkerSocketPath, createBackupFilename, and
the deprecated ENV_FILE_PATH const.

Refactors (behavior-preserving): EnvManager's three open-coded 5-key credential
blocks collapse to one CREDENTIAL_KEYS loop (whitelist semantics kept, no
Object.assign); parseEnvFile line-parser → util.parseEnv; fetchWithTimeout's manual
setTimeout race → AbortSignal.timeout (caller timeout-message contract preserved).

Net ~-106 lines. tsc clean; full suite 2253 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-4): sweep dead/redundant code from src/server

Deletes (zero-caller verified): jobs/outbox.ts (canonical path is
IngestEventsService/EndSessionService) + its test; the src/server/mcp/ surface
(live MCP server is src/servers/mcp-server.ts) + its test; the always-inert
ServerBetaProviderRegistry/EventBroadcaster boundaries (no Active variant exists)
+ their /v1/info payload keys; the unused isServerClassified guard; dead option
sessionDebounceWindowMs (route + IngestEventsService).

Dedupe: hoist the 5 byte-identical auth helpers shared by auth.ts/postgres-auth.ts
into request-auth-helpers.ts; collapse resolveSummaryQueue/resolveEventQueue into
one resolveQueue(lane).

Kept (plan premise didn't verify): the ServerV1Routes runtime? option — a live
smoke test sets and asserts it. Disabled* boundary subclasses kept (real Active
counterparts chosen at runtime).

Net ~-570 source lines. tsc clean; full suite 2242 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-7): dead-code sweep + dedup across services + npx-cli

Deletes (zero-caller verified, tests removed in lockstep): ProcessManager
getChildProcesses + parseElapsedTime (ps-reaper leftovers), ModeManager
getObservationConcepts/validateType/getTypeLabel, AgentFormatter
renderAgentFileHeader/renderAgentColumnKey/renderAgentContextIndex (return-[] inert,
HeaderRenderer arms collapsed to Human-only), asyncHandler export,
PriorMessages.userMessage (always ''), and 3 dead barrels (services/context,
services/server, services/server/Middleware).

Dedup: shared toError() helper (src/utils/to-error.ts) across 8 in-scope sites;
asMs hoisted to telemetry/common; spawnPlugin() folds 4 spawnHidden blocks;
countObserverSessionRows extracts the CleanupV12_4_3 count trio; IS_WINDOWS imported
from paths.ts (was redeclared 3x); detectOsVersion inlined to os.release().

Kept (plan premise didn't verify): bun-resolver's spawnSync (npx-cli runs under
Node — Bun.which would ReferenceError); context-generator.ts (a build entrypoint +
test mock seam) — barrel still removed; Middleware.ts consumer Server.ts repointed
to the real middleware.

Net ~-279 lines. tsc clean; full suite 2232 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-5): delete orphans + dead UI/util code across cli/supervisor/ui

Orphan deletes (zero-caller verified): src/bin/ (import-xml-observations,
cleanup-duplicates — not wired to any entrypoint), src/adapters/ (abandoned parallel
impl; real adapters are src/cli/adapters/), core/schemas/context-pack.ts +
core/schemas/index.ts barrel. Companion tests removed in lockstep.

Dead functions: 7 cursor-utils helpers (kept the 6 live registry/context writers),
logger correlationId/sessionId/timing.

YAGNI trims: gemini-cli metadata block (never read), useStats collapsed to
fire-and-forget (+ dead Stats/WorkerStats/DatabaseStats types), useTheme
resolvedTheme, useContextPreview.refresh from the public result; env-sanitizer
ENV_PROXY_VARS folded into ENV_EXACT_MATCHES.

Dedup: useGitHubStars + formatNumber inlined into their single consumer
GitHubStarsButton (kept lowercase-k display — matches GitHub, Intl would uppercase);
waitForExit extracted in process-registry and shared with shutdown's reapSession
(placed low to avoid a circular import).

Net ~-1085 lines (incl. test deletions). tsc + viewer typecheck clean;
full suite 2181 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-2a): integrations/sync dead exports + scoped Chroma cleanup

Dead-export deletes (zero-caller verified): syncContextToAgentsMd,
updateWindsurfContextForProject, handleWindsurfCommand, detectClaudeCode (+ orphaned
exec/promisify imports), getScriptExtension/detectPlatform, getVersionCheckAbsolutePath
(+ companion plugin-distribution test edit). ChromaSyncState flush/resetCache/dirty
(bump/replace persist eagerly, so dirty was always false).

Scoped to the Chroma write path only (SQLite/search/telemetry copies stay live):
dropped the created_at ISO field (kept created_at_epoch, the field actually read back)
and the discovery_tokens param/field from ChromaSync's local Stored interfaces + the
3 live call sites.

Stdlib/shrink: parseSemver/compareSemver → localeCompare numeric (ordering verified);
OpenClaw roots hoisted to one const; bootstrapWatermarks max-loop → Math.max;
Goose YAML builders collapsed to one withHeader flag.

Defers the 5-installer consolidation to a separate pass. Net ~-187 lines.
tsc clean; full suite 2181 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-2b): delete dead fetchAndInjectOpenCodeContext

Orphaned when slice-2a removed syncContextToAgentsMd (its only caller chain).
Confirmed zero callers. Kept fetchRealContextFromWorker (live at :323).

Per feasibility analysis, the plan's 5-installer table-driven consolidation
(headline -2400) was descoped: the 5 installers write 5 different config schemas
and 4 different context formats with thin test coverage, so a 5->1 collapse would
relocate divergence into callbacks and risk untested IDE-config write paths for a
line-count-only win. Plan gate ('only if behavior genuinely identical') not met.

tsc clean; full suite 2181 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(slice-3): worker dead-code sweep + provider/search consolidation

Search pipeline: delete the unreachable half built speculatively (TimelineBuilder,
ResultFormatter instance methods [kept the one live static], search/filters/,
SearchStrategy.canHandle/name + BaseSearchStrategy, HybridSearchStrategy no-op
search(), dead SearchOrchestrator wrappers, ChromaQueryResult) + companion tests.
Dedup the 5 genuinely-identical chroma->recency->hydrate->FTS paths into one
hybridSemanticHydrate helper and the 3 timeline renderers into renderTimeline()
(search()/decisions()/changes()/howItWorks() left as-is — different shape).

Providers: extract OpenAICompatibleProvider base from the Gemini/OpenRouter twins,
preserving every per-provider divergence via flags/abstracts (truncation guard,
token estimation, empty-response handling, endpointClass, the Gemini-only RPM
throttle). Dedup parseRetryAfterMs into retry.ts.

http/agents/session: delete the IngestEventBus machinery + ingestPrompt, the
read-only POST /api/processing dup, FallbackErrorHandler.shouldFallbackToClaude,
RateLimitStore.getAll/clear, SessionCompletionHandler.completeByDbId,
SessionCleanupHelper, 3 dead agent types, and no-op empty-passthrough validateBody;
collapse the 6 CorpusRoutes 404 blocks into one helper.

Kept (false-dead, caller verified): SSEEventPayload (live via WorkerRef), RateLimitStore.get
(live via shouldAbortForQuota). Both adversarial reviews confirmed behavior preserved.

Net ~-2,640 lines. tsc clean; full suite 2080 pass / 0 fail; dead-ref sweep zero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(plugin): regenerate distribution artifacts after ponytail audit

Rebuilds the worker/mcp/server-beta/transcript-watcher/context-generator bundles
and the viewer UI from the post-audit source (Slices 0-7).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(env): require Node >=20.12.0 to match util.parseEnv usage

EnvManager adopted stdlib util.parseEnv (added in Node 20.12.0) during
the ponytail audit, but engines.node still advertised >=20.0.0. On Node
20.0-20.11 parseEnv is undefined: loadClaudeMemEnv() silently returns {}
(credentials never load) and saveClaudeMemEnv() throws. Declare the real
floor in both the npm package and the generated plugin manifest (via its
generator in build-hooks.js so rebuilds stay consistent).

Greptile PR #3021 P1.

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-21 13:12:41 -07:00
Alex Newman
e5fc0a2495 docs(plans): add cmem-sdk ship/release plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:49:31 -07:00
Alex Newman
a600b89d72 feat(telemetry): backfill historical token-savings economics (#2934)
* feat(telemetry): backfill historical token-savings economics

The 8-month historical_activity backfill rolled up observation counts but
never the token economics, so tokens_saved_vs_naive only existed for the
~6 days since context_injected went live (Jun 9). This adds the historical
counterpart using the SAME formula live uses:

  read_tokens           = sum(ceil(len(observations.text)/CHARS_PER_TOKEN_ESTIMATE))
  tokens_saved_vs_naive = max(0, discovery_tokens - read_tokens)  per UTC day

Both inputs are already persisted in SQLite (session_summaries.discovery_tokens
and observations.text), so the savings series now extends across the full
backfill window instead of starting at Jun 9. Conservative by construction
(once-per-observation read cost, not a replay of real injections) and flagged
backfilled:true.

Generation-side cost (cost_usd/tokens_input/tokens_output) is NOT recoverable
here — it was never written to SQLite — so it is intentionally excluded; see
plans/generation-cost-persistence.md for the forward-only fix.

- backfill.ts: read_tokens rollup + per-day savings derivation, CHARS_PER_TOKEN_ESTIMATE import
- scrub.ts: whitelist read_tokens
- tests: fixture gains observations.text; +2 cases (ceil math + floor-at-0)
- plans/: spec for the generation-cost persistence follow-up

https://claude.ai/code/session_01YWJPckEtd2sLAtng39rasu

* fix(telemetry): version the backfill marker so enriched rollup reaches existing installs

The historical backfill was gated solely on the marker file existing, so
every install that already backfilled under the prior version (#2912) would
hit the one-shot return and never receive the new read_tokens /
tokens_saved_vs_naive economics — the enriched series would only ever reach
fresh installs.

Add BACKFILL_VERSION to the marker. isBackfillComplete() now treats a marker
written by an older version (or a legacy marker with no version field, i.e.
version 1) as incomplete, so already-backfilled installs re-run and pick up
the enriched keys. The re-run is idempotent and does not double count: every
event keeps its deterministic per-(installId, event, day) uuid, so PostHog's
historical-migration dedup replaces each event in place rather than appending
a second row.

- backfill.ts: BACKFILL_VERSION constant, version on BackfillMarker, version-aware
  gate, stamp version at both marker-write sites
- tests: current-version marker skips; legacy (versionless) and older-version
  markers re-run and rewrite at the current version; assert version stamped

https://claude.ai/code/session_01YWJPckEtd2sLAtng39rasu

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 00:00:46 -07:00
Alex Newman
ccd907dc58 feat(telemetry): PostHog historical backfill — anonymized daily rollups + inferred install date (#2912)
* feat(telemetry): add PostHog historical backfill module (Phase 1)

One-time anonymized daily-rollup backfill: collectDailyRollups with pinned
aggregation semantics, sessions-first install-day inference, deterministic
v5 event uuids, 9-step gated transport (marker/consent/debug gates,
historicalMigration client, marker only on clean shutdown). Whitelist
additions in scrub.ts/common.ts, extended PostHog test mock, full (a)-(k)
test coverage. Per plans/2026-06-11-posthog-historical-backfill.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(telemetry): wire historical backfill into worker startup + docs disclosure

Fire-and-forget runHistoricalBackfill call in initializeBackground after
the worker_started capture (non-blocking, marker-gated one-shot, retries
on next start if delivery fails). New "Historical backfill" section in
docs/public/telemetry.mdx documenting what is sent, the once-per-install
marker, identical consent gates, and upload-time geo caveat. Phases 3-4
of plans/2026-06-11-posthog-historical-backfill.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(telemetry): document dual-layer error handling on backfill call site

runHistoricalBackfill never rejects by contract; the .catch is the
unhandled-rejection backstop for the fire-and-forget call. Addresses
Greptile review on #2912.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:10:39 -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
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
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
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
6259b20c90 chore: bump version to 13.4.1 2026-06-07 18:45:06 -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
b2b6f1482e docs(plans): cmem-sdk — mark Chroma REQUIRED, not optional
Per user correction (2026-05-29): claude-mem without semantic search
is broken. The original plan framed Chroma as "(optional)" in the
architecture diagram, listed `chroma?: boolean | ChromaOptions` as a
disable toggle in `createCmemClient` options, and described Phase 6
search as "FTS if Chroma disabled / Chroma if enabled" — all of which
treats Chroma as a feature flag.

Updated framing:

- Executive Decision: Chroma is required. `createCmemClient` REJECTS
  if `uvx chroma-mcp` cannot start.
- Phase 3 options: `chroma?: ChromaOptions` (tuning only, no
  enabled-false). Construction includes `chromaSync.ensureReady()`.
- Phase 6: Chroma is the default primary search path. The Postgres FTS
  branch is preserved only as a runtime-failure safety net (mirror of
  `SearchManager.ts:255`), surfaces `{ degraded: true }`, and emits a
  `logger.error('CHROMA', …)` so the broken state is visible.
- Phase 6 verification: `createCmemClient` with chroma-mcp unavailable
  MUST reject. Killing chroma-mcp mid-session yields `{ degraded: true }`
  with a logged error; a subsequent cold-start `createCmemClient`
  rejects again.
- Phase 6 anti-patterns: no `chroma.enabled = false` option.
- Doc-references table: the worker's `CLAUDE_MEM_CHROMA_ENABLED` env
  gate is documented as a worker-side footgun that the SDK deliberately
  does not honor.
- Correction log added at the bottom of the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:24:46 -07:00
Alex Newman
166b956e88 Merge remote-tracking branch 'origin/main' into mutual-aardvark
# Conflicts:
#	package.json
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/npx-cli/commands/install.ts
#	src/server/runtime/ServerService.ts
#	src/server/runtime/create-server-service.ts
#	src/services/worker-service.ts
#	tests/hooks/server-client.test.ts
2026-05-29 14:22:31 -07:00
Alex Newman
dcfee09cb5 docs(plans): add cmem-sdk + server rename implementation plan
9-phase plan for the cmem-sdk package (embeddable claude-mem I/O on
Postgres runtime) and the foundational `server-beta` → `server` rename.

Phase 1 (rename) is independently shippable and fixes the silent
runtime regression in `runtime-selector.ts` where only the literal
`server-beta` was accepted as the runtime value. Phases 2-9 ship the
SDK on top.

Plan: plans/2026-05-25-cmem-sdk-and-server-rename.md
Deck: plans/2026-05-25-cmem-sdk-and-server-rename-slides.pdf

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:34:18 -07:00
Alex Newman
4b9894acce docs: add plan-12 provider & extensibility roadmap (routes remaining feature requests)
Consolidates the surviving net-new feature requests (#2522 Vertex, #2690 backfill,
#2498 incremental scan, #2463 tool_response filter, #2423 per-directory disable,
#2566 introspection/telemetry, #2513 logger audit policy, #2645 i18n, #2467 Read
semantics) into one tracked master, separating planned roadmap from the now-cleared
defect backlog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:49:27 -07:00
Alex Newman
c46563c68d docs: add plan-07..11 architectural fix plans
Plan masters #2685-#2689 covering server runtime GA, OpenCode integration,
data-pipeline integrity, build/artifact hygiene, and observer output fidelity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 16:14:51 -07:00
Alex Newman
a10d1b342f docs(plans): add architectural plan files for issues #2376-#2381
Six numbered plan documents covering:
- 01 Hook IO Discipline (#2376)
- 02 Spawn-Contract Templating (#2377)
- 03 Worker / Daemon Lifecycle Hardening (#2378)
- 04 Installer Failure Transparency (#2379)
- 05 Observer SDK Tool Enforcement (#2380)
- 06 Worker Env Isolation (#2381)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:31:02 -07:00
Alex Newman
36b0929fae Server-beta: Postgres storage + independent runtime + BullMQ queue (Phases 1–3) (#2351)
* Add server beta runtime foundation

* Address server beta review findings

* Resolve server beta review comments

* Tighten server beta review follow-ups

* Harden server beta auth and search

* Avoid unnecessary FTS rebuilds

* Block scoped keys from creating projects

* Release BullMQ claims best effort on close

* Address server beta review blockers

* Reset BullMQ claims best effort

* Add Postgres observation storage foundation

* feat(server-beta): add independent runtime service

Introduce src/server/runtime/ as a self-contained server-beta runtime
that owns its lifecycle, Postgres bootstrap, and HTTP boundary without
depending on WorkerService.

ServerBetaService wraps the existing Server class, exposes
/healthz and /v1/info with runtime="server-beta", and persists state
to dedicated paths (.server-beta.pid|.port|.runtime.json). The four
boundary managers (queue, generation worker, provider registry, event
broadcaster) are intentionally disabled in this phase and report their
status through /v1/info; later phases activate them.

Adds plans/2026-05-07-finish-bullmq-branch-ship-plan.md to track the
remaining work for this branch.

Phase 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): route CLI lifecycle and bundle separate runtime

scripts/build-hooks.js now produces plugin/scripts/server-beta-service.cjs
as a separate Node CJS bundle, alongside the existing worker-service
bundle. The server-beta runtime is now installable independently.

src/npx-cli/commands/server.ts routes start|stop|restart|status to the
server-beta lifecycle instead of the legacy worker. The worker keeps its
own start|stop|restart|status under the worker namespace; the two
runtimes can be operated independently.

src/services/worker-service.ts adds a server-* command parser branch
that delegates to the sibling server-beta-service.cjs bundle so
direct worker-service invocations still route to the right runtime.

tests/npx-cli-server-namespace.test.ts updated to expect server-beta
lifecycle routing.

Includes rebuilt plugin/scripts/*.cjs bundles produced by
build-and-sync.

Phase 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): add BullMQ job queue primitives

Introduce src/server/jobs/ as the queue-side primitives that Phase 3 of
the server-beta runtime needs to operate.

types.ts defines a discriminated union over the four job kinds (event,
event-batch, summary, reindex) and maps each to a per-kind BullMQ queue
name and deterministic-ID prefix.

job-id.ts builds deterministic, colon-free BullMQ jobIds from
(kind, team, project, source). The colon ban exists because BullMQ uses
':' as a Redis key separator internally; embedding ':' in jobIds
breaks scan and state lookups.

ServerJobQueue.ts is a thin wrapper over BullMQ Queue + Worker that
enforces autorun:false, default concurrency 1, and an attached error
listener — all per BullMQ docs requirements. Test seams accept queue
and worker factories so unit tests do not need Redis.

outbox.ts publishes through the Postgres ObservationGenerationJob
repository as canonical history. enqueueOutbox writes the row first,
then publishes to BullMQ; if BullMQ throws, the row is transitioned to
failed and a failed event is appended. reconcileOnStartup re-enqueues
queued + processing rows after a restart, replacing terminal BullMQ
jobs that may still be holding the deterministic ID slot. markCompleted
and markFailed wrap transitionStatus and append the matching event row.

Includes 20 unit tests covering deterministic ID stability, colon-free
output, queue lifecycle, error-listener attachment, double-start
refusal, idempotent enqueue, BullMQ failure rollback, startup
reconciliation, max-attempts skipping, and completion / failure /
retry transitions.

Phase 3 commit 1 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): activate queue boundary in runtime service

Wire ActiveServerBetaQueueManager into the server-beta runtime graph.
The active manager owns one ServerJobQueue per generation kind (event,
event-batch, summary, reindex) and surfaces lane metadata through
boundary health.

Selection is opt-in and fail-fast: if CLAUDE_MEM_QUEUE_ENGINE is set to
bullmq the active manager is constructed (and any Redis/config error
throws — no silent fallback to SQLite, per Phase 3 anti-pattern guard).
For any other engine the disabled boundary remains so worker-era and
test setups stay compatible.

Widens ServerBetaBoundaryHealth.status to a discriminated union
('disabled' | 'active' | 'errored') with optional details. The disabled
adapter still emits status='disabled', which keeps the existing
server-beta-service test green.

ServerBetaService receives the manager through a new optional
queueManager field on CreateServerBetaServiceOptions so test graphs
and Phase 4 wiring can inject custom managers.

Adds tests/server/runtime/active-queue-manager.test.ts covering bullmq
guard, active health shape, per-kind queue access, close behavior, and
post-close errored health.

Phase 3 commit 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): cap /v1/events/batch at 500 events

Prevents unbounded array DoS surface flagged in PR review.

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-05-08 01:20:07 -07:00
Alex Newman
a2872dabfa docs: plan Codex plugin version mismatch fix 2026-05-06 19:08:12 -07:00
Alex Newman
9902a15b21 Remove stale internal docs and reports (#2290) 2026-05-04 20:22:29 -07:00
Alex Newman
9e2973059a UX redesign: installer + provider rename + /learn-codebase + welcome card + SessionStart hint (#2255)
* feat(ux): claude-mem UX improvements with installer enhancements

Squashed PR #2156 commits for clean rebase onto main:
- feat(installer): add provider selection, model prompt, worker auto-start
- refactor: rename *Agent provider classes to *Provider
- feat: add /learn-codebase skill and viewer welcome card
- feat(worker): inject welcome hint when project has zero observations
- fix(pr-2156): address greptile review comments
- fix(pr-2156): address coderabbit review comments
- fix(pr-2156): persist CLAUDE_MEM_PROVIDER for non-claude in non-TTY mode
- fix(pr-2156): file-backed settings reads in installer + env-first SKILL doc

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: rebuild plugin artifacts after rebase onto v12.4.7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): strip claude-mem internals from learn-codebase

The learn-codebase skill, install next-step copy, WelcomeCard, and
welcome-hint previously walked the primary agent through worker endpoints
and synthetic observation payloads. The PostToolUse hook already captures
every Read/Edit the agent makes — the agent should have no awareness that
the memory layer exists. Collapse the skill to one instruction ("read every
source file in full") and rephrase touchpoints to describe only what the
user observes (Claude reading files), not what happens behind the scenes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): preflight version mismatch + settings-aware port resolution

Two related fixes for build-and-sync's worker restart step:

1. Read CLAUDE_MEM_WORKER_PORT from ~/.claude-mem/settings.json the same
   way the worker does, instead of computing the default port from the
   uid alone. Previously, users with a custom port saw a misleading
   "Worker not running" message because the restart POST hit the wrong
   port and got ECONNREFUSED.

2. Add a preflight check that aborts the sync when the running worker's
   reported version does not match the version we are about to build.
   Claude Code's plugin loader pins the worker to a specific cache
   version per session, so syncing into a newer cache directory has no
   effect until the user runs `claude plugin update thedotmack/claude-mem`
   to bump the pin. The preflight surfaces this explicitly with the exact
   command to run; --force bypasses it for intentional cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(learn-codebase): note sed for partial reads of large files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: strip comments codebase-wide

Removed prose comments from all tracked source. Preserved directives
(@ts-ignore, eslint-disable, biome-ignore, prettier-ignore, triple-slash
references, webpack magic, shebangs). Deleted two tests that asserted
on comment text rather than runtime behavior.

Net: 401 files, -14,587 / +389 lines, -10.4% bytes.

Verified: typecheck passes, build passes, test count unchanged from
baseline (22 pre-existing fails, all unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(installer): move runtime setup into npx, eliminate hook dead air

Smart-install ran 3 times during a fresh install — the worst run was silent,
fired by Claude Code's Setup hook after `claude plugin install`, producing
~30s of dead air that looked like the plugin was hung.

This change makes `npx claude-mem install` the single place heavy work
happens, with a visible spinner. Hooks become runtime-only.

- New `src/npx-cli/install/setup-runtime.ts` module: ensureBun, ensureUv,
  installPluginDependencies, read/writeInstallMarker, isInstallCurrent.
  Marker schema preserved exactly ({version, bun, uv, installedAt}) so
  ContextBuilder and BranchManager readers keep working.
- `npx claude-mem install`: ungated copy/register/enable for every IDE,
  inserts a "Setting up runtime" task with honest "first install can take
  ~30s" spinner. The claude-code shell-out to `claude plugin install` is
  removed — npx already populated everything Claude reads.
- New `npx claude-mem repair` command for post-`claude plugin update`
  recovery, force-reinstalls runtime.
- Setup hook now runs `plugin/scripts/version-check.js` (29ms wall) instead
  of smart-install. Mismatch prints "run: npx claude-mem repair" on stderr.
  Always exits 0 (non-blocking, per CLAUDE.md exit-code strategy).
- SessionStart loses the smart-install entry; 2 hooks remain (worker start,
  context fetch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(installer): delete smart-install sources, retarget tests

- Delete scripts/smart-install.js + plugin/scripts/smart-install.js (both
  are source files kept in sync manually; both must go).
- Delete tests/smart-install.test.ts (covered surface is gone).
- tests/plugin-scripts-line-endings: drop smart-install.js entry.
- tests/infrastructure/plugin-distribution: retarget two assertions at
  version-check.js (the new Setup hook script).
- New tests/setup-runtime.test.ts: 9 tests covering marker read/write,
  isInstallCurrent semantics. Marker schema invariant verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(installer): describe npx-driven setup + version-check Setup hook

Sweep public docs and architecture notes to reflect the new flow:
npx installer does Bun/uv setup with a visible spinner; Setup hook runs
sub-100ms version-check.js; users hit `npx claude-mem repair` after a
`claude plugin update`.

- docs/architecture-overview.md: hook lifecycle table + npx flow paragraph
- docs/public/configuration.mdx: tree + hook config example
- docs/public/development.mdx: build output line
- docs/public/hooks-architecture.mdx: full rewrite of pre-hook section,
  timing table, performance table
- docs/public/architecture/{overview,hooks,worker-service}.mdx: tree
  comments, JSON config example, Bun requirement section

docs/reports/* untouched (historical incident reports).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): mergeSettings writes via USER_SETTINGS_PATH

Greptile P1 (#2156): `settingsFilePath()` only resolved
`process.env.CLAUDE_MEM_DATA_DIR`, while `getSetting()` reads via
`USER_SETTINGS_PATH` which `resolveDataDir()` populates from BOTH the env
var AND a `CLAUDE_MEM_DATA_DIR` entry persisted in
`~/.claude-mem/settings.json`. Result: a user with the data dir saved in
settings.json but not exported in their shell would have provider/model
settings silently written to `~/.claude-mem/settings.json` while
`getSetting()` read from `/custom/path/settings.json` — read/write split.

Drop `settingsFilePath()` and the now-unused `homedir` import; reuse the
already-imported `USER_SETTINGS_PATH` constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): parse --provider, --model, --no-auto-start install flags

Greptile P1 (#2156): InstallOptions has fields `provider`, `model`,
`noAutoStart`, but the install case in the npx-cli switch only parsed
`--ide`. The other three flags were silently dropped — `npx claude-mem
install --provider gemini` was a no-op.

Extract a `parseInstallOptions(argv)` helper, share it between the bare
`npx claude-mem` and `npx claude-mem install` paths, and validate
`--provider` against the allowed set. Update help text accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): pipe runtime-setup output, always show IDE multiselect

Two issues caught in a docker test of the installer:

1. The bun.sh installer, uv installer, and `bun install` were using
   stdio: 'inherit', dumping their stdout/stderr through clack's spinner
   region — visible as raw "downloading uv 0.11.8…" / "Checked 58
   installs across 38 packages…" text streaming under the spinner. Switch
   to stdio: 'pipe' and surface captured stderr only on failure (via a
   shared describeExecError() helper that includes stdout when stderr is
   empty). Spinner stays clean on the happy path.

2. promptForIDESelection() silently picked claude-code when no IDEs were
   detected, never showing the user the multiselect. On a fresh machine
   with no IDEs present yet (e.g. our docker test container), the user
   never got to choose. Now: always show the full IDE list when
   interactive; mark detected ones with [detected] hints and pre-select
   them; show a warn line if zero are detected explaining they should pick
   what they plan to use. Non-TTY callers still get the silent
   claude-code default at the call site (unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): skip marketplace work for claude-code-only, offer to install Claude Code

Two related UX fixes from a docker test:

**Delay between "Saved Claude model=…" and "Plugin files copied OK"**

After dropping the needsManualInstall gate, every install was unconditionally
running `copyPluginToMarketplace` (which copied the entire root node_modules
tree — thousands of files, dozens of seconds) and `runNpmInstallInMarketplace`
(npm install --production) even when only claude-code was selected. Neither
is needed for claude-code: that path uses the plugin cache dir + the
installed_plugins.json + enabledPlugins flag, all of which we already write.

- Drop `node_modules` from `copyPluginToMarketplace`'s allowed-entries list;
  the dependency-install task populates it on the destination side anyway.
- Re-introduce `needsMarketplace = selectedIDEs.some(id => id !== 'claude-code')`
  scoped *only* to `copyPluginToMarketplace`, `runNpmInstallInMarketplace`,
  and the pre-install `shutdownWorkerAndWait` (also pointless for claude-code-
  only flows since we're not overwriting the worker's running cache dir
  source). All other tasks (cache copy, register, enable, runtime setup) stay
  unconditional.

**Claude Code missing → silent install of an IDE that isn't there**

When the user picked claude-code on a machine without it (e.g. a fresh
container), the install completed but `claude` was unavailable and the only
hint was a generic warn line. Replace with an explicit pre-flight prompt:

  Claude Code is not installed. Claude-mem works best in Claude Code, but
  also works with the IDEs below.
  ? Install Claude Code now?
    ◆ Yes — install Claude Code (recommended)
    ◯ No — pick another IDE below
    ◯ Cancel installation

If the user picks "Yes", run `curl -fsSL https://claude.ai/install.sh | bash`
(or the PowerShell equivalent on Windows), then re-detect IDEs and proceed
with claude-code pre-selected. If the install fails or the user picks "No",
the multiselect still appears with claude-code visible (just unmarked
[detected]), so they can opt in or pick another IDE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): detect Claude Code via `claude` CLI, not ~/.claude dir

The directory `~/.claude` can exist (e.g. mounted in Docker, or created
by tooling) without Claude Code actually being installed. Detect the
`claude` command in PATH instead so the installer correctly offers to
install Claude Code when missing.

* docs(learn-codebase): add reviewer note explaining the cost tradeoff

The skill intentionally reads every file in full to build a cognitive
cache that pays off across the rest of the project. Add a brief note
so reviewers (human or bot) understand the tradeoff before flagging
the unbounded read as a cost issue.

* fix: address Greptile P1 feedback on welcome hint and learn-codebase

- SearchRoutes: skip welcome hint when caller passes ?full=true so
  explicit full-context requests aren't intercepted by the hint.
- learn-codebase: replace `sed` instruction with the Read tool's
  offset/limit parameters, since Bash is gated in Claude Code by
  default.

* feat(install): ASCII-animated logo splash on interactive install

Plays a ~1s bloom animation of the claude-mem sunburst logomark when
the installer starts in an interactive terminal — geometrically rendered
via 12 ray curves around a center disc, in the brand orange. The
wordmark and tagline type on alongside the final frame.

Auto-skipped on non-TTY, in CI, when NO_COLOR or CLAUDE_MEM_NO_BANNER
is set, or when the terminal is too narrow.

Inspired by ghostty +boo.

* feat(banner): replace rotation frames with angular-sector bloom generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): replace rotation frames with angular-sector bloom generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): three-act choreography renderer with radial gradient and diff redraw

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): update preview script to support small/medium/hero tier selection

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): add COLORTERM=truecolor to test-installer sandbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): auto-apply PATH for Claude Code with spinner UX

The Claude Code install.sh prints a Setup notes block telling users to
manually edit "your shell config file" to add ~/.local/bin to PATH —
which left fresh installs unable to launch claude from the command line.

After a successful install, detect ~/.local/bin/claude on disk and, if
the dir is missing from PATH, append the right export line to .zshrc /
.bash_profile / .bashrc / fish config (idempotent, marked with a
comment). Also updates process.env.PATH for the current install run.

Wraps the curl|bash install in a clack spinner (interactive only) so the
~4 minute native-build download doesn't look frozen — output is captured
silently and dumped on failure for debuggability. Non-interactive mode
keeps inherited stdio for CI logs.

Verified end-to-end in the test-installer docker sandbox: spinner
animates, .bashrc gets the export, fresh login shell resolves claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): video-frame ASCII renderer with three-act choreography

Generator switched from a single Jimp-rendered logo to pre-extracted
video frames concatenated with \x01 separators and gzip-deflated, ported
from ghostty's boo wire format. Renderer rewritten around three acts
(ignite → stagger bloom → text reveal + breathe) with adaptive sizing,
radial gradient, and diff-based redraw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(onboarding): unify install / SessionStart / viewer around one first-success moment

Three surfaces now point at the same north-star moment — open the viewer, do
anything in Claude Code, watch an observation appear within seconds — with the
same verbatim timing and privacy lines, and a single canonical "how it works"
explainer instead of three diverging copies.

- Canonical explainer at src/services/worker/onboarding-explainer.md served via
  GET /api/onboarding/explainer; mirrored into plugin/skills/how-it-works/SKILL.md
- SessionStart welcome hint rewritten as third-person status (no imperatives
  Claude tries to execute), pinned with a default-value regression test
- Post-install Next Steps reframed as "two paths": passive default + optional
  /learn-codebase front-load; drops /mem-search and /knowledge-agent from this
  surface; adds verbatim timing + privacy lines and /how-it-works link
- /api/stats response gains firstObservationAt for the viewer stat row
- Viewer WelcomeCard branches on observationCount === 0: empty state shows live
  worker-connection dot + "waiting for activity"; has-data state shows
  observations · projects · since [date] and two example prompts. v2 dismiss key
- jimp added to package.json to fix pre-existing banner-frame build break

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(banner): play unconditionally; only honor CLAUDE_MEM_NO_BANNER

The 128-col / TTY / CI / NO_COLOR gates silently swallowed the banner in
narrower terminals, CI logs, and any non-TTY pipe — including Docker runs
where -it should preserve the experience but column width was the wrong
gate. Remove the implicit gates; keep the explicit opt-out only.

If a frame wraps in a narrow terminal, that's better than the banner
not playing at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(banner): restore 15:33 gating logic per user request

Reverts eb6fc157. Restores isBannerEnabled to the state at commit
8e448015 (2026-04-30 15:33): TTY check, !CI, !NO_COLOR, !CLAUDE_MEM_NO_BANNER,
and cols >= BANNER.width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): wrap remaining slow steps with spinners

Each IDE installer (Cursor, Gemini CLI, OpenCode, Windsurf, OpenClaw,
Codex CLI, MCP integrations) now runs inside a clack task spinner with
per-step progress messages instead of silent dynamic-import + cpSync.
Pre-overwrite worker shutdown (up to 10s) and the post-install health
probe (up to 3s) also get spinners.

Internal console.log/error/warn from each IDE installer is buffered
during the spinner; if the install fails, captured output is replayed
afterward via log.warn so users can see what broke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): observation count + IDE pre-selection regressions

WelcomeCard's "no observations yet" empty state was triggered when a
project filter narrowed the feed to zero rows, even with thousands of
observations elsewhere. Source the count from global stats.database
to match firstObservationAt's scope.

Restore initialValues: [] in the IDE multiselect — pre-selecting every
detected IDE was the exact regression #2106 was filed for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): trichotomy worker state + cache fallback for script path

ensureWorkerStarted now returns 'ready' | 'warming' | 'dead' instead of
boolean. The spawned-but-still-warming case (common in Docker cold
starts and slow first-time inits) was being misreported as 'did not
start', which contradicted the next-steps panel saying 'still starting
up'. Install task message and Next Steps headline now agree on the
actual state.

Also fixes the actual root cause of 'Worker did not start' on
claude-code-only installs: the worker script path was hardcoded to the
marketplace dir, which is left empty when no non-claude-code IDE is
selected. Now falls back to pluginCacheDirectory(version) when the
marketplace copy isn't present.

Verified end-to-end in docker/claude-mem with --ide claude-code,
--ide cursor, and a fresh container — install task and headline
agree on 'Worker ready at http://localhost:<port>' in all cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: align CLAUDE.md and public docs with current code

Sweep across CLAUDE.md and 10 high-traffic docs/public/ MDX files to
remove point-in-time references and align with the actual current
shape of the codebase. Highlights:

- Hardcoded port 37777 → per-user formula (37700 + uid % 100) on the
  front-door pages (introduction, installation, configuration,
  architecture/overview, architecture/worker-service, troubleshooting,
  hooks-architecture, platform-integration).
- Default model 'sonnet' → 'claude-haiku-4-5-20251001' (matches
  SettingsDefaultsManager).
- Node 18 → 20 (matches package.json engines).
- Lifecycle hook count corrected (5 events).
- Removed the nonexistent 'Smart Install' component and pre-built
  directory tree referencing files that no longer exist
  (context-hook.ts, save-hook.ts, cleanup-hook.ts, etc.); replaced
  with the real worker dispatcher shape.
- Removed CLAUDE.md '#2101' issue tag (kept the design rationale).
- Replaced obsolete hooks.json example with a description of the real
  bun-runner.js / worker-service.cjs hook event shape.

Lower-traffic doc pages still hardcode 37777 — left for a separate
global pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): land strip-comments around real parsers (postcss, remark, parse5)

Each language gets a real parser to locate comments, then we splice ranges
out of the original source. The library never serializes — that's how
remark-stringify produced 243 reformat-noise diffs in the first attempt
versus the 21 real strip targets here.

  JS/TS/JSX  -> ts.createSourceFile + getLeadingCommentRanges
  CSS/SCSS   -> postcss.parse + walkComments + node.source offsets
  MD/MDX     -> remark-parse (+ remark-mdx) + AST html / mdx-expression nodes
  HTML       -> parse5 with sourceCodeLocationInfo
  shell/py   -> kept hand-rolled hash stripper (no library worth the dep)

Preserves: shebangs, @ts-* directives, eslint-disable, biome-ignore,
prettier-ignore, triple-slash refs, webpack magic, /*! license keep,
@strip-comments-keep file marker. JS/TS handler runs a parse-roundtrip
check and refuses to write if syntax errors increased (catches the
worker-utils.ts breakage class from the 2026-04-29 attempt).

npm scripts:
  strip-comments         (apply)
  strip-comments:check   (CI-style, exits non-zero if changes needed)
  strip-comments:dry-run (list, no writes)

Verified --check on this repo: 21 changes, -4.0% bytes, no parse-error
regressions, no reformat-suspect false positives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: strip comments codebase-wide via parser-backed tool

21 files changed, -17,550 bytes (-4.0%) of narrative comments removed
across .ts / .tsx / .js / .mjs and the .gitignore. JS/TS comments stripped
via ts.createSourceFile + getLeadingCommentRanges — same canonical lexer,
same behavior as the 2026-04-29 strip, no reformat noise.

Preexisting baseline (unchanged):
  typecheck: 16 errors at HEAD, 16 errors after strip (line numbers shift,
             no new error classes — verified via diff of sorted error lists)
  build:     fails at HEAD with CrushHooksInstaller.js unresolved import
             (preexisting, unrelated to this strip)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): drop Crush integration references after extract

The Crush integration was extracted to its own branch on May 1, but the
import at install.ts:280 (and the case block + ide-detection entry +
McpIntegrations config + npx-cli help text) still referenced the now-
removed CrushHooksInstaller.js, breaking the build.

Removes:
- case 'crush' block in install.ts
- crush entry in ide-detection.ts
- CRUSH_CONFIG and registration in McpIntegrations.ts
- 'crush' from the IDE Identifiers help line in index.ts

Rebuilds worker-service.cjs to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(banner): mark generated banner-frames.ts with @strip-comments-keep

Without this, every build/strip cycle ping-pongs five lines of doc
comments in and out of the auto-generated output. The keep-marker tells
strip-comments.ts to skip the file entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): drop banner-frame regen from build script

generate-banner-frames.mjs requires PNG frames in /tmp/cmem-banner-frames
that only exist after the maintainer runs ffmpeg locally on the source
video. CI has neither the video nor the frames, so the build broke on
Windows. The output (src/npx-cli/banner-frames.ts) is committed, so the
regen is a one-shot dev step — not a build step. Run the script directly
when the video changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): unstick the spinner — kill claim-self-lock, wake on fail, auto-broadcast

Three surgical changes that cure the stuck-spinner bug at the source.

Phase 1.1 (L9): claimNextMessage no longer self-excludes its own worker_pid.
A single UPDATE-RETURNING grabs the oldest pending row by id. Removes the
LiveWorkerPidsProvider plumbing that was never injected — Supervisor enforces
single-worker via PID file, so the multi-worker SQL was defending against a
configuration the project does not support.

Phase 1.2 (L19): SessionManager.markMessageFailed wraps PendingMessageStore.markFailed
and emits 'message' on the per-session EventEmitter. The iterator's waitForMessage
now wakes immediately on re-pend instead of parking for 3 minutes. ResponseProcessor
and SessionRoutes routed through the new wrapper.

Phase 1.3 (L24): PendingMessageStore takes an optional onMutate callback fired
from every mutator (enqueue, claimNextMessage, confirmProcessed, markFailed,
transitionMessagesTo, clearFailedOlderThan). SessionManager wires it; WorkerService
passes broadcastProcessingStatus. Ten manual broadcast calls deleted across
SessionCleanupHelper, SessionEventBroadcaster, SessionRoutes, DataRoutes, and
worker-service. Caller discipline becomes structurally impossible to forget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): delete dead code — legacy routes, processPendingQueues, decorative guards

Pure deletions. Phase 2 of kill-the-asshole-gates.

- Legacy /sessions/:sessionDbId/* routes (handleSessionInit, handleObservations,
  handleSummarize, handleSessionStatus, handleSessionDelete, handleSessionComplete)
  bypassed all five ingest gates and were a parallel write path. Folded the
  initializeSession + broadcastNewPrompt + syncUserPrompt + ensureGeneratorRunning
  + broadcastSessionStarted work into the canonical /api/sessions/init handler so
  the hook makes one round trip instead of two.
- processPendingQueues (~104 lines, zero callers) — replaced in Phase 6 by a
  one-statement startup sweep.
- spawnInProgress Map and crashRecoveryScheduled Set — decorative dedupe over
  generatorPromise and stillExists checks that already provide the real safety.
- STALE_GENERATOR_THRESHOLD_MS — pre-empted live generators and raced with the
  finally block; the 3min idle timeout already kills zombies.
- MAX_SESSION_WALL_CLOCK_MS — ran a SELECT on every observation to enforce 24h.
  Runaway-spend protection lives in the API key, not in claude-mem.
- Missing-id 400 in shared.ts ingestObservation — Zod already enforces min(1)
  on contentSessionId and toolName at the route schema.
- SessionCompletionHandler import + completionHandler field on SessionRoutes
  (orphaned after handler deletions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): SQL-backed getTotalQueueDepth — single source of truth

Was: iterate this.sessions.values() and sum getPendingCount per session.
Now: SELECT COUNT(*) FROM pending_messages WHERE status IN ('pending','processing').

The in-memory sessions Map drifted from the DB rows whenever a generator exited
without confirm/fail, leading to false-positive isProcessing in the UI. Phase 1.3's
auto-broadcast fires on every mutation, but it broadcast a stale Map count.
Reading from the DB makes the UI's spinner state match what the queue actually holds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): typed abortReason replaces wasAborted boolean

Was: a boolean wasAborted that lumped every abort together. The finally block
branched on !wasAborted, so any abort skipped restart — including idle aborts
with pending work, which is exactly the case where we DO want to restart.

Now: ActiveSession.abortReason is a typed enum 'idle' | 'shutdown' | 'overflow'
| 'restart-guard'. The finally block consumes the reason and only skips restart
for 'shutdown' and 'restart-guard'. Idle and overflow aborts fall through, so
if pending work exists they trigger restart correctly.

Dropped 'stale' and 'wall-clock' from the union — Phase 2 deleted those paths.
Natural-completion abort (post-success) intentionally has no reason; it's not
gating restart logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): unify the two generator-exit finally blocks

Was: worker-service.ts:startSessionProcessor and SessionRoutes:ensureGeneratorRunning
each had their own ~70-line finally block with divergent restart-guard handling.
The worker-service path called terminateSession on RestartGuard trip and orphaned
pending rows (the L16 bug); the SessionRoutes path drained them. Two places to
update when rules changed.

Now: handleGeneratorExit in src/services/worker/session/GeneratorExitHandler.ts
owns the contract:
  1. Always kill the SDK subprocess if alive.
  2. Always drain processingMessageIds via sessionManager.markMessageFailed
     (which wakes the iterator — Phase 1.2).
  3. shutdown / restart-guard reasons: drain pending rows via
     transitionMessagesTo('failed'), finalize, remove from Map. Fixes L16.
  4. pendingCount=0: finalize normally and remove from Map.
  5. pendingCount>0: backoff respawn via per-session respawnTimer (no global Set;
     Phase 2.4 deleted that). RestartGuard trip drains to 'abandoned'.

Both finally blocks are now ~10-line wrappers that translate local state into the
canonical abortReason and delegate. Restored completionHandler injection into
SessionRoutes (was dropped in Phase 2 cleanup; needed by the unified helper for
finalizeSession).

Behavior change: SessionRoutes' previous "keep idle session in memory" was
deliberately replaced by the plan's "remove from Map on natural completion" —
next observation reinitializes via getMessageIterator → initializeSession.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(worker): startup orphan sweep — reset 'processing' rows at boot

When the worker dies (crash, kill, restart), any pending_messages rows it left
in 'processing' state are by definition orphans (the only worker is dead).
Single SQL UPDATE at boot resets them to 'pending' so the iterator can claim
them again. Replaces the deleted processPendingQueues function (Phase 2.2).

Runs in initializeBackground after dbManager.initialize() and before the
initializationComplete middleware releases blocked HTTP requests, so no
in-flight request can race the sweep. NOT on a periodic timer — after boot,
every 'processing' row has a live consumer and a periodic sweep would race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): simplify enqueue catch, replace memorySessionId throw with re-pend

7.1: queueObservation's catch was logging two ERROR-level messages and rethrowing.
The rethrow is correct (FK violations / disk full / schema drift should crash
loudly), but the verbose ERROR logging pretended the error was recoverable.
Reduced to one INFO line + rethrow.

7.2: ResponseProcessor's memorySessionId guard was throwing if the SDK hadn't
included session_id on the first user-yield, terminal-failing the entire batch.
Now warns and re-pends in-flight messages via sessionManager.markMessageFailed
(which wakes the iterator — Phase 1.2). The next iteration tries again with
memorySessionId hopefully captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): mirror builds to installed-version cache for hot reload

When package.json bumps past Claude Code's installed pin, sync-marketplace
wrote new code to cache/<buildVersion>/ but the worker loaded from
cache/<installedVersion>/, so worker:restart reloaded the same old code.

Replace the exit-on-mismatch preflight with a mirror step: when versions
differ, also rsync plugin/ into cache/<installedVersion>/ so worker:restart
hot-reloads new code without a Claude Code session restart. The
build-version cache still gets written for the eventual
`claude plugin update`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: delete dead barrel files and orphan utilities

- src/sdk/index.ts (re-exports parser+prompts; nothing imported the barrel)
- src/services/Context.ts (re-exports ./context/index.js; no importers)
- src/services/integrations/index.ts (no importers)
- src/services/worker/Search.ts (3-line barrel of ./search/index.js)
- src/services/infrastructure/index.ts: drop CleanupV12_4_3 re-export
- src/utils/error-messages.ts (getWorkerRestartInstructions never imported)
- src/types/transcript.ts (170 LoC of types, zero importers)
- src/npx-cli/_preview.ts (banner dev preview, no script wires it)

Build + tests still pass; observations still flowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(parser): drop unused detectLanguage

Only the user-grammar-aware variant detectLanguageWithUserGrammars()
is actually called.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(types): drop unused SdkSessionRecord + ObservationWithContext

Both interfaces in src/types/database.ts had zero importers anywhere
in src or tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(npx-cli): drop unused getDetectedIDEs + claudeMemDataDirectory

getDetectedIDEs has no callers — install.ts uses detectInstalledIDEs
directly. claudeMemDataDirectory has no callers either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ProcessManager): drop dead orphan-reaper + signal-handler helpers

Each had zero callers in src/ or tests/:
  - cleanupOrphanedProcesses + enumerateOrphanedProcesses
  - ORPHAN_PROCESS_PATTERNS + ORPHAN_MAX_AGE_MINUTES
  - forceKillProcess
  - waitForProcessesExit
  - createSignalHandler
  - resetWorkerRuntimePathCache

The orphan reaper was retired in PATHFINDER Plan 02 ("OS process groups
replace hand-rolled reapers", commit 94d592f2) — these were the leftover
pieces. shutdown.ts uses the supervisor's own kill-pgid path instead.

parseElapsedTime kept (covered by tests/infrastructure/process-manager.test.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): delete 11 unreferenced DX/forensic scripts

None of these are referenced by package.json npm scripts or docs/.
All last touched on Apr 29 only as part of the comment-stripping
pass — the feature code itself is older and orphaned:

  analyze-transformations-smart.js
  debug-transcript-structure.ts
  dump-transcript-readable.ts
  endless-mode-token-calculator.js
  extract-prompts-to-yaml.cjs
  extract-rich-context-examples.ts
  find-silent-failures.sh
  fix-all-timestamps.ts
  format-transcript-context.ts
  test-transcript-parser.ts
  transcript-to-markdown.ts

These are standalone tools — runtime behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): delete unused extraction/ and types/ subdirs

- scripts/extraction/{extract-all-xml.py, filter-actual-xml.py, README.md}
  point at ~/Scripts/claude-mem/ — the user's pre-relocation path that no
  longer exists. Zero references in package.json, src/, or tests/.
- scripts/types/export.ts duplicates ObservationRecord etc. and has no
  importers (CodexCliInstaller imports transcripts/types, not this).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(BranchManager): drop dead getInstalledPluginPath

OpenCodeInstaller has its own (used) getInstalledPluginPath; the
BranchManager copy never had any external callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ChromaSyncState): unexport DocKind (used internally only)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gemini): drop stale earliestPendingTimestamp / processingMessageIds

Both fields were removed from ActiveSession in earlier queue-engine
cleanup. Tests had been silently keeping them because the mock sessions
use 'as any' to bypass strict typing, so the dead fields rode along
without complaint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop 3 unused module-level constants

- src/npx-cli/banner.ts: CURSOR_HOME, CLEAR_DOWN (banner uses
  CLEAR_SCREEN which combines clear-down + cursor-home into a single
  CSI sequence; the standalone constants were leftovers).
- src/services/worker/BranchManager.ts: DEFAULT_SHELL_TIMEOUT_MS
  (BranchManager only uses GIT_COMMAND_TIMEOUT_MS / NPM_INSTALL_TIMEOUT_MS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(opencode-plugin): drop dead workerPost helper

Only the fire-and-forget variant (workerPostFireAndForget) is actually
called. workerPost was the await-result version with no remaining caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop 8 truly-unused interface fields

Verified each by grepping for `.field`, `"field"`, `'field'`, and
`field:` patterns across src/ + tests/ + plugin/scripts. Where the
only remaining usage was the assignment site, removed the assignments too.

- GitHubStarsData: watchers_count, forks_count (only stargazers_count read)
- TableColumnInfo: dflt_value (PRAGMA returns it but no caller reads it)
- IndexInfo: seq (PRAGMA returns it but no caller reads it)
- ObservationRecord: source_files (legacy field, no readers)
- HookResult.hookSpecificOutput: permissionDecisionReason
- WatchTarget: rescanIntervalMs (set in config, never read)
- ShutdownResult: confirmedStopped (write-only — assigned but no
  reader; updated all 3 return sites to drop it)
- ModePrompts: language_instruction (multilingual support never wired)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(npx-cli): reuse InstallOptions type instead of inline duplicate

parseInstallOptions had its return type written out inline as an
anonymous duplicate of InstallOptions. Use the canonical type
(import type — zero bundle cost).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(integrations): drop unused Platform type alias

The detectPlatform() function that returned this type was deleted earlier
in the branch (along with getScriptExtension that consumed it). The type
itself outlived its consumer; only string literals "Platform:" survive in
console.log diagnostics, which don't reference the alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): broadcast processing_status when summarize is queued

broadcastSummarizeQueued was an empty no-op even though
handleSummarizeByClaudeId calls it after enqueueing. The PendingMessageStore
onMutate callback already fires broadcastProcessingStatus on enqueue, but
calling it explicitly from broadcastSummarizeQueued ensures the spinner
ticks on the moment a summary is requested even if the onMutate chain has
any timing race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): keep spinner on while summary generates

ClaudeProvider's SDK can pull multiple synthetic prompts (e.g.
observation + summarize) before producing responses. Each pull pushed
an ID to session.processingMessageIds. When the SDK's first
observation response came back, ResponseProcessor.confirmProcessed
deleted ALL pending message rows — including the still-in-flight
summary — so getTotalQueueDepth dropped to 0 and the spinner turned
off, even though the summary took another ~22s to actually generate.

Tag each in-flight message with its type ({id, type}) so the response
processor can pop only the FIFO message of the matching type
(observation vs summarize). The summary row stays in 'processing'
until its own response arrives, keeping the spinner lit through the
entire summary window.

Also updates Gemini/OpenRouter providers and GeneratorExitHandler for
the new shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): clear summary from queue on any SDK response

Switch ResponseProcessor from type-aware FIFO matching to strict FIFO
popping (each SDK response → 1 in-flight message consumed). This way
the summary always clears when the SDK responds, even when the
response is unparseable or the summary doesn't actually generate
content — preventing stuck spinner / queue-depth-stuck-at-1.

Spinner behavior is preserved: messages enqueued after the summary
keep the queue depth elevated, and only when the SDK has responded
to every prompt does the queue drain to zero.

Also: when the consumed message is a 'summarize' and parsing fails,
treat it as best-effort and confirmProcessed (no retry) — summaries
that can't be parsed shouldn't keep retrying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(viewer): redesign welcome card and remove source filters

The first-start welcome card now explains the three feed card types
(observation/summary/prompt) with color-coded badges, points users at
the gear icon for settings and the project dropdown for filtering, and
plugs /mem-search for recall — replacing the old two-line "ask:" prompts.

Source filter tabs (Claude/Codex/etc.) are removed from the header.
Filtering by AI provider was nonsense from a user POV; the project
dropdown is the only header filter now. Source tracking is also
stripped from useSSE, usePagination, App state, and CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): keep welcome card in feed column, swap rows for 3 squares

Two visible problems in the previous design: the card stretched
edge-to-edge while feed cards sit in a centered 650px column, and
the body was a stack of long horizontal rows that scanned line-by-line.

Both fixed: Feed now accepts a pinnedTop slot so the welcome card
renders inside the same .feed-content column as observation cards.
Body is now a 3-column grid of square feature blocks — Live feed,
Tune it, Recall it — each with a custom inline SVG illustration
(stacked cards with color-coded stripes, gear+sliders, magnifier
over cards). Old text-row sections (welcome-card-types,
welcome-card-tips, welcome-card-section, welcome-card-tip-icon)
are removed. Squares stack to one column under 600px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(viewer): convert welcome card to glassy modal with stylized logo

Card now opens as a centered modal with a frosted/glass backdrop
(blur + saturate) so it doubles as a proper help dialog when reopened
from the header's question-mark button. Removed the observation count,
project count, and "since" date — those don't make sense for a
first-launch surface and felt out of place in a help context.

Header art swapped from the small webp logomark to the new
high-resolution sun/sunburst PNG (claude-mem-logo-stylized.png),
shipped as a checked-in asset in src/ui and plugin/ui.

Bigger throughout: 28px h2, 16px tagline, 88px illustrations,
26px feature padding, 1:1 aspect-ratio squares. Backdrop click and
Esc both close. Mobile collapses the grid to one column and drops
the aspect-ratio constraint.

Reverted the unused pinnedTop slot on Feed.tsx since the welcome
card is now a true overlay rather than an in-feed pinned card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): make welcome modal actually glassy

Previous version had a 55%-opacity black backdrop that almost fully
blocked the underlying UI — the "glass" was just a dark plate.

Now the backdrop is fully transparent (no darkening at all), the
panel itself drops to 55% bg-card opacity with its existing
backdrop-filter blur(28px) saturate(170%), and the feature squares
drop to 35% bg-tertiary so they layer as glass-on-glass over the
already-blurred panel. The header and feed below now read clearly
through the modal's frosted blur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): bulletproof square features via padding-bottom + clamp() fluid type

Squares were rendering taller than wide because aspect-ratio is treated
as a minimum — content can push the box past 1:1. Switched to the
classic padding-bottom: 100% trick: percentage padding resolves against
the parent's width, so the box is ALWAYS W × W regardless of content.
Inner content sits in an absolutely-positioned flex column that can't
push the shell taller.

Whole modal is now desktop-first and fluid via clamp() — no media-query
stair-steps for type, padding, gaps, border-radius, illustration size,
or modal width. Single mobile breakpoint at <600px collapses the grid
to one column and reverts the padding-bottom trick so each feature can
grow to natural content height.

Tightened the three feature descriptions so they fit comfortably inside
the square at the desktop size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(viewer): 15% black overlay + heavier modal shadow for elevation

Backdrop goes from transparent to rgba(0,0,0,0.15) — just enough
darkening to push the modal visually forward without burying the
underlying UI. Modal shadow stacked: 40px/120px ambient + 16px/48px
contact, both deeper, plus the existing inset 1px highlight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): clear pending_messages queue on build-and-sync

Rewrites scripts/clear-failed-queue.ts to talk directly to SQLite via
bun:sqlite — the previous HTTP endpoints (/api/pending-queue/*) were
removed during the queue engine rewrite, so the script was orphaned.
Wires `npm run queue:clear` into `build-and-sync` so each rebuild
starts with a clean queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): collapse parser to binary valid/invalid + clearPendingForSession model

- Parser: { valid: true, observations, summary } | { valid: false } — drops kind/skipped enum dispatch
- ResponseProcessor: two branches only (parseable → store + clearPendingForSession; else → no-op)
- Drop processingMessageIds + per-message claim/confirm/markFailed lifecycle across 3 providers
- PendingMessageStore: 226 → 140 lines; remove markFailed/transitionMessagesTo/confirmProcessed/clearFailedOlderThan/getAllPending/peekPendingTypes... wait keep peekPendingTypes
- Schema migration v31+v32: drop retry_count, failed_at_epoch, completed_at_epoch, worker_pid columns
- SessionQueueProcessor: delete two 1s recovery sleeps (let iterator end on error)
- Server.ts/SettingsRoutes.ts: replace four magic-number setTimeout exit-flush patterns with flushResponseThen helper
- GeneratorExitHandler: 183 → 117 lines (drain in-flight loop gone)

Net: -181 lines. No more silent data loss via maxRetries=3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): address review comments batch 1

- install.ts: needsMarketplace true when claude-code selected (P1, was no-op)
- install.ts: throw on invalid --model so CLI exits non-zero
- install.ts: skip worker health checks + adapt next-step copy when --no-auto-start
- install.ts: repair regenerates plugin cache when missing
- index.ts: readFlag rejects missing/flag-shaped values
- index.ts: route flag-first invocations (e.g. `--provider claude`) to install
- banner.ts: fail-open if frame payload decode throws
- SearchRoutes.ts: 5s TTL cache for settings reads on hot hook path (P2)
- detect-error-handling-antipatterns.ts: trailing-brace strip whitespace-tolerant
- investigate-timestamps.ts: compute Dec 2025 epochs at runtime (was Dec 2024)
- regenerate-claude-md.ts: include workingDir in fallback walker so root is covered
- sync-marketplace.cjs: parseWorkerPort validates 1..65535 before http.request
- sync-to-marketplace.sh: resolve SOURCE_DIR from script location, not cwd
- Dockerfile.test-installer: bash --login sources .bashrc via .bash_profile
- docs/configuration.mdx: drop nonexistent .worker.port file refs, use settings.json
- docs/architecture-overview.md: dynamic port + queue model after parser collapse
- docs/architecture/worker-service.mdx: dynamic port example + drop port-file claim
- docs/platform-integration.mdx: WORKER_BASE_URL pattern, drop hardcoded 37777
- install/public/install.sh: Node 20 floor (was 18) to match docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): reset claimed messages to pending on early-return paths

ResponseProcessor returns early in two cases:
- parser invalid (unparseable response)
- memorySessionId not yet captured

Both paths previously left the just-claimed message in `status='processing'`,
which counts toward `getPendingCount`. The generator-exit handler then sees
`pendingCount > 0` and respawns the generator, looping until the restart
guard trips and `clearPendingForSession` deletes the message — silent data
loss.

Calling `resetProcessingToPending` on these paths lets the next generator
pass re-claim the message and try again, instead of burning the restart
budget on no-op respawns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): swebench fallback row + troubleshooting port path

- evals/swebench/run-batch.py: append fallback prediction row when
  orchestrator future raises, preserving "never drop an instance" guarantee
- docs/troubleshooting.mdx: drop nonexistent .worker.port / worker.port file
  references; use settings.json + /api/health for port discovery

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): memoize per-project observation count for welcome-hint hot path

handleContextInject runs on every PostToolUse hook (after every Read/Edit).
The welcome-hint block ran a COUNT(*) on observations for every call once
CLAUDE_MEM_WELCOME_HINT_ENABLED was true. Observation counts are
monotonically increasing — once a project has any observations it always
will — so cache the positive result in a Set and skip the COUNT(*) on
subsequent requests.

Combined with the 5s settings TTL added earlier, the steady-state cost on
the hook hot path drops to a Set lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): use clearProcessingForSession on AI-success path

clearPendingForSession deletes ALL rows for the session. On the success
path of processAgentResponse, that's wrong: messages that arrived as
'pending' during the (1-5s) AI response latency get deleted along with
the 'processing' row we just consumed. In a hook burst (three quick
PostToolUse hooks), B and C land while A is in flight; A's success then
nukes B and C — silent data loss.

Add a status-scoped clearProcessingForSession to PendingMessageStore +
SessionManager, and use it in ResponseProcessor's success path. The
unconditional clearPendingForSession remains correct in
GeneratorExitHandler for hard-stop / restart-guard-trip paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(pr-2255): use clearProcessingForSession on AI-success path"

This reverts commit a08995299c.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:05:56 -07:00
Alex Newman
d13662d5d8 Cynical deletion: close 27 issues by removing defenders + tolerators (#2141)
* fix: mirror migration 28 in SessionStore so pending_messages.tool_use_id and worker_pid columns are created (#2139)

SessionStore's inline migration list jumped from v27 to v29, skipping
rebuildPendingMessagesForSelfHealingClaim. The worker uses SessionStore
directly via worker/DatabaseManager.ts and bypasses the canonical
MigrationRunner, so fresh installs ended up at "max v29" with neither
column present — every queue claim and observation insert failed.

Adds addPendingMessagesToolUseIdAndWorkerPidColumns following the existing
mirror precedent (addObservationSubagentColumns / addObservationsUniqueContentHashIndex).
Uses ALTER TABLE + column-existence guards so already-broken DBs at v29
self-heal on next worker boot.

Verified on fresh DB and on a synthetic v29-without-v28 broken DB:
both columns and indexes (idx_pending_messages_worker_pid,
ux_pending_session_tool) appear after one boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: wrap v28 mirror dedup+index creation in transaction

Addresses Greptile P2 review on PR #2140: matches the existing pattern in
addObservationsUniqueContentHashIndex (v29 mirror at SessionStore.ts:1127)
and runner.ts rebuildPendingMessagesForSelfHealingClaim. A crash between
the dedup DELETE and the schema_versions INSERT no longer leaves the DB
in a half-applied state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): cynical-deletion plan for 29 open issues

9-phase plan applying delete-first lens to triaged issue corpus.
Headlines: kill defenders (orphan cleanup, EncodedCommand spawn,
restart-port-steal) and tolerators (silent JSON drops, drifted SSE
filters). Each phase closes a named subset of issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: delete process-management theater (Phase 1: DEL-1 + DEL-2)

Delete aggressiveStartupCleanup, the PowerShell -EncodedCommand
spawn branch, and the restart-with-port-steal sequence. Replace
daemon spawning with a single uniform child_process.spawn path
using arg-array form, keeping setsid on Unix when available.

The defenders (orphan cleanup, duplicate-worker probes, port
stealing) bred more bugs than they fixed. PID file with start-time
token already provides correct OS-trust ownership; restart now
requests httpShutdown, waits 5s for the port to free, then exits 1
if it didn't (user resolves). Net -247 lines.

Closes #2090, #2095 (already fixed at session-init.ts:78), #2107,
#2111, #2114, #2117, #2123, #2097, #2135.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: observer-sessions trust boundary via CLAUDE_MEM_INTERNAL env (Phase 2: DEL-9)

Replace the cwd === OBSERVER_SESSIONS_DIR discriminator (which every
consumer must repeat and inevitably drifts) with a single env-var
trust boundary set once at spawn time in buildIsolatedEnv.

- buildIsolatedEnv now sets CLAUDE_MEM_INTERNAL=1, covering all three
  spawn sites (SDKAgent, KnowledgeAgent.prime, KnowledgeAgent.executeQuery)
- shouldTrackProject checks the env var first (cwd check stays as
  belt-and-braces fallback)
- New shared shouldEmitProjectRow predicate — SSE broadcaster and
  pagination filter share the same predicate so they can never drift
  apart (#2118)
- ObservationBroadcaster filters observer rows from SSE stream
- PaginationHelper hardcoded 'observer-sessions' replaced with
  OBSERVER_SESSIONS_PROJECT const
- project-filter basename match pass — *observer-sessions* now matches
  basename, not just full path (globToRegex's [^/]* can't cross /)
  (#2126 item 1)
- New `claude-mem cleanup [--dry-run]` subcommand wires CleanupV12_4_3
  through to the worker for #2126 item 5

Closes #2118, #2126.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: strip proxy env vars before spawning worker (Phase 4: CON-1)

User's HTTP_PROXY/HTTPS_PROXY config was bleeding into internal AI
calls when claude-mem spawns the claude subprocess, causing
connection failures. Strip unconditionally — no passthrough knob,
which rejects #2099's whitelist proposal.

Closes #2115, #2099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: fail-fast on silent drops in stdin/file-context/memory-save (Phase 5: FF-1)

Three independent fail-fast fixes:

#2089 — stdin-reader silent drop. Non-empty stdin that fails JSON.parse
now rejects with a clear error instead of resolving undefined. Empty
stdin still resolves undefined.

#2094 — PreToolUse:Read truncation Edit deadlock. file-context handler
no longer returns a fake truncated Read result via updatedInput.
Removes userOffset/userLimit/truncated machinery; injects the timeline
via additionalContext only and lets the real Read pass through. Read
state and Claude's expectation now stay consistent, eliminating the
infinite Edit retry loop.

#2116 — /api/memory/save metadata drop + project bug. Schema accepts
metadata as a documented JSON column (migration 30 adds observations.
metadata TEXT, mirrored in SessionStore). Schema also tightened to
.strict() so unknown top-level fields fail fast instead of being
silently dropped. Project resolution now consults metadata.project as
a fallback before defaultProject.

Closes #2089, #2094, #2116.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: small deletions — Zod externalize / Gemini fallback / session timeout / installCLI alias (Phase 6)

DEL-4 (#2113): Externalize zod from mcp-server.cjs and context-generator.cjs
hook bundles so OpenCode's runtime resolves a single Zod copy. Worker
keeps Zod bundled (it's a daemon subprocess, not in OpenCode's hook
bundle). Added zod to plugin/package.json so externalized requires
resolve at runtime.

DEL-5 (#2087): Delete the never-wired GeminiAgent → Claude fallback.
fallbackAgent was always null in production. On 429 the agent now
throws cleanly (message stays pending for retry). Removed
setFallbackAgent, FallbackAgent interface, and the 429 fallback
branch from both GeminiAgent and OpenRouterAgent. Updated docs
that claimed automatic Claude fallback.

DEL-6 (#2127, #2098): Raise MAX_SESSION_WALL_CLOCK_MS from 4h to
24h. The timeout is a real guard against runaway-cost loops (per
issue #1590), but 4h kills legitimate long Claude Code days. 24h
preserves the guard while never hitting in normal use. No knob —
a session approaching this age is a bug worth investigating, not
a value worth tuning.

DEL-8 (#2054): Delete installCLI() alias function. Saves 4 keystrokes
at the cost of cross-platform shell-config mutation surface — not
worth it. Canonical entry is npx claude-mem (and bunx). Uninstall
now strips legacy alias/function lines from ~/.bashrc, ~/.zshrc,
and the PowerShell profile.

Closes #2087, #2098, #2113, #2127, #2054.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: de-hardcode worker port + multi-account commit (Phase 3: CON-2 + DEL-7)

Replace hardcoded 37777 fallbacks with SettingsDefaultsManager.get(
'CLAUDE_MEM_WORKER_PORT') in npx-cli (runtime/install/uninstall),
opencode-plugin, OpenClaw installer, SearchRoutes example URLs.
Timeline-report SKILL.md now resolves WORKER_PORT from settings.json
at the top and uses ${WORKER_PORT} in all curl invocations.
Remaining 37777 literals are doc comments + viewer build-time form-
field placeholder (which is replaced by /api/settings on mount).

hooks.json: add cygpath POSIX→Windows path translation between _R
resolution and node invocation. No-op on macOS/Linux. Closes the
Windows + Git Bash MODULE_NOT_FOUND in #2109.

CLAUDE.md gains a Multi-account section documenting CLAUDE_MEM_DATA_DIR
+ optional CLAUDE_MEM_WORKER_PORT — every existing path/port code
path now honors them.

Closes #2103, #2109, #2101.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: install/uninstall improvements (Phase 7: #2106)

5 fixes for the install/uninstall flow:

Item 1 — multiselect default. install.ts no longer pre-selects every
detected IDE; user explicitly opts in.

Item 3 — shutdown-before-overwrite. New
src/services/install/shutdown-helper.ts shared by install and
uninstall: POSTs /api/admin/shutdown then polls /api/health until
the worker stops responding. install calls it before
copyPluginToMarketplace so reinstall over a running worker doesn't
conflict; uninstall calls it before deletion.

Item 4 — uninstall path coverage. Removes ~/.npm/_npx/*/node_modules/
claude-mem, ~/.cache/claude-cli-nodejs/*/mcp-logs-plugin-claude-mem-*,
~/.claude/plugins/data/claude-mem-thedotmack/. Best-effort: per-path
try/catch so a single permission failure doesn't abort uninstall.
chroma-mcp shutdown is implicit via the worker's GracefulShutdown
cascade in item 3's helper.

Item 5 — install summary documents "Close all Claude Code sessions
before uninstalling, or ~/.claude-mem will be recreated by active
hooks."

Item 6 — real-port query. After install, fetches /api/health on the
configured port with 3s timeout. Reports actually-bound port if the
response carries it; falls back to requested port. No retry loop.

Closes #2106 (items 1, 3, 4, 5, 6). Items 2, 7 closed separately
as already-fixed and insufficient-detail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: pin chroma-mcp to 0.2.6 (Phase 8: DEL-3 lite)

Replace unpinned 'chroma-mcp' arg with chroma-mcp==0.2.6 in both
local and remote modes. Pinning makes installs deterministic across
machines and across time, eliminating the dependency-drift class
of bugs.

Verified 0.2.6 in a clean uv cache: starts cleanly, no httpcore/
httpx ImportError, no --with flags needed. The --with flags removed
in a0dd516c are not required at this pin (transitive deps resolve
correctly when the top-level version is fixed).

#2102's three protections (transport cleanup on failure, stale onclose
handler guard, 10s reconnect backoff) confirmed intact.

Closes #2046, #2085, #2102.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: update stale assertions for per-UID port + migration 30 (Phase 9)

SettingsDefaultsManager.CLAUDE_MEM_WORKER_PORT default is per-UID
(37700 + uid%100), not literal '37777'. Three assertions in
settings-defaults-manager.test.ts now compute the expected value
the same way the source does.

migration-runner.test.ts: drop expect(versions).toContain(19)
(version 19 was a noop never recorded — pre-existing bug at parent),
add expect(versions).toContain(30) for the new observations.metadata
column added in Phase 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address Greptile P1/P2 review comments on PR #2141

P1: spawnDaemon return value was unchecked in worker-service.ts restart
case, so a failed spawn silently exited 0 with a misleading "Worker
restart spawned" log. Now error and exit 1 when restartPid is undefined.

P2: shutdown-helper.ts health-poll catch treated AbortError (timeout)
the same as connection-refused, so a slow worker could be reported
confirmedStopped while still holding file locks. Now distinguish:
AbortError continues polling; other errors return confirmedStopped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: rebuild plugin artifacts after merging main

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #2141

- hooks.json: quote $HOME in cache lookup so paths with spaces work
- timeline-report SKILL.md: fall back when process.getuid is unavailable (Windows)
- opencode-plugin: validate CLAUDE_MEM_WORKER_PORT before using
- uninstall.ts: only strip alias lines, not function declarations (multi-line bodies left intact)
- MemoryRoutes: trim whitespace-only project before precedence resolution
- SessionStore migration 21: preserve metadata column if observations already has it
- stdin-reader test: restore full property descriptor to avoid cross-test pollution

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-04-25 21:23:24 -07:00