gadget/gadget-drone/docs/subprocess.md
Rob Colbert e155a7ffcf Phase 2: SubProcess observability in DroneManager and DroneInspector
Adds real-time subprocess monitoring to the IDE via the callback-chain and
subscribe/broadcast socket patterns:

Shared types (packages/api):
- New subprocess.ts message types: SubProcessStat, RequestProcessStatsMessage,
  SubscribeDroneMessage, UnsubscribeDroneMessage
- New socket events in socket.ts: requestProcessStats, subscribeDrone,
  unsubscribeDrone (ClientToServer); drone:log, drone:status (ServerToClient)

Drone (gadget-drone):
- onRequestProcessStats handler calls SubProcessService.ps() + summarize(),
  returns typed SubProcessStat[] with status detection (exitCode/killed)

Backend routing (gadget-code):
- SocketService: getDroneSessionByRegistrationId(), droneMonitorIndex map,
  addDroneMonitor()/removeDroneMonitor()/broadcastToMonitors()
- CodeSession: requestProcessStats proxy, subscribeDrone/unsubscribeDrone
- DroneSession: broadcast drone:log + drone:status to monitor subscribers
  in addition to existing chat-session routing

Frontend socket layer:
- SocketClient: requestProcessStats(), subscribeDrone(), unsubscribeDrone()
- drone:log + drone:status events forwarded to event bus

Frontend components:
- LogRenderer — reusable log rendering core extracted from LogPanel
- SubProcessTable — btop-style process table with status dots
- DroneMonitorGauge — canvas oscilloscope waveform (studio-equipment aesthetic)
- DroneMonitor — 3-gauge container (CPU/red, NETWORK/cyan, FILE I/O/green)

Frontend pages:
- DroneManager: live log streaming, SubProcessTable, DroneMonitor gauges,
  1-second polling, auto-scroll log with pause-on-scroll-up
- Home/DroneInspector: process count summary, mini LogRenderer, navigate-to-manager link

Documentation:
- Updated gadget-drone/docs/subprocess.md Phase 2 section
- Updated gadget-code/docs/ui-design-guide.md Phase 2 section
- Updated docs/socket-protocol.md with new events, sequences, and patterns
2026-05-14 13:52:59 -04:00

9.4 KiB

Gadget Drone Sub-Processes

The Gadget Code agent manages child processes through gadget-drone's SubProcess system. In the Gadget ecosystem, we refer to a managed child process spawned by gadget-drone as a SubProcess.

Phase Status

  • Phase 1 (Complete): SubProcessService + SubprocessTool internal to drone.
  • Phase 2 (Complete): SubProcess observability in DroneManager and DroneInspector. Live log streaming via subscribe/broadcast pattern. Canvas-based resource gauges (CPU, Network, File I/O — mocked). See Phase 2 Details below.

SubProcessService

Source

SubProcessService is a singleton service that manages child processes on behalf of Gadget Drone. It is started and stopped as part of the standard service lifecycle in gadget-drone.ts. On stop, all managed subprocesses are killed (SIGINT with graceful timeout, falling back to SIGKILL) and their log files are removed.

API

Method Signature Description
create (project: IProject, cmd: string, args?: string[]) => Promise<SubProcess> Spawns a child process, starts writing stdout/stderr to log files under <gadgetDir>/subprocess-logs/.
ps () => SubProcess[] Returns an array of all managed subprocesses.
summarize (sp: SubProcess) => SubProcessSummary Returns a serializable summary (without ChildProcess/WriteStream refs).
killPid (pid: number) => Promise<boolean> Kills a specific subprocess by PID and removes its log files.
killProject (projectId: GadgetId) => Promise<void> Kills all subprocesses for a given project.
killSubProcess (sp: SubProcess) => Promise<boolean> Kills a subprocess and removes its log files.
killAll () => Promise<boolean> Kills all managed subprocesses and removes all log files.
haltPid (pid: number) => Promise<boolean> Stops a subprocess by PID but retains its log files for inspection.
haltSubProcess (sp: SubProcess) => Promise<boolean> Stops a subprocess but retains its log files.
getLog (pid: number, stream: "stdout" | "stderr") => Promise<string> Reads the full content of a subprocess's stdout or stderr log file.

Lifecycle

The service is registered in GadgetDrone.startServices() / stopServices(). During stop(), killAll() is called, ensuring no orphaned processes remain when the drone shuts down.

Process Termination

Termination follows a graceful escalation:

  1. Send SIGINT to the process.
  2. Wait up to 5 seconds for the process to exit.
  3. If still alive, send SIGKILL.
  4. Wait up to another 5 seconds.
  5. Remove stdout/stderr listeners.
  6. Close log file streams.

For kill, log files are deleted after termination. For halt, log files are preserved.

SubprocessTool

Source

The subprocess tool is registered in the agent's toolbox for modes: Build, Test, Ship, Develop. It provides a single function with a cmd parameter that dispatches to the appropriate operation.

Tool Definition

Name: subprocess Category: system

Parameters:

Parameter Type Required Description
cmd string (enum) yes One of: create, list, kill, killAll, halt, log
command string for create The executable to spawn (e.g., node, npm, python)
args string[] for create Command-line arguments for the spawned process
pid number for kill, halt, log The managed process ID
which string (enum) for log "stdout" or "stderr"

Commands

create

Spawns a new child process in the project directory. Logs stdout and stderr to <gadgetDir>/subprocess-logs/.

Response:

SUBPROCESS CREATED
pid: 12345
stdout: /path/to/.gadget/subprocess-logs/subproc-12345.stdout.log
stderr: /path/to/.gadget/subprocess-logs/subproc-12345.stderr.log

list

Returns all currently managed subprocesses with their PIDs, project slugs, and timestamps.

Response:

SUBPROCESS LIST
pid: 12345 | project: my-project | created: ... | updated: ...

kill

Terminates a subprocess and removes its log files. Uses SIGINT with graceful escalation.

Response:

SUBPROCESS KILLED
pid: 12345

killAll

Terminates all managed subprocesses and removes all log files.

Response:

SUBPROCESS KILLALL
All managed subprocesses have been terminated and their logs removed.

halt

Terminates a subprocess but retains its log files. Useful for inspecting logs after process exit.

Response:

SUBPROCESS HALTED
pid: 12345
The process has been stopped. Log files are retained for inspection.

log

Reads stdout or stderr output from a subprocess's log file.

Response:

SUBPROCESS LOG (stdout)
pid: 12345
---
<log content>

Integration Points

System Prompts

All agent modes include a Process Management block in their system prompt. The block is loaded from process-management-block.md and rendered via {{process_management_block}} in the mode templates. This tells the agent about the subprocess tool and how to use it.

Shutdown

gadget-drone.ts calls SubProcessService.stop() during shutdown, which triggers killAll() to clean up any remaining child processes.

Tests

File What it covers
src/services/subprocess.test.ts SubProcessService: create, ps, killPid, killAll, haltPid, getLog, error cases
src/tools/system/subprocess.test.ts SubprocessTool: all 6 commands, parameter validation, service error handling

Phase 2: SubProcess Observability

Architecture

Phase 2 exposes the drone's managed subprocesses to the frontend via a callback-chain pattern over Socket.IO, identical to the existing fileTreeRequest/fileReadRequest flow:

IDE (DroneManager.tsx)
  ──emit("requestProcessStats", registrationId, cb)──▶
                                                        │
                                          CodeSession.onRequestProcessStats()
                                            ──droneSession.socket.emit("requestProcessStats", cb)──▶
                                                                                                      │
                                                                                        GadgetDrone.onRequestProcessStats()
                                                                                          │ SubProcessService.ps()
                                                                                          │ .summarize() each
                                                                                          │ → SubProcessStat[]
                                                                                          │
                                                                                        ◀──cb(true, { processes })──
                                            ◀──ack callback fires──
  ◀──cb(success, data)──

Socket Protocol

Direction Event Signature
IDE → Backend requestProcessStats (registrationId: string, cb: RequestProcessStatsCallback) => void
Backend → Drone requestProcessStats (cb: RequestProcessStatsCallback) => void (no registrationId — drone identifies itself)

Callback type:

type RequestProcessStatsCallback = (
  success: boolean,
  data?: { processes?: SubProcessStat[]; message?: string }
) => void;

SubProcessStat Type

Defined in packages/api/src/messages/subprocess.ts:

interface SubProcessStat {
  pid: number;
  command: string;
  args: string[];
  projectId: GadgetId;
  projectSlug: string;
  projectName: string;
  status: "running" | "stopped" | "error";
  createdAt: string;   // ISO 8601
  updatedAt: string;   // ISO 8601
  stdoutFileName: string;
  stderrFileName: string;
}

Handler Behavior

The onRequestProcessStats handler in gadget-drone.ts:

  1. Calls SubProcessService.ps() to get all tracked subprocesses
  2. For each, calls summarize() for serializable fields
  3. Determines status: checks sp.process.exitCode !== null || sp.process.killed
  4. Extracts command and args from sp.process.spawnargs
  5. Returns cb(true, { processes }) on success, cb(false, { message }) on error

Live Log Streaming (Subscribe/Broadcast)

In addition to request/response stats, the drone continuously emits log events through its GadgetLogTransportSocket. Phase 2 adds a monitor broadcast path:

  1. IDE sends subscribeDrone(registrationId) → Backend adds IDE's socket to a monitor set
  2. IDE sends unsubscribeDrone(registrationId) → Backend removes from monitor set
  3. Drone emits logDroneSession.onLog() forwards to chat session AND broadcasts drone:log to all monitoring sockets
  4. Frontend receives drone:log events and renders via LogRenderer

The monitor tracking lives in SocketService.droneMonitorIndex — a Map<registrationId, Set<socketId>>. Cleanup happens automatically on socket disconnect.

Polling Interval

  • DroneManager: 1-second interval polling requestProcessStats
  • DroneInspector (dashboard): 1-second interval polling
  • Resource gauges (CPU, Network, File I/O): 200ms internal animation (mocked data)
  • All timers are killed on unmount or drone deselection