Skip to content

Report the gateway's real state at the end of setup - #11

Merged
dimavrem22 merged 3 commits into
mainfrom
fix/setup-restarts-live-gateway
Jul 28, 2026
Merged

dimavrem22 merged 3 commits into
mainfrom
fix/setup-restarts-live-gateway

Conversation

@dimavrem22

@dimavrem22 dimavrem22 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Setup could finish with a green sign-off over a gateway that was dead or still running the previous .env. Three commits, all found by running the sibling bridges on a fresh macOS install.

1. A rerun left the gateway on the old config

configureAutostart()'s background path called startDaemon(), which refuses on a live PID:

if (existing !== undefined && pidAlive(existing, send)) {
  console.error(`Gateway already running (pid ${existing}). Use \`stop\` or \`restart\`.`);
  return 1;
}

The wizard ignored that return code, so setup still exited 0 while the gateway kept serving the .env it had just replaced — a rotated signing key or switched identity silently did not take effect. The only hint was a console.error line the operator had to notice and act on.

The boot-autostart path already got this right, and its comment names the failure: "enable --now is a no-op on an already-running service, which would keep a stale gateway on old credentials — so enable, then always restart." The background path now matches, and says what it did:

Start it in the background now (until you reboot)? [y/n] (default: yes): y
  A background gateway is already running (pid 4242) on the old config.
  Restarting it so it picks up the new settings.

No second prompt — answering yes to "start it" when one is running can only mean "make it running with the config I just wrote".

runningDaemonPid() exposes the paths-plus-liveness probe so the wizard does not reassemble daemonPaths + readPidFile + pidAlive itself. Both lifecycle calls go through injectable Ctx deps, matching startDaemonFn/installAutostartFn.

2. The sign-off read the same whether or not anything was running

Setup complete. / Check it anytime with: inkbox-opencode doctor printed either way, and never named the identity. So the one case where you needed a start command never mentioned one.

configureAutostart() now reports liveness — read off exit codes startDaemon/restartDaemon were already returning and discarding. Live:

╭─────────────────────────────────────────────────────╮
│ Your OpenCode agent is set up and running on Inkbox. │
│                                                     │
│   Inkbox identity:   test-agent                     │
│   Check its health:  inkbox-opencode doctor         │
╰─────────────────────────────────────────────────────╯

Not live: the old sign-off, plus the inkbox-opencode start line that was missing.

3. "Started" was claimed before the gateway had survived

startDaemon returns 0 the moment it has spawned the child, and never checks it is still there:

writePidFile(pidFile, child.pid);
console.log(`Gateway started (pid ${child.pid}). Logs: ${logFile}`);
return 0;

A bad bind kills the gateway a second or two later — well after that return — so setup printed "started" and then the banner over a process that was already gone. This bridge was in fact more exposed than the Python siblings, which at least slept 1.5s and re-read the pid.

Now polls for liveness across a 5s window before claiming it, and points at the log when the pid disappears:

  The gateway exited right after starting.
  Check the log:  ~/.inkbox-opencode/gateway.log
  Then rerun:     inkbox-opencode start

Window and delay are both injectable so tests neither wait nor spin.

What this bridge already got right

Worth recording, since the sibling PRs carry fixes for all of these:

  • .env path — the wizard writes to path.join(gatewayHome(env), ".env"), never cwd. The Python bridges fell back to cwd while their daemons read the state dir, so running setup from a home directory put an API key in ~/.env.
  • doctor — already loads env files via envFileCandidates/readEnvFile. The Python ones called read_config() directly and reported every credential missing while the gateway ran fine on them.
  • No fork — already spawn(process.execPath, [entry, "run"]). The Python bridges forked a wizard that had already driven the SDK, handing the child a half-initialised copy of httpx's connection pools; it could die before logging was configured. Their fix was to adopt exactly the shape this bridge already had.

Separate, not fixed here

All three bridges default to port 8767 (src/config.ts:305 and the equivalents). Running any two on one machine collides on the bind. Picking distinct defaults is a coordinated change across three repos rather than something to slip into this PR.

Notes

  • The scripted-answers helper that walks a test to the autostart step moved to module scope so the new cases can reach it.
  • Version 0.2.5 → 0.2.6, CHANGELOG updated.

659 tests pass, tsc --noEmit clean.

Siblings: inkbox-ai/claude-code-plugin#47, inkbox-ai/codex-plugin#29.

dimavrem22 and others added 2 commits July 28, 2026 03:09
The autostart step's background path called startDaemon(), which logs
"Gateway already running (pid N). Use `stop` or `restart`." and returns 1
when a PID is live. On a rerun the wizard still exited 0 while the
gateway kept serving the .env it had just replaced -- a rotated signing
key or a new identity silently did not take effect.

