Skip to main content

Module workspace

Module workspace 

Source
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ยง

BootWaveTarget
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.
DespawnCleanupPending ๐Ÿ”’
One labelโ€™s in-flight despawn cleanup, released when it drops.
HealProgress
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.
KickRequest ๐Ÿ”’
One enqueue onto the workspace-level worker-kick channel (#259). Built by maybe_kick_worker_on_connected and maybe_kick_lead_on_connected (and any future kick site); drained by the workspaceโ€™s start_kick_dispatcher task, which fires one Command::Prompt per KICK_DISPATCH_INTERVAL tick. Same payload shape as the existing Command::Prompt carries (no attachments are ever part of a kick prompt - kicks are pure text).
PooledAgent ๐Ÿ”’
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.
ProjectCooling ๐Ÿ”’
A projectโ€™s walk blocked by a rotation cooldown, as the walkโ€™s own BudgetExhausted reports it.
SeatDeath ๐Ÿ”’
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.
SessionChipInfo
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.
SpawnRollback ๐Ÿ”’
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.
WorkerTagFacts ๐Ÿ”’
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.
WorkerTagRetryBudget ๐Ÿ”’
Retry budget for the detached tag-write: how many attempts tag_session_with_retry gets to find the workerโ€™s <session_id>.jsonl, and the delay between them.
Workspace
spawned forge_agent::Agent handles, one per active session.
WorktreeClaim ๐Ÿ”’
WorktreeClaimGuard ๐Ÿ”’
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.
LiveWorkerRefusal
Why insert_live_worker_if_label_absent refused an insert. Decided under the same lock acquisition as the insert, so the refusal and the pool state can never disagree.
PendingInteractionKind
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.
ResumeTarget ๐Ÿ”’
The session a worker label resumes onto.
SessionChipState
Visual category for a session chip. The renderer maps these to foreground colors + (for Bailed alone) a leading โš  glyph.
SettingsSaveError
Why a settings save did not write.
TagWriteResult ๐Ÿ”’
Discriminator for how a successful apply_worker_tag_or_rollback arm decided to transition a worker to Running. The Succeeded arm clears needs_tag; the DeferredNotFound arm keeps it set so the opportunistic retry on first DeliverWorkerPrompt knows to try again.
WorktreeOp ๐Ÿ”’
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.
WriteRefusal ๐Ÿ”’
Replace path with contents through 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โ€™s auto_compact_window is 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_kick of 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_connected paths fire Command::Prompt within 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 --resume that 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 model key 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_retry to find the workerโ€™s <session_id>.jsonl and append the tag row. claude CLI writes the file lazily on the first user turn - workers with an initial_prompt usually land within ~100 ms-2 s of Connected. 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 first DeliverWorkerPrompt; for those we exit the retry with Io(NotFound), mark the entry needs_tag = true, and retry opportunistically when the first turn arrives.
WORKER_TAG_RETRY_DELAY ๐Ÿ”’
Delay between tag_session retry 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 model into 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_mode into 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 projects dir, or the canonical config dir itself when the resolved tree is not named projects (the walk descends into <root>/projects, so the targetโ€™s parent would miss it). None when there is no projects tree.
classify_oauth_usage_error ๐Ÿ”’
Map a failed probe to the forge_gateway::UsageFetchStatus bucket. 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โ€. Unmappable never 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_test already 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 None for it: a failed network probe must not wipe a latest an 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 ๐Ÿ”’
settings with model pinned to model, every other key kept.
project_identities ๐Ÿ”’
The configured projects as the session store keys them: the catalog key a dynamic_workers row carries, plus the org and name a sessions row is keyed by.
real_cli_version_prober ๐Ÿ”’
The real probe: claude --version and npmโ€™s latest dist-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 ๐Ÿ”’
path with its longest existing prefix resolved through symlinks and the rest kept as spelled: canonicalize fails 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 --new clears 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 and SessionUpdate::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_session with a retry loop scoped to the JSONL- not-yet-on-disk race after Connected. claude CLI creates <session_id>.jsonl lazily; until that lands, find_session_file returns None and tag_session surfaces Io(NotFound). We retry only on that variant - any other error (permission denied, disk full, invalid UUID, encode error) propagates immediately. The async tokio::time::sleep is mandatory: this runs in a tokio task spawned from apply_worker_tag_or_rollback (or retry_worker_tag_opportunistic), never on a blocking thread.
transition_worker_to_failed ๐Ÿ”’
Shared transition: flip a workerโ€™s status to Failed with a human-readable diagnostic, and emit WorkerStatusChanged { StatusChanged }. Called by the Connected-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, update needs_tag according to the tag-write outcome, and emit WorkerStatusChanged { 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. subscribe marks 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ยง

CliVersionProbe ๐Ÿ”’
One probe call, boxed so the task that runs it is not generic over the futureโ€™s type.
CliVersionProber ๐Ÿ”’
How the probe task gets a snapshot. Workspace::new passes the real probe; every test constructor passes a script, so the scheduling around the probe is drivable without spawning claude --version or reaching npm.
ForgeAnswer ๐Ÿ”’
A forge commandโ€™s answer: what the reader is told, and how loudly.
LiveSeatsGate ๐Ÿ”’
A testโ€™s pause inside Workspace::live_seats: the sender fires once a capture is held, and a message on the receiver releases it.
ParkedBrowserHandOff ๐Ÿ”’
One parked browser hand-off: the session that asked, the hand-off itself, and the sender the blocked browser_hand_off handler awaits.
ParkedSlackDraft ๐Ÿ”’
Multi-session orchestrator. Owns the project catalog snapshot loaded from <config_dir>/forge.toml and 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.
ParkedTerminalHandOff ๐Ÿ”’
One parked terminal hand-off: the session that asked, the hand-off itself, and the sender the blocked terminal__hand_off handler awaits. No clock ends it - only an answer, or the handlerโ€™s own drop.
PricingFetch
One pricing fetch, boxed so refresh_pricing is not generic over the futureโ€™s type.
PricingFetcher
How the pricing refill gets its table. Workspace::new passes the real litellm fetch; a test constructor passes a script, so the refillโ€™s fetch-to-rescan path is drivable without the network.
ServiceStatusProbe ๐Ÿ”’
One service-status probe call, boxed for the same reason the version probeโ€™s is.
ServiceStatusProber ๐Ÿ”’
How the service-status probe gets its answer. Workspace::new passes the real fetch; a test passes a script, so the boot path is drivable without reaching the statuspage.
SlackDeliveryKey ๐Ÿ”’
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.