mirror of
https://github.com/thedotmack/claude-mem.git
synced 2026-09-20 04:23:02 +08:00
fix(worker): single version oracle — never rank plugin cache dirs by mtime
The 2026-07-22 restart storm: claude-mem had four resolvers that ranked plugin cache dirs by directory mtime (worker-utils cacheWorkerScriptCandidates, the inline node L() and Codex Windows resolvers in hook-shell-template, and the POSIX `ls -dt` hook resolver), while the staleness detector compared the live worker against the marketplace package.json. When Claude Code stamped the superseded 13.11.0 cache dir with .orphaned_at, its mtime became newest, so every recycle respawned 13.11.0 while hooks running 13.12.0 kept demanding a restart — 485 respawns in ~2 hours, each spawning SDK and Chroma children, until the host exhausted its process table. The fix is an invariant, not a mitigation: one deterministic version oracle shared by detection and respawn. - resolveWorkerScript() ranks candidates by version descending (numeric major.minor.patch, release ahead of prerelease at the same base, reverse-lexical tiebreak) and skips .orphaned_at-stamped dirs; stable sort keeps cache → marketplace → cwd precedence on ties. - ensureWorkerRunning() resolves once and feeds the same result to both checkVersionMatch(port, expectedVersion) and the lazy-spawn script, so a mismatch can only ever resolve toward the version the respawn will actually produce — the loop is structurally impossible. - checkVersionMatch now takes the caller's expected version; getInstalledPluginVersion (the second oracle) is deleted. - All three generated bootstrap resolvers (mcp node launcher, Codex Windows launcher, POSIX hook prelude) embed the identical ordering; the POSIX variant uses a zero-padded sort key and balanced (pattern) case forms (unmatched parens are a parser error inside $(...)). - scripts/build-hooks.js gains --write-shell-templates, the regeneration mode its hand-edit tripwire error message points at. - Regression coverage: comparator/selection unit tests plus a functional shell-matrix test that rebuilds the exact storm layout (orphaned old dir with newest mtime) and asserts the new version wins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+34
-12
@@ -159,15 +159,25 @@ async function verifyShellTemplateCanonical() {
|
||||
|
||||
const manifest = shellTemplateManifest(buildShellCommand, buildCodexWindowsCommand);
|
||||
|
||||
// The regeneration mode the mismatch errors point at: after an intentional
|
||||
// generator change, rewrite the committed launcher strings from the same
|
||||
// manifest the verifier checks, so the two can never drift.
|
||||
const writeMode = process.argv.includes('--write-shell-templates');
|
||||
|
||||
for (const [filePath, spec] of Object.entries(manifest)) {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
let dirty = false;
|
||||
if (spec.kind === 'mcp') {
|
||||
const actual = parsed.mcpServers?.['mcp-search']?.args?.[1] ?? '';
|
||||
if (actual !== spec.command) {
|
||||
throw new Error(
|
||||
`Hand-edited shell string detected in ${filePath} (mcp-search). It no longer matches src/build/hook-shell-template.ts. ` +
|
||||
`Update the generator (and this manifest) instead of hand-editing the launcher.`
|
||||
);
|
||||
if (!writeMode) {
|
||||
throw new Error(
|
||||
`Hand-edited shell string detected in ${filePath} (mcp-search). It no longer matches src/build/hook-shell-template.ts. ` +
|
||||
`Regenerate via \`node scripts/build-hooks.js --write-shell-templates\` after an intentional generator change.`
|
||||
);
|
||||
}
|
||||
parsed.mcpServers['mcp-search'].args[1] = spec.command;
|
||||
dirty = true;
|
||||
}
|
||||
} else {
|
||||
for (const [dottedPath, expected] of Object.entries(spec.commands)) {
|
||||
@@ -175,22 +185,34 @@ async function verifyShellTemplateCanonical() {
|
||||
const expectedCommand = typeof expected === 'string' ? expected : expected.command;
|
||||
const actual = entry?.command ?? null;
|
||||
if (actual !== expectedCommand) {
|
||||
throw new Error(
|
||||
`Hand-edited shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` +
|
||||
`Regenerate via the canonical generator instead of hand-editing the command.`
|
||||
);
|
||||
if (!writeMode || !entry) {
|
||||
throw new Error(
|
||||
`Hand-edited shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` +
|
||||
`Regenerate via \`node scripts/build-hooks.js --write-shell-templates\` after an intentional generator change.`
|
||||
);
|
||||
}
|
||||
entry.command = expectedCommand;
|
||||
dirty = true;
|
||||
}
|
||||
if (typeof expected !== 'string') {
|
||||
const actualWindows = entry?.commandWindows ?? null;
|
||||
if (actualWindows !== expected.commandWindows) {
|
||||
throw new Error(
|
||||
`Hand-edited Windows shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` +
|
||||
`Regenerate via the canonical generator instead of hand-editing commandWindows.`
|
||||
);
|
||||
if (!writeMode || !entry) {
|
||||
throw new Error(
|
||||
`Hand-edited Windows shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` +
|
||||
`Regenerate via \`node scripts/build-hooks.js --write-shell-templates\` after an intentional generator change.`
|
||||
);
|
||||
}
|
||||
entry.commandWindows = expected.commandWindows;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dirty) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(parsed, null, 2) + '\n');
|
||||
console.log(` ✏️ Regenerated shell templates in ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Rule C safety net (bun-runner.js fixBrokenScriptPath) must stay documented.
|
||||
|
||||
Reference in New Issue
Block a user