The boot-autostart path already handles this: it always issues a service
restart rather than `enable --now` for exactly this reason. Give the
background path the same property by restarting when a PID is live, and
say so rather than leaving the operator to read it out of a log line.

runningDaemonPid() exposes the paths-plus-liveness probe so the wizard
can ask without reassembling it, and both lifecycle calls are injectable
so tests never touch a real process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Setup always signed off with "Setup complete / check it with doctor",
whether or not anything was actually running, and never named the
identity the agent had just been wired to.

configureAutostart now reports whether a gateway ended up running, by
reading the exit codes startDaemon/restartDaemon already return. When one
is live, close on a banner naming the identity and the health command.
When nothing is listening, keep the sign-off and add the start command
that was missing from it.

The scripted-answers helper that walks a test to the autostart step moves
to module scope so the new cases can reach it too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dimavrem22

Copy link
Copy Markdown
Contributor Author

Pushed 49d45f0 — ports the closing banner from inkbox-ai/hermes-agent-plugin#77.

Setup always signed off the same way whether or not anything was running, and never named the identity the agent had just been wired to. configureAutostart() now returns whether a gateway ended up live — reading the exit codes startDaemon/restartDaemon already return, no new probing. When one is live:

╭─────────────────────────────────────────────────────╮
│ Your OpenCode agent is set up and running on Inkbox. │
│                                                     │
│   Inkbox identity:   test-agent                     │
│   Check its health:  inkbox-opencode doctor         │
╰─────────────────────────────────────────────────────╯

When nothing is listening the old sign-off stays — plus inkbox-opencode start, which it never mentioned even though that was exactly the situation where you needed it.

4 more tests driving the wizard end to end: banner content, box geometry, the to-do fallback, and a daemon that exits 1 (which must not print the banner). The scripted-answers helper that walks a test to the autostart step moved to module scope so the new cases can reach it. 657 tests, tsc clean.

startDaemon returns 0 the moment it has spawned the child -- it never
checks that the gateway survived. A bad bind kills it a second or two
later, well after that return, so setup printed "started" and then the
status banner over a process that was already gone.

Poll for liveness across a short window before claiming it, and point at
the log when the pid disappears. Both the window and the delay are
injectable so tests neither wait nor spin.

The sibling Python bridges needed a larger fix here (they forked the
caller, which this one never did -- it already spawns a fresh interpreter
the way the service units do). This is the one piece that ports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dimavrem22

Copy link
Copy Markdown
Contributor Author

Pushed 3952164 — the one fix from inkbox-ai/claude-code-plugin#47 and inkbox-ai/codex-plugin#29 that ports here. I checked all five; four were already fine.

What ported: startDaemon returns 0 the moment it has spawned the child and never checks that the gateway survived:

writePidFile(pidFile, child.pid);
console.log(`Gateway started (pid ${child.pid}). Logs: ${logFile}`);
return 0;

A bad bind kills it a second or two later, well after that return — so setup printed "started" and then the status banner over a process that was already gone. This bridge was actually more exposed than the Python two, which at least slept 1.5s and re-read the pid.

Now polls for liveness across a 5s window before claiming it, and points at the log when the pid disappears. Window and delay are both injectable so tests neither wait nor spin.

What did not port, and why:

Fix in the Python bridges Here
Restart a live bridge instead of no-opping already ported in fce7b49
Status banner on a live gateway already ported in 49d45f0
Wizard wrote .env to cwd, daemon read the state dir already correct — path.join(gatewayHome(env), ".env"), never cwd
doctor never loaded a .env already correct — uses envFileCandidates/readEnvFile
start forked the caller, inheriting broken SDK state never applied — this bridge already does spawn(process.execPath, [entry, "run"]), which is exactly what the Python two were just changed to

That last row is worth noting: this bridge had the right daemon shape all along. The Python ones were forking a wizard that had already driven the SDK, which handed the child a half-initialised copy of httpx's pools — dying before logging was even configured.

Separate, fleet-wide, not fixed here: all three bridges default to port 8767 (src/config.ts:305 and the equivalents). Running any two on one machine collides on the bind. Worth picking distinct defaults, but that is a coordinated change across three repos rather than something to slip into this PR.

2 new tests: a gateway that dies right after starting must not print the banner, and the confirm must poll rather than probe once. 659 tests, tsc clean.

@dimavrem22 dimavrem22 changed the title Restart a live gateway instead of refusing on rerun Report the gateway's real state at the end of setup Jul 28, 2026
@dimavrem22
dimavrem22 marked this pull request as ready for review July 28, 2026 07:05
@dimavrem22
dimavrem22 merged commit d4f41e6 into main Jul 28, 2026
29 of 31 checks passed
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