* feat(runner): HAPI_DISABLE_VERSION_HANDOFF opt-out for mtime self-restart
The heartbeat in cli/src/runner/run.ts triggers spawnHappyCLI(['runner','start'])
+ process.exit(0) when getInstalledCliMtimeMs() differs from startedWithCliMtimeMs.
The same mtime guard fires in controlClient.isRunnerRunningCurrentlyInstalledHappyVersion
when a fresh CLI invocation inspects the live runner.
For operators who own process supervision (systemd, tmux, custom rebuild
pipelines, etc.), source-file mtimes shift for reasons unrelated to npm
upgrades. The clean exit defeats Restart=on-failure under systemd and
leaves the machine offline.
Setting HAPI_DISABLE_VERSION_HANDOFF=1 in the runner's environment now skips
both checks while keeping the rest of the heartbeat (session pruning, state
file persistence) intact. Default behavior is unchanged for npm consumers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runner): preserve original argv across self-restart and verify handoff
The mtime-driven self-restart in cli/src/runner/run.ts spawned
`hapi runner start` with no arguments, then process.exit(0)'d
unconditionally after a 10s sleep. Two failure modes:
1. The forwarded `runner start-sync` lost the operator's --workspace-root
flags (anything passed at the original invocation). Browse + spawn
silently degraded to "no workspace roots".
2. If the replacement runner failed to come up at all (build was mid-flight,
binary missing, etc.) the original runner still exited cleanly. Under
systemd Restart=on-failure that means no runner is brought back, and
the machine drops off the hub until manual intervention.
Changes:
- persistence.ts: add startedWithArgv?: string[] to RunnerLocallyPersistedState
- run.ts: snapshot process.argv.slice(2) at startup, persist it on initial
state write and on every heartbeat, replay it as the new runner's argv
(default to ['runner','start-sync'] when nothing was captured)
- controlClient.ts: new waitForRunnerHandoff(oldPid, {timeoutMs}) polls
runner.state.json for a different live PID
- run.ts: only clearInterval + process.exit(0) when handoff is confirmed.
On spawn failure or 30s timeout, refresh the mtime baseline (so we don't
respawn-loop on the same drift) and stay alive so the machine keeps
serving.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runner): address Codex review findings on #814
Two Major correctness fixes flagged by upstream Codex review on PR #814:
(1) Stale-mtime poisoning on failed handoff (run.ts:854,867)
The previous failure paths assigned
startedWithCliMtimeMs = installedCliMtimeMs
which the next heartbeat persisted to runner.state.json. Downstream
isRunnerRunningCurrentlyInstalledHappyVersion() then reported the
still-stale runner as current, masking the failure until the *next*
genuine mtime change. Symptom: an mtime change that briefly failed
to hand off would be silently forgotten.
Fix: leave startedWithCliMtimeMs immutable. Gate handoff entry on
a new nextHandoffAttemptAt timestamp; failure paths bump it by
HANDOFF_RETRY_BACKOFF_MS (5 min) via deferHandoffRetry(). The
heartbeat continues to write the honest "still on the old code"
mtime, and the runner naturally re-attempts after the cooldown.
(2) HAPI_DISABLE_VERSION_HANDOFF not honored by live runner
(controlClient.ts:192, persistence.ts)
The env var was only checked in the invoking CLI process. Under the
documented systemd use case the env is set on the service unit but
NOT on the operator's interactive shell - so a shell `hapi runner
start` would still treat mtime drift as stale and kill the supervised
runner during a rebuild. The exact regression this layer was built
to prevent.
Fix: capture HAPI_DISABLE_VERSION_HANDOFF at runner start time into
state.startedWithVersionHandoffDisabled, persisted via the heartbeat.
The controlClient mtime check now OR's the live env var with the
persisted snapshot, so any caller honours the running runner's
opt-out regardless of their own environment.
Tests: cli typecheck clean; 14/14 runner unit tests pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runner): address Codex #814 [Major] argv-capture + handoff race
Two additional Major findings on the runner self-restart layer that
were not addressed in a49fc57:
1. run.ts:672 - process.argv.slice(2) returns ['start-sync', ...] in
compiled binary mode (raw argv is [hapi, runner, start-sync, ...]),
so the handoff spawned `hapi start-sync ...` which resolveCommand
treats as an unknown top-level and falls back to Claude. Replaced
with getCliArgs() (the project's canonical argv normalizer) plus a
defensive guard that falls back to ['runner', 'start-sync'] if the
captured argv does not begin with 'runner'.
2. run.ts:892 - waitForRunnerHandoff did not actually keep the old
runner alive. The child's startRunner() unconditionally called
stopRunner() before acquiring the lock or writing its own state,
so the parent's /stop handler resolved shutdown and exited BEFORE
the child committed. If the child then failed (lock contention,
auth error, anything between stopRunner and writeRunnerState),
the machine went offline with no runner at all.
New handoff protocol:
- Parent sets HAPI_RUNNER_HANDOFF_FROM_PID=<pid> on the spawned
child's env, then releases the lock BEFORE entering
waitForRunnerHandoff (breaks the parent-holds-lock /
child-needs-lock-to-write-state deadlock).
- On wait-timeout the parent re-acquires the lock (long-retry, 30s)
and defers retry; if re-acquire fails (third party took the
lock) the parent exits cleanly so it does not stay alive without
the lock invariant.
- Child detects the env signal; if state.pid matches and that pid
is alive, this is an authorized handoff: skip stopRunner(),
skip the version-match early-exit, and acquire the lock with a
longer retry window (60 attempts x 500ms) so it waits through
the parent's asynchronous release.
CLI typecheck clean. 14/14 runner unit tests still pass. The wider
46/664 failures in the CLI suite are pre-existing in this branch
(unrelated: AppServerEventConverter, cursorEventConverter, hook
server, Query) - baseline before this commit has 42+; my changes do
not regress them.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Consolidate process lifecycle management (kill, check alive) into a new
utility module with proper Windows/Unix handling, replacing scattered
process.kill() calls with consistent async APIs.
Replace CLI version string comparison with file modification time (mtime) based detection. This provides a more reliable way to detect when the CLI binary has been updated, especially for bun-compiled executables where package.json may not be accessible.
Changes:
- Add getInstalledCliMtimeMs() utility to check CLI binary or package.json mtime
- Store startedWithCliMtimeMs in daemon state on startup
- Use mtime comparison in version check loop for daemon auto-restart
- Move version flag handling to early CLI startup before daemon initialization
- Fix machine active state merging to prefer newer activeAt timestamp
This improves daemon restart behavior when CLI is updated via package managers.
Consolidate CLI and server runtime directories from ~/.config/hapi/ (CLI) and ~/.hapi-server/ (Server) to a single ~/.hapi/ directory. Unify environment variables from HAPI_HOME_DIR (CLI) and HAPI_BOT_DATA_DIR (Server) to a single HAPI_HOME variable across both applications. Update all documentation and configuration references accordingly. Bump bun-types to 1.3.5.
This commit rebrands the project from "Happy" to "HAPI" throughout the codebase, including documentation, comments, logs, and tool references. It also adds comprehensive README files for the server and web components, clarifies the monorepo structure in AGENTS.md and root README.md, and removes the outdated roadmap.md file.
Changes include:
- Rebrand references from Happy to HAPI in CLI, server, and web components
- MCP tool names updated from mcp__happy__ to mcp__hapi__
- Process/service names updated consistently
- New server/README.md with deployment and configuration guide
- New web/README.md with stack and development instructions
- Updated root README.md with quickstart guide
- Updated AGENTS.md with cleaner structure documentation
- Removed cli/roadmap.md (now superseded by documentation)
Implements comprehensive token management with intelligent priority resolution.
Token sources (highest to lowest priority): environment variable, settings file,
interactive prompt. Changes include:
- Updated home directory from ~/.happy to ~/.config/hapi (env var: HAPI_HOME_DIR)
- Added cliApiToken field to Settings interface for persistent storage
- Made cliApiToken mutable in Configuration class (private field with getter/setter)
- Created ui/tokenInit.ts module for token initialization with fallback prompt
- Enhanced auth command: login (interactive input), logout (clear token), status (show source)
- Updated doctor command to correctly identify token source and mask saved token in output
- Token initialization called early in startup flow to ensure availability before API use