Part of Daemon runtime.
Under daemon.driver: code a job scope's Session is lib/office/code/job-driver.ts (Daemon › Code driver says which turns it takes). Its id, driverId, is a uuid in the scope's record (.forge/runtime/daemons/<id>.json), made once and kept across turns and forge dash restarts; a value that is not a uuid is replaced. While a turn runs the scope reports running, busy and sessionId: driverId, and facts().ids includes it, so the dashboard and the reconciler see the job driven by its own session.
A turn:
- a session child left from a fallback turn stops first;
- then
forge job claim <id> --session <driverId>(exit 3, another live driver: every open request of the turn, whatever its type, is closedrejected,job is driven by session <driver>; exit 4, blocked: each non-resume request is still applied as below, then the resumes are closeddonewith the CLI's message; any other failure:rejectedwith it, and anerror); - then each request of the turn, oldest first, through
forge job apply <id> --request <rid> --session <driverId>(argv holds only the job id and a request id in the request grammar, never request text): exit 0 logsappliedwithrequest,requestType,status(done,rejectedorpending, a plain resume left for the step loop),resultand the request'screatedAt; exit 3 logsapply-skipped; anything else is anerrorandlastError, and the turn goes on; - then
forge job advance <id> --session <driverId> --epoch <epoch> --json, acted on through the moves table (lib/office/code/moves.ts, pure).
A spawn starts one forge job agent run … --role <role> --ipc per role in [action, ...spawns] that has no runner of this driver in flight, all in one tick, waits for each to report started (or exit), then advances at once when the action is parallel and otherwise after a runner exits. A wait on agents waits for the next runner exit while one is live, and otherwise ends each role in running lost through forge job agent <id> end --status lost (a phantom no driver runs) and advances. A wait on a gate, or done, ends the turn.
A procedure (only RC1's recall eval: its reviews are no job agents forge job agent run can run) waits for the live runners, then hands off: the lease is let go, the turn's resumes are closed, one fresh resume is queued (from: code-driver, the fixed HANDOFF_TEXT) and given to the wrapped session as its turn.
A failed advance is tried again, up to 3 in a turn, and its own repeated-failure park (parked: true) ends the turn; one capped at --max is tried again, up to 10. Advance's exit 3 is a takeover: the live runners stop.
A runner that ends done, or stale_step, advances; limit lets the other runners finish and ends the turn with its requests pending until resetsAt (else a backoff from retryMinMs, doubling to retryMaxMs), during which route() is not fresh; untrusted stops the pool on the trust change; auth, budget, error or timeout advance, so step's L1 respawns the role, and park the second time for the same role in a turn; exit 5 (exit, spawn) parks; anything it can't read (a signal, exit 2, unparseable output) ends the role lost first and kills the role child's group the runner reported, then parks. A secret_found from any child parks on human.
A park is forge job set <id> --state waiting --waiting-on escalation --reason=code driver: <what>: <detail> --epoch <epoch>, one line of at most 200 characters in the joined --reason= form, made only while the job is running; one that can't be made is an error and lastError, never silent. More than 200 advances in a turn park too.
The turn's end (a gate, done, a park, a takeover, a limit, a stop): no child is left running, releaseSession runs over driverId, the turn's resumes are closed done (RESUME_TAKEN_UP) except after a limit or a stop, and the pool reconciles and ticks.
IPC: a runner sends {type: 'forge-role', role, event} messages, started once its start is recorded, child with the role child's pid, reply at the child's first assistant message, ask with requestId, tool, input, description, agentId, blockedPath, rules, dirs for each can_use_tool, and cancel for one claude withdrew; the driver answers an ask with {type: 'forge-answer', requestId, decision, why?} to that runner only.
Asks go through the relay (lib/office/code/relay.ts): each field is checked by type and length, a prompt shows in /api/state under the job with role, and it is answered through POST /api/daemon/permission and audited as daemon-permission like a session's, with the same unasked denies, daemon.permissionTimeoutMinutes and prompt history.
Log: the scope log gets forge-daemon lines driver-start, claim, applied, apply-skipped, ran (one per advance entry), role-start (with the runner's pid), role-reply, role-end, fallback (only a hand-off turn routed to the session later), handoff, park, retry, driver-end and error, each with a message and its at.
A dashboard approve's time to the next agent start is the role-start line's at minus the applied line's createdAt. role-reply carries handoffMs, the milliseconds from the exit of the runner whose end led to this spawn to this child's first assistant message (null for a turn's first role): the hand-off metric.
Stop (a session or pool stop, a close): SIGTERM to every forge child, so each runner interrupts its role child and ends the role interrupted; SIGKILL after graceMs; the lease is let go and the requests stay pending. The exit handler's killSync sends SIGTERM to all of them, and the next pool boot releases driverId with every recorded id.