You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[plan-16] Canonical Install Identity & Bundle Integrity — one resolvable, dependency-complete plugin root, and recycles that cannot kill a working worker #3604
[plan-16] Canonical Install Identity & Bundle Integrity — one resolvable, dependency-complete plugin root, and recycles that cannot kill a working worker
There is no single answer to "which copy of claude-mem is installed and runnable". Hooks, the supervisor's successor spawn, npx claude-mem (install/repair/doctor/status), the SDK subprocess, and the marketplace auto-updater each resolve the plugin root independently, and none of them verifies the copy has its runtime dependencies:
worker-service.cjs externalizes zod, shell-quote, tree-sitter-*, ajv*, better-auth, and (13.9.x) ../sqlite/{SessionStore,observations/files}.js; the bundle requires modules the build never emits and a node_modules the plugin update / marketplace pull never installs (Cannot find module 'zod/v3', Cannot find module '../sqlite/observations/files.js'). tree-sitter-cli's binary is fetched by an install script suppressed by --ignore-scripts; the Windows resolver checks an extensionless path; grammars declare conflicting tree-sitter peer majors (ERESOLVE); npm 12 rejects the --allow-scripts CLI flag the installer passes.
The version-mismatch recycle compares the marketplace manifest version to the worker's /api/health version and kills the incumbent before a successor exists, then lazy-spawns from whichever path the resolver returns — often the marketplace copy or an older cache dir — and waits for a plugin-version match a fresh worker can never report when the manifest is stale. Result: unbounded recycle loops (86–449/hour) that feed plan-14's leak and plan-15's ghost ports, and a healthy worker replaced by a guaranteed-to-fail one.
doctor checks marketplace node_modules, repair installs into the version cache, status reports "not installed" beside a healthy worker; .orphaned_at is honored by hook shells but not by the running daemon; stale cache versions are never GC'd; the marketplace has no root .claude-plugin/marketplace.json.
The shipped bundle set is not audited: DEP0190 (args + shell:true) survives in dist/npx-cli, shell-quote and sync-hub transitive deps carry known CVEs, an agent-directive template ships in plugin/modes/, and Windows CI is red for reasons unrelated to the PR under test.
One resolver, three consumers.resolvePluginRoot() (shared TS, compiled into the hook prelude via hook-shell-template.ts, worker-utils.ts, and npx-cli) returns the highest semver cache dir that (a) has a worker-service.cjs above a size floor, (b) has no .orphaned_at, (c) has node_modules/zod/v3 (or whatever the bundle's external manifest lists), honoring CLAUDE_MEM_WORKER_SCRIPT_PATH first. The marketplace checkout is never a spawn target. installed_plugins.jsoninstallPath is preferred when present.
Bundle owes nothing to the filesystem it can't see. Inline every pure-JS external (zod, shell-quote, ajv, sqlite/observations/files); ship plugin/sqlite/ for anything that must stay external; add a build-time guard that scans plugin/scripts/*.cjs for require("../…")/external ids and fails unless the target is in the tarball. npm pack --dry-run diff is asserted in CI.
Dependencies materialize with the copy. Plugin update / first hook / repair run bun install --production (or vendored node_modules) inside the version dir with an .install-version marker written after success; tree-sitter-cli binary is fetched by an explicit non-lifecycle step with .exe resolution on Windows; grammars aligned to one tree-sitter major or moved behind an optional step; allowScripts declared in package.json/.npmrc, never as a CLI flag. New version dirs are published atomically (temp + rename).
Recycle only into a proven successor. Version comparison is semver-strict-newer against the resolved installed root, not the marketplace manifest; the successor is spawned first on a probe port, must answer /api/health with its own token, then the incumbent is asked to hand off (plan-15 handoff); failure keeps the incumbent and opens a per-boot circuit breaker (max 3 recycles/hour, logged). .orphaned_at is checked by the daemon at boot and each supervisor tick → graceful shutdown.
doctor / status / repair agree. All three use resolvePluginRoot(); doctor reports the resolved root, its version, and its dependency completeness; repair fixes that root; "not installed" is only reported when the resolver returns nothing. Marketplace registration writes .claude-plugin/marketplace.json at the root the host discovers.
exit 0; doctor green; smart_search returns symbols on a .ts file
Windows
Bun worker
tree-sitter grammars
either load under Node or degrade per-language with a WARN, never 0 symbols silently
all
tarball
npm pack
every require() target in plugin/scripts/*.cjs resolves inside the tarball
all
repo
CI
osv-scanner clean, DEP0190 lint clean, packaging lint clean, Windows job green on an empty PR
The matrix lives in CI (tests/infrastructure/plugin-distribution.test.ts, a fresh-install Docker job per Node/npm version, and the Windows job). A regression must fail CI before a user can file.
Out of scope
What happens to children of the killed incumbent (plan-14) and how the port is handed over/reclaimed (plan-15). Hook shell prelude cost and fail-open semantics (plan-17).
[plan-16] Canonical Install Identity & Bundle Integrity — one resolvable, dependency-complete plugin root, and recycles that cannot kill a working worker
Defect
There is no single answer to "which copy of claude-mem is installed and runnable". Hooks, the supervisor's successor spawn,
npx claude-mem(install/repair/doctor/status), the SDK subprocess, and the marketplace auto-updater each resolve the plugin root independently, and none of them verifies the copy has its runtime dependencies:~/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/(ls -dt … | head -1), accepts 1-byte gutted stubs and.orphaned_atdirs, and falls back to~/.claude/plugins/marketplaces/thedotmack/plugin/— a checkout that never hasnode_modules.resolveWorkerScriptPath()/resolveSuccessorScript()repeat the same mtime sort in code (fixed on the hook side by fix(worker): single version oracle — never rank plugin cache dirs by mtime (restart-storm fix) #3371 in 13.12.x; still present in successor resolution per Stale-version recycle loop leaks chroma-mcp processes (759 procs / ~71 GB RSS) until the machine OOMs #3216/Version-mismatch recycle respawns the worker frommarketplaces/…/plugin/, which never hasnode_modules— guaranteed respawn failure, thenUserPromptSubmitblocks every prompt #3565).worker-service.cjsexternalizeszod,shell-quote,tree-sitter-*,ajv*,better-auth, and (13.9.x)../sqlite/{SessionStore,observations/files}.js; the bundle requires modules the build never emits and anode_modulesthe plugin update / marketplace pull never installs (Cannot find module 'zod/v3',Cannot find module '../sqlite/observations/files.js').tree-sitter-cli's binary is fetched by an install script suppressed by--ignore-scripts; the Windows resolver checks an extensionless path; grammars declare conflictingtree-sitterpeer majors (ERESOLVE); npm 12 rejects the--allow-scriptsCLI flag the installer passes./api/healthversion and kills the incumbent before a successor exists, then lazy-spawns from whichever path the resolver returns — often the marketplace copy or an older cache dir — and waits for a plugin-version match a fresh worker can never report when the manifest is stale. Result: unbounded recycle loops (86–449/hour) that feed plan-14's leak and plan-15's ghost ports, and a healthy worker replaced by a guaranteed-to-fail one.doctorchecks marketplacenode_modules,repairinstalls into the version cache,statusreports "not installed" beside a healthy worker;.orphaned_atis honored by hook shells but not by the running daemon; stale cache versions are never GC'd; the marketplace has no root.claude-plugin/marketplace.json.args+shell:true) survives indist/npx-cli,shell-quoteand sync-hub transitive deps carry known CVEs, an agent-directive template ships inplugin/modes/, and Windows CI is red for reasons unrelated to the PR under test.Children
../sqlite/observations/files.js)doctor/statusreport "not installed" right after a successful install #3534 — observations never compress;doctor/statusreport "not installed" right after a successful installmarketplaces/…/plugin/, which never hasnode_modules— guaranteed respawn failure, thenUserPromptSubmitblocks every prompt #3565 — Version-mismatch recycle respawns the worker frommarketplaces/…/plugin/, which never hasnode_modules— guaranteed respawn failureRelated PRs to evaluate/rebase: #3371 (semver resolver, merged), #3095 (tree-sitter runtime provisioning, community-edge only), #3146 (inline
parseFileList), #2887 (bundle zod, community-edge only), #3055 (CLAUDE_MEM_WORKER_SCRIPT_PATH), #2918, #2710, #2597, #2531, #3066, #2945, #2598, #1677, #2699.Fix sequence
resolvePluginRoot()(shared TS, compiled into the hook prelude viahook-shell-template.ts,worker-utils.ts, andnpx-cli) returns the highest semver cache dir that (a) has aworker-service.cjsabove a size floor, (b) has no.orphaned_at, (c) hasnode_modules/zod/v3(or whatever the bundle's external manifest lists), honoringCLAUDE_MEM_WORKER_SCRIPT_PATHfirst. The marketplace checkout is never a spawn target.installed_plugins.jsoninstallPathis preferred when present.sqlite/observations/files); shipplugin/sqlite/for anything that must stay external; add a build-time guard that scansplugin/scripts/*.cjsforrequire("../…")/external ids and fails unless the target is in the tarball.npm pack --dry-rundiff is asserted in CI.repairrunbun install --production(or vendorednode_modules) inside the version dir with an.install-versionmarker written after success; tree-sitter-cli binary is fetched by an explicit non-lifecycle step with.exeresolution on Windows; grammars aligned to onetree-sittermajor or moved behind an optional step;allowScriptsdeclared inpackage.json/.npmrc, never as a CLI flag. New version dirs are published atomically (temp + rename)./api/healthwith its own token, then the incumbent is asked to hand off (plan-15 handoff); failure keeps the incumbent and opens a per-boot circuit breaker (max 3 recycles/hour, logged)..orphaned_atis checked by the daemon at boot and each supervisor tick → graceful shutdown.resolvePluginRoot();doctorreports the resolved root, its version, and its dependency completeness;repairfixes that root; "not installed" is only reported when the resolver returns nothing. Marketplace registration writes.claude-plugin/marketplace.jsonat the root the host discovers.osv-scanneron all lockfiles,check-spawn-env-discipline.cjsagainstdist/(DEP0190), and a packaging lint that fails on tracked*CLAUDE.mdoutside an allowlist. Windows CI pinned to a runner with a working toolchain (or native grammar build skipped) so it is trustworthy; the flaky mock/port tests (CI: 'typecheck · build · test · bundle-size' job flakes with mass HealthMonitor/ProcessManager mock failures (order-dependent), plus CORS port-collision flake #3392, SettingsDefaultsManager loadFromFile tests fail on a dev machine running claude-mem (ambient CLAUDE_MEM_* env leaks into expectations) #3056) are isolated so the matrix can be gated on green.Test matrix
claude plugin updateN → N+1 mid-sessiondoctorexplains the skew.orphaned_at/ 1-byte stub / nonode_modulesnpx claude-mem install(npm 10/11/12, Node 20/22/24/26)doctorgreen;smart_searchreturns symbols on a.tsfile0 symbolssilentlynpm packrequire()target inplugin/scripts/*.cjsresolves inside the tarballThe matrix lives in CI (
tests/infrastructure/plugin-distribution.test.ts, a fresh-install Docker job per Node/npm version, and the Windows job). A regression must fail CI before a user can file.Out of scope
What happens to children of the killed incumbent (plan-14) and how the port is handed over/reclaimed (plan-15). Hook shell prelude cost and fail-open semantics (plan-17).