LearnNewsExamplesServices
Frontmatter
id13049
titleFleet Manager instance lifecycle: start / stop / restart / health
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-grace
createdAtJun 13, 2026, 2:58 AM
updatedAtJun 21, 2026, 2:16 AM
githubUrlhttps://github.com/neomjs/neo/issues/13049
authorneo-opus-grace
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 13, 2026, 5:04 AM

Fleet Manager instance lifecycle: start / stop / restart / health

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jun 13, 2026, 2:58 AM

Context

First leaf of the Fleet Manager sub-epic #13015 (pillar 1 of the Agent Harness #13012). Operator direction (2026-06-13): "agent harness biggest ROI." This is the FM MVP's start/stop verb — consumed by every later FM surface (settings-pane, NL-MCP) — and the one slice not gated by the #12984 session-id canonicalization.

The Problem

#13015's recorded operator pain: running the swarm means N manually-launched harness instances (Claude Desktop windows + terminal Codex), each started and killed by hand. The registry (sibling leaf #13031) holds the definition; nothing yet acts on it — there is no programmatic start/stop/restart/health for a defined agent's harness process.

The Architectural Reality

  • New Brain-side singleton FleetLifecycleService under the existing ai/services/fleet/ domain (sibling to FleetRegistryService) — structural-pre-flight Stage-1 fast-path.
  • Process supervision via Node's child_process (spawn / signal / reap) — Node-side only.
  • Registry integration: resolves the agent definition via FleetRegistryService.getAgent(id) and the PAT via the Brain-internal resolveCredential(id), injected into the child env.
  • Launch resolution: the launch command comes from the agent's metadata.launch = {command, args}harnessType is classification, not a launcher, so the service never guesses a brittle per-type command. (Refined from an earlier "per-harnessType strategy" — see Evolution.)
  • Stderr supervision: the child's stderr is drained into a bounded tail so a noisy harness cannot block on pipe backpressure.

The Fix

A FleetLifecycleService (singleton) under ai/services/fleet/:

  • start(id) — resolve def + credential, spawn the harness (launch from metadata.launch; PAT in the child env), track PID + state, drain stderr.
  • stop(id) — graceful SIGTERM, then SIGKILL on timeout.
  • restart(id)stopstart.
  • status(id) / listRunning() — running / stopped / PID / uptime / last-exit / bounded recent-stderr.

Contract Ledger

Field Value
Target Surface ai/services/fleet/FleetLifecycleService.mjs (NEW) — start / stop / restart / status / listRunning
Source of Authority #13015 (FM MVP: "spawn/stop one external harness instance"); #13031 (the registry it consumes)
Proposed Behavior Brain-side process supervision of registry-defined harness instances. Launch command from the agent's metadata.launch. PAT injected Node-side into the child env (inject-if-present), never surfaced, never in argv. Child stderr drained (only a non-secret byte count retained — content never stored or surfaced, since the child may echo the PAT).
Fallback Unknown agent / no metadata.launchstart refuses (throws). Spawn failure → state: failed + captured error. Null credential → start proceeds without a token (a tokenless local harness is valid); the secret never leaks.
Docs Service JSDoc (Anchor & Echo).
Evidence Unit tests with a stubbed spawn (no real binary): lifecycle round-trip + credential-injection (child env only) + refusals + stderr drain & no-leak (a secret echoed to stderr is never surfaced via status()).

Decision Record impact

aligned-with ADR 0020 (harness concept) + #13012 / #13015 design rules. depends-on #13031 (registry — merged). The #12984 session-id canonicalization gates the identity-env + wake-subscription provisioning — out of scope here (separate provisioning leaf); this leaf does process supervision only.

Acceptance Criteria

  • FleetLifecycleService in ai/services/fleet/ with start / stop / restart / status / listRunning.
  • start(id) resolves def + credential from FleetRegistryService, spawns the harness (launch from metadata.launch), injects the PAT into the child env only (never argv).
  • stop graceful (SIGTERMSIGKILL on timeout); restart stops then starts fresh.
  • status / listRunning report running / stopped / pid / uptime / last-exit / stderr byte-count (never stderr content).
  • Credential policy: PAT injected if present (tokenless agents start); the PAT never leaks (argv / record / log). start refuses an unknown agent or one with no launch spec.
  • Stderr supervision: child stderr drained (no pipe backpressure); only a non-secret byte count retained — content never surfaced through status().
  • Unit tests with a stubbed spawn: lifecycle round-trip + credential-injection + refusals + stderr.
  • Linked as a sub-issue of #13015.

Out of Scope

  • Identity-env + wake-subscription provisioning#12984-gated (separate provisioning leaf).
  • Repo provisioning (clone/worktree) — separate FM leaf.
  • The settings-pane UI (Body-side, apps/) — separate leaf.
  • The Electron build root — parent-epic leaf (#13033).
  • Wedged / idle watchdog detection — enrichment beyond v1 health.

Avoided Traps

  • PAT in argv — rejected: secrets go in the child env, never argv (visible in ps).
  • Browser-side process control — rejected: Brain-side only (two-hemisphere rule).
  • Undrained piped stderr — rejected: a piped-but-undrained stderr blocks a noisy child on pipe backpressure; drained, counting bytes only.
  • Surfacing captured child stderr — rejected: the child's stderr is untrusted and may echo the injected PAT; only a non-secret byte count is retained, never content — a status() read carries no secret. (Caught cycle-2, @neo-gpt.)
  • Per-harnessType hardcoded launchers — rejected: the service can't reliably guess how to launch an arbitrary external harness; the operator specifies metadata.launch.
  • Real external binaries in unit tests — rejected: tests stub the spawn.

Evolution

The contract was refined during implementation (PR #13054), reconciled here after @neo-gpt's cross-family review flagged ticket-vs-code drift:

  • Launch via metadata.launch, not a per-harnessType command mapharnessType classifies an agent; it does not encode a launch command, and hardcoding per-type launchers is brittle/wrong across installs.
  • Credential policy = inject-if-present, not refuse-on-null — the unconditional invariant is never leak (never argv / record / log); refusing a tokenless agent is policy, and a local harness may legitimately have no PAT. start still refuses an unknown agent / missing launch spec.
  • Stderr capture → byte-count only (cycle-2) — the cycle-1 fix surfaced a bounded stderr tail through status(); since an external harness may echo its injected PAT to stderr, that re-opened the credential boundary. Now only a non-secret byte count (stderrBytes) is retained — content is never stored or surfaced.

Related

Parent: #13015 (Fleet Manager MVP). Epic: #13012. Consumes: #13031 (registry — merged). #12984 (gates the provisioning leaf, not this one). PR: #13054. Source archaeology: Discussion #10119.

Release classification: post-release (FM MVP product line; nothing v13.x-blocking).

Origin Session ID: 7c6c8fd9-219d-4eae-9d70-ef18ec945a14 Retrieval Hint: "fleet manager instance lifecycle start stop restart health child_process spawn harness FleetLifecycleService"

Authored by Claude Opus 4.8 (Claude Code, @neo-opus-grace / Grace).

tobiu referenced in commit 9e5b7c0 - "feat(ai): Fleet Manager instance lifecycle service (#13049) (#13054) on Jun 13, 2026, 5:04 AM
tobiu closed this issue on Jun 13, 2026, 5:04 AM