Skip to content

Phase 1A.1: bundleable server foundation for desktop app - #169

Merged
aterrylu merged 2 commits into
mainfrom
terry/desktop-app
May 13, 2026
Merged

Phase 1A.1: bundleable server foundation for desktop app#169
aterrylu merged 2 commits into
mainfrom
terry/desktop-app

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Summary

Phase 1A.1 of the desktop-app initiative: adds the runtime contract + build pipeline that Phase 1B's Electron desktop app will use to spawn the autonomos-server as a child process. Purely additivemake dev, make prod, and pm2 continue to work unchanged.

What ships

Area Change
Runtime New --port and --embedded argv flags; --help short-circuits before migrations
Runtime Localhost-only binding (127.0.0.1) in embedded mode
Runtime Structured stdout signal AUTONOMOS_READY port=<N> after listener is up
Runtime Fixes latent fragility — existsSync(dashboardDist) returned true on a tsc-only artifact dir; now checks for index.html explicitly
Build bun build --target=node produces a Node-runnable bundle + embedded dashboard
Test ./scripts/test-1a1-isolated.sh — full smoke test with isolated config dir and dedicated port; never touches ~/.autonomos/
Docs Consolidated design notes, Phase 1A.1 proposal, Phase 1B/1C sketches

Build deviation from original proposal

bun build --compile (static binary) was blocked by an ABI mismatch: Bun 1.3.10 runtime uses ABI 137, node-pty's prebuilt is ABI 141. Both the compiled binary path AND --target=bun fail because the Bun runtime can't load node-pty at all.

Pivoted to --target=node. The bundle runs under Node, which loads node-pty cleanly. For Phase 1B, Electron bundles Node + this bundle in the .app — same end-user outcome.

Static-binary aspiration deferred until either Bun's ABI matches node-pty OR we switch the PTY implementation to Bun's native API. Captured in ~/.claude/projects/.../memory/project_nodepty_bun_compile.md so future-us doesn't waste time retrying the same approach.

Phase 1B integration contract

Spawn:      node dist/<platform>/index.js --port=0 --embedded
Discovery:  parse "AUTONOMOS_READY port=<N>" from child stdout
Shutdown:   send SIGTERM; child exits within ~2s

Test plan

  • ./scripts/test-1a1-isolated.sh passes end-to-end:
    • AUTONOMOS_READY signal received in 2s
    • /api/host returns 200
    • Dashboard / serves embedded HTML
    • SIGTERM triggers clean shutdown in 2s
    • ~/.autonomos/ untouched (isolation works)
  • Regression: make dev path (tsx) still boots the server cleanly with the new code
  • Regression: existing --help short-circuits before migrations / provider validation
  • Type-check passes: tsc -b clean
  • Optional follow-up: run on a Linux box to verify cross-platform bundle works (deferred — not blocking)

What's NOT in this PR (deferred)

  • Bun static-binary path (blocked by node-pty, see deviation note above)
  • CLI subcommand framework (status, stop, upgrade, install-service) — Phase 1C
  • install.sh web installer — Phase 1C
  • launchd/systemd-user supervision — Phase 1C
  • pm2 → OS-native migration — Phase 1C
  • The Electron shell itself — Phase 1B

🤖 Generated with Claude Code

Adds the runtime contract + build pipeline that Phase 1B's Electron desktop app
will use to spawn the autonomos-server as a child process. Purely additive — the
existing make dev / make prod / pm2 paths continue to work unchanged.

What lands:
- packages/server/src/cli-args.ts: --port and --embedded argv parser, --help
- packages/server/src/embedded-mode.ts: localhost-only bind + AUTONOMOS_READY
  stdout signal for parent-process discovery
- packages/server/src/index.ts: wires both, also fixes a latent fragility where
  existsSync(dashboardDist) returned true on a tsc-only artifact dir (now checks
  for index.html explicitly, prefers _embedded_dashboard over fallback path)
- packages/server/build/embed-dashboard.ts: copies dashboard/dist into the
  server tree so the bundler sees it
- packages/server/build/build-binary.ts: orchestrates bun build --target=node,
  copies embedded dashboard next to bundled JS for runtime resolution
- scripts/test-1a1-isolated.sh: full isolated smoke test (separate config dir,
  dedicated port, never touches ~/.autonomos/)
- docs/research/: consolidated design notes + Phase 1A.1 proposal + Phase 1B/1C
  sketches capturing the full plan

Build deviation from original proposal: bun build --compile is blocked by a
hard ABI mismatch between Bun 1.3.10 (ABI 137) and node-pty's prebuilt
(ABI 141). Both the static-binary path AND --target=bun fail because the Bun
runtime can't load node-pty at all. Pivoted to --target=node so the bundle
runs under Node, which loads node-pty cleanly. Phase 1B's Electron bundles
Node + this bundle. Static-binary aspiration deferred until either Bun's ABI
matches or PTY is refactored to Bun's native API.

Smoke test results (./scripts/test-1a1-isolated.sh on darwin-arm64):
  ✓ AUTONOMOS_READY signal received in 2s
  ✓ /api/host returns 200
  ✓ Dashboard / serves embedded HTML
  ✓ SIGTERM triggers clean shutdown in 2s
  ✓ ~/.autonomos/ untouched (test ran in isolation)
  ✓ make dev path still boots cleanly (regression check)

Contract Phase 1B will rely on:
  Spawn: node dist/<platform>/index.js --port=0 --embedded
  Discovery: parse "AUTONOMOS_READY port=<N>" from child stdout
  Shutdown: send SIGTERM, child exits within ~2s

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

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — Phase 1A.1 is cleanly additive: new CLI flags, embedded-mode plumbing, and a node-target bundle pipeline behind a smoke test that exercises the readiness signal end-to-end. Existing make dev / make prod / pm2 paths are untouched.

Spot-checks that came back clean:

  • --port=0 path: cliArgs.port ?? (Number(process.env.PORT) || 3000) correctly preserves an explicit 0 (since ?? is nullish-only) and feeds it to serve(), then server.address() is read inside the listening callback so the OS-assigned port is what gets announced. Matches the stated contract.
  • parsePort: rejects NaN, non-integers, negatives, and >65535; --port with missing value throws with a clear message.
  • Dashboard resolution fix: checking for index.html (not directory existence) genuinely fixes the latent tsc -b/Vite-bundle confusion described in the PR.
  • AUTONOMOS_READY signal: emitted on stdout in the listening callback, after the actual port is known — exactly what an Electron parent needs to pattern-match.
  • Bind host: 127.0.0.1 in embedded mode, undefined (host default) otherwise. No behavioral change for standalone.

Minor / follow-up only (not blocking):

  • The parseCliArgs call + --help exit are sandwiched between two groups of import statements (lines 14–48 in index.ts). Due to ESM hoisting this still runs after all imports, so the help short-circuit only avoids the heavy top-level work (provider validation, seedDefaultTemplates, migrateIfNeeded) — not the module-load side effects. That matches the PR's claim, but biome's organizeImports will likely shuffle the imports together on next save, which is the cleaner shape.
  • parseCliArgs itself has no unit test. It's small and pure, and the isolated smoke test exercises the happy path via --port/--embedded, so this is a low-priority backfill — useful if more flags accrete.
  • AUTONOMOS_READY is emitted before initGateway() / resumeActiveAgents() / initScheduler() run. The HTTP listener is up so requests succeed, but agent state may still be hydrating for a short window. Matches existing standalone init ordering, so not a new issue — worth a sentence in the Phase 1B integration contract so the Electron shell doesn't assume "ready = fully hydrated."

…ss semantics

CI failure: biome's organizeImports rule flagged the parseCliArgs call sandwiched
between import groups. Reviewer @nox-0x flagged the same issue separately.
Moved all imports to the top, then the parseCliArgs + --help short-circuit, then
the rest of the imperative startup work. Also added a comment clarifying that
--help only avoids imperative work, not import-time side effects (since ESM
hoists all imports regardless).

Review observation #3 (readiness semantics): AUTONOMOS_READY signal means "HTTP
listener accepting connections" — NOT "agents fully hydrated." Gateway init,
resumeActiveAgents, and scheduler startup run in the same tick but may finish
slightly later. Documented this in:
  - embedded-mode.ts (next to the signal emitter)
  - phase-1b-sketch.md (in the integration contract section)

So Phase 1B's Electron shell can load the webview as soon as the signal fires,
with the understanding that "fully populated UI" may lag by ~ms.

Verified:
- npx biome check packages/ → clean (only pre-existing warnings unrelated to 1A.1)
- ./scripts/test-1a1-isolated.sh → all checks still pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aterrylu
aterrylu merged commit 1ccc22c into main May 13, 2026
1 check passed
@aterrylu
aterrylu deleted the terry/desktop-app branch May 13, 2026 06:52
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.

2 participants