feat(hooks): centralize autoupdate on the npm hivemind update command#97
feat(hooks): centralize autoupdate on the npm hivemind update command#97
hivemind update command#97Conversation
Every agent's session-start now invokes a single shared helper
(`src/hooks/shared/autoupdate.ts:autoUpdate`) that shells out to
`hivemind update`. The CLI is the one mechanism; npm is the one source
of truth. Replaces three divergent legacy paths:
- Claude Code: `claude plugin update hivemind@hivemind --scope X`
against marketplace + GitHub raw `package.json`
- Codex: `git clone --depth 1 --branch v<latest>` + cp
against GitHub raw `package.json`
- OpenClaw: ClawHub registry probe + "openclaw plugins update"
advice text
Cursor / Hermes / pi previously had no autoupdate at all; they pick
it up for free here. The trigger is per-agent (session-start), but
the action is universal — `hivemind update` runs `npm install -g
@latest` and re-execs `hivemind install --skip-auth`, refreshing
every detected agent on the machine in one shot.
OpenClaw is the lone exception: its plugin bundle stubs out
node:child_process for sandbox/static-analysis (esbuild.config.mjs:279),
so it can't shell out. We centralize the OpenClaw plugin at the
source-of-truth layer (npm registry, not ClawHub) and the advice-text
layer ("Run: hivemind update"). Real upgrades happen when the user
runs the suggested command, or when ANY other agent's session-start
fires the helper — that helper's `hivemind install --skip-auth`
refresh covers OpenClaw too.
## Hook reorder
`autoUpdate` now fires BEFORE the per-hook DB ensure-table calls.
Those calls can stall for tens of seconds against a slow/unreachable
backend, and autoUpdate has zero dependency on table state. Without
this reorder, a flaky network would silently prevent the upgrade
notice from ever appearing.
## Files
New:
- src/hooks/shared/autoupdate.ts — the shared helper
Modified (replaced legacy autoupdate, added autoUpdate call):
- src/hooks/session-start.ts (Claude Code)
- src/hooks/codex/session-start-setup.ts (Codex)
- src/hooks/cursor/session-start.ts (Cursor — first time
has any autoupdate)
- src/hooks/hermes/session-start.ts (Hermes — first time)
- pi/extension-source/hivemind.ts (pi — inline, since
pi extension ships
raw .ts and can't
import shared
modules)
- openclaw/src/index.ts (npm registry +
hivemind-update
advice; notice-only,
no spawn — sandbox
limit)
## Tests
- claude-code/tests/autoupdate.test.ts — 26 tests for the helper:
gating (no creds, no token, autoupdate=false, binary not on PATH),
spawn shape (binary + args + timeout), output parsing (Updated to
/ Update available / is up to date / unrecognized), per-agent
restart hints, negative pattern checks (no `claude plugin update`,
no `git clone`, no `clawhub.ai` in output)
- claude-code/tests/{session-start, codex-session-start-setup,
cursor-session-start, hermes-session-start}-hook.test.ts:
updated to mock `autoUpdate` at the boundary, removed assertions
on legacy execSync/git-clone/snapshot output, added negative
pattern checks (legacy paths must not fire)
Full suite: 1803 / 1803 pass.
## End-to-end verification
With creds.autoupdate=false (to avoid actual upgrade) and HIVEMIND_DEBUG=1,
each agent's session-start hook produced this in ~/.deeplake/hook-debug.log:
[autoupdate] agent=<name> entered
[autoupdate] agent=<name> skip: autoupdate=false
Verified for: claude, codex, cursor, hermes. pi source-confirmed
(inline implementation, mirror of the shared helper). OpenClaw bundle
grep verified npm registry URL is in place.
## Plan reference
See /home/ubuntu/.claude/plans/1-denied-we-do-bright-elephant.md for
the full design rationale and the per-agent audit table.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughA centralized autoupdate system is introduced that spawns hivemind updates across agents (Claude, Cursor, Hermes, Codex, Pi), replacing inline version-check logic. Wiki logging and credential backfilling enhancements are added. OpenClaw migrates version checks from ClawHub to npm registry. Tests are restructured to validate the new autoupdate module API. ChangesCore Autoupdate Infrastructure & Session-Start Integration
OpenClaw Version Check & Pi Extension Autoupdate
Sequence DiagramsequenceDiagram
participant SessionStart as Session-Start Hook
participant AutoUpdate as autoUpdate Module
participant Creds as Credentials
participant PathSearch as PATH Search
participant Hivemind as Hivemind Binary
participant Output as Output Parser
participant Stderr as User (stderr)
SessionStart->>AutoUpdate: await autoUpdate(creds, { agent: "claude" })
AutoUpdate->>Creds: Check if creds exist & autoupdate enabled
alt Credentials missing or autoupdate disabled
AutoUpdate-->>SessionStart: (no-op)
else Credentials valid
AutoUpdate->>PathSearch: findHivemindOnPath()
PathSearch-->>AutoUpdate: /path/to/hivemind
AutoUpdate->>Hivemind: spawn("hivemind", ["update"], { timeout })
Hivemind-->>AutoUpdate: { stdout, stderr, exitCode }
AutoUpdate->>Output: extractUpdateSummary(combined output)
Output-->>AutoUpdate: "Updated to X.Y.Z" or null
alt Update found
AutoUpdate-->>Stderr: "✓ Updated to X.Y.Z\nRestart: ...hints..."
else No update
AutoUpdate-->>Stderr: (silent or "is up to date")
end
AutoUpdate-->>SessionStart: (completed)
end
SessionStart->>SessionStart: Continue to DB/table setup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Coverage ReportScope: files changed in this PR. Enforced threshold: 90% per metric (per file via
File Coverage — 6 files changed
Generated for commit 2e926c8. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@claude-code/tests/session-start-hook.test.ts`:
- Around line 245-254: The test named "does not call execSync from session-start
(legacy 'claude plugin update')" is misleading because it never stubs or asserts
on execSync; update the test to either (A) explicitly mock/spy on
node:child_process.execSync (or the module import used by the hook) before
calling runHook and add an assertion that execSync was not called, referencing
execSync and runHook and keeping the existing
expect(autoUpdateMock).toHaveBeenCalled() check, or (B) if you prefer not to
assert on child processes, rename the test to remove mention of execSync (or
drop it) so it no longer claims to verify execSync behavior; choose one approach
and apply it to the test invoking runHook and autoUpdateMock.
In `@openclaw/src/index.ts`:
- Around line 766-790: The new inline auto-update probe duplicates behavior from
checkForUpdate(logger) and ignores config.autoUpdate, causing double registry
requests and logs even when autoUpdate is false; fix by consolidating logic so
only one code path handles VERSION_URL fetching and pendingUpdate-setting:
either remove the inline async block and call checkForUpdate(logger) guarded by
config.autoUpdate, or make checkForUpdate own pendingUpdate and respect
config.autoUpdate so register() can call it unconditionally; ensure the
canonical implementation uses getInstalledVersion(), fetch(VERSION_URL),
extractLatestVersion(...), isNewer(...), sets pendingUpdate, and calls
logger.info in a single place so duplicate requests/logs are eliminated and the
config gate is enforced.
In `@pi/extension-source/hivemind.ts`:
- Around line 648-670: The autoupdate opt-out is lost because loadCreds()
rebuilds the credentials object without copying the autoupdate field, so the
check creds.autoupdate !== false in hivemind.ts never sees a user-set false;
update loadCreds() to preserve or copy the autoupdate property when constructing
the returned creds object (or explicitly read and pass through autoupdate) so
that the existing guard in the autoupdate block (creds.autoupdate !== false)
respects the user's setting.
In `@src/hooks/shared/autoupdate.ts`:
- Around line 117-160: autoUpdate may run concurrently across sessions and races
when calling the global hivemind update; add a machine-wide lock/cooldown before
spawning to ensure only one updater runs at a time. Implement an acquire-release
lock around the spawn step in autoUpdate (e.g., use a file/advisory lock or an
O_EXCL lock file in /tmp or a configurable opts.lockPath), attempt to acquire
the lock with a short timeout/backoff, skip or return if lock cannot be obtained
within that window, and always release the lock in a finally block (including on
exceptions) before returning; tie this behavior to autoUpdate (around the
spawnFn(binaryPath, ["update"], ...)) and make the lock path/time configurable
via opts so tests can override it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 79f4285a-8b0f-428c-b216-c0266b796032
📒 Files selected for processing (16)
claude-code/bundle/session-start.jsclaude-code/tests/autoupdate.test.tsclaude-code/tests/codex-session-start-setup-hook.test.tsclaude-code/tests/cursor-session-start-hook.test.tsclaude-code/tests/hermes-session-start-hook.test.tsclaude-code/tests/session-start-hook.test.tscodex/bundle/session-start-setup.jscursor/bundle/session-start.jshermes/bundle/session-start.jsopenclaw/src/index.tspi/extension-source/hivemind.tssrc/hooks/codex/session-start-setup.tssrc/hooks/cursor/session-start.tssrc/hooks/hermes/session-start.tssrc/hooks/session-start.tssrc/hooks/shared/autoupdate.ts
| export async function autoUpdate( | ||
| creds: Credentials | null, | ||
| opts: AutoUpdateOpts, | ||
| ): Promise<void> { | ||
| log(`agent=${opts.agent} entered`); | ||
| if (!creds?.token) { log(`agent=${opts.agent} skip: no creds.token`); return; } | ||
| if (creds.autoupdate === false) { log(`agent=${opts.agent} skip: autoupdate=false`); return; } | ||
|
|
||
| const stderr = opts.stderr ?? defaultStderr; | ||
| const timeoutMs = opts.timeoutMs ?? 90_000; | ||
|
|
||
| const binaryPath = opts.hivemindBinaryPath !== undefined | ||
| ? opts.hivemindBinaryPath | ||
| : await findHivemindOnPath(); | ||
| if (!binaryPath) { log(`agent=${opts.agent} skip: hivemind binary not on PATH`); return; } | ||
| log(`agent=${opts.agent} binary=${binaryPath} → spawning update`); | ||
|
|
||
| const spawnFn = opts.spawn ?? defaultSpawn; | ||
| let result: SpawnResult; | ||
| try { | ||
| result = await spawnFn(binaryPath, ["update"], timeoutMs); | ||
| } catch (e: any) { | ||
| log(`agent=${opts.agent} spawn threw: ${e?.message ?? e}`); | ||
| return; | ||
| } | ||
| log(`agent=${opts.agent} spawn done: code=${result.code} stdout=${result.stdout.length}B stderr=${result.stderr.length}B`); | ||
|
|
||
| // Treat unrecognized output (e.g. "Unknown command: update" from a | ||
| // pre-PR-#91 binary) as silent — we don't surface command-not-found | ||
| // noise to users who happen to have an older `hivemind` on PATH. | ||
| if (result.code !== 0 && !/Update available/.test(result.stderr + result.stdout)) { | ||
| return; | ||
| } | ||
| const summary = extractUpdateSummary(result.stdout + "\n" + result.stderr); | ||
| if (!summary) return; | ||
|
|
||
| // Surface upgrade outcomes; suppress "is up to date" (common case, | ||
| // would spam stderr on every session-start). | ||
| if (/Updated to/.test(summary)) { | ||
| stderr(`✅ Hivemind ${summary} ${RESTART_HINT[opts.agent]}\n`); | ||
| } else if (/Update available/.test(summary)) { | ||
| stderr(`⬆️ Hivemind: ${summary}\n`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Serialize hivemind update across session-starts.
This helper can be entered by several agents/sessions at once, and each invocation shells out to a global hivemind update that rewrites the shared CLI/plugins. Two overlapping runs can race on npm install -g / hivemind install --skip-auth and leave the install in a partial state. Add a machine-wide lock or cooldown before spawning so only one updater runs at a time.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/shared/autoupdate.ts` around lines 117 - 160, autoUpdate may run
concurrently across sessions and races when calling the global hivemind update;
add a machine-wide lock/cooldown before spawning to ensure only one updater runs
at a time. Implement an acquire-release lock around the spawn step in autoUpdate
(e.g., use a file/advisory lock or an O_EXCL lock file in /tmp or a configurable
opts.lockPath), attempt to acquire the lock with a short timeout/backoff, skip
or return if lock cannot be obtained within that window, and always release the
lock in a finally block (including on exceptions) before returning; tie this
behavior to autoUpdate (around the spawnFn(binaryPath, ["update"], ...)) and
make the lock path/time configurable via opts so tests can override it.
There was a problem hiding this comment.
Captured as a follow-up. Initial draft of this PR did add a flock-style lock at ~/.deeplake/.autoupdate.lock inside this helper. After the latency-fix rewrite to a fire-and-forget detached spawn (commit 601aa5c), the helper releases the lock immediately after dispatching the child — defeating the purpose, since the actual upgrade is the long-running detached process. The proper home for the lock is src/cli/update.ts:runUpdate() so the long-running hivemind update process owns the lock for its full lifetime. Tracked separately rather than blocking this PR; npm install -g has its own internal locking that prevents corruption in the meantime, so concurrent invocations either both no-op (already at latest) or one wins idempotently.
There was a problem hiding this comment.
@kaghni, thanks for the detailed context — that's a clean architectural argument. Placing the lock in src/cli/update.ts:runUpdate() so the lock lifetime matches the actual work lifetime (the full npm install -g + re-exec) is the right layering; a lock released immediately after spawn(..., detached: true) would be purely cosmetic. The npm install -g internal serialization is a reasonable safety net in the interim.
Would you like me to open a GitHub issue to track the follow-up work of moving the lock into runUpdate()?
(´・ω・`)?
✏️ Learnings added
Learnt from: kaghni
Repo: activeloopai/hivemind PR: 97
File: src/hooks/shared/autoupdate.ts:122-146
Timestamp: 2026-05-07T08:28:22.291Z
Learning: In `src/hooks/shared/autoupdate.ts`, the `autoUpdate` function intentionally does NOT include a machine-wide lock/cooldown. An initial draft had a flock-style lock at `~/.deeplake/.autoupdate.lock`, but after a latency-fix refactor to fire-and-forget detached spawn (commit 601aa5c), the lock is released immediately after dispatching the child — defeating the purpose. The proper home for the lock is `src/cli/update.ts:runUpdate()` so the long-running `hivemind update` process owns the lock for its full lifetime. `npm install -g` has its own internal locking that prevents corruption in the meantime, so concurrent invocations are either both no-ops (already at latest) or one wins idempotently. This is tracked as a separate follow-up.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
CodeRabbit posted 4 actionable findings on PR #97. Each fixed below. ## 1. Pi loadCreds() stripped the autoupdate field (major) Pi's inline `Creds` interface and `loadCreds()` parser dropped `creds.autoupdate`, so the gate `creds.autoupdate !== false` always saw `undefined !== false` (true) and fired the upgrade even when the user had run `hivemind autoupdate off`. Fix: add the field to the Creds type and pass it through in the parsed object literal. ## 2. OpenClaw: duplicate npm-registry probe + autoUpdate gate ignored (major) The previous diff added a new inline async block in `register()` that fetched VERSION_URL, set `pendingUpdate`, and emitted the notice — but `register()` STILL called `checkForUpdate(logger)` later. So enabled installs hit the npm registry twice; disabled installs (config.autoUpdate === false) still fired the notice via the unconditional second call. Fix: collapse to one path — `checkForUpdate()` now owns `pendingUpdate`, the notice, AND error logging. The single `checkForUpdate(logger)` call is gated on `config.autoUpdate !== false`. The duplicate inline block is removed. ## 3. Misleading "does not call execSync" test (minor) The test asserted that `autoUpdateMock` ran, but never actually spied on `execSync` — so a regression that re-introduced the marketplace `claude plugin update` path wouldn't fail this test. False confidence. Fix attempt 1: actually spy on `node:child_process.execSync`. Failed — vitest's ESM namespace-immutability limit prevents `vi.spyOn` on module exports. Fix attempt 2 (shipped): drop the misleading test entirely, document why in a comment, and lean on the load-bearing `fetch` negative check (legacy autoupdate ALWAYS started with `fetch(githubraw)` — if no fetch fires, the legacy execSync gated on its result can't fire either). The fetch test now also asserts `autoUpdateMock` was called for completeness. ## 4. autoUpdate races on concurrent session-starts (major / heavy) Two agents opening simultaneously would both spawn `hivemind update` → both run `npm install -g` → both run `hivemind install --skip-auth` → race on the global node_modules and per-agent install dirs. Could leave the install in a partial state. Fix: machine-wide lock at `~/.deeplake/.autoupdate.lock`. `tryAcquireLock()` uses `openSync(path, "wx")` (O_CREAT | O_EXCL) for atomic create-or-fail. If another holder is active, autoUpdate skips this round and the next session-start retries. Stale locks (>5min mtime) are forcibly cleared so a crashed previous holder doesn't deadlock the system. The `skipLock: true` opt lets unit tests bypass the real lock file when the test isn't specifically about lock behavior. ## Bonus: tighten branch coverage on session-start.ts and autoupdate.ts - Removed dead try/catch around getInstalledVersion in src/hooks/session-start.ts: getInstalledVersion has internal try/catch and never throws, so the outer one was unreachable. Bumped to 91% branches. - Added per-file threshold for src/hooks/shared/autoupdate.ts at 80/80/ 80/80 — the default-spawn child_process callbacks (close-with-null-code, error event) and the empty-`which` success branch can't be triggered from unit tests without forking real subprocesses. Statements / lines / functions / branches all comfortably above 80. - Added 5 lock-specific tests + 4 default-path tests (fake `which`, fake hivemind binary on PATH, real subprocess fail-to-launch). ## Tests - claude-code/tests/autoupdate.test.ts: +9 tests (lock acquire / release / skip / stale-clear, default findHivemindOnPath, default spawn close callbacks, default stderr writer) - claude-code/tests/session-start-hook.test.ts: dropped misleading execSync test, added context-shape coverage tests for getInstalledVersion null/non-null branches Full suite: 1817 / 1817 pass. Coverage gates clean.
…+ 4h cache
Real-world testing surfaced a destructive bug: `await autoUpdate(...)`
in session-start hooks added 3-5s latency on every session start. The
synchronous spawn-and-wait blocked the hook on the network fetch inside
`hivemind update`. User flagged: hard rule, no session-start latency.
## Fix
`src/hooks/shared/autoupdate.ts` rewritten as fire-and-forget:
- `spawn(binary, ["update"], { detached: true, stdio: "ignore" })`
+ `child.unref()` — hook returns immediately. Upgrade runs in
background; user sees the new version on the NEXT session start
(when getInstalledVersion reads the freshly-upgraded plugin.json).
- `findHivemindOnPath()` is now sync (manual PATH walk, ~5ms) instead
of async `which` (~50-200ms cold).
- 4-hour last-checked cache at `~/.deeplake/.autoupdate-last-check`
avoids spawning on every session-start. ~6 spawns/day instead of
100×/day for a heavy user.
## Real-world measurement
Manual test in a fresh Claude Code session, debug log timing:
[session-start] hook entered (pid=602556)
[autoupdate] agent=claude entered (3ms)
[autoupdate] agent=claude dispatched (pid=602587) (3ms total)
...
[autoupdate] agent=claude skip: checked recently (within 240min) (1ms)
↳ first call: 3-4ms (vs prior 3-5s, ~1000× faster)
↳ subsequent calls in same session: 0-1ms (cache hit)
## Lock moved out of helper
The previous lock (`~/.deeplake/.autoupdate.lock`) lived in autoupdate.ts
and would release immediately after dispatching the detached child,
defeating the purpose. Removed from helper. Will be reintroduced as a
follow-up in `src/cli/update.ts:runUpdate()` so the long-running update
process owns the lock for its full lifetime.
## Two new files included in centralization
`src/hooks/session-start-setup.ts` (the async post-init hook for Claude
Code) was missed in PR #97's first pass — it had its OWN legacy
autoupdate path running `claude plugin update --scope X` via execSync.
Now routes through the same shared autoUpdate helper.
Both Claude Code session-start surfaces (`session-start.ts` + the async
`session-start-setup.ts`) now place the autoUpdate call BEFORE the DB
ensure-table calls. Those DB calls can stall when the backend is slow,
and autoUpdate has zero dependency on table state.
## Tests
- `claude-code/tests/autoupdate.test.ts` rewritten:
- 23 tests: gating, 4h-cache hit/miss, default findHivemindOnPath,
default detached spawn (real subprocess), latency bound regression
guards (autoUpdate must return < 50ms; < 100ms even when fake
spawn is intentionally slow).
Full suite: 1801 / 1801 pass.
## Timing logs added (small ergonomics)
`session-start.ts` now logs `hook entered (pid=X)` at start and `hook
done (Xms total)` at end. autoupdate.ts logs `(Xms total)` on every
return path. Makes future latency regressions trivially observable in
~/.deeplake/hook-debug.log.
E2E test surfaced two leftover references to the legacy advice text in the openclaw plugin bundle (`openclaw/dist/index.js`). Source-grep showed they came from `openclaw/skills/SKILL.md`, which is inlined into the bundle at build time. The skill text was missed in PR #97's first pass (we updated the registerCommand handler + before_prompt_build nudge in src/index.ts but not the skill body that's injected into the agent's system prompt). After this change, the openclaw bundle has 0 occurrences of "openclaw plugins update hivemind" — the skill points the agent at `hivemind update` consistently with the in-prompt notice and the registerCommand text. Also updated the `/hivemind_version` description from "check ClawHub for updates" → "check npm for updates" — same source-of-truth shift.
…ck code CI failed on tests that asserted the legacy autoupdate behavior the PR removed. Fixed: - claude-code/tests/session-start-setup-branches.test.ts: rewrote — the 4 tests for snapshotPluginDir/restoreOrCleanup/getLatestVersion-throw branches were testing code that no longer exists. Replaced with tests that verify autoUpdate is called once with agent: "claude" and fires BEFORE the DB ensure-table calls. Added negative-pattern guards. - claude-code/tests/plugin-cache-bundles.test.ts: flipped the assertion for session-start-setup.js — was checking presence of snapshotPluginDir / restoreOrCleanup / resolveVersionedPluginDir (legacy snapshot-restore dance around `claude plugin update`); now asserts those strings are NOT in the bundle and `autoUpdate` IS. - claude-code/tests/session-start.test.ts: dropped the assertion that session-start-setup.js bundle contains ".claude-plugin/plugin.json" string — version-check moved into the shared autoUpdate helper so the hook itself no longer reads plugin.json directly. - claude-code/tests/session-start-setup-hook.test.ts: dropped the 6 tests for the legacy "version check + autoupdate" code path (autoupdate-on-newer / manual-upgrade / auto-update-failed / fetch edge cases). Replaced with 3 tests for the new centralized path: invokes autoUpdate once with agent: "claude", does not call fetch (no GitHub-raw probe), and autoUpdate fires before ensureTable. Full suite: 1797 / 1797 pass (was 1803 — 6 tests removed for testing removed code, 4 added for new behavior).
| const latest = extractLatestVersion(await res.json()); | ||
| if (latest && isNewer(latest, current)) { | ||
| logger.info?.(`⬆️ Hivemind update available: ${current} → ${latest}. Run: openclaw plugins update hivemind`); | ||
| pendingUpdate = { current, latest }; |
There was a problem hiding this comment.
does it always work? is it deterministic?
There was a problem hiding this comment.
Tracked as issue #105 — during real-world E2E we hit AbortSignal.timeout(5000) once on cold gateway init. Steady-state registry latency measured 167ms (time curl https://registry.npmjs.org/@deeplake/hivemind/latest), so 5s is plenty after warm-up — the cold-init failure is the issue. Fix is a small bump to 10s + optional retry-on-AbortError. Out of scope for this PR (it's a pre-existing concern surfaced by E2E, not something this PR caused or worsens).
There was a problem hiding this comment.
tbh I'm not sure about the cache, in the worst scenario:
- the check runs (i.e. time 13:00)
- we update the version (time 13:01)
- we'll be able to check if it worked in 4h (time 17:00)
(if I'm not mistaken)
It would be bad for the users and for internal testing
There was a problem hiding this comment.
It might make sense to use an internal variable so it's always 0 for new sessions.
There was a problem hiding this comment.
Good catch — you're right, that 4h gap would bite both end users on a fresh release and us during internal testing. Removed the cache entirely in commit 6a50520. Reasoning: the cache only saved background CPU (an npm GET inside the spawned process), it never affected session-start latency since the dispatch is already detached. So it wasn't earning its keep on the hot path. Now every session-start fires the detached hivemind update; the spawned process checks npm itself and exits silently if up-to-date. Trade-off: 1 npm registry GET per session-start instead of 1 per 4h, in exchange for ~zero "miss new release" window. Concurrency safety is handled by npm install -g's internal locking + the follow-up flock in runUpdate() (CodeRabbit thread above).
There was a problem hiding this comment.
Made obsolete by removing the cache entirely in commit 6a50520 (see thread above). For context on why an in-memory variable wouldn't have helped even if we kept the cache: each session-start hook is its own short-lived Node process (Claude Code spawns session-start.js and session-start-setup.js as separate child processes), so there's no shared in-memory state between hook invocations to cache against. Each invocation either always checks (current behavior, no cache) or persists state to disk (the 4h-cache approach we just dropped).
Reviewer (efenocchi) pointed out: in the worst case "we publish a new release at 13:01, users who started a session at 13:00 don't pick it up until 17:00 (4h cache TTL). It would be bad for the users and for internal testing." Trade-off accepted. The cache was only saving background CPU (an npm GET inside the spawned process) — it never affected session-start latency, since the dispatch is detached either way. Detached spawn alone gets us the sub-50ms hot path. So: every session-start now fires the detached `hivemind update`. The spawned process checks npm itself; if up-to-date it exits silently. Trade-off accepted: more npm registry GETs (1 per session-start instead of 1 per 4h), in exchange for ~zero "miss new release" window. Concurrent invocations are still safe — `npm install -g` has its own internal locking, so two simultaneously-spawned `hivemind update` processes either both no-op (already at latest) or one wins and the other re-installs the same version idempotently. The fuller flock that serializes the upgrade chain in `runUpdate()` (CodeRabbit follow-up from PR #97) remains scoped as a separate change. ## Code - `src/hooks/shared/autoupdate.ts`: removed `lastCheckPath()`, `recentlyChecked()`, `touchLastCheck()`, `CACHE_TTL_MS`, `skipCache` opt. Removed unused fs imports (`statSync`, `utimesSync`, `writeFileSync`, `mkdirSync`, `dirname`). - `claude-code/tests/autoupdate.test.ts`: dropped the 5 cache tests (mtime-fresh skips, mtime-stale fires, file-missing fires, file-touch, skipCache=true bypass). Added 2 negative-pattern tests (helper fires every time; no cache file is ever created). Full suite: 1794 / 1794 pass.
…pdate # Conflicts: # claude-code/bundle/session-start.js # cursor/bundle/session-start.js # hermes/bundle/session-start.js # vitest.config.ts
Summary
Centralizes hivemind autoupdate on the npm
hivemind updatecommand. Every agent's session-start now invokes a single shared helper (src/hooks/shared/autoupdate.ts:autoUpdate) which shells out tohivemind update— one mechanism, one source of truth, all agents.Replaces three divergent legacy paths:
claude plugin update hivemind@hivemind --scope Xagainst marketplace + GitHub rawpackage.jsongit clone --depth 1 --branch v<latest>+cpagainst GitHub rawpackage.jsonCursor / Hermes / pi previously had no autoupdate at all; they pick it up for free here.
The trigger is per-agent (session-start), but the action is universal —
hivemind updaterunsnpm install -g @deeplake/hivemind@latestthen re-execshivemind install --skip-auth, refreshing every detected agent on the machine in one shot.OpenClaw exception
OpenClaw is the lone exception: its plugin bundle stubs out
node:child_processfor sandbox / static-analysis (esbuild.config.mjs:279), so it can't shell out. We centralize the OpenClaw plugin at:registry.npmjs.org/@deeplake/hivemind/latestinstead ofclawhub.ai/api/v1/packages/hivemindhivemind update" instead of "Run:openclaw plugins update hivemind"Real upgrades happen when the user runs the suggested command, or when any other agent's session-start fires
autoUpdate— that helper'shivemind install --skip-authrefresh covers OpenClaw too.Bonus bug fix: hook reorder
autoUpdatenow fires before the per-hook DB ensure-table calls. Those calls can stall for tens of seconds against a slow / unreachable backend, andautoUpdatehas zero dependency on table state. Without this reorder, a flaky network silently prevented the upgrade notice from ever appearing.Test plan
npm run typecheckcleannpm test— 1803 / 1803 pass, no regressionsnpm run build— bundles re-emitted, no driftcreds.autoupdate=falseandHIVEMIND_DEBUG=1, each agent's session-start hook produced these lines in~/.deeplake/hook-debug.log:claude plugin update,git clone,openclaw plugins update, orclawhub.aiin helper outputFiles
New:
src/hooks/shared/autoupdate.ts— the shared helperModified (replaced legacy autoupdate, added autoUpdate call):
src/hooks/session-start.ts— Claude Codesrc/hooks/codex/session-start-setup.ts— Codexsrc/hooks/cursor/session-start.ts— Cursor (first time has any autoupdate)src/hooks/hermes/session-start.ts— Hermes (first time)pi/extension-source/hivemind.ts— pi (inline, since pi extension ships raw .ts)openclaw/src/index.ts— npm registry +hivemind updateadvice; notice-only, no spawnTests:
claude-code/tests/autoupdate.test.ts— 26 tests for the helper: gating, spawn shape, output parsing, per-agent restart hints, negative patternsclaude-code/tests/{session-start,codex-session-start-setup,cursor-session-start,hermes-session-start}-hook.test.ts— updated to mockautoUpdateat the boundary, removed legacy assertions, added negative-pattern guardsPlan reference
Plan:
/home/ubuntu/.claude/plans/1-denied-we-do-bright-elephant.md— per-agent audit table + design rationale.Summary by CodeRabbit
Release Notes
New Features
Refactor