Expand description
The orchestrator.
Re-exportsยง
use std::collections::HashMap;๐use std::collections::HashSet;๐use std::path::Path;๐use std::path::PathBuf;๐use std::sync::Arc;๐use std::time::Duration;๐use std::time::SystemTime;๐use std::time::UNIX_EPOCH;๐use anyhow::Result;๐use forge_agent::AgentHandle;๐use forge_agent::client::SessionLaunchSettings;๐use forge_agent::env::cli_version::CliVersionInfo;๐use forge_primitives::cloud::service_status::ServiceIssue;๐use forge_primitives::settings::SettingsEditOp;๐use forge_primitives::settings::SettingsEditRequest;๐use forge_primitives::tasks::TaskId;๐use forge_primitives::AvailableAgent;๐use forge_primitives::AvailableCommand;๐use forge_primitives::Message;๐use forge_primitives::SDKSessionInfo;๐use forge_primitives::SessionId;๐use std::io::Write as _;๐use crate::gotify::GotifySubsystem;๐use crate::mcp::peers::types::MessageId;๐use crate::mcp::peers::types::WrappedKind;๐use crate::mcp::peers::types::WrappedPrompt;๐use parking_lot::Mutex;๐use tokio::sync::mpsc;๐use tracing::Instrument;๐use forge_gateway::AccountKey;๐use forge_gateway::ProviderHost as _;๐use crate::config::ConfigReload;๐use crate::config::LoadedConfig;๐use crate::config::LoadedProject;๐use crate::config::load_from_dir;๐use crate::domain_session::DomainSession;๐use crate::error::WorkspaceError;๐use crate::protocol::Command;๐use crate::protocol::DispatchError;๐use crate::protocol::NoticeSeverity;๐use crate::protocol::PromptSource;๐use crate::protocol::SessionUpdate;๐use crate::session_task::SessionTask;๐use crate::spawn;๐use crate::target::ProjectKey;๐use crate::target::SessionSlot;๐use crate::target::SessionTarget;๐use crate::update_fanout::SeatUpdate;๐use crate::update_fanout::SubscriberRole;๐use crate::update_fanout::UpdateFanout;๐use crate::views::AccountLoadingRow;๐use crate::views::ProjectView;๐use crate::views::SessionView;๐
Modulesยง
- testing ๐
- Test-only constructors, seed helpers and dispatch interception for
Workspace, so tests here and cross-crate tests can drive a workspace without a real subprocess.
Structsยง
- Boot
Wave Target - One project a boot opens, and the kick its last state earns: the restart note when a turn was cut off, nothing when the seat was parked.
- Despawn
Cleanup ๐Pending - One labelโs in-flight despawn cleanup, released when it drops.
- Heal
Progress - Where a dead seatโs heal has got to while it is between attempts: the attempt number the row names, and when the next one fires.
- Kick
Request ๐ - One enqueue onto the workspace-level worker-kick channel
(#259). Built by
maybe_kick_worker_on_connectedandmaybe_kick_lead_on_connected(and any future kick site); drained by the workspaceโsstart_kick_dispatchertask, which fires oneCommand::PromptperKICK_DISPATCH_INTERVALtick. Same payload shape as the existingCommand::Promptcarries (no attachments are ever part of a kick prompt - kicks are pure text). - Pooled
Agent ๐ - Pool entry wrapping the live
Arc<AgentHandle>, the account key the subprocess is bound to, the permission mode that spawn resolved for its project, and the session registration a respawn re-sends. - Project
Cooling ๐ - A projectโs walk blocked by a rotation cooldown, as the walkโs own
BudgetExhaustedreports it. - Seat
Death ๐ - A seatโs occupant that died, held until a reader has been shown the seat AND the recovery has announced itself - so a crash nobody watched is still readable when someone looks, and the recovery that answers it is announced exactly once. The two are spent independently: showing the seat takes the rowโs trace, and the connect takes the announcement, whichever comes first.
- Session
Chip Info - Per-session chip the Projects pane renders next to each row.
Carries the assigned account display name + the visual-state
category derived by
Workspace::session_chip_for. - Spawn
Rollback ๐ - Undo a spawnโs own side effects when it is refused before the session is pooled: the id it recorded, the seat state it moved, and a domain handle it created. Armed from the id record to the pool insert; an unarmed guard does nothing, so the pooled and refused paths both end in a plain drop.
- Worker
TagFacts ๐ - The two facts the connect-time tag path decides on: whether the JSONL still needs its tag, and whether this spawn minted the row a rollback would delete.
- Worker
TagRetry ๐Budget - Retry budget for the detached tag-write: how many attempts
tag_session_with_retrygets to find the workerโs<session_id>.jsonl, and the delay between them. - Workspace
- spawned
forge_agent::Agenthandles, one per active session. - Worktree
Claim ๐ - Worktree
Claim ๐Guard - A held claim on a composed worker-worktree path. Released on drop, so every early return lets the next spawn or despawn through.
Enumsยง
- Adoption ๐
- What a boot should do with the pre-split store.
- Live
Worker Refusal - Why
insert_live_worker_if_label_absentrefused an insert. Decided under the same lock acquisition as the insert, so the refusal and the pool state can never disagree. - Pending
Interaction Kind - What a session is waiting on a person for. The kind a needs-you row names, since โasked you a questionโ and โa permission prompt is waitingโ are different asks with the same mark.
- Resume
Target ๐ - The session a worker label resumes onto.
- Session
Chip State - Visual category for a session chip. The renderer maps these to
foreground colors + (for
Bailedalone) a leadingโglyph. - Settings
Save Error - Why a settings save did not write.
- TagWrite
Result ๐ - Discriminator for how a successful
apply_worker_tag_or_rollbackarm decided to transition a worker toRunning. TheSucceededarm clearsneeds_tag; theDeferredNotFoundarm keeps it set so the opportunistic retry on firstDeliverWorkerPromptknows to try again. - Worktree
Op ๐ - Which operation holds a composed path. Re-entrancy needs the same owner AND the same operation, so a despawn cannot slip into a resumeโs claim on one label and delete the tree that resume already ensured.
- Write
Refusal ๐ - Replace
pathwithcontentsthrough a temp file in the same directory: a reader sees the old file or the new one, never a half-written one, and the config watcherโs re-arm survives the rename.
Constantsยง
- AUTO_
COMPACT_ ๐WINDOW_ ENV - The env the CLI takes its auto-compaction window from: the
[models]tableโsauto_compact_windowis stamped here for the projectโs model. - CLI_
VERSION_ ๐REFRESH_ INTERVAL - How often the claude version probe re-runs after the boot fetch. Versions change rarely, so a few minutes is ample - the point is only to recover a transient boot failure, not to track releases tightly.
- DYNAMIC_
WORKER_ ๐RESTART_ NOTE - Forge-supplied resume kick for a worker whose row carries no
resume_kickof its own. On a resuming re-spawn forge delivers this constant as the workerโs first turn - telling it to continue rather than start the task over. - KICK_
DISPATCH_ ๐INTERVAL - Minimum gap between successive worker-kick dispatches (#259).
Multi-worker teams hit Anthropicโs per-IP burst limit when all
maybe_kick_worker_on_connectedpaths fireCommand::Promptwithin the same tick at boot; routing every kick through a workspace-level mpsc + a drainer task that sleeps this interval between sends spreads them out enough to avoid the burst rejection. The first kick in an empty channel fires with zero added latency; only subsequent kicks pay the sleep. Worst case at typical team sizes (7-10 workers) is ~5-7 s to fully kick the cohort, vs ~1 s pre-#259 with most kicks rejected. - LEAD_
DELEGATION_ ๐PREAMBLE - Delegation block appended to a Lead sessionโs system prompt.
- LEAD_
RESTART_ NOTE - The first turn a boot spawn delivers to a lead it hands a stored
session id to. A lead has no kick of its own, so this note is what
turns a resumed conversation into work continued rather than a seat
idle at a prompt. The handoff and the board are the source of what
was in flight: a
--resumethat the CLI refuses falls back to a fresh session, so the history is named conditionally rather than relied on. - MODEL_
SLOT_ ๐VARIABLES - The CLIโs model-slot variables: the projectโs
modelkey is stamped into all of them at spawn, so one model serves the main loop, subagents and background slots alike. - SPAWNED_
SEAT_ ๐STATE - The state a spawn marks its seat with: open, no turn in flight. The rollback restores only while the row still holds it, so a close landing mid-spawn is never undone.
- WORKER_
TAG_ ๐RETRY_ ATTEMPTS - Max attempts for
tag_session_with_retryto find the workerโs<session_id>.jsonland append the tag row. claude CLI writes the file lazily on the first user turn - workers with aninitial_promptusually land within ~100 ms-2 s ofConnected. 30 attempts at 100 ms = ~3 s wall caps the window generously without burning resources. Idle-spawned workers (no prompt) never produce a JSONL at all until the firstDeliverWorkerPrompt; for those we exit the retry withIo(NotFound), mark the entryneeds_tag = true, and retry opportunistically when the first turn arrives. - WORKER_
TAG_ ๐RETRY_ DELAY - Delay between
tag_sessionretry attempts when the JSONL is not yet on disk. 100 ms is short enough to land within one or two ticks of claudeโs first write and large enough that 30 retries cap the total wait at a few seconds.
Functionsยง
- adopt_
legacy_ ๐store - Adopt the pre-split machine-wide store into this config dirโs own, once.
- adopt_
legacy_ ๐store_ from - The adoption itself, with the default-dir verdict already decided.
- apply_
project_ ๐model - Stamp the projectโs canonical
modelinto the launch settings, which is how the sessionโs model reaches the CLI. A project that declares no model stamps nothing, so the callerโs pin (forge defaults it to the literal โopusโ) stands. - apply_
project_ ๐permission_ mode - Stamp the projectโs
permission_modeinto the launch settingsโpermissions.defaultMode. A spawn that resolved to no project stamps nothing, so the launcherโs session default applies. - auth_
repair_ ๐hint - The repair line the usage poller logs under an auth-classified failure, keyed on how the account authenticates. A sessionโs credentials are fixed at its spawn, so both classes point at the flat-key edit on the account block, never a re-authentication of the shared config dir.
- auto_
continue_ ๐prompt - The words a continued seatโs model receives, and what the row says to whoever reads it: forge is the one that resumed the turn, and the work already done is not to be repeated.
- catalog_
scan_ ๐root - The scan root for one config dir: the canonical parent of its
resolved
projectsdir, or the canonical config dir itself when the resolved tree is not namedprojects(the walk descends into<root>/projects, so the targetโs parent would miss it).Nonewhen there is no projects tree. - classify_
oauth_ ๐usage_ error - Map a failed probe to the
forge_gateway::UsageFetchStatusbucket. Separates HTTP 429 (the common multi-instance throttle case) from the auth-related failures (Expired/NoCredentials/Unauthorized) and transport failures (Network), so each class keeps its own meaning rather than a generic โfetch errorโ.Unmappablenever reaches the classifiers - both callers handle a 200 that maps to nothing before classifying. - default_
pricing_ ๐fetcher - The fetch a constructor that names none gets.
- departures_
from_ ๐edits - The project pairs and account names a batch leaves behind, read off the batch and the loaded document rather than a second load: a removal drops an entry, and a set on one of the naming fields renames it. The loaded candidate is checked again after validation, so a shape this misses is still caught.
- forge_
misuse ๐ - The line a forge name invoked wrongly is answered with.
- inert_
pricing_ ๐fetcher - The fetch a test workspace gets: nothing to fetch, so a kicked refill
has nothing to reach - the same no-network promise
new_for_testalready makes for its boot probes. - is_
default_ ๐config_ dir - Whether this path is the home directoryโs
.claude, the default config dir. - load_
session_ ๐tag_ cache - The previous runโs tag scans, or an empty cache when there is no store: a missing cache costs a full re-scan, never a wrong answer.
- merge_
cli_ ๐version - Merge a freshly-probed snapshot over the held one, keeping a
previously-resolved field when the new probe came back
Nonefor it: a failed network probe must not wipe alatestan earlier one found. - open_db ๐
- Open this config dirโs redb store, creating the directories first.
Returns
None(with a warn) when they canโt be created or the DB canโt open - forge then runs without durable crons, subscriptions, tasks or dynamic workers this session (hard rule #14: no cwd fallback). - org_
pins ๐ - The gatewayโs per-org walk order. Orgs are load-validated to share one pin, so the first project of an org states it.
- persist_
session_ ๐tag_ cache - pin_
model_ ๐into_ settings settingswithmodelpinned tomodel, every other key kept.- project_
identities ๐ - The configured projects as the session store keys them: the catalog
key a
dynamic_workersrow carries, plus the org and name asessionsrow is keyed by. - real_
cli_ ๐version_ prober - The real probe:
claude --versionand npmโslatestdist-tag. - real_
pricing_ ๐fetcher - The real fetch: the litellm pricing file.
- real_
service_ ๐status_ prober - The real probe: the public statuspage summary.
- record_
session_ ๐tool_ surface - Record the surface a spawn hands its child.
- resolve_
existing_ ๐prefix pathwith its longest existing prefix resolved through symlinks and the rest kept as spelled:canonicalizefails wholesale once the last component is gone, and the two sides of a comparison then use different spellings of the same directory.- resume_
kick_ ๐for - The restart note a boot spawn hands its seat: only a spawn carrying a
stored id to resume delivers one, so a first boot or
--newclears whatever a previous spawn left. A resume the CLI refuses still takes the note, whose wording does not depend on history. - run_
background_ ๐catalog_ scan - The boot catalog scan, off the boot path. Reads the workspaceโs own
config_dir(canonicalized, so the redb tag-cache keys are the same ones any other scan writes even when the config dir path carries a symlink component) with the redb tag cache - only bytes appended since the last scan are read end to end - swaps the grouped catalog in one lock, prunes cache rows of transcripts that no longer exist, and only then publishes readiness andSessionUpdate::CatalogLoaded. - run_
cli_ ๐version_ probe - The claude version probe, off the boot path. Each result is merged into the store, which wakes the views when it changed what they draw.
- run_
service_ ๐status_ probe - The statuspage probe, off the boot path. Its answer is held before it is announced, so a view that attaches afterwards reads it rather than having missed the one update that carried it.
- session_
env_ ๐for - Env for a spawn: the account env with
projectโs declared keys merged over it. The spawn resolves the project first and refuses a target that maps to none, so there is always one to merge. - spawn_
background_ ๐catalog_ scan - Kick off the catalog scan on the tokio runtime. Idempotent via
started; a caller with no runtime gets a warn and an immediately-ready flag with an empty catalog rather than a scan nobody can await. A monitor on the scan task publishes readiness even if the scan dies, so a reader never waits on a readiness that will never come. - spawn_
background_ ๐service_ status_ probe - Kick off the statuspage probe on the tokio runtime: one fetch, then the answer is held. A caller with no runtime gets a warn and holds no status rather than a task nobody would run.
- store_
cli_ ๐version - Merge one probe result into the store and wake the views when it moved what they draw.
- store_
path ๐ - This config dirโs redb store:
<app_support>/stores/<stable-key>.redb. - tag_
session_ ๐with_ retry - Wrap
mutations::tag_sessionwith a retry loop scoped to the JSONL- not-yet-on-disk race afterConnected. claude CLI creates<session_id>.jsonllazily; until that lands,find_session_filereturnsNoneandtag_sessionsurfacesIo(NotFound). We retry only on that variant - any other error (permission denied, disk full, invalid UUID, encode error) propagates immediately. The asynctokio::time::sleepis mandatory: this runs in a tokio task spawned fromapply_worker_tag_or_rollback(orretry_worker_tag_opportunistic), never on a blocking thread. - transition_
worker_ ๐to_ failed - Shared transition: flip a workerโs status to
Failedwith a human-readablediagnostic, and emitWorkerStatusChanged { StatusChanged }. Called by theConnected-never-arrived paths (subprocess exit, ConnectionFailed event, resume rejected by claude when the fall-through-to-fresh path declines to retry). - transition_
worker_ ๐to_ running - Shared transition: flip a workerโs status to
Running, updateneeds_tagaccording to the tag-write outcome, and emitWorkerStatusChanged { StatusChanged }. Idempotent. - unready_
slots ๐ - The slots whose task has not answered yet, read off the channel values.
- wait_
for_ ๐teardown - Wait for one slotโs teardown answer.
subscribemarks the current value as seen, so the value is checked before waiting on a change. - warn_
shutdown_ ๐expired - Name each session that outlasted the shutdownโs wait. One line per slot: the shared deadline says nothing about which task was slow, and the value is the only thing that does.
- write_
atomically ๐
Type Aliasesยง
- CliVersion
Probe ๐ - One probe call, boxed so the task that runs it is not generic over the futureโs type.
- CliVersion
Prober ๐ - How the probe task gets a snapshot.
Workspace::newpasses the real probe; every test constructor passes a script, so the scheduling around the probe is drivable without spawningclaude --versionor reaching npm. - Forge
Answer ๐ - A forge commandโs answer: what the reader is told, and how loudly.
- Live
Seats ๐Gate - A testโs pause inside
Workspace::live_seats: the sender fires once a capture is held, and a message on the receiver releases it. - Parked
Browser ๐Hand Off - One parked browser hand-off: the session that asked, the hand-off itself,
and the sender the blocked
browser_hand_offhandler awaits. - Parked
Slack ๐Draft - Multi-session orchestrator. Owns the project catalog snapshot
loaded from
<config_dir>/forge.tomland the pool of currently One composed Slack message held for a decision: the session that asked, the draft itself, and the sender the blocked Slack handler awaits. - Parked
Terminal ๐Hand Off - One parked terminal hand-off: the session that asked, the hand-off itself,
and the sender the blocked
terminal__hand_offhandler awaits. No clock ends it - only an answer, or the handlerโs own drop. - Pricing
Fetch - One pricing fetch, boxed so
refresh_pricingis not generic over the futureโs type. - Pricing
Fetcher - How the pricing refill gets its table.
Workspace::newpasses the real litellm fetch; a test constructor passes a script, so the refillโs fetch-to-rescan path is drivable without the network. - Service
Status ๐Probe - One service-status probe call, boxed for the same reason the version probeโs is.
- Service
Status ๐Prober - How the service-status probe gets its answer.
Workspace::newpasses the real fetch; a test passes a script, so the boot path is drivable without reaching the statuspage. - Slack
Delivery ๐Key - The dedupe key for a delivered Slack message: one destination is
(project, owner, conversation, ts, edited), so a lead and a worker in
one project never starve each other as โalready deliveredโ. The edit
ts is part of the identity because an edit keeps the messageโs own
ts: without it a correction reads as a re-run of the delivery it corrects and never reaches the session.