Skip to content

fix(worker): resolve cache version by semver, not mtime#3354

Open
jamincollins wants to merge 1 commit into
thedotmack:mainfrom
jamincollins:fix/worker-cache-version-resolution
Open

fix(worker): resolve cache version by semver, not mtime#3354
jamincollins wants to merge 1 commit into
thedotmack:mainfrom
jamincollins:fix/worker-cache-version-resolution

Conversation

@jamincollins

Copy link
Copy Markdown

Problem

cacheWorkerScriptCandidates() (src/shared/worker-utils.ts) picks which
cache/thedotmack/claude-mem/<version>/scripts/worker-service.cjs to spawn
by sorting version directories on mtimeMs. An in-place plugin update
extracts the old and new version directories in the same pass, so they
frequently end up with identical mtimes — at that point
Array.prototype.sort's comparator returns 0 for every pair, and the
resulting order is whatever readdirSync happened to return, not version
order.

This is exactly the scenario in #2424, which was closed as not planned. I'm
reopening it as a PR because it reproduced live rather than theoretically:

What I observed

On a machine with both 13.10.2 and 13.11.0 cached (mtimes tied to the
second), the resolver kept picking 13.10.2. Every worker restart re-ran
the same resolution, so:

  1. Worker spawns from the stale 13.10.2 script.
  2. checkVersionMatch() notices the plugin (13.11.0) and running worker
    (13.10.2) disagree and requests a recycle.
  3. The recycle's own spawn hits the identical tied-mtime resolution and
    picks 13.10.2 again.
  4. Repeat — the worker crash-loop-restarted every few seconds.

Each cycle left behind an orphaned chroma-mcp/uvx process pair (the old
worker's ChromaMcpManager never got a chance to tear its subprocess down
cleanly before being replaced). Over roughly 10 minutes this produced ~75
orphaned pairs (~150 processes, several GB RSS) before I killed the loop
manually — a variant of the accumulation described in #499/#789/#803/#1090,
but caused by the resolver, not by ChromaMcpManager's own cleanup paths.

Fix

Sort candidates by the version directory name itself (semver
descending) rather than by mtime. The directory name is the version
identifier, so this is deterministic regardless of extraction order or
mtime ties — no filesystem timestamp involved at all.

Testing

  • Added tests/shared/worker-utils-cache-version-resolution.test.ts:
    unit tests for the new comparator (major/minor/patch ordering, equal
    versions, a realistic multi-version directory listing).
  • bun test tests/shared/ — 126 pass, 0 fail (no regressions).
  • tsc --noEmit clean.

I didn't touch the equivalent mtime-sort in src/build/hook-shell-template.ts
(the generated shell/Node launcher embedded in hooks.json/.mcp.json/
codex hooks) — that path has its own byte-matching generator test and is a
separate, larger surface; happy to follow up there if this approach looks
right to you.

…mack#2424)

cacheWorkerScriptCandidates() picked the worker-service.cjs to spawn by
sorting cache/thedotmack/claude-mem/<version> directories on mtime. An
in-place plugin update extracts both the old and new version directories
in the same pass, frequently leaving them with identical mtimes — at that
point Array.prototype.sort's tie-break is whatever order readdirSync
happened to return, which is not version order.

Observed in the wild: with 13.10.2 and 13.11.0 both present and mtime-tied,
the resolver kept picking 13.10.2. Each worker restart re-ran the same
resolution, so the worker crash-looped by design (checkVersionMatch
detected the plugin/worker mismatch, requested a recycle, and the recycle
spawn hit the identical stale resolution again) — restarting every few
seconds and leaking an orphaned chroma-mcp/uvx process pair per cycle
(~150 processes / several GB RSS accumulated in under 10 minutes on the
machine this was diagnosed on).

Fix: sort candidates by the version directory name itself via a small
semver-descending comparator, not by filesystem mtime. The directory name
*is* the version identifier, so this is deterministic regardless of
extraction order or mtime ties.

Issue thedotmack#2424 previously proposed this and was closed as not planned; filing
this PR because the failure mode reproduced live rather than theoretically.
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes cached worker script selection deterministic. The main changes are:

  • Sort cached version directories by descending version name instead of filesystem mtime.
  • Add compareVersionDescending() for worker cache version ordering.
  • Add tests for major, minor, patch, equal-version, and realistic cache directory ordering.

Confidence Score: 5/5

Safe to merge with low risk.

The change is narrow and covered by targeted tests for the comparator behavior used by cache worker candidate ordering. No correctness or security issues were found in the changed files.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Executed a focused Bun test for worker-utils-cache-version-resolution.test.ts; the command exited with code 0 and a test log was produced.
  • Executed a broader Bun test across the shared tests; the command exited with code 0 and a comprehensive tests log was produced.
  • Ran the root typecheck; the command exited with code 0 and a typecheck log was produced.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/shared/worker-utils.ts Replaces cache worker candidate ordering with deterministic descending version-name comparison before resolving worker script paths; no issues found.
tests/shared/worker-utils-cache-version-resolution.test.ts Adds focused tests for descending version comparison across major, minor, patch, equality, and realistic cache listings; no issues found.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Resolver as resolveWorkerScriptPath()
  participant Cache as cache/thedotmack/claude-mem
  participant Comparator as compareVersionDescending()
  participant Worker as worker-service.cjs

  Resolver->>Cache: read version directories
  Cache-->>Resolver: cached version names
  Resolver->>Comparator: sort names descending by version components
  Comparator-->>Resolver: newest version first
  Resolver->>Worker: return first existing scripts/worker-service.cjs
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Resolver as resolveWorkerScriptPath()
  participant Cache as cache/thedotmack/claude-mem
  participant Comparator as compareVersionDescending()
  participant Worker as worker-service.cjs

  Resolver->>Cache: read version directories
  Cache-->>Resolver: cached version names
  Resolver->>Comparator: sort names descending by version components
  Comparator-->>Resolver: newest version first
  Resolver->>Worker: return first existing scripts/worker-service.cjs
Loading

Reviews (1): Last reviewed commit: "fix(worker): resolve cache version by se..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant