Skip to content

fix(update): restart pm2 genie-serve on stale-inode + truthful verify probe - #1675

Merged
namastex888 merged 2 commits into
devfrom
fix/update-restart-stale-serve
May 6, 2026
Merged

namastex888 merged 2 commits into
devfrom
fix/update-restart-stale-serve

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

genie update was leaving the running pm2 genie-serve process serving pre-update bytes from a deleted node_modules inode. The verify probe couldn't surface it because readServerHealth was a tautology — it returned { version: VERSION } from the calling CLI's compile-time constant rather than probing the daemon's actual state.

Repro (today, on this host)

$ genie update --next      # bumps 4.260506.2 → 4.260506.3
$ genie --version
4.260506.3                 # ✓ CLI binary on disk
$ ls -la /proc/2831346/cwd
... -> /home/genie/.bun/install/global/node_modules/.old-F13E840E5DFD4535 (deleted)
                           # ✗ daemon still running .2 from a deleted inode
$ cat ~/.genie/logs/update-diagnostics-*.json | jq .verify
{ "kind": "ok", "cliVersion": "4.260506.2", "serverVersion": "4.260506.2" }
                           # ✗ verify reports "ok" — it lied

Fix

1. Truthful readServerHealth — reads /proc/<pid>/cwd (Linux) to detect the kernel (deleted) marker AND reads the on-disk package.json for the true installed version. Non-Linux platforms degrade gracefully.

2. New restartServeIfStale — runs in runPostUpdateMaintenanceSafe BEFORE verify. Detects pm2 + genie-serve + stale inode, issues pm2 restart genie-serve --update-env, polls for new pid. No-op when pm2 is missing, no genie-serve entry, or daemon already on live inode.

3. New daemon-stale-inode VerifyResult variant — banner carries pid, deleted-cwd path, and the pm2 restart remediation. Exits 1 alongside health-unreachable so CI / scripted updaters notice instead of silently completing a half-update.

Test plan

  • decideVerify precedence: inode-stale wins over version-mismatch (stronger signal)
  • Post-restart ok regression lock (no false positives once restart succeeds)
  • formatVerifyBanner surfaces pid, (deleted) cwd, and pm2 restart genie-serve
  • Source-shape locks: restart-before-verify ordering, exit-code 1 path, Linux-gated /proc probe, pm2 jlist parsing
  • 57 existing update tests + 5 new variants pass; typecheck + lint clean
  • Live dogfood: run genie update --next once @next 4.260506.4 is published with this fix and confirm /proc/<new-pid>/cwd no longer carries (deleted) after the flow completes

🤖 Generated with Claude Code

… probe

`genie update` left the running pm2 `genie-serve` process serving pre-update
bytes from a deleted node_modules inode (visible via `/proc/<pid>/cwd ->
.../.old-<hash> (deleted)`) until the operator manually ran
`pm2 restart genie-serve`. The verify probe couldn't surface it because
`readServerHealth` was a tautology — it returned `{ version: VERSION }` from
the calling CLI's compile-time constant rather than probing the daemon's
actual state.

Two-pronged fix:

1. `readServerHealth` now reads `/proc/<pid>/cwd` (Linux) to detect the
   kernel `(deleted)` marker, AND reads the on-disk `package.json` for the
   true installed version. Non-Linux platforms degrade gracefully (no
   inode probe, optimistic same-version inference).

2. New `restartServeIfStale` runs in `runPostUpdateMaintenanceSafe` BEFORE
   the verify probe. Detects pm2 + `genie-serve` + deleted-inode cwd, then
   issues `pm2 restart genie-serve --update-env` and polls for a new pid.
   No-op when pm2 is missing, no genie-serve entry, or daemon already on
   live inode.

New `daemon-stale-inode` `VerifyResult` variant carries pid + cwd + the
`pm2 restart` remediation in the banner. Exits 1 (alongside
`health-unreachable`) when verify catches a still-stale state — CI /
scripted updaters now see a hard failure instead of a silent half-update.

Tests:
- `decideVerify` precedence: inode-stale wins over version-mismatch
- post-restart `ok` path regression lock
- banner contains pid, cwd `(deleted)` marker, and pm2 restart string
- source-shape locks: restart-before-verify ordering, exit-code 1 path,
  Linux-gated `/proc` probe, pm2 `jlist` parsing

57 existing update tests + 5 new variants pass.
@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 06eb6f4f-a19c-456e-9e4b-24da53d1548d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/update-restart-stale-serve

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements detection and automatic remediation for stale daemon inodes, a condition where the genie-serve process continues running from a deleted directory after a package update. The changes include adding Linux-specific probes via /proc, integrating an automatic PM2 restart into the maintenance lifecycle, and updating the verification banner with actionable fix instructions. Feedback focused on a performance optimization for the readInstalledPackageVersion function, recommending memoization to avoid the overhead of repeated synchronous shell executions during health polling cycles.

Comment on lines +283 to +300
function readInstalledPackageVersion(): string | null {
const candidates: string[] = [
join(homedir(), '.bun', 'install', 'global', 'node_modules', '@automagik', 'genie', 'package.json'),
];
const npmPrefix = safeExec('npm prefix -g', 1500);
if (npmPrefix) {
candidates.push(join(npmPrefix, 'lib', 'node_modules', '@automagik', 'genie', 'package.json'));
}
for (const path of candidates) {
try {
const pkg = JSON.parse(readFileSync(path, 'utf-8')) as { version?: unknown };
if (typeof pkg.version === 'string' && /^\d+\.\d+/.test(pkg.version)) return pkg.version;
} catch {
// try next candidate
}
}
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The readInstalledPackageVersion function performs a synchronous shell execution (npm prefix -g) which can be relatively slow (often 100ms+). Since this function is called inside readServerHealth, which in turn is called within the pollHealth loop (line 343) every 500ms, it introduces significant overhead and latency to the health probe.

Because the installed version on disk is guaranteed to be static once the maintenance phase begins, you should memoize this result to avoid repeated shell-outs during the polling cycle.

let cachedInstalledVersion: string | null | undefined;

function readInstalledPackageVersion(): string | null {
  if (cachedInstalledVersion !== undefined) return cachedInstalledVersion;

  const candidates: string[] = [
    join(homedir(), '.bun', 'install', 'global', 'node_modules', '@automagik', 'genie', 'package.json'),
  ];
  const npmPrefix = safeExec('npm prefix -g', 1500);
  if (npmPrefix) {
    candidates.push(join(npmPrefix, 'lib', 'node_modules', '@automagik', 'genie', 'package.json'));
  }
  for (const path of candidates) {
    try {
      const pkg = JSON.parse(readFileSync(path, 'utf-8')) as { version?: unknown };
      if (typeof pkg.version === 'string' && /^\d+\.\d+/.test(pkg.version)) {
        cachedInstalledVersion = pkg.version;
        return cachedInstalledVersion;
      }
    } catch {
      // try next candidate
    }
  }
  cachedInstalledVersion = null;
  return null;
}
References
  1. It is acceptable to use hardcoded numeric limits (magic numbers) in non-critical fallback logic, especially when they serve as intentional caps to prevent performance issues like excessive I/O.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 109c9b83e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +408 to +409
const result = await runCommandSilent('pm2', ['jlist'], undefined, 3000);
if (!result.success) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid spawning PM2 daemon during routine update checks

runPostUpdateMaintenanceSafe always calls restartServeIfStaleSafe, and this helper immediately executes pm2 jlist here. PM2 commands talk to a daemon and will launch one when none exists (per PM2 CLI/docs behavior around daemon startup), so on machines that merely have pm2 installed but do not supervise genie-serve, genie update can create an unintended background PM2 daemon and ~/.pm2 state as a side effect. Add a pre-check that PM2 is already active or that genie-serve is actually PM2-managed before calling pm2 jlist.

Useful? React with 👍 / 👎.

…le-serve

# Conflicts:
#	src/genie-commands/__tests__/update.test.ts
@namastex888
namastex888 merged commit 7cd0b72 into dev May 6, 2026
16 checks passed
@automagik-genie
automagik-genie deleted the fix/update-restart-stale-serve branch September 25, 2026 04:51
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