Skip to content

Detached engine lifecycle: adopt-safe restart, fleet-aware self-shutdown, Quit command, and UI toggle (#1594) - #1603

Merged
jeonghun-jj-lee merged 10 commits into
feature/free-tier-fleetfrom
amico/issue-1594-detached-engine-lifecycle
Sep 28, 2026
Merged

jeonghun-jj-lee merged 10 commits into
feature/free-tier-fleetfrom
amico/issue-1594-detached-engine-lifecycle

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

Summary

Implements #1594 and its four sub-issues as one integration branch:

Slice Issue What
S1 #1595 Adopt-safe engine lifecycle — seeds a ServerManager on the adopted path so Stop/Restart work on every window
S2 #1596 Engine /keepalive self-shutdown — fleet-aware idle predicate (grace × turns × subscribers × role-exempt)
S3 #1597 Amicode: Quit command — stop engine + close window in one action, with in-flight warning
S4 #1598 Engine on/off toggle in the app status cluster — push-state/bridge-action split, hidden on fleet clients

Changes

S1 — Adopt-safe lifecycle (874213bc)

  • ServerManager.seed() static method: creates a manager in running-daemonized state from the handshake's PID/port
  • Shared wireReadyState() function extracted from cold-spawn and adopted paths (chat-open guarded to first-ready)
  • Adopted path now constructs a full ServerManager, so Stop/Restart work identically on every window
  • 5 new tests (server_manager + server_lifecycle)

S2 — /keepalive self-shutdown (ec8eabe0)

  • Engine-side POST /keepalive route (authenticated, reads graceSeconds from body)
  • Self-shutdown timer with fleet-aware idle predicate: self-exit only when grace elapsed AND 0 in-flight turns AND 0 SSE subscribers AND not role-exempt
  • AMICO_ENGINE_ROLE_EXEMPT=1 env flag for fleet server/hub roles
  • SSE connection tracking at the HTTP server level
  • 10 new tests (engine_self_shutdown)

S3 — Quit command (82b698ea)

  • amicode.quit command with stop-then-close ordering (stop before window teardown, otherwise detach lets the engine survive)
  • In-flight turn warning with confirm gate
  • 5 new tests (quit_command)

S4 — Engine toggle (c21fa602)

  • Extension→app engine-state push (on/booting/off) at every lifecycle transition
  • amicode.stopServer added to bridge allowlist
  • SolidJS toggle in the status popover: green/yellow/gray dot, locked while booting, hidden on fleet clients
  • Fleet-role push on app-ready for client detection
  • 5 + 11 new tests (engine_state_push + engine-toggle)

Test Results

297 test files passed, 5017 tests green, 0 failures (60 skipped = pre-existing fleet e2e + slow tests).
2 test files skipped are pre-existing fleet_peer_e2e.test.ts (SSH forwarding infra, unrelated).

Closes

Closes #1595, closes #1596, closes #1597, closes #1598.
Part of #1594.

amicode-ci added 8 commits September 27, 2026 21:28
…d path (#1595)

On any adopted window (every reload after the first cold-spawn), the extension
now creates a ServerManager via the new `seed()` static method, which sets the
manager into the running/daemonized state with the adopted engine's port and PID.

Changes:
- ServerManager.seed(opts, { port, pid }) — creates a manager representing an
  already-running engine. stop() kills by PID, start() cold-spawns fresh.
- Extracted wireReadyState() — shared ready-wiring (SSE connect, keepalive,
  stray sweep, chat-open, provider signal) used by both cold-spawn onReady
  and adopted-path inline boot. Chat-open fires only on first-ready.
- Adopted path seeds serverManager with the same options a cold-spawn uses,
  so amicode.restartServer kills the adopted engine and respawns correctly.
- Hash computation (binaryHash, configHash) moved before the adopt/cold-spawn
  split so the seeded manager's afterHealthy writes a valid handshake.
- detach() path in deactivate is unchanged — no kill-on-deactivate.
Extension → App engine-state push:
- pushEngineState(on|booting|off) broadcasts to all live app panels
- Wired at every lifecycle transition: onReady → on, keepalive gone → off,
  restart → booting then on, stop → off, start fail → off
- pushFleetRole() broadcasts fleet topology (client/server/standalone)
- Both fire on app-ready (persistent) so new panels get current state

Bridge allowlist:
- Added amicode.stopServer to BRIDGE_ALLOWED_COMMANDS (companion to
  the existing amicode.restartServer)

App-side engine toggle component:
- engine-toggle.ts: pure parsers + command sender (no DOM deps, testable)
- engine-toggle.tsx: SolidJS component in the status popover body
  - Green dot / yellow pulsing dot / gray dot for on/booting/off
  - Clicking on → stops engine, clicking off → restarts engine
  - Locked (non-interactive, cursor-not-allowed) while booting
  - Hidden on fleet-client windows (fleet-role message)
  - Defaults to 'booting' before first push (safe cold-boot state)

Tests:
- Extension: pushEngineState posts correct message shape (5 tests)
- Extension: BRIDGE_ALLOWED_COMMANDS includes amicode.stopServer
- App: parseEngineStateMessage/parseFleetRoleMessage parsers (11 tests)
- App: sendEngineCommand sends correct bridge commands
@coderabbitai

coderabbitai Bot commented Sep 28, 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: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 3beb6147-d058-4f8a-be9b-2cbc82f4b879

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

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.

amicode-ci added 2 commits September 27, 2026 22:50
- Rename engine-toggle.ts → engine-toggle-utils.ts to avoid TS5056 collision
  with engine-toggle.tsx (both produce engine-toggle.d.ts)
- Drop .ts/.tsx extensions from imports (tsgo TS5097)
- Widen closeWindow type to PromiseLike<void> (vscode.commands.executeCommand
  returns Thenable, not Promise — TS2322 on CI's stricter tsc)
- Update manifest hashes for the renamed/changed files
…1229)

Fix 1: session.ts getUsage — guard Decimal.mul against non-numeric cost
fields (e.g. {} from malformed costInfo) with safeNum helper.

Fix 2: system.ts — add Muse Glimmer model detection and {{MODEL_NAME}}
substitution in PROMPT_META for both Muse Spark and Muse Glimmer. Add
provider-ID-based Kimi matching for kimi-for-coding/moonshotai/moonshotai-cn.

Fix 3: request.ts — add x-parent-session-id header to opencode provider
branch (was only in the non-opencode branch).

Fix 4: registry.ts — change webSearchEnabled from exact providerID match
to prefix match (startsWith) so opencode-go and similar variants work.

Fix 5: provider.ts — narrow Bedrock DeepSeek prefix from 'deepseek' to
'deepseek.r1' so native model IDs like deepseek.v3.2 are not erroneously
prefixed with us. while cross-region profiles like deepseek.r1-v1:0 still
get the prefix.

Fix 6: compaction.ts — serialize conversation as text ([User]:, [Assistant]:,
[Assistant tool call]:, [Tool result]:, [Attached ...]) into buildPrompt's
context array and send as a single user message instead of separate model
messages + user prompt. Plugin context appended after </conversation> tags.
Handles file attachments in user messages.
@jeonghun-jj-lee
jeonghun-jj-lee marked this pull request as ready for review September 28, 2026 03:31
@jeonghun-jj-lee
jeonghun-jj-lee merged commit 2484f96 into feature/free-tier-fleet Sep 28, 2026
11 of 12 checks passed
@jeonghun-jj-lee
jeonghun-jj-lee deleted the amico/issue-1594-detached-engine-lifecycle branch September 28, 2026 03:33
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