Skip to main content

Workspace

Struct Workspace 

Source
pub struct Workspace {
Show 123 fields config_dir: PathBuf, pub(crate) config: RwLock<Arc<LoadedConfig>>, settings: Sender<Option<Arc<SettingsDocument>>>, settings_version: OnceLock<String>, settings_write: Mutex<()>, reload_generation: AtomicU64, reload_gate: Mutex<()>, pub(crate) systemone: RwLock<Option<Arc<SystemOneClient>>>, pub(crate) vault: RwLock<Option<Arc<VaultSession>>>, pub(crate) voice: OnceLock<Arc<VoiceService>>, pub(crate) voice_preview_error: Mutex<Option<String>>, catalog: Arc<Mutex<HashMap<ProjectKey, Vec<SDKSessionInfo>>>>, pub(crate) app_support: Option<PathBuf>, pub(crate) pool: Mutex<HashMap<SessionSlot, PooledAgent>>, pub(crate) test_spawn_handle: Mutex<Option<AgentHandle>>, pub(crate) test_spawn_server: Mutex<Option<McpServer>>, pub(crate) accounts: Arc<AccountPool>, pub(crate) dictate: Arc<DictateState>, pub(crate) dictate_runtime: Mutex<DictateRuntime>, pub(crate) browser_host: OnceLock<Arc<dyn BrowserHost>>, live_seats_lock: Mutex<()>, live_seats_generation: AtomicU64, live_seats_gate: Mutex<Option<(Sender<()>, Receiver<()>)>>, pub(crate) attached_clients: AtomicUsize, pub(crate) document_handoffs: Mutex<HashMap<Uuid, ParkedDocument>>, pub(crate) dictate_catalogue: Mutex<CatalogueState>, pub(crate) dictate_install: Mutex<InstallState>, pub(crate) plugin_cli: Mutex<PluginCli>, pub(crate) plugin_run: Mutex<Option<PluginUpdateRun>>, pub(crate) plugin_action_running: AtomicBool, pub(crate) dictate_activate: Mutex<ActivateState>, pub(crate) dictate_bench: Mutex<BenchState>, pub(crate) dictate_bench_cancel: AtomicBool, pub(crate) speaking_bench: Mutex<SpeakingBenchState>, pub(crate) speaking_bench_cancel: AtomicBool, pub(crate) bench_claim: AtomicBool, pub(crate) read_aloud_error: Mutex<Option<String>>, pub(crate) test_catalogue_source: Mutex<Option<CatalogueSource>>, pub(crate) test_cleanup_source: Mutex<Option<CleanupSource>>, pub(crate) test_catalogue_dir: Mutex<Option<PathBuf>>, pub(crate) test_read_aloud_dir: Mutex<Option<PathBuf>>, pub(crate) update_tx: UpdateFanout, pub(crate) command_senders: Mutex<HashMap<SessionSlot, UnboundedSender<Command>>>, session_teardowns: Mutex<HashMap<SessionSlot, Sender<bool>>>, shutting_down: AtomicBool, live_workers: Mutex<HashMap<ProjectKey, Vec<WorkerEntry>>>, pub(crate) idle_sweeps: Mutex<HashMap<SessionSlot, SweepClock>>, worktree_claims: Arc<Mutex<HashMap<PathBuf, WorktreeClaim>>>, despawn_cleanups: Mutex<HashSet<(ProjectKey, String)>>, spawn_failures: Mutex<HashMap<SessionSlot, String>>, deaths: Mutex<HashMap<SessionSlot, SeatDeath>>, heal_progress: Mutex<HashMap<SessionSlot, HealProgress>>, healing: OnceLock<Arc<Healing>>, pub(crate) held_work_seats: HeldSeats, pub(crate) domain_handles: Mutex<HashMap<SessionSlot, Arc<Mutex<DomainSession>>>>, pub(crate) review_origin: Mutex<HashMap<(String, String), SessionSlot>>, pub(crate) review_activity: Mutex<HashMap<SessionSlot, Vec<ReviewActivity>>>, pub(crate) supervisor: Arc<Supervisor>, dictate_preflight_started: AtomicBool, pub(crate) voice_prewarm_started: AtomicBool, pub(crate) voice_prewarm_dispatched: AtomicBool, pub(crate) gateway: Arc<Gateway>, gateway_ready: AtomicBool, gateway_port: u16, gateway_url: Mutex<Option<String>>, gateway_bind_error: Mutex<Option<String>>, kick_dispatcher_tx: UnboundedSender<KickRequest>, kick_dispatcher_rx_slot: Mutex<Option<UnboundedReceiver<KickRequest>>>, _single_instance_lock: Option<File>, pub(crate) crons: Mutex<Vec<CronEntry>>, pub(crate) crons_load_failed: AtomicBool, pub(crate) tasks: Mutex<Vec<Task>>, pub(crate) parked_by_slot: Mutex<HashMap<SessionSlot, ParkedForSlot>>, pub(crate) pending_handoffs: Mutex<HashMap<SessionSlot, PendingBrief>>, pub(crate) swapping: Mutex<HashMap<SessionSlot, u64>>, pub(crate) swap_epochs: AtomicU64, pub(crate) held_prompts: Mutex<HashMap<SessionSlot, Vec<HeldPrompt>>>, pub(crate) gotify_subs: Mutex<Vec<GotifySubscription>>, pub(crate) db: Arc<Mutex<Option<Db>>>, catalog_loaded: Arc<AtomicBool>, pricing_fetcher: PricingFetcher, catalog_scan_started: AtomicBool, cli_version: Arc<Mutex<Option<CliVersionInfo>>>, last_fatal_error: Mutex<Option<AppError>>, service_status: Arc<Mutex<Option<ServiceIssue>>>, service_status_probe_started: AtomicBool, pub(crate) gotify_connected: Mutex<bool>, pub(crate) gotify_app_index: Mutex<HashMap<String, u64>>, pub(crate) gotify_subsystem: Mutex<Option<GotifySubsystem>>, pub(crate) slack: Arc<SlackWorkspaces>, pub(crate) slack_subs: Mutex<Vec<SlackSubscription>>, pub(crate) slack_subsystem: Mutex<BTreeMap<String, Sender<()>>>, pub(crate) slack_connected: Mutex<BTreeMap<String, bool>>, pub(crate) slack_inflight: Mutex<HashMap<(String, String), Vec<SlackPostStamp>>>, pub(crate) slack_user_ids: Mutex<BTreeMap<String, String>>, pub(crate) slack_user_names: Mutex<BTreeMap<(String, String), String>>, pub(crate) slack_author_failures: Mutex<HashSet<(String, String)>>, pub(crate) slack_draft_user_failures: Mutex<HashSet<(String, String)>>, pub(crate) slack_thread_failures: Mutex<HashSet<(String, String, String)>>, pub(crate) slack_thread_full_reads: Mutex<HashMap<(String, String, String), Instant>>, pub(crate) slack_thread_first_walks: Mutex<HashSet<(String, String, String)>>, pub(crate) slack_thread_edit_watermarks: Mutex<HashMap<(String, String, String), String>>, pub(crate) slack_ledger_failures: Mutex<HashSet<(String, String)>>, pub(crate) slack_drafts: Mutex<HashMap<Uuid, (SessionSlot, SlackDraft, Sender<SlackDraftAnswer>)>>, pub(crate) browser_handoffs: Mutex<HashMap<Uuid, (SessionSlot, HandOff, Sender<HandOffEnding>)>>, pub(crate) terminals: Mutex<HashMap<SessionSlot, Arc<TerminalEntry>>>, pub(crate) terminal_handoffs: Mutex<HashMap<Uuid, (SessionSlot, TerminalHandOff, Sender<HandOffEnding>)>>, pub(crate) next_terminal_id: AtomicU16, pub(crate) settings_writes: Mutex<HashMap<Uuid, (SessionSlot, SettingsWriteAsk, Option<Sender<bool>>)>>, pub(crate) parked_vault_asks: Mutex<HashMap<Uuid, (SessionSlot, VaultDraft, Option<Sender<VaultAskAnswer>>)>>, pending_seq: Mutex<HashMap<SessionSlot, u64>>, pub(crate) slack_recently_delivered: Mutex<HashMap<(String, Option<String>, String, String, Option<String>), Instant>>, pub(crate) slack_load_failed: AtomicBool, pub(crate) slack_user_id_retries: Mutex<BTreeMap<String, SystemTime>>, pub(crate) slack_verification_started: AtomicBool, respawn_in_flight: Mutex<HashSet<ProjectKey>>, unwakeable_crons: Mutex<HashSet<CronId>>, cooling_crons: Mutex<HashMap<CronId, SystemTime>>, command_intercept: Mutex<Option<Vec<Command>>>, test_extra_projects: Mutex<Vec<LoadedProject>>, test_user_preferences: Mutex<Option<Value>>, test_claude_pid: Mutex<HashMap<SessionSlot, u32>>, pub(crate) test_scan_gate: Mutex<Option<Arc<Semaphore>>>,
}
Expand description

spawned forge_agent::Agent handles, one per active session.

Construct via Workspace::new; consume via Workspace::get_agent_handle; drain on exit via Workspace::shutdown.

Fields§

§config_dir: PathBuf

Retained so error messages can reference the forge.toml path the workspace was constructed from, and so the per-account config-dir binding can re-resolve from here.

§config: RwLock<Arc<LoadedConfig>>

pub(crate) so the impl block in crate::gotify can read the [gotify] section. Swapped whole by Workspace::reload_config, which is why readers take an owned snapshot rather than a reference.

§settings: Sender<Option<Arc<SettingsDocument>>>

The effective-config document, rebuilt on every applied reload and shared by every socket connection. None until the server names the build that carries it.

§settings_version: OnceLock<String>

The forge version the document carries, set once by the server at boot. A workspace with no server (tests) leaves it empty.

§settings_write: Mutex<()>

One settings save at a time: the file is read, edited, validated and replaced under this lock, so two saves cannot interleave.

§reload_generation: AtomicU64

Odd while a reload is swapping state, even when settled. A spawn reads it before resolving and re-checks after, so a resolution that straddled the swap retries rather than pairing an old project pin with the new pool.

§reload_gate: Mutex<()>

Held across a reload’s odd-to-even swap, so a spawn that sees an odd generation waits on this rather than spinning on the counter.

§systemone: RwLock<Option<Arc<SystemOneClient>>>

The [systemone] decision client, built at boot when the section is present and enabled; the session MCP servers are handed a facade over it. None keeps the systemone tools out of every session. Rebuilt by Workspace::reload_config.

§vault: RwLock<Option<Arc<VaultSession>>>

The [vault] credential session, built at boot when the section is present; the family’s tools are handed a facade over it. None keeps the vault tools out of every session. Rebuilt by Workspace::reload_config only when the section itself changed, so an unrelated settings save never locks a live vault.

§voice: OnceLock<Arc<VoiceService>>

The per-seat voice service, built on first use: its sink and its note dispatch hold a Weak<Workspace>, which does not exist while the value is still being constructed.

§voice_preview_error: Mutex<Option<String>>

The last voice preview’s render failure, for the settings card to draw. Cleared when the next preview is asked for.

§catalog: Arc<Mutex<HashMap<ProjectKey, Vec<SDKSessionInfo>>>>

Catalog of sessions per project. Populated by the background catalog scan once Workspace::start_catalog_scan runs; mutated in-place by Workspace::record_connected_session each time a freshly spawned session reaches Connected, so the Projects pane’s drilldown stays current without forcing a full disk re-scan. Held under a Mutex because multiple in-process tasks (the pane render, the connect-flow event handler) reach for it across await points; Arc so the scan task can swap its contents without holding an Arc<Workspace> cycle.

§app_support: Option<PathBuf>

forge’s machine-local app-support base, resolved once at construction: the single-instance lock, the durable store and the dictation catalogue cache all sit under it. None means boot resolution failed (already warned there); hard rule #14 forbids a cwd fallback.

§pool: Mutex<HashMap<SessionSlot, PooledAgent>>

Live Agents keyed by the session slot. parking_lot::Mutex so the public methods can take &self. pub(crate) so sibling modules (spawn::handle_deliver_worker_prompt_to_lead, mcp::workers::facade::ProdWorkerFacade) can probe pool membership for lead-delivery gating without an extra method wrapper.

§test_spawn_handle: Mutex<Option<AgentHandle>>

A stand-in for the handle a cold spawn would create. The settings a spawn launches with are otherwise unobservable: a pooled slot never reaches the launch, and a cold one ends in a real subprocess. None in production and in every test that does not install one, so a spawn runs always.

§test_spawn_server: Mutex<Option<McpServer>>

The forge MCP server the last spawn built, kept because a stand-in replaces Agent::spawn, so the server it would have been handed is otherwise dropped unobserved - and a test that has to drive a tool through the spawn’s OWN composition (which facade it was built with) needs it.

§accounts: Arc<AccountPool>

The account state map, owned by the gateway and reached through its pool. It carries account health state updated on every spawn and refreshed by the in-memory usage poller, and it is what the gateway’s selection walk reads to choose an account.

§dictate: Arc<DictateState>

Dictation preflight: the per-model progress the launchpad renders, the flag Escape sets, and the loaded engine held for the run. Populated by start_dictate_preflight; inert when [dictate] enabled is false.

§dictate_runtime: Mutex<DictateRuntime>

Live dictation: the takes being fed frames and the submitted takes still awaiting a transcript. Driven by Command::DictateStream / Command::DictateStop.

§browser_host: OnceLock<Arc<dyn BrowserHost>>

The server’s browser fleet, installed once at boot by forge-server. None is a workspace with no browser host (tests, a view): the browser tools then name their refusal.

§live_seats_lock: Mutex<()>

Captures do not overlap: the generation ranks whole captures, so the mint and the reads it labels happen under this one lock.

§live_seats_generation: AtomicU64

The generation each Self::live_seats capture takes, so the fleet can drop a snapshot older than the one it last applied.

§live_seats_gate: Mutex<Option<(Sender<()>, Receiver<()>)>>

A test’s hold between Self::live_seats’s mint and its reads, so a capture can be opened across a seat change. Test-only.

§attached_clients: AtomicUsize

How many client connections are attached, noted by the transport per connection. A document hand-off reads it before claiming an open: only a client that draws a document viewer is counted.

§document_handoffs: Mutex<HashMap<Uuid, ParkedDocument>>

The parked document hand-offs, under their ids: each holds the awaiting tool’s sender until the reader’s answer releases it.

§dictate_catalogue: Mutex<CatalogueState>

The model catalogue: the last fetched feed and what the last check did. Loaded from its cache at boot, refreshed by start_dictate_catalogue and Command::DictateCatalogueCheck, and read by the models page through dictate_models.

§dictate_install: Mutex<InstallState>

Where the last model install got to. Set by Command::DictateInstall, read by the models page through dictate_install.

§plugin_cli: Mutex<PluginCli>

How a plugin action reaches claude plus the disk scan the actions and the Extensions read share. Real in production; a test swaps it so a whole action runs without shelling out or touching ~/.claude.

§plugin_run: Mutex<Option<PluginUpdateRun>>

The last plugin update run or check, in flight or finished: what a view attaching mid-run reads, and the guard that keeps one run at a time (a second press is refused while this is unfinished).

§plugin_action_running: AtomicBool

One plugin CLI action at a time; cleared when its task ends, panic included, so a crashed task never wedges the surface.

§dictate_activate: Mutex<ActivateState>

Where the last model activation got to. Set by Command::DictateActivate, read by the models page through dictate_activate.

§dictate_bench: Mutex<BenchState>

Where the last bench got to, and the flag its Stop sets. Read by the models page through dictate_bench and bench_results.

§dictate_bench_cancel: AtomicBool§speaking_bench: Mutex<SpeakingBenchState>

Where the speaking loop’s bench got to, and the flag its Stop sets. Read by the models page through speaking_bench and its results.

§speaking_bench_cancel: AtomicBool§bench_claim: AtomicBool

One bench at a time across both directions: a single claim taken before either state is written, so two presses cannot both pass the check and load two engines.

§read_aloud_error: Mutex<Option<String>>

The last read-aloud write’s failure, when there was one: a stop whose write fails has no dispatch left to answer, so the page reads it here instead. Cleared when the next recording starts.

§test_catalogue_source: Mutex<Option<CatalogueSource>>

A test’s own catalogue source, so a check can run against a loopback server instead of GitHub. Not present in production builds.

§test_cleanup_source: Mutex<Option<CleanupSource>>

A test’s own cleanup source, on the same terms.

§test_catalogue_dir: Mutex<Option<PathBuf>>

A test’s own catalogue directory, so a check never writes to the real app-support dir. Not present in production builds.

§test_read_aloud_dir: Mutex<Option<PathBuf>>

A test’s own read-aloud set directory, so an arming never writes to the real app-support dir. Not present in production builds.

§update_tx: UpdateFanout

Fan-out SessionUpdate sender: every producer inside the workspace holds a clone, and it hands each update to every subscriber. pub(crate) so the impl block in crate::crons can reach it.

§command_senders: Mutex<HashMap<SessionSlot, UnboundedSender<Command>>>

Per-session Command sender map. Populated when Self::get_agent_handle spawns the first SessionTask for a key; cleared on Self::release_session_with_cascade and Self::shutdown.

§session_teardowns: Mutex<HashMap<SessionSlot, Sender<bool>>>

Per-session teardown-completion signals, minted beside the command channel. The task marks its slot done as it drops - after its exit path has reaped the subprocess - and every waiter subscribes rather than taking the one receiver: the despawn holds its worktree removal until then (#1767), and a shutdown holds its exit until every slot answers. An entry stays after its task fires and is replaced when the slot respawns, so the map holds one small sender per slot this process has ever spawned - the price of a late reader still finding the answer.

§shutting_down: AtomicBool

Set before a shutdown tears anything down. From then on no session may start - the boot wave, a cron tick and the socket all race the drain - and the mint refuses under the same lock the drain takes, which is what keeps a late spawn out of the awaited set.

§live_workers: Mutex<HashMap<ProjectKey, Vec<WorkerEntry>>>

Per-project list of live worker sessions. In-memory only - wiped on forge restart by design (workers are ephemeral at the forge UI level; their JSONLs persist on disk). Mutated via insert_live_worker / remove_latest_worker / drain_live_workers.

§idle_sweeps: Mutex<HashMap<SessionSlot, SweepClock>>

The per-lead clock of the injected board sweep, keyed by the lead’s slot. In memory only, so a restart re-arms every lead: the sweep is a moment, not a schedule the store should own. pub(crate) so the impl block in crate::idle_sweep can reach it.

§worktree_claims: Arc<Mutex<HashMap<PathBuf, WorktreeClaim>>>

One claim per composed worker-worktree path, held while a spawn’s ensure/insert or a despawn’s removal is in flight. The live registry is per project; this is the repository-wide one, so two projects composing the same path cannot both act on it.

§despawn_cleanups: Mutex<HashSet<(ProjectKey, String)>>

The (project, label) pairs whose despawn cleanup is still running. The label’s worktree is being torn down for that whole window, so a spawn taking the label now would enter a directory the removal is about to delete. Held from the cleanup’s handoff until it finishes, and released by its own Drop so a panicking cleanup cannot leave a label blocked forever. In-memory only, like the rest of the worker registry.

§spawn_failures: Mutex<HashMap<SessionSlot, String>>

Why the last spawn or connection for a slot failed, keyed by slot. Survives the failed attempt itself, which is what a DomainSession cannot: a connection failure releases the session, so by the time a view reads the slot there is nothing left to say the attempt ever happened. Written before that release and cleared when a session for the slot comes up again, so it reports an unrecovered failure rather than a history.

§deaths: Mutex<HashMap<SessionSlot, SeatDeath>>

The occupant that died on a slot, keyed by slot, held until a reader has been shown the seat again or a new turn commits there. Self::spawn_failures says an attempt failed; this says the seat HAD something running and it ended under its own process, which is what lets the recovery be announced as a resume or a restart under a new id rather than reading as a plain connect - and what keeps the failure and its reason on the row until someone has looked.

§heal_progress: Mutex<HashMap<SessionSlot, HealProgress>>

Where each dead seat’s heal has got to while it is restarting, keyed by slot. The healing supervisor writes it between attempts; every row that draws the seat reads it, and a lead has no worker entry for the state to live on instead.

§healing: OnceLock<Arc<Healing>>

The auto-heal supervisor, built on the first death that reaches it. It holds a Weak<Workspace>, which does not exist while the value is still being constructed.

§held_work_seats: HeldSeats

The seats a view is showing, each with the working-tree scan loop it runs. Empty is the quiet case: a seat nobody holds is not scanned.

§domain_handles: Mutex<HashMap<SessionSlot, Arc<Mutex<DomainSession>>>>

Shared DomainSession handles, one per active SessionTask. pub(crate) so crate-internal spawn and delivery paths can reach a session’s DomainSession directly.

§review_origin: Mutex<HashMap<(String, String), SessionSlot>>

The session that submitted the reviews on a (project, branch) - the target for a worker’s review-activity notice. Set by Self::submit_review; latest submit wins (the reviewer is one human whose session may rekey across a resume). pub(crate) so the impl block in crate::review can reach it.

§review_activity: Mutex<HashMap<SessionSlot, Vec<ReviewActivity>>>

Review actions a caller took during its current turn, keyed by the caller’s SessionSlot. Appended by Self::review_reply / Self::review_resolve and drained into one notice per review at the caller’s turn end (Self::drain_review_activity). pub(crate) so the impl block in crate::review can reach it.

§supervisor: Arc<Supervisor>

The process’s subsystem supervisor: every long-lived background task registers here, so a death is named and a restart is bounded.

§dictate_preflight_started: AtomicBool

Set the first time Self::start_dictate_preflight runs, so a second call cannot spawn a second prewarm over the same models. Set before the [dictate] enabled gate: the flag reads “the boot wave asked for the prewarm”, which is the one call a test can watch without a 3 GB fetch.

§voice_prewarm_started: AtomicBool

Records that the boot wave asked for the prewarm, set before the [tts] enabled gate: the flag reads “the wave asked”, which is the one call a test can watch without the weights. It is not a dispatch guard; the guard only stops a second load of the same engine.

§voice_prewarm_dispatched: AtomicBool

Set once the prewarm is dispatched, after the enabled gate, so a second call cannot spawn a second load of the same engine.

§gateway: Arc<Gateway>

The gateway’s listener + session bindings. Children are stamped with a base URL that names this listener, and every spawn registers here.

§gateway_ready: AtomicBool

true once the listener’s port is bound. The boot gate stays shut until it is, because every session’s base URL names it.

§gateway_port: u16

The port the listener binds: [gateway] port or the default.

§gateway_url: Mutex<Option<String>>

The listener’s bound URL, stored when the port is taken. The stamp reads THIS, not the config value, so the base URL a child is stamped with is structurally the one that answers.

§gateway_bind_error: Mutex<Option<String>>

The bind failure, if the listener could not start. Surfaced by the launchpad’s gate label; None while loading or bound.

§kick_dispatcher_tx: UnboundedSender<KickRequest>

Sender half of the worker-kick channel (#259). Cloned via Self::enqueue_kick by maybe_kick_worker_on_connected (and any future kick site). The matching receiver lives in kick_dispatcher_rx_slot until Self::start_kick_dispatcher takes it out and spawns the drainer task.

§kick_dispatcher_rx_slot: Mutex<Option<UnboundedReceiver<KickRequest>>>

Single-take slot holding the matching receiver. Self::start_kick_dispatcher pops it on first call and hands it to the drainer task; subsequent calls find None and no-op (mirrors start_usage_poller’s guard against duplicate spawns).

§_single_instance_lock: Option<File>

Single-instance lock file held open for the process lifetime. Workspace::new takes an exclusive flock on a machine-local lockfile keyed by the config dir (see crate::single_instance) so a second forge on the same config dir refuses to start; the flock releases when this File drops (Workspace teardown / process exit / crash). Held purely for that side effect - never read. None in testing_stub and on the degraded acquire path.

§crons: Mutex<Vec<CronEntry>>

Durable forge crons (mcp__forge__cron). In-memory working set, loaded at boot from the machine-local redb store (crate::store::cron) and persisted back after every mutation - create/delete, the scheduler’s fire-advance, and boot catch-up - through the one Workspace::with_crons_mut path. The single-instance guard keeps this the only process holding that store, so this mutex alone serialises writes. pub(crate) so the impl block in crate::crons can reach it.

§crons_load_failed: AtomicBool

Set at boot when the durable crons could not be loaded: the store could not open, or its read failed. The in-memory set is empty while rows may still be on disk, so it is not the registry. cron__list refuses on it rather than answering an empty array a caller would read as “you have no crons”.

It gates cron__list only: under the same failed load delete_cron still answers NoSuchCron from the empty set, and create_cron still promises durability while persist_cron returns quietly. Both predate this cut and the charter’s list-then-delete flow is gated by the list refusal; the siblings are #2943.

§tasks: Mutex<Vec<Task>>

The live task list (mcp__forge__tasks). In-memory working set, loaded from the machine-local store at boot and persisted back after every mutation through the one Workspace::with_tasks_mut path. pub(crate) so the impl block in crate::tasks can reach it.

§parked_by_slot: Mutex<HashMap<SessionSlot, ParkedForSlot>>

Payloads addressed to a slot that had no live session when they arrived - a peer prompt, a fired cron, a Gotify notification, a Slack message - keyed by (org, project, label) (None = lead), the triple the sessions table uses. The slot’s session drains its own bucket on first Connected. pub(crate) so the impl block in crate::parked can reach it.

§pending_handoffs: Mutex<HashMap<SessionSlot, PendingBrief>>

The brief a handoff wrote, waiting for the successor it was written for to connect. Taken by the session task on that connect, which is what makes the brief the new occupant’s first turn. pub(crate) so the impl block in crate::handoff can reach it.

§swapping: Mutex<HashMap<SessionSlot, u64>>

Seats whose occupant is being swapped by a handoff: from the moment the brief is built until the successor connects, a prompt aimed at one is held rather than run by the session leaving.

§swap_epochs: AtomicU64

Mints a swap’s token, so a build that lost the seat cannot spawn.

§held_prompts: Mutex<HashMap<SessionSlot, Vec<HeldPrompt>>>

Prompts held for a swapping seat, delivered to its successor after the brief, in arrival order.

§gotify_subs: Mutex<Vec<GotifySubscription>>

Active Gotify subscriptions (mcp__forge__gotify). The set the stream matches each inbound message against. Durable ones (lead / team-worker) are also persisted to db and reloaded here at boot; ephemeral ad-hoc-worker ones live only in memory. pub(crate) so the impl block in crate::gotify can reach it.

§db: Arc<Mutex<Option<Db>>>

Machine-local redb store. Backs durable crons (crate::store::cron), Gotify and Slack subscriptions (crate::store::gotify, crate::store::slack) and persisted sessions (crate::store::sessions). None when the DB couldn’t open (degrade to in-memory-only, no persistence) or in testing_stub. pub(crate) so the impl block in crate::review can reach it.

§catalog_loaded: Arc<AtomicBool>

Whether the boot catalog scan has populated catalog. The scan runs in the background off the boot path; the Projects pane and the session lookups read the catalog it fills.

§pricing_fetcher: PricingFetcher

How Workspace::refresh_pricing fetches the litellm table. The real fetch in production; a test constructor scripts it so the refill is drivable without the network.

§catalog_scan_started: AtomicBool

Idempotence guard for Workspace::start_catalog_scan.

§cli_version: Arc<Mutex<Option<CliVersionInfo>>>

The installed and npm-published claude CLI versions, merged from every probe so far. One answer for every viewer, so it lives here rather than in the view that happened to probe first; None until the boot probe lands.

§last_fatal_error: Mutex<Option<AppError>>

The last fatal error, held so a view that was not subscribed when it fired can still learn of it: the update carries no state of its own and nothing else in the core records it.

§service_status: Arc<Mutex<Option<ServiceIssue>>>

The statuspage’s last answer, held so a view that attached after the probe landed can read it rather than having missed the one update that carried it. None until the probe answers.

§service_status_probe_started: AtomicBool

Idempotence guard for the service-status probe.

§gotify_connected: Mutex<bool>

Whether the Gotify stream is currently connected. Set by the subsystem pump on Connected / Disconnected, and cleared by the stop path and the pump’s exit guard; read by the Inspector’s status line. pub(crate) so the impl block in crate::gotify can reach it.

§gotify_app_index: Mutex<HashMap<String, u64>>

Gotify application name -> numeric appid map, fetched from the server’s /application list on subsystem start and refreshed on each reconnect. Resolves an application name filter to the appid inbound messages carry, and the reverse lookup for the envelope. pub(crate) so the impl block in crate::gotify can reach it.

§gotify_subsystem: Mutex<Option<GotifySubsystem>>

Shutdown handle for the running Gotify subsystem. Some while the pump runs; dropping/sending stops it, and the pump’s own exit guard clears it when the task ends. None when idle (no subscriptions) or unconfigured. Guards against double-starting. pub(crate) so the impl block in crate::gotify can reach it.

§slack: Arc<SlackWorkspaces>

One Slack client per [[slack]] entry, resolved at boot. Empty keeps the Slack connector dormant. pub(crate) so the impl blocks in crate::slack and crate::mcp::slack can reach it.

§slack_subs: Mutex<Vec<SlackSubscription>>

Active Slack subscriptions (mcp__forge__slack). The set each workspace’s pump sweeps. Durable ones are also persisted to db and reloaded here at boot. pub(crate) so the impl block in crate::slack can reach it.

§slack_subsystem: Mutex<BTreeMap<String, Sender<()>>>

Shutdown handles for the running Slack pumps, one per workspace label. Slack has a pump per [[slack]] entry where Gotify has a single server and a single handle.

§slack_connected: Mutex<BTreeMap<String, bool>>

Per-workspace pump liveness for the Inspector’s SLACK section. Keyed by label for the same reason.

§slack_inflight: Mutex<HashMap<(String, String), Vec<SlackPostStamp>>>

Conversations forge has a post in flight into, keyed (workspace, the resolved conversation), each entry a stamp per live call: one post call with its ledger write, or one upload’s shares read. The sweep skips a conversation while any stamp is within budget, because the message can reach history before its ts is ledgered; a stamp past the request timeout is expired, never obeyed, so a stalled call cannot silence the conversation.

§slack_user_ids: Mutex<BTreeMap<String, String>>

The authenticated user’s id per workspace, resolved once by the boot auth.test and needed to recognise <@U...> mentions.

§slack_user_names: Mutex<BTreeMap<(String, String), String>>

Display names resolved per (workspace, user id), so a message from a known author costs no users.info call.

§slack_author_failures: Mutex<HashSet<(String, String)>>

(workspace, user id) pairs whose failed lookup has been reported, so a persistent failure says so once rather than on every tick.

§slack_draft_user_failures: Mutex<HashSet<(String, String)>>

(workspace, user id) pairs whose failed DRAFT tag lookup has been reported. Kept apart from the author set so a draft’s miss does not spend the once-per-author line the sweep relies on.

§slack_thread_failures: Mutex<HashSet<(String, String, String)>>

(workspace, conversation, parent ts) whose failed thread read has been reported, so a persistent failure says so once rather than on every park.

§slack_thread_full_reads: Mutex<HashMap<(String, String, String), Instant>>

When each followed thread was last read whole, keyed (workspace, conversation, parent ts). In memory: a restart costs one extra whole read per thread, which is cheaper than a schema.

§slack_thread_first_walks: Mutex<HashSet<(String, String, String)>>

Followed threads whose next walk is their first since the row was created, keyed the same way.

§slack_thread_edit_watermarks: Mutex<HashMap<(String, String, String), String>>

Cache of each thread row’s persisted edit stamp, keyed the same way: an edit is measured against this, never against the reply cursor. A miss reads the row, because the stamp has to outlive the process.

§slack_ledger_failures: Mutex<HashSet<(String, String)>>

Conversations whose own-post ledger could not be read this run, keyed (workspace, conversation). The read is retried on every message; this only decides whether the failure is worth a line a reader sees, once rather than per message.

§slack_drafts: Mutex<HashMap<Uuid, (SessionSlot, SlackDraft, Sender<SlackDraftAnswer>)>>

Composed Slack messages held for the user’s decision, keyed by draft id and carrying the session that asked. The owner is stored beside it so an answer is only ever applied by the session it was addressed to.

§browser_handoffs: Mutex<HashMap<Uuid, (SessionSlot, HandOff, Sender<HandOffEnding>)>>

Browser hand-offs parked for the person at a client, keyed by their id and carrying the session that asked. The owner is stored beside each so an answer is only ever applied by the session it was addressed to; the blocked browser_hand_off handler awaits the sender, with no timeout, for as long as the person takes.

§terminals: Mutex<HashMap<SessionSlot, Arc<TerminalEntry>>>

The per-seat terminals, created lazily on first use and living as long as the seat: /new, /resume and a handoff leave the PTY, its shell and its scrollback untouched; a release ends the seat and its terminal.

§terminal_handoffs: Mutex<HashMap<Uuid, (SessionSlot, TerminalHandOff, Sender<HandOffEnding>)>>

Terminal hand-offs parked for the person at a client, keyed by their id and carrying the session that asked. The blocked terminal__hand_off handler awaits the sender, with no timeout, for as long as the person takes.

§next_terminal_id: AtomicU16

The id the next terminal takes. One per seat today, but the wire carries an id so a second terminal per seat is additive rather than a protocol change.

§settings_writes: Mutex<HashMap<Uuid, (SessionSlot, SettingsWriteAsk, Option<Sender<bool>>)>>

Configuration writes held for the reader’s approval, keyed by the write’s id and carrying the session that asked. Its own registry for the same reason the draft’s is: a held write exists on a seat with no domain, and nothing reaches forge.toml until the blocked handler’s oneshot is answered.

§parked_vault_asks: Mutex<HashMap<Uuid, (SessionSlot, VaultDraft, Option<Sender<VaultAskAnswer>>)>>

Vault calls held for the reader’s approval, keyed by the call’s id and carrying the session that asked. Its own registry for the same reason the settings write’s is: a held call exists on a seat with no domain, and no operation runs in the vault until the blocked handler’s oneshot is answered.

§pending_seq: Mutex<HashMap<SessionSlot, u64>>

How many asks have ever arrived for a seat. The ask’s identity on the wire: one ask replacing another reads as a different ask, and answering one of several leaves the number where it was.

§slack_recently_delivered: Mutex<HashMap<(String, Option<String>, String, String, Option<String>), Instant>>

Slack messages handed to a session recently, keyed by (project, owner, conversation, ts). A sweep re-runs a batch whenever a 429 lands mid-sweep, a watermark write fails, or the process dies between the two - this is what makes that re-run idempotent rather than a re-delivery.

§slack_load_failed: AtomicBool

Set at boot when the durable Slack subscriptions could not be read. The Inspector SLACK section reads it: without it the empty subscription set would hide the failure.

§slack_user_id_retries: Mutex<BTreeMap<String, SystemTime>>

The last time each workspace’s user-id retry ran, so a failing auth.test is retried at most once a minute rather than per sweep.

§slack_verification_started: AtomicBool

Set the first time Workspace::start_slack_verification runs. Subsequent calls early-return to avoid spawning duplicate probes.

§respawn_in_flight: Mutex<HashSet<ProjectKey>>

Per-project in-flight guard for the lead Connected hook’s catalog scan. Inserted synchronously when respawn_workers_for_lead starts; removed when the async scan completes and dispatches its SpawnWorker commands. A concurrent second Connected (e.g. a fast /new reconnect) checking this set sees the entry and skips its own respawn, preventing duplicate worker sets while the scan is in flight. The existing live_workers.is_empty() gate covers the post-dispatch case.

§unwakeable_crons: Mutex<HashSet<CronId>>

The cron ids the LAST fire_due_crons pass found unwakeable, so the first warning about one is a WARN and the repeats are not.

Not a cache: nothing but that warning’s level reads it, and it holds only what the most recent pass saw, replaced wholesale at the end of each pass. That is what stops an entry outliving its condition - a cron that fires, advances or is deleted is simply absent from the next pass’s set, so one that goes unwakeable again is new again and warns at WARN again. A set that never cleared would silence a cron that is firing.

§cooling_crons: Mutex<HashMap<CronId, SystemTime>>

The cron ids the last fire_due_crons pass found cooling, with the time each last warned. Replaced wholesale each pass, like unwakeable_crons, so an episode that clears drops its marker and a later one warns again.

§command_intercept: Mutex<Option<Vec<Command>>>

Test-only intercept buffer for app-level Commands. When Some, dispatch captures the command into the buffer instead of routing it to the spawn::* handler - used by respawn tests to assert what would have been dispatched without spinning up real subprocesses. Always None in production (no enable hook outside test cfg).

§test_extra_projects: Mutex<Vec<LoadedProject>>

Test-only project overlay. Entries appended via seed_test_project are searched first in find_project_view_by_name so tests can drive the Connected-hook respawn trigger without writing a real forge.toml. Empty in production.

§test_user_preferences: Mutex<Option<Value>>

Test-only stand-in for the CLI’s per-user preferences document, so a view fixture reads an ignore preference without the machine’s real $HOME/.claude.json. None means no fixture seeded one, and the read goes to the file.

§test_claude_pid: Mutex<HashMap<SessionSlot, u32>>

Test-only stand-in for a seat’s live claude pid, so a fixture seat’s held loop walks a real process tree with no CLI behind it. A seat with no entry reads the pid off its own handle, as production does.

§test_scan_gate: Mutex<Option<Arc<Semaphore>>>

Test-only gate every stale read waits on before it scans, so a fixture can hold a read in flight and see what a hold answers first. None means reads run as they do in production.

Implementations§

Source§

impl Workspace

Source

pub fn dictate_bench(&self) -> BenchState

The bench’s state, for the page’s read.

Source

fn read_aloud_dir(&self) -> Option<PathBuf>

The directory the read-aloud set lives under, beside forge’s other machine-local stores; None when no app-support dir resolves. A test points it at its own dir rather than the real one.

Source

pub fn read_aloud_state(&self) -> ReadAloudState

The read-aloud set, as the page draws it: the recordings this machine has, whether one is being recorded right now, the passage they are read from, and the last write’s failure when there was one.

Source

pub(crate) fn delete_read_aloud(&self, id: &str) -> Result<(), DispatchError>

Drop one recording from the read-aloud set, by the id the page read.

The id is a directory name this server wrote, and it is checked against that shape before anything is removed: a page-supplied path must never name a directory outside the set.

Source

pub(crate) fn start_read_aloud( &self, initiator: Option<u64>, ) -> Result<(), DispatchError>

Begin recording the read-aloud set: the page’s own microphone feeds the frames, and no engine and no transcript are involved - the passage’s words are known, so recording transcribes nothing.

One capture at a time, like every other: refused while a take is live, and while a recording is already running - the refusal names whatever holds it.

Source

pub fn read_aloud_push(&self, samples: &[f32], initiator: Option<u64>) -> bool

Feed a client’s frames into the running recording, when that connection is the one that started it.

The audio is capped at the same span a take holds, so a recording nobody stops cannot grow without bound; the frames past the cap are dropped rather than silently stored as a shorter reading.

Source

pub(crate) fn finish_read_aloud( self: &Arc<Self>, keep: bool, initiator: Option<u64>, ) -> Result<(), DispatchError>

Stop the recording: keep writes it as the set, and a stop that does not keeps nothing. The write runs off the runtime thread, and its failure lands in the read rather than in this answer - the dispatch has already gone by the time the bytes hit the disk.

Source

pub fn bench_results(&self) -> Vec<BenchResult>

Every saved result, newest first, capped at what the page draws.

Source

pub(crate) fn start_bench( self: &Arc<Self>, target: BenchTarget, tier: Tier, ) -> Result<(), DispatchError>

Run one model over one tier’s corpus on this machine’s own material.

Refused while a take is live (the microphone is the machine’s, and a bench would hold a second engine through it) and while another bench runs - one at a time, like a download. The outcome rides SessionUpdate::DictateModelsChanged, the way every other dictate action’s does.

Source

pub(crate) fn delete_bench_result( &self, target: &BenchTarget, tier: Tier, corpus: &str, ) -> Result<(), DispatchError>

Drop one saved result: the row the page asked to delete, by its key.

Source

pub(crate) fn stop_bench(&self) -> Result<(), DispatchError>

Stop the run in flight. What it measured so far is discarded rather than saved: a partial corpus is not a result.

Source

pub(crate) fn bench_running(&self) -> bool

Whether a bench holds the machine, which is what a swap waits on. Both benches count: each loads its own engines, and a swap under either would pull a model out from under a run.

Source

fn claim_bench(&self) -> Result<(), DispatchError>

Take the one bench slot, or answer that another run holds it. The claim is a single atomic, so two presses cannot both pass the check and load two engines.

Source

fn release_bench(&self)

Source

pub fn speaking_bench(&self) -> SpeakingBenchState

The speaking bench’s state, for the page’s read.

Source

pub fn speaking_bench_results(&self) -> Vec<SpeakingBenchResult>

Every saved speaking result, newest first, capped like the words bench’s list.

Source

pub(crate) fn start_speaking_bench( self: &Arc<Self>, ) -> Result<(), DispatchError>

Score the speaking loop on the public corpus: synthesize each transcript, hear it back through the settled recognizer, and record the round trip. Refused while a take is live or another bench runs - one at a time, like the words bench.

The run gets a thread of its own with a current-thread runtime: it awaits the engine’s chunks and blocks on recognition, so it must not hold a worker from the server’s runtime for minutes.

Source

pub(crate) fn stop_speaking_bench(&self) -> Result<(), DispatchError>

Stop the speaking run in flight. What it measured so far is discarded rather than saved: a partial corpus is not a result.

Source

async fn run_speaking_bench(self: Arc<Self>)

Gather the public corpus, load both engines, and run the loop.

Source

fn settle_speaking_bench(&self, outcome: Result<SpeakingBenchResult, String>)

A speaking run that ended: a result is saved, a stop is idle, and any other ending is a failure with its own reason.

Source

fn save_speaking_result(&self, result: &SpeakingBenchResult)

Source

async fn run_bench(self: Arc<Self>, target: BenchTarget, tier: Tier)

Gather the corpus, build the target’s config, and run it.

Source

fn settle_bench_success(&self, result: &BenchResult)

A run that produced a result: save it, release the one bench slot, and go idle. The release is here rather than only on the failure paths, or one successful run would hold the slot for the process’s life.

Source

fn settle_bench(&self, target: &BenchTarget, reason: String)

A run that ended without a result: idle when it was stopped on request - a partial corpus is not a result, and nothing is saved - and failed with its own reason otherwise.

Source

fn fail_bench(&self, target: BenchTarget, reason: String)

Source

fn save_bench_result(&self, result: &BenchResult)

Source§

impl Workspace

Source

pub fn biometric_enrolments(&self) -> Result<Vec<BiometricEnrolment>, String>

Every device’s enrolment, read from the machine-local store.

A store that cannot be read is an error rather than an empty list: “no device is enrolled” and “the enrolments could not be read” send a reader looking in different places.

Source

pub(crate) fn enrol_biometric( &self, id: Uuid, device: &str, purpose: &str, blob: &str, ) -> Result<(), String>

Register one purpose’s wrapped blob under id, creating the device’s enrolment when the id is new and replacing that purpose’s bytes when it is not. The person’s press is this command’s only caller; a rotation rewrite goes through Self::rewrite_biometric, which cannot create.

Source

pub(crate) fn rewrite_biometric( &self, id: Uuid, purpose: &str, blob: &str, ) -> Result<bool, String>

Replace an existing purpose’s blob, refusing to create.

The person’s press is what creates a row; this is the rotation rewrite, which may only update one. A device the person revoked has no row, so its rewrite answers false and the client locks rather than re-registering itself.

Source

pub(crate) fn release_biometric( &self, id: Uuid, purpose: &str, ) -> Result<Option<BiometricBlob>, String>

The blob enrolled for purpose under id, stamped as used.

None is “this device has nothing enrolled for that purpose”, which is the answer a client falls back to the password on. A store failure is an error, because a client that cannot tell the two apart would silently skip the password path.

Source

pub(crate) fn revoke_biometric( &self, id: Uuid, purpose: Option<&str>, ) -> Result<(), String>

Drop one purpose’s blob, or None for the device’s whole enrolment. Revoking the last purpose drops the enrolment with it: a device holding nothing is a row with no meaning.

Source

pub(crate) fn lock_vault(&self) -> bool

Lock the shared vault now, on the reader’s own press rather than the idle clock. The process holds one vault session, so any seat’s lock is every seat’s; parked cards are re-stamped so a dock that already drew one asks for the password again.

false when no vault is configured, which is the only state a view would not have drawn the action in.

Source

fn announce_biometric_enrolments(&self)

Tell every view the enrolment set moved, as the whole set the core holds. A write that cannot be read back announces nothing and says so; the next read still answers the truth.

Source§

impl Workspace

Source

pub fn board_rows( &self, project: &str, now: SystemTime, stale_secs: u64, ) -> Vec<BoardRow>

Every live row of project, with its derived facts.

Source

pub fn fleet_rows(&self) -> Vec<FleetRow>

One row per project, for the fleet page.

Source

pub fn unaccounted(&self, project: &str) -> Vec<String>

Live workers of project holding no open row.

Source§

impl Workspace

Source

pub(crate) async fn register_browser_hand_off( &self, caller: &SessionSlot, reason: &str, target: &str, ) -> Result<(Uuid, Receiver<HandOffEnding>), String>

Hold a browser hand-off for the person at a client, and hand back its id plus the receiver the caller awaits.

No timeout rides the wait. The ask dock’s own timelines say a held prompt sits for hours, so nothing here ends it but an answer or the caller’s own drop. The one case answered up front is a stream nobody can answer on: holding a session on a prompt no view can draw would park it forever, which is the failure this path exists to refuse.

Source

async fn browser_hand_off_started( &self, caller: &SessionSlot, target: &str, ) -> Result<u16, String>

Take the fleet’s hold for a hand-off, awaited so its refusal is the tool call’s. Answers the tab number the fleet resolved the page id to, or its own sentence for a page that is not open or not this seat’s.

Source

pub(crate) fn tell_browser_hand_off_finished( &self, caller: &SessionSlot, tab: u16, )

Tell the fleet a hand-off left its tab: the service acts on a tab that is still held, and a resolution for anything else touches nothing.

Source

pub(crate) fn browser_handoff_waiting( &self, id: Uuid, caller: &SessionSlot, ) -> bool

Whether a hand-off with this id is still registered to caller, without removing it - the read the dispatch guard makes before an answer.

Source

pub(crate) fn resolve_browser_hand_off( &self, id: Uuid, caller: &SessionSlot, answer: HandOffAnswer, ) -> bool

Remove a parked hand-off, answer its waiter, and tell every view it is gone. false when the id is not this caller’s to resolve.

Source§

impl Workspace

Source

pub fn dictate_models(&self) -> DictateModelsSnapshot

Everything the models page draws.

Source

pub fn start_dictate_catalogue(self: &Arc<Self>)

Load the cached feed, then fetch a fresh one when it is due. One task per forge run, started beside the dictation preflight; a no-op unless [dictate] enabled is set.

The cache read is synchronous - one small file on the boot path - and the fetch never is: it is what the state’s CatalogueCheck reports on.

Source

pub(crate) fn check_dictate_catalogue( self: &Arc<Self>, ) -> Result<(), DispatchError>

The models page’s Check now: a fresh fetch, refused when there is nothing to check against or one is already running.

Source

fn spawn_catalogue_refresh(self: &Arc<Self>) -> bool

Fetch the feed off the runtime, land it, and push the re-read view. Answers false when a check is already in flight, deciding that under the same lock take that moves the state to CatalogueCheck::Checking: two near-simultaneous dispatches cannot both fetch.

Source

pub(crate) async fn fetch_catalogue_once(&self) -> Result<Catalogue, Error>

Fetch the feed once and land it: the freshness line, the rows and the cache. Answers the catalogue, or the error the fetch failed with; the last-known feed stands either way.

Shared by the background check and the active-model resolution, which needs the feed’s entry for a config key’s variant.

Source

pub(crate) fn catalogue_source(&self) -> CatalogueSource

Where the feed is fetched from. Test builds can point this at a loopback server; production always takes the real one.

Source

pub(crate) fn cleanup_source(&self) -> CleanupSource

Where the cleanup feed is fetched from, on the same terms - and a test serving the speech feed serves this one from the same loopback, so a check in a test reads one server rather than reaching the Hub.

Source

fn catalogue_dir(&self) -> Option<PathBuf>

Where the fetched feed is cached: the workspace’s own app-support base (re-resolving the machine dir here would let a test’s fetch write into the real tree), or a test’s own override.

Source§

impl Workspace

Source

fn chases_for_project(&self, project: &str) -> Vec<TaskChase>

Every chase already fired for project.

Source

fn history_for_project(&self, project: &str) -> Vec<TaskTransition>

project’s transitions, for the rung that reads when a wait resolved.

Source

fn record_chase(&self, chase: &TaskChase)

Record one fired chase, durably, so the rung never repeats.

Source

fn forget_drift(&self, project: &str, label: &str)

Forget one worker’s naming: its slate cleared, so the record goes with the state and the next drift is named at once.

Source

pub fn chase_sweep(&self, deliver: &dyn Deliver, now: SystemTime) -> usize

One pass of the chase over every project: derive the crossings, walk the ladder for each, deliver, and record what fired. Returns how many messages reached a session.

Source

pub fn start_chase_sweep(self: &Arc<Self>)

Spawn the chase sweep: a background pass every CHASE_INTERVAL_SECS that delivers the crossings. The registry makes a second call a no-op; started once at boot. Mirrors Workspace::start_cron_scheduler.

Source§

impl Workspace

Source

pub(crate) fn with_crons_mut<R>( &self, f: impl FnOnce(&mut Vec<CronEntry>) -> R, ) -> R

Lock the durable cron list and apply f. Every cron-list mutation routes through here to take the lock, and the mutation site persists what it changed from inside its own closure - a row at a time, never the whole in-memory set. A boot read that failed leaves that set EMPTY while the db handle stays live, so a whole-set write would erase every durable row the read never loaded.

Source

fn persist_cron(&self, cron: &CronEntry)

Persist one cron row, replacing any prior record for its id. Called from inside the mutation’s lock.

Source

fn delete_stored_cron(&self, id: &CronId)

Delete one cron row by id. Called from inside the mutation’s lock.

Source

pub(crate) fn push_cron(&self, entry: CronEntry)

Append a cron and persist it. Backs cron__create.

Source

pub(crate) fn remove_cron(&self, project_name: &str, id: &CronId) -> bool

Remove the cron id in project_name regardless of who owns it, persist, and report whether an entry was removed. Backs the fire-router’s dead-slot removal; the owner-scoped cron__delete uses Self::remove_cron_owned_by.

Source

pub(crate) fn remove_cron_owned_by( &self, project_name: &str, id: &CronId, label: Option<&str>, ) -> Option<CronEntry>

Remove the cron id in project_name only when the slot that owns it matches label (None = a lead cron, Some(label) = that worker’s), persist, and return the entry removed (None when nothing matched). Backs cron__delete so a caller deletes only its own crons.

Source

pub(crate) fn delete_crons_for_worker( &self, project_key: &ProjectKey, label: &str, )

Remove worker label’s crons in project_key from the in-memory set and the store, scoped to (project name, team_role == Some(label)). Backs spawn::teardown_worker so a despawned dynamic worker’s crons go with its durable row. The key resolves to a name via the same view lookup remove_gotify_subscriptions_for_worker uses.

Source

fn announce_cron_schedules_changed(&self, project_name: &str)

Tell every view the project’s schedule set moved, as the set the write just left, on the project’s lead seat. A name no project carries has no seat to route on and announces nothing.

Source

pub fn durable_crons_unavailable(&self) -> bool

Whether this run’s boot could not load the durable crons. cron__list refuses on it.

Source

pub fn crons_for_project(&self, project_name: &str) -> Vec<CronEntry>

The crons registered for project_name, whatever their owner. The two scoping consumers narrow it - cron__list to the caller’s own crons, the Inspector SCHEDULES snapshot to the active tab’s - while the refused-delete determination reads it whole, because telling “not in this project” from “not yours” needs the other owners’ rows.

Source

pub(crate) fn all_crons_snapshot(&self) -> Vec<CronEntry>

A snapshot of every cron across all projects. Backs the scheduler’s per-tick due-check.

Source

pub(crate) fn remember_cron_name( &self, slot: &SessionSlot, uuid: &str, cron_id: &str, description: Option<&str>, )

Remember the schedule a fired prompt came from, so its row can name the schedule after a reload the delivery frame does not survive.

Best-effort: a store that will not write costs a reloaded row its title, which is exactly what it had before the pairing existed.

Source

pub fn cron_names( &self, slot: &SessionSlot, uuids: &[&str], ) -> BTreeMap<String, CronName>

The schedules the given fired prompts came from, keyed by the prompt’s own id: the read half of the pairing the delivery frame carries.

Source

pub(crate) fn advance_or_remove_cron(&self, id: &CronId, fired_at: SystemTime)

Advance a fired cron and persist: a recurring cron records last_fire and moves next_fire to the next future slot (removed if it somehow has none); a run-once is removed. A direct state mutation - the fire’s prompt delivery goes through the Command bus separately.

Source

pub fn fire_due_crons(self: &Arc<Self>, now: SystemTime)

Fire every cron due at now: deliver each prompt into its project session (spawning it if asleep, unless the entry carries wake: false for a job that must not wake a sleeping owner, in which case a sleeping owner leaves the fire due and unadvanced) and advance/remove the entry. Delivery routes through the Command bus (a session action); the advance is a direct state write - kept separate per the cron state-vs-delivery split. now is injected so tests are deterministic. Also the boot catch-up: calling this once at startup fires every cron that came due while forge was down, advancing each past its missed slots (catch-up-once).

Source

pub fn start_cron_scheduler(self: &Arc<Self>)

Spawn the cron scheduler: a background task that wakes every CRON_TICK_INTERVAL (~60s), fires every due cron, and exits when the workspace drops. The registry makes a second call a no-op. Mirrors Workspace::start_usage_poller; started once at boot.

Source§

impl Workspace

Source

pub fn seed_test_cron(&self, entry: CronEntry)

Register a durable cron directly, bypassing the MCP create path. Cross-crate test access to the otherwise pub(crate) cron store so a view can exercise the Inspector’s refresh_forge_crons resolution against a seeded cron.

The store write without the announcement: a fixture seeds state, it does not perform the write a view is owed a frame for.

Source§

impl Workspace

Source

pub(crate) fn register_document_hand_off( &self, seat: &SessionSlot, path: &str, reviewable: bool, ) -> Option<(Uuid, Receiver<DocumentHandOffEnding>)>

Hold a document hand-off for the reader, and hand back its id plus the receiver the caller awaits.

No client, no park. A forge with no attached client returns None rather than a receiver: holding a session on an ask no view can draw would park it for ever, which is the failure this refuses. The raise goes out as an ANSWERING prompt, the same way a browser hand-off’s does - the ask needs a client that can answer it.

Source

pub(crate) fn document_handoff_waiting( &self, id: Uuid, seat: &SessionSlot, ) -> bool

Whether a hand-off with this id is still registered to seat, without removing it - the read the dispatch guard makes before an answer.

Source

pub(crate) fn resolve_document_hand_off( &self, id: Uuid, seat: &SessionSlot, ending: DocumentHandOffEnding, ) -> bool

Remove a parked hand-off, answer its waiter, and tell every view it is gone. false when the id is not this seat’s to resolve.

Source

pub async fn hand_off_document( &self, seat: &SessionSlot, path: &str, reviewable: bool, ) -> Result<(String, DocumentHandOffEnding), String>

Hand one document to the reader: read it out of seat’s own tree, ask the clients drawing that seat to open it, and park until the reader answers.

The answer is a decision, not the open. The reader accepts, reads and comments; the viewer’s approve or feedback releases the call with the decision, note and filed review, NotNow releases it with their decline, and the waiter’s own drop with DocumentHandOffEnding::Abandoned. A refused read raises nothing and parks on nothing.

Source§

impl Workspace

Source

pub fn gotify_config(&self) -> Option<GotifyConfig>

The [gotify] server connection from forge.toml, or None when the section is absent. None keeps the Gotify subsystem dormant and makes gotify__subscribe error.

Source

pub(crate) fn add_gotify_subscription( &self, sub: GotifySubscription, durable: bool, )

Register a Gotify subscription in the active set. Durable ones (lead / team-worker) also persist to the redb store; ephemeral ad-hoc-worker ones stay in memory only and drop on restart.

Source

pub(crate) fn remove_gotify_subscription_owned_by( &self, project: &str, id: Uuid, owner: Option<&str>, ) -> Option<GotifySubscription>

Remove the subscription id in project only when its owner matches owner (None = a lead subscription, Some(label) = that worker’s), from both the active set and (when present) the redb store. Returns the row as it stood (None when nothing matched). Backs the owner-scoped gotify__unsubscribe so a caller removes only what it subscribed, mirroring Self::remove_cron_owned_by. Worker teardown uses Self::remove_gotify_subscriptions_for_worker instead and is deliberately not owner-gated.

Source

pub(crate) fn remove_gotify_subscriptions_for_worker( &self, project_key: &ProjectKey, label: &str, )

Remove the worker label’s Gotify subscriptions in project_key from both the active set and the redb store, scoped to (project name, team_role == Some(label)). Backs spawn::teardown_worker so a despawned durable dynamic worker can’t orphan a persisted sub. Subscriptions are project-scoped by NAME, so the key is resolved to a name via the same view lookup resolve_identity uses when subscribing.

Source

pub fn gotify_subscriptions_for_project( &self, project: &str, ) -> Vec<GotifySubscription>

The active subscriptions for project, whatever their owner. The scoping consumers narrow it - gotify__list to the caller’s own subscriptions, the Inspector GOTIFY snapshot to the active tab’s stamped project name - while the refused-unsubscribe determination reads it whole, because telling “not in this project” from “not yours” needs the other owners’ rows.

Source

pub(crate) fn announce_connector_subscriptions_changed( &self, project_name: &str, )

Tell every view the project’s connector subscriptions moved, as the two sets the core holds, on the project’s lead seat. One update carries both connectors: they are one section of the board’s project row, and a reader that patches it needs the pair. A name no project carries has no seat to route on and announces nothing.

Lives here rather than on either connector: the section is the pair, so both seams announce through the one site.

Source

pub fn gotify_connected(&self) -> bool

Whether the Gotify stream is currently connected. Backs the Inspector GOTIFY status line.

Source

pub fn start_gotify_subsystem(self: &Arc<Self>)

Start the Gotify subsystem when it’s configured, has at least one active subscription, and isn’t already running. Idempotent. Spawns the forge-connectors pump, which runs the reconnecting stream task and translates its events into the gotify_* state below and GotifyHost::deliver calls. Called at boot and after a subscribe grows the active set.

The pump’s exit guard clears the handle when the task ends, so a pump that dies no longer wedges the subsystem: the next call here starts a new one.

Source

fn spawn_gotify_subsystem_task<F>( self: &Arc<Self>, token: Arc<()>, pump: F, ) -> JoinHandle<()> ⓘ
where F: Future<Output = ()> + Send + 'static,

Spawn the Gotify pump behind the guard that clears this run’s handle and liveness however the task ends: a return, a cancellation, or an unwind. Returns the task handle so a test can await the end.

Source

pub fn stop_gotify_subsystem_if_idle(&self)

Stop the Gotify subsystem once no subscriptions remain: signal the stream task to exit and mark disconnected. No-op while any subscription is active or the subsystem isn’t running.

Source

pub(crate) fn stop_gotify_subsystem(&self)

Signal the running pump to stop and mark the connector disconnected, whatever subscriptions remain. The workspace’s shutdown calls this so a clean stop reaches the pump’s own exit rather than leaving the handle armed for the task’s drop.

Source§

impl Workspace

Source

pub fn seed_test_gotify_subscription(&self, sub: GotifySubscription)

Register a Gotify subscription directly, bypassing the MCP subscribe path. Cross-crate test access so a view can exercise the Inspector’s refresh_gotify resolution, mirroring Self::seed_test_cron.

The active-set write without the announcement: a fixture seeds state, it does not perform the write a view is owed a frame for.

Source§

impl Workspace

Source

pub(crate) fn set_pending_brief(&self, slot: &SessionSlot, brief: PendingBrief)

Hold the brief a handoff has written until its successor connects.

Source

pub(crate) fn take_pending_brief( &self, slot: &SessionSlot, ) -> Option<PendingBrief>

Take the brief a swap left for slot, if one is waiting.

Source

pub(crate) fn clear_pending_brief(&self, slot: &SessionSlot)

Forget a brief whose swap never reached its successor.

Source

pub(crate) fn begin_swap(&self, slot: &SessionSlot) -> u64

Mark the seat as swapping its occupant: from the moment the brief is built until the successor connects, a prompt aimed at it is held rather than run by the session on its way out.

Source

pub(crate) fn swap_in_progress(&self, slot: &SessionSlot) -> bool

Whether a swap is in flight on this seat.

Source

pub fn handoff_in_flight(&self, slot: &SessionSlot) -> bool

Whether a handoff owns this seat’s composer right now: its judgment turn is running, or the swap it opened is still building or spawning.

The domain’s own flag is cleared at the swap, before the successor connects - so a page attaching in that window would otherwise read a live composer over a seat that has no session at all.

Source

fn end_machine_turn(&self, slot: &SessionSlot)

End the machine turn a handoff raised, wherever its build stops.

Every path out of the build that leaves no successor needs this. The frame at the handoff’s begin is the live page’s only source, and a successor’s record read is what normally clears it - so a build that stops without one would leave the seat’s composer down for good.

Source

pub(crate) fn swap_is_current(&self, slot: &SessionSlot, token: u64) -> bool

Whether token still owns the seat’s swap.

A build that lost the seat must not spawn. A replacement during ground_truth closes the gate, and a later handoff may open a new one; only the token says whether the NewSession this build is about to dispatch is still its own.

Source

pub(crate) fn dispatch_peer_prompt_under( self: &Arc<Self>, caller: &SessionSlot, key: &SessionSlot, text: String, uuid: String, ) -> Result<(), DispatchError>

Dispatch a peer message’s prompt, holding it for the successor when a swap is in flight and keeping its sender so a handoff that fails can answer it.

Source

pub(crate) fn hold_prompt(&self, slot: &SessionSlot, prompt: HeldPrompt) -> bool

Hold a prompt for the successor. true when a swap took it.

Source

pub(crate) fn end_swap(&self, slot: &SessionSlot) -> Vec<HeldPrompt>

End the swap and take everything held for it, in arrival order.

Source

pub(crate) fn record_handoff( &self, record: &HandoffRecord, predecessor_reads: &[String], )

Write one handoff to the store, which is what at_percent is tuned from later. A store that cannot write is logged, never a reason to hold up the swap: the session is already handing over.

Source

pub(crate) fn pending_brief_for( &self, slot: &SessionSlot, ) -> Option<PendingBrief>

The brief waiting for slot’s successor, when one is.

Source

pub(crate) fn handoff_for_successor( &self, slot: &SessionSlot, occupant: &str, ) -> Option<(HandoffRecord, Vec<String>)>

The handoff occupant arrived by, if it arrived by one.

Source

pub(crate) fn measure_successor( self: &Arc<Self>, slot: SessionSlot, occupant: String, cwd: String, predecessor_reads: Vec<String>, )

Measure the successor’s first turns off its own transcript and write them onto the record. Runs detached: the read is a whole transcript and the seat has work to do.

Source

pub(crate) fn start_handoff(self: &Arc<Self>, request: SwapRequest)

Run a handoff: build the ground truth, announce the swap, and start the new session on the same order /new uses.

The build reads the transcript and probes git, so it runs off the actor that decided it. The swap signal goes out before the spawn, so the seat knows the join before the successor’s transcript starts.

Source

async fn run_handoff(self: Arc<Self>, request: SwapRequest)

Source§

impl Workspace

Source

pub fn idle_sweep(&self, deliver: &dyn Deliver, now: SystemTime) -> usize

One pass over every project: deliver the injected sweep to each lead that is idle, or overdue for one. Returns how many prompts reached a session.

Source

pub fn start_idle_sweep(self: &Arc<Self>)

Spawn the idle sweep: a background pass every IDLE_SWEEP_INTERVAL_SECS that injects the board sweep into a lead with nothing in flight. The registry makes a second call a no-op; started once at boot. Mirrors Workspace::start_chase_sweep.

Source§

impl Workspace

Source

pub fn dictate_install(&self) -> InstallState

The install’s state, for the page’s read.

Source

pub fn installed_models(&self) -> Vec<InstalledModel>

Every model downloaded from the feed on this machine, oldest first.

Source

pub(crate) fn uninstall_model(&self, file: &str) -> Result<(), DispatchError>

Remove one model this machine downloaded: the file and the record.

The file name has to be one this machine recorded, because it crosses from a page and joins onto the models directory: a name no record carries is refused rather than removed, so ../../ and anything else a caller invents names nothing.

Refused while a role RUNS the file - the engine holds it loaded - and named by that role, because the swap that frees it is the caller’s next step rather than this one’s. The record’s own runtime pick goes with it: a pick left naming a file that is gone refuses the next boot, and the page that removed the file is the one that can clear it.

Source

pub(crate) fn start_install( self: &Arc<Self>, variant: String, ) -> Result<(), DispatchError>

Install one feed variant: fetch its doc, take the quant the row draws, and download that file into the models directory.

The outcome rides SessionUpdate::DictateModelsChanged, the same way a catalogue check’s does.

Source

fn fail_install(&self, file: String, reason: String)

Take the install no further, naming the step that stopped.

Source

fn note_download(&self, file: &str, got: u64, total: u64)

Source

fn set_install_file(&self, file: &str, total: u64)

Name the file while the doc is still being read, so the page’s bar has something to say from the first tick.

Source

fn record_installed(&self, model: &InstalledModel) -> bool

Record a downloaded model, answering whether the store took it.

false is a store that could not write, which is the caller’s to act on: a file nothing recorded is a file the page can neither show nor remove, so the install path takes the bytes back rather than leaving one behind.

Source

pub(crate) fn push_models(&self)

The page’s re-read, pushed the way a landed check pushes one.

Source

fn variant_for_install( &self, variant: &str, ) -> Option<(CatalogueSource, CatalogueEntry)>

The feed source and the entry a variant names, when both are known.

Source

async fn run_install(self: Arc<Self>, variant: String)

Fetch, check and record one variant. Every failure lands as an InstallState::Failed whose reason is its own, so the page says which step stopped rather than that something did.

Source

async fn run_speaking_install(self: Arc<Self>, speaking: SpeakingVariant)

Install one speaking variant: read the Hub’s current revision, take the variant’s ONNX file and every named voice at that revision, and record the set only once every digest matched.

Source

fn record_speaking_install(&self, install: &SpeakingInstall) -> bool

Record one speaking install, answering whether the store took it.

Source§

impl Workspace

Source

pub fn active_models(&self) -> Vec<(DictateRole, ActiveModel)>

The models the roles run, as the preflight resolved them: the compiled pins until that resolution lands, then whatever the config key, the runtime pick or the pin answered, per role.

Source

pub(crate) async fn resolve_active( &self, settings: &DictateSettings, ) -> Result<Vec<(DictateRole, ActiveModel)>, String>

Resolve every role’s model, fresh: the config key when set, else the runtime pick, else the compiled pin.

The key’s variant resolves through the installed record first, a local read; a variant that is not on disk reaches the feed’s doc table, which is the download source and the only network call here. A key that resolves through neither is an error naming the key, which the preflight renders as the reason forge stops.

Source

pub(crate) fn active_choice(&self, role: DictateRole) -> Option<ActiveChoice>

The runtime pick recorded for one role, when the store answers one.

A failed read reads as no pick - the role falls to the pin - and it says so, because a corrupt row silently reverting the pick would look like a pick that never happened.

Source

async fn resolve_variant( &self, key: &str, variant: &str, ) -> Result<ModelSpec, String>

The model a config key names: the installed record, else the feed’s doc table, else nothing.

Source

async fn catalogue_entry_for(&self, variant: &str) -> Option<CatalogueEntry>

The feed’s entry for one variant: the loaded feed, or one catalogue fetch when the loaded feed does not carry it.

The fetch is what keeps a config key working on a fresh machine, whose local store and cache both predate the variant.

Source

pub(crate) fn record_resolved_installs( &self, resolved: &[(DictateRole, ActiveModel)], )

Record a config-resolved model the preflight has just fetched, so the page’s installed set and the next boot’s resolution both see it.

Only the config leg reaches here: a runtime pick was installed before it could be picked, and the compiled pins are not recorded, because nothing here names their variant without the feed.

Source§

impl Workspace

Source

pub fn dictate_activate(&self) -> ActivateState

The activation’s state, for the page’s read.

Source

pub(crate) fn start_activate( self: &Arc<Self>, role: DictateRole, file: String, ) -> Result<(), DispatchError>

Make one installed model the role’s active model on the running forge: build the new engine while the old still serves, swap, then drop the old.

A role forge.toml pins is refused by name - the page can still download and bench it, but the runtime cannot move it - and a live take refuses the swap rather than being dropped under.

Source

pub(crate) fn start_deactivate( self: &Arc<Self>, role: DictateRole, ) -> Result<(), DispatchError>

Return one role to its [dictate] key or compiled default: clear the runtime pick and swap the engine back, in activation’s own build-then-swap order.

Source

fn pinning_key(&self, role: DictateRole) -> Option<String>

The config key that pins one role, when one does.

Source

fn pin_for(&self, role: DictateRole) -> Option<ModelSpec>

The compiled default one role runs, when it has one: the cleanup role has none while normalizer is off.

Source

async fn run_activate(self: Arc<Self>, role: DictateRole, file: String)

Load one installed model and swap the role onto it.

Source

async fn activate_speaking( self: &Arc<Self>, variant: &str, ) -> Result<String, String>

Swap one speaking variant in, building the engine before the swap so a failed load leaves the previous one serving.

Source

async fn run_deactivate(self: Arc<Self>, role: DictateRole, pin: ModelSpec)

Swap the role back to its compiled default and drop the pick.

Source

async fn swap_role_to( &self, role: DictateRole, spec: ModelSpec, from: ActiveFrom, ) -> Result<String, String>

Build the engine the role’s new model needs while the old one still serves, swap the Arc, then drop the old - never drop first, which would leave the role with no engine at all if the new load fails.

Answers the RFC 3339 stamp the pick is recorded with, or the reason the load failed; a failure changes nothing.

Source

fn fail_activate(&self, role: DictateRole, file: String, reason: String)

Source

fn record_pick(&self, role: DictateRole, variant: &str, at: &str)

Record one role’s runtime pick, so the next boot answers it.

Source

fn clear_pick(&self, role: DictateRole)

Drop one role’s runtime pick, so the role answers its config key or its compiled pin again.

Source§

impl Workspace

Source

pub(crate) fn park_peer_prompt( &self, slot: &SessionSlot, sender: &SessionSlot, wrapped: WrappedPrompt, )

Park a peer prompt for slot, drained by the session that next connects as that slot.

Source

pub(crate) fn peer_prompt_is_parked( &self, slot: &SessionSlot, id: &MessageId, ) -> bool

Whether slot’s bucket still holds the peer prompt with id. The wake path parks before it dispatches a spawn, so an entry the spawn’s own failure expired means the delivery was not accepted.

Source

pub(crate) fn park_cron( &self, slot: &SessionSlot, cron: &CronEntry, missed: bool, )

Park a fired cron prompt for slot, missed-marked when it came due while the owner was asleep. The entry’s identity rides along so the drained echo can name the schedule it came from.

Source

pub(crate) fn park_gotify( &self, slot: &SessionSlot, notification: GotifyNotification, )

Park a Gotify notification for slot.

Source

pub(crate) fn park_slack(&self, slot: &SessionSlot, messages: Vec<SlackMessage>)

Park a Slack batch for slot.

Source

pub(crate) fn hold_peer_prompt_unless_turn_ended( &self, slot: &SessionSlot, sender: &SessionSlot, wrapped: WrappedPrompt, turn_running: impl FnOnce() -> bool, ) -> Option<WrappedPrompt>

Hold a peer prompt for slot until the turn it arrived under ends, when the seat is idle again and the prompt can start a turn of its own - unless the turn has already ended, in which case the prompt comes back for delivery now.

turn_running is read while the bucket’s own lock is held, and the turn-end drain ends the turn’s markers before it takes the bucket: the take’s lock is what orders the two, so a delivery is either enqueued before a drain (and delivered by it) or reads the ended turn after that drain’s take (and is delivered by its caller) - never left behind a drain that already passed.

Source

pub(crate) fn take_turn_held_for_slot( &self, slot: &SessionSlot, ) -> Vec<ParkedPeer>

Take (and clear) the prompts held for slot’s running turn, in the order they arrived.

Source

pub(crate) fn take_parked_for_slot(&self, slot: &SessionSlot) -> ParkedForSlot

Take (and clear) everything parked for the connecting session’s slot. The caller passes the triple it was spawned with, rather than resolving one from its cwd: a cwd under no configured project would resolve to nothing and leave the payloads parked forever, silently.

A prompt held for a turn end rides the peer list: the slot that connected is a different, idle session, and dropping the hold with the bucket would lose it silently.

Source

pub(crate) fn expire_parked_for_slot( self: &Arc<Self>, slot: &SessionSlot, reason: PeerFailureReason, )

Drop everything parked for slot, acknowledging each peer message back to its sender so a message that never landed is not left looking delivered. The parked message is the only payload with a sender, so the Gotify and Slack drops have no one to tell and are logged instead.

Source§

impl Workspace

Source

pub fn plugin_update_run(&self) -> Option<PluginUpdateRun>

The last plugin update run or check, in flight or finished, for the Extensions read. None until one has happened this process.

Source

pub async fn plugins_inventory(&self) -> Option<PluginsInventorySnapshot>

The disk-only plugin inventory, scanned on the call: registry, marketplace manifests and the plugin cache, no claude spawn. None when no plugins root resolves.

Source

pub fn set_plugin_scan_for_test( &self, scan: impl Fn() -> Option<PluginsInventorySnapshot> + Send + Sync + 'static, )

Replace the disk scan, so a cross-crate test can answer the Extensions read with a fixture instead of this machine’s plugins root. Test-only. Takes a closure rather than the seam, so the crate’s own types stay private.

Source

fn plugin_cli(&self) -> PluginCli

Source

fn begin_plugin_action(&self) -> Result<(), DispatchError>

Take the single-action gate, or refuse naming the one in flight.

Source

fn plugin_home() -> Result<String, DispatchError>

The home directory plugin calls with no project context run in. Unavailable means the action fails rather than substituting another directory (hard rule 14).

Source

fn declared_project_path(&self, name: &str) -> Result<String, DispatchError>

The declared project’s path, by its forge.toml name.

Source

pub(crate) fn start_plugin_install( self: &Arc<Self>, plugin_id: String, scope: String, project: Option<&str>, ) -> Result<(), DispatchError>

Install one plugin at a scope.

Source

pub(crate) fn start_plugin_update( self: &Arc<Self>, plugin_id: String, scope: String, ) -> Result<(), DispatchError>

Update one installed plugin, in the scope and project the registry records for it.

The registry lookup is a disk scan, so it runs inside the spawned task: the dispatch is fire-and-forget and its outcome rides the Plugins frames, a refusal included - a plugin the registry does not hold in that scope reports as a failed action naming it.

Source

fn start_plugin_cli_action( self: &Arc<Self>, cwd: String, args: Vec<String>, message: String, dispatch_id: Option<u64>, ) -> Result<(), DispatchError>

One generic CLI action: gate, spawn, report. The outcome rides SessionUpdate::PluginsCliActionSucceeded / Failed.

Source

pub(crate) fn start_plugin_update_all( self: &Arc<Self>, ) -> Result<(), DispatchError>

Update exactly the stale installed plugins, one CLI call per entry, continuing past failures. The set is computed from a disk scan taken inside the run’s own task, so the dispatch never walks the tree on the runtime and the run updates what the marketplace reports now.

Nothing stale is not a refusal: the run lands finished with no rows, exactly as a check with nothing to report does, and the page draws its ordinary state.

Source

pub(crate) fn start_plugin_check(self: &Arc<Self>) -> Result<(), DispatchError>

Refresh the inventory through the CLI and report which installed plugins have a newer marketplace version, applying nothing.

Source

pub(crate) fn start_marketplace_add( self: &Arc<Self>, source: String, dispatch_id: Option<u64>, ) -> Result<(), DispatchError>

Add a marketplace by owner/repo or URL, at user scope.

Source

pub(crate) fn start_marketplace_repair( self: &Arc<Self>, name: String, ) -> Result<(), DispatchError>

Repair a drifted marketplace with the CLI’s remove-and-re-add pair. The re-add source is the repo for a git marketplace and the clone location for a directory one. The registry lookup is a disk scan, so it runs inside the task.

Source§

impl Workspace

Source

const MIN_POPULATED_REFS: usize = 3

Branch refs a repo must hold before a majority-dead sweep of it is trusted.

Measured against real clones, counting branch refs rather than names: a --depth 1 or --single-branch clone holds two, its local head and the one remote-tracking ref it fetched, whatever those branches are called. A full clone of a four-branch repo holds five. Three sits in that gap.

It has to be refs and not names. The name set over-includes on purpose, one phantom per slash, so release/2.0/rc1 adds 2.0/rc1 and rc1 beside itself and pushes a two-ref clone to three names - over the bound, guard silent. One slash is not enough to show it: release/2.0 gives two names against two refs, which is why a test built on it passes whichever quantity the guard reads.

Three is not a tuning knob, it is the only position where the guard exists. A solo repo - one main, no remote - knows ONE name, which is BELOW the shallow clone’s two. So any value that spares the solo repo also spares the shallow clone, and lowering to two sweeps the shallow clone anyway. There is no threshold that separates them; a solo repo with most of its review branches merged is refused, and that is the cost of the guard rather than something to calibrate away.

Source

pub fn load_review_threads( &self, project: &str, branch: &str, ) -> Result<Vec<ReviewThread>, String>

Persisted review threads for (project, branch), empty when the DB isn’t open or the read fails. project is the forge.toml project NAME (worktree-agnostic). Query-style, so this is a direct method - review-thread persistence is local redb IO, not an agent-driving action that needs the Command bus. Load the persisted review threads for (project, branch). Ok with an empty vec when the store isn’t open or the branch has no row; Err with a display string when an existing row fails to decode / read, so the overlay can surface the failure instead of showing a silently-empty review pane.

Source

pub fn upsert_review_thread( &self, project: &str, branch: &str, thread: ReviewThread, ) -> bool

Insert or replace one review thread by id in (project, branch). Returns whether the write was confirmed - false when the store isn’t open or the write failed, so the caller can leave the comment in the at-risk (not-yet-durable) bucket.

Source

pub fn delete_review_thread( &self, project: &str, branch: &str, id: &str, ) -> bool

Remove one review thread by id from (project, branch). Returns whether a thread was removed - false when the store isn’t open, the write failed, or no thread carried the id.

Source

pub fn set_review_thread_status( &self, project: &str, branch: &str, id: &str, status: ReviewStatus, )

Set the status of one review thread by id, bumping its updated_at.

Source

pub fn delete_branch_review_state(&self, project: &str, branch: &str)

Delete all of (project, branch)’s review state - threads and reviews together, in one transaction. Called on worktree teardown once the branch itself is gone, and by the boot sweep for a branch deleted since, so orphaned rows don’t linger and a later branch reusing the name inherits nothing.

Source

pub fn sweep_dead_review_branches(&self) -> usize

Drop the review threads and reviews of every branch that no longer exists in the repo they were filed against, and report how many branches were cleared.

Every branch is judged by exact membership in one listing per project - see forge_agent::env::worktree::repo_branch_names for why a per-branch ref pattern cannot answer this. A project whose root does not answer as a work-tree root is skipped whole, as is one absent from forge.toml: there is nothing to check against.

Source

pub fn start_review_branch_sweep(self: &Arc<Self>)

Run Self::sweep_dead_review_branches off the startup path. Two git calls per project, both blocking, so it goes on the blocking pool rather than holding boot up.

Source

pub fn load_reviews( &self, project: &str, branch: &str, ) -> Result<Vec<ReviewSet>, String>

Load the submitted reviews for (project, branch), oldest first. Ok with an empty vec when the store isn’t open or the branch has no row; Err with a display string when an existing row fails to decode, so the overlay can surface the failure.

Source

pub fn review_replies_waiting( &self, project: &str, branch: &str, ) -> Option<(usize, SystemTime)>

Worker answers on (project, branch) still owed a reviewer turn, paired with when the oldest of them landed. None when nothing waits or the row can’t be read.

The tally is the same awaits_reviewer count that drain_review_activity puts on a notice and that the /diff hydrate parks, over the same rows - so recomputing here reproduces what those two would have written rather than inventing a second number. since comes off the waiting threads’ own updated_at, which for a thread awaiting the reviewer is when the agent replied, so a recompute dates the wait from the answer rather than from the recompute.

Source

pub fn review_branches(&self, project: &str) -> Result<Vec<String>, String>

The branches under project that hold submitted reviews. Ok with an empty vec when the store isn’t open; Err on a read failure. review__list asks this only after its own branch came back empty, so a review filed against another branch can’t read as “no reviews”.

Source

pub fn submit_review( &self, project: &str, branch: &str, summary: Option<String>, thread_ids: &[String], origin: SessionSlot, ) -> Option<ReviewSet>

Seal a new review for (project, branch), filing the listed still-unfiled threads into it and appending it with the optional summary. Records origin as the notice target for the branch (the reviewer’s session, so a later worker review-turn pings it). Returns the minted forge_primitives::ReviewSet (None when the store isn’t open or the write failed) so the caller can surface its number.

Source

pub fn review_list( &self, project: &str, branch: &str, ) -> Result<Vec<ReviewSummary>, String>

The review__list view for (project, branch): submitted reviews with per-review comment tallies, newest first. Ok (possibly empty) when the store is closed or the branch has no reviews; Err when a stored row fails to decode, so the MCP surfaces the failure instead of a silent empty list.

Source

pub fn review_get( &self, project: &str, branch: &str, review_id: &str, ) -> Result<Option<ReviewDetail>, String>

The review__get view for one review on (project, branch): its overview plus the comments filed under it, each carrying the anchored code. Ok(None) when no such review; Err on a decode failure.

Source

pub fn review_reply( &self, caller: &SessionSlot, project: &str, branch: &str, comment_id: &str, author_label: &str, text: &str, at: &str, ) -> Result<CommentAnchorView, String>

Append a worker reply to comment_id on (project, branch) and return the comment’s anchor with the status after the append (Open -> Addressed; Resolved / Outdated unchanged). Records the reply in caller’s turn activity buffer so the reviewer is notified at turn end. Err when the store is closed, the write fails, or no comment with that id exists in this scope, so a stale or cross-branch id is rejected rather than silently ignored.

Source

pub fn review_resolve( &self, caller: &SessionSlot, project: &str, branch: &str, comment_id: &str, ) -> Result<CommentAnchorView, String>

Mark comment_id on (project, branch) Resolved and return the comment’s anchor with its new status. Records the resolve in caller’s turn activity buffer. Err when the store is closed, the write fails, or no comment with that id exists in this scope.

Source

fn note_review_activity( &self, caller: &SessionSlot, project: &str, branch: &str, comment_id: &str, replied: bool, )

Append one review action to caller’s turn buffer, resolving the review the action answers - the latest round the comment has a turn in. An unfiled comment is skipped: there’s no review to notify about.

Source

pub(crate) fn drain_review_activity( &self, caller: &SessionSlot, ) -> Vec<SessionUpdate>

Drain caller’s accumulated review activity into one SessionUpdate::ReviewActivityNotice per touched review, routed to the review’s submit origin. A review with no recorded origin is dropped (the reviewer still sees the state on /diff reopen) rather than mis-routed to the caller. Called at the caller’s turn end so a multi-comment review turn produces a single batched tally instead of one line per reply. Empty when the caller took no review actions this turn.

Source§

impl Workspace

Source

pub(crate) fn register_settings_write( &self, caller: &SessionSlot, ask: SettingsWriteAsk, ) -> (Uuid, Receiver<bool>, bool)

Hold a composed write and hand back its id plus the receiver the caller awaits. Addressed to caller, so the prompt surfaces in the session that will read the reply rather than in whichever session happens to be focused.

Source

pub(crate) fn settings_write_waiting( &self, id: Uuid, caller: &SessionSlot, ) -> bool

Whether a write with this id is still registered to caller, without removing it - the read the dispatch guard makes before an answer.

Source

pub(crate) fn resolve_settings_write( &self, id: Uuid, caller: &SessionSlot, approved: bool, ) -> bool

Answer a held write with the reader’s decision, returning whether one was waiting for THIS caller. True exactly once, for the entry that was there; a write held for another session, or one already answered and still applying, is refused rather than answered.

The entry stays until the write is settled (Self::settle_settings_write): the answer unblocks the caller, which applies the edit and then reports the outcome, so the resolved update says what happened to the write rather than merely that a button was pressed.

Source

pub(crate) fn settle_settings_write( &self, id: Uuid, caller: &SessionSlot, ending: SettingsWriteEnding, ) -> bool

Settle an answered or abandoned write: remove the entry and emit the ending every view reads. Idempotent, because the caller’s drop guard and the answer path both reach it and only the first has an entry.

Source

pub(crate) fn abandon_settings_writes(&self, slot: &SessionSlot)

End every write slot still holds as abandoned.

A parked write outlives its own future. The blocked MCP handler’s future is dropped when the session’s task goes, but a detached control task is not aborted by the client’s disconnect, so the registry sweep is what tells every other view the dock is over. Worker labels are reused, and an entry left behind would draw the previous occupant’s write on the next session’s dock.

Source

pub(crate) fn settings_write_asks(&self, slot: &SessionSlot) -> Vec<PendingAsk>

Every held write this seat owns, in the registry’s own order - a HashMap keeps no arrival order to offer, the same exception two Slack drafts already have. A seat with one write sees the same frame and read; only two at once can differ between them.

Source§

impl Workspace

Source

pub(crate) fn add_slack_subscription( &self, sub: SlackSubscription, durable: bool, )

Register a Slack subscription in the active set. Durable ones also persist to the redb store; ephemeral ad-hoc-worker ones stay in memory only and drop on restart.

Source

pub(crate) fn slack_delivery_seen( &self, project: &str, team_role: Option<&str>, message: &SlackMessage, ) -> bool

Whether this exact message has already been handed to its destination. The owner is part of the key: a lead and a worker in one project are two destinations, and one key would starve the second as “already delivered”. The edit ts is part of it too: a correction is a new delivery, while a sweep re-run is not.

Source

pub(crate) fn slack_delivery_commit( &self, project: &str, team_role: Option<&str>, messages: &[SlackMessage], )

Record a hand-off so a sweep re-run drops it. Committed only after a successful hand-off: a failed dispatch leaves the keys unrecorded, so the next sweep re-delivers instead of losing the messages behind a cursor that moved on.

Source

pub(crate) fn register_slack_draft( &self, caller: &SessionSlot, draft: SlackDraft, ) -> (Uuid, Receiver<SlackDraftAnswer>)

Hold a composed draft and hand back its id plus the receiver the caller awaits. The draft is addressed to caller, so its prompt surfaces in the session that will read the reply rather than in whichever session happens to be focused.

Source

pub(crate) fn slack_draft_waiting(&self, id: Uuid, caller: &SessionSlot) -> bool

Whether a draft with this id is still registered to caller, without removing it - the read the dispatch guard makes before an answer.

Source

pub(crate) fn remove_slack_subscriptions_for_worker( &self, project_key: &ProjectKey, label: &str, )

Remove every Slack subscription the worker label owns in project_key, from the active set and the store. Worker teardown calls this so a despawned worker cannot strand records - and so a delivery for it does not fall through to the lead.

Source

pub(crate) fn resolve_slack_draft( &self, id: Uuid, caller: &SessionSlot, resolution: DraftResolution, ) -> bool

Answer a held draft, returning whether one was waiting for THIS caller. That return is also how a caller asks whether a draft is still outstanding: it is true exactly once, for the entry that was there. A draft held for another session is refused rather than answered, and a caller that has gone away is not an error.

The removal is the one point every removal a view could have parked on funnels through - an answer, or a dead waiter - so the stand-down is emitted here. The one other removal, register_slack_draft’s no-answerable-subscriber path, drops a draft the update may already have reached an observer with: a subscriber that could ANSWER is what makes a draft answerable at all, and an observer draws no dock to stand down.

Source

pub(crate) fn set_slack_watermark( &self, workspace: &str, conversation: &str, ts: &str, )

Record the last ts delivered for a conversation in a workspace, through the store so it survives a restart.

Source

pub(crate) fn record_slack_own_post( &self, workspace: &str, conversation: &str, ts: &str, )

Record a message forge posted, so the sweep refuses it by identity rather than by author. forge posts as the user, so this is the only thing that tells its own words from the ones Ved typed.

Source

pub(crate) fn slack_own_post( &self, workspace: &str, conversation: &str, ts: &str, ) -> bool

Whether forge posted this message itself. A ledger that cannot be read answers false: a self-post delivered once costs what a lost ledger costs, while refusing would drop what the user wrote. The failure is worth a line once, not once per message, because a persistent fault would otherwise echo every post back to the agent.

Source

pub(crate) fn begin_slack_post( self: &Arc<Self>, workspace: &str, conversation: &str, ) -> SlackPostInFlight

Mark one call’s post in flight until the guard drops, so a sweep that runs mid-post skips the conversation instead of delivering a message whose ts is not ledgered yet. A guard covers ONE call and its ledger write: an operation of several calls takes one per call, or a slow but healthy Slack would retire the mark mid-operation.

Source

pub(crate) fn slack_post_in_flight( &self, workspace: &str, conversation: &str, ) -> Option<Duration>

How long the oldest live post call in conversation has been in flight, or None. A stamp past the request timeout is expired rather than obeyed, so a stalled call cannot silence the conversation. A stamp crossing half the budget says so once, on the transition; both are forge’s own problems, so both are warnings.

Source

pub(crate) fn name_slack_conversation( &self, workspace: &str, conversation: &str, name: &str, )

Fill in the display name on this workspace’s conversation subscriptions that have none. A durable record is written through the store; an ephemeral one is named in memory only. Only unnamed records are touched: a conversation renamed since it was subscribed keeps the stored name.

Source

pub(crate) fn slack_has_cursor( &self, workspace: &str, conversation: &str, ) -> bool

Whether the conversation (or the __mentions__ stream) has a sweep cursor yet. The cursor belongs to the conversation and is shared by every owner watching it, so a second subscriber seeds only when this is false.

Source

pub(crate) fn follow_slack_thread( &self, workspace: &str, conversation: &str, parent_ts: &str, owner: SlackThreadOwner, since: &str, )

Track a thread because a delivered message anchors it, owned by the matching subscription. A new row’s cursor starts at since - the delivered message’s own ts, not the parent’s: the parent of a mention can be weeks old, and a cursor seeded from it would idle-drop the thread before its first reply walk. A row that exists keeps its cursor and gains the owner if it was not there.

Source

fn forget_thread_clocks( &self, workspace: &str, conversation: &str, parent_ts: &str, )

Drop a removed thread’s in-memory clocks, so a thread re-followed under the same parent starts clean.

Source

pub(crate) fn mark_slack_thread_participating( &self, workspace: &str, conversation: &str, parent_ts: &str, )

Record that the user is in a thread, so every later walk reads past the cursor rather than the whole thread.

Source

fn slack_thread_idle_days(&self, workspace: &str) -> u64

The idle window this workspace drops a thread after, from its [[slack]] entry: the followed set is per workspace, so its window is too. A workspace whose entry is gone keeps the default.

Source

pub(crate) fn slack_stale_after(&self, workspace: &str) -> Duration

The age a delivered block reaches before its header says so, from its [[slack]] entry. A workspace whose entry is gone keeps the default.

Source

pub(crate) fn slack_followed_threads( &self, workspace: &str, conversation: &str, ) -> Vec<SlackFollowedThread>

The threads tracked for a conversation, pruned while listed: an owner with no remaining subscription in the workspace is dropped from the record, a row whose last owner goes is deleted, and a row idle past the workspace’s window is dropped with a debug log.

Source

pub(crate) fn slack_thread_watermark( &self, workspace: &str, conversation: &str, parent_ts: &str, ) -> Result<Option<String>>

A thread’s reply cursor, or None when the thread has none yet.

Source

pub(crate) fn set_slack_thread_watermark( &self, workspace: &str, conversation: &str, parent_ts: &str, ts: &str, )

Advance a thread’s reply cursor, through the store so it survives a restart.

Source

pub(crate) fn slack_thread_edit_watermark( &self, workspace: &str, conversation: &str, parent_ts: &str, ) -> Option<String>

A thread’s newest judged edit stamp. The map is a cache; a miss reads the row, so a restart judges edits against what the last process delivered instead of re-offering the thread’s whole edit history.

Source

pub(crate) fn set_slack_thread_edit_watermark( &self, workspace: &str, conversation: &str, parent_ts: &str, ts: &str, )

Advance a thread’s judged edit stamp, through the store so a restart does not re-offer the correction it clears. The mark only moves forward: a caller that read a stale value must not roll a newer one back.

Source

fn cache_slack_thread_edit_watermark( &self, key: &(String, String, String), ts: &str, )

Cache a thread’s edit stamp, moving it forward only.

Source

pub(crate) fn auto_subscribe_slack_conversation( &self, workspace: &str, conversation: &str, since: &str, name: Option<&str>, ) -> bool

Watch conversation because a mention arrived in it, owned by whoever subscribes to mentions in that workspace - not the lead, and not whichever session happens to be active. All mode is deliberate: pulled into a conversation by a mention, the agent should see what is said next rather than only the next mention. The cursor starts at since (the mention that pulled us in), so the channel’s history is not swept. Returns whether a record was added. A label that is empty or the conversation id itself (the search path’s fallback) stores as no name.

Source

pub fn slack_connected_workspaces(&self) -> BTreeMap<String, bool>

Per-workspace pump liveness, for the Inspector’s SLACK section. One entry per workspace a pump has reported on.

Source

pub(crate) fn watch_slack_conversation( &self, workspace: &str, project: &str, team_role: Option<&str>, conversation: &str, mode: SlackWatchMode, durable: bool, ) -> SlackSubscription

Watch a conversation for this owner, handing back the record that already exists when there is one - a second record renders as a second Inspector row for one channel. A watch whose mode differs updates that record, since the caller asked for something it did not have and there is no other route to change a mode. The check and the write share one guard, so two subscribes racing cannot both create.

Source

pub fn slack_subscriptions_for_project( &self, project: &str, ) -> Vec<SlackSubscription>

Every Slack subscription owned in project, whichever session owns it. Backs the pump, which needs the whole set.

Source

pub(crate) fn remove_slack_subscription_owned_by( &self, project: &str, id: Uuid, owner: Option<&str>, ) -> Option<SlackSubscription>

Remove the subscription id in project only when its owner matches owner (None = a lead subscription, Some(label) = that worker’s), from both the active set and the redb store. Returns the row as it stood (None when nothing matched). Backs the owner-scoped slack__unsubscribe so a caller removes only what it subscribed.

Source

pub(crate) fn clear_slack_cursors_after_removal( &self, removed: &SlackSubscription, )

Clear the sweep cursors the removed subscription was the last cover for. “Has cursor” must mean a live pending window: a cursor left behind by the last unsubscribe replays the whole gap to a later subscriber as fresh deliveries, and the mention cursor has no baseline arm at all, so its clearing is what makes the re-subscribe seed fresh.

Source

fn prune_slack_threads(&self, workspace: &str)

Prune every followed thread in workspace against the same predicate the sweep’s listing uses: an owner survives only while it holds ANY remaining subscription in the workspace. Runs when a subscription is removed, so a thread whose conversation is no longer swept does not freeze until some sweep happens to list it - Self::slack_followed_threads re-prunes as a backstop. The surviving set decides because an owner key removed with one subscription may still hold others: a Mentions target anchors threads in every conversation its mentions touched.

Source

pub fn start_slack_verification(self: &Arc<Self>)

Prove each workspace’s token once, logging the team it belongs to or the failure. Idempotent, and a no-op with no [[slack]] entry.

Source

pub fn slack_subscription_load_failed(&self) -> bool

Whether boot could not read the durable Slack subscriptions.

Source

pub(crate) fn retry_slack_user_id(self: &Arc<Self>, label: &str)

Re-prove one workspace’s token in the background, so a boot-time auth failure heals itself instead of leaving the mention stream down until restart. Rate-limited to one attempt a minute per workspace; the caller is the host’s user-id miss.

Source

pub fn start_slack_subsystem(self: &Arc<Self>)

Start one pump per configured workspace that has at least one subscription. Idempotent per workspace, and a no-op with no [[slack]] entry.

Source

pub fn stop_slack_subsystem_if_idle(&self)

Stop the Slack pump for any workspace that no longer has a subscription, and mark it disconnected. Pumps are per workspace, so one workspace losing its subscriptions must not keep another’s pump alive - nor leave its own running with nothing to watch.

Source§

impl Workspace

Source

pub(crate) fn with_tasks_mut<R>(&self, f: impl FnOnce(&mut Vec<Task>) -> R) -> R

Lock the task list and apply f. Every mutation routes through here to take the lock, and the mutation site persists what it changed from inside its own closure - a row at a time, never the whole in-memory set. A boot read that failed leaves that set EMPTY while the db handle stays live, so a whole-set write would erase every durable row the read never loaded.

Source

fn persist_task(&self, task: &Task)

Persist one task row, replacing any prior record for its id. Called from inside the mutation’s lock.

Source

fn delete_stored_task(&self, project_name: &str, id: &TaskId)

Delete one task row by project and id. Called from inside the mutation’s lock.

Source

pub(crate) fn resolve_task_id( &self, project_name: &str, stated: &TaskId, ) -> Result<TaskId, IdError>

Resolve a caller-stated id to the row it names: an exact id, or a unique prefix of one. Backs the delete path, which resolves before it removes so an id naming nothing still removes nothing.

Source

pub(crate) fn task_project_outside( &self, project_name: &str, stated: &TaskId, ) -> Option<String>

The project holding a row whose exact id is stated, when that row is outside project_name. Backs the refusal that keeps an id another project owns apart from one no project has; a stated prefix resolves only inside the caller’s own project, so only a full id is classified here.

Source

pub(crate) fn push_task(&self, task: Task)

Append a task and persist, recording its creation in the history. Backs tasks__create.

Source

pub(crate) fn update_task( &self, project_name: &str, id: &TaskId, by: By, f: impl FnOnce(&mut Task), ) -> Result<Option<Task>, MoveError>

Apply f to the task id in project_name, persist, record the transition, and return the record as the write left it (None when no such task is there). Open to every session in the project.

Rules ride on top of f, so no caller can forget them: a transition into completed on a row minted verify=user lands in waiting (the verify gate, never inherited from the epic); a patch cannot take a user-bound row’s look gate away - clearing its verify is refused by name; a completion of a row already waiting on the user’s look is refused by name; a cancel of a row waiting on the user is the withdraw door - it records the reason the caller states in detail, and a cancel without one is refused (#2340); a move off waiting drops the wait the row was holding, so a completion of any other waiting row applies and clears - except on a verify=user row, where a seat’s completion is refused by name while the row waits: the gate would trade its wait for a look park and answer with the status unchanged (#2414). The user’s own move off waiting to completed keeps that path and parks on the look; and completing a ROOT row closes it - the whole tree moves to the archive, refused while any child is still open or while any row in the tree is parked on the user (#2251).

Source

fn move_task( &self, project_name: &str, id: &TaskId, by: By, gate: bool, f: impl FnOnce(&mut Task) -> Result<(), MoveError>, ) -> Result<Option<Task>, MoveError>

The one commit path every task move runs through. gate is the verify gate: it fires on a transition into completed, and the user’s own approval is the one move that passes it (approving IS the verification). f may refuse before anything moves.

Source

pub(crate) fn user_move_task( &self, project_name: &str, id: &TaskId, to: TaskStatus, ) -> Result<Option<Task>, MoveError>

The user moves a row’s state directly - the drag across the board. Any wait the row was holding drops with the move, and the verify gate holds for the user’s own drag too: a drag into completed on a verify=user row lands in waiting for a look, not done.

Source

pub(crate) fn attest_task( &self, project_name: &str, id: &TaskId, slot: &SessionSlot, evidence: &str, ) -> Result<Option<Task>, MoveError>

The lead completes a row waiting on the user’s look, recording the approval the user gave. Only a lead attests, only a look wait is the look’s to clear (a decision is the user’s to answer), and the quoted evidence is required. The row completes as the user’s own approve would, wearing the attestation the board draws.

Source

pub(crate) fn reserve_task( &self, project_name: &str, id: &TaskId, reserved: bool, ) -> Result<Option<Task>, MoveError>

The user reserves a row’s pick (or releases it): the sweep leaves a reserved row for them rather than dispatching it.

Source

pub(crate) fn reopen_task( &self, project_name: &str, id: &TaskId, ) -> Result<Option<Task>, MoveError>

The user strikes a lead attestation: the look gate is re-minted and the row parks on the user again, recorded in the history.

Source

pub(crate) fn approve_task( &self, project_name: &str, id: &TaskId, ) -> Result<Option<Task>, MoveError>

The user’s approval of a row waiting on their look: the wait clears, the verify gate is satisfied, and the row completes - a root closing its tree like any completion.

Source

pub(crate) fn send_back_task( &self, project_name: &str, id: &TaskId, words: &str, ) -> Result<Option<Task>, MoveError>

The user sends a verified row back: it returns to its owner in progress, the words land in the detail, and the attempt ticks.

Source

pub(crate) fn answer_task( &self, project_name: &str, id: &TaskId, words: &str, ) -> Result<Option<Task>, MoveError>

The user answers a row waiting on a question: the row resumes in progress with the words in its detail.

Source

pub(crate) fn rank_task( &self, project_name: &str, id: &TaskId, to: RankMove, ) -> Result<Option<(Task, bool)>, MoveError>

Move a row in its queue: Top above everything, Up and Down one place, and Before where a drag landed - directly above the row it was dropped on, or the end of the queue when it names none. The whole queue is renumbered in one write, so the order a reader sees is the order the next read returns. The bool says whether the row’s place changed: a pick-up-and-put-back lands the same position.

Source

pub(crate) fn assign_task( &self, project_name: &str, id: &TaskId, owner: Option<SessionSlot>, ) -> Result<Option<(Task, Option<SessionSlot>)>, MoveError>

Give a row an owner, or take it back. The returned record carries the owner it had before, read under the same lock as the write so a concurrent claim cannot slip between the two.

Source

pub(crate) fn claim_task( &self, project_name: &str, slot: &SessionSlot, by_id: Option<&TaskId>, in_epic: Option<&TaskId>, ) -> Result<Task, ClaimError>

Claim the top of a queue: the row by_id, or the first ready row of in_epic by rank. Owner and in_progress land in one write, the attempt ticks, and the claim is refused - by name - for a row a live seat holds, a row that is waiting, or a caller already working another row.

Source

pub(crate) fn wait_task( &self, project_name: &str, id: &TaskId, slot: &SessionSlot, kind: WaitingKind, detail: Option<String>, on: Option<&TaskId>, ) -> Result<Task, MoveError>

Put a row the caller owns into waiting, stating what it waits on.

Source

pub(crate) fn resolve_wait_on( &self, project_name: &str, blocker: &TaskId, ) -> Vec<Task>

Clear every wait on blocker - any terminal end of it counts, and so does its deletion - and hand the released rows back so the caller can message their owners.

Source

pub(crate) fn seat_is_live(&self, slot: &SessionSlot) -> bool

Whether a seat has something behind it right now: a registered domain or agent, or a live worker registry row.

Source

fn record_transition( &self, project_name: &str, id: &TaskId, from: Option<TaskStatus>, to: TaskStatus, by: By, )

Append one status transition to the durable history.

Source

pub(crate) fn remove_task_tree( &self, project_name: &str, id: &TaskId, ) -> Result<Vec<Task>, MoveError>

Remove the task id in project_name together with every descendant, persist, and return the records removed (empty when nothing was). Backs tasks__delete. The cascade happens here rather than at the call site so a parent can never be deleted out from under children that would then have nobody to close them.

A tree holding a row waiting on the user - a verification park or a decision wait - is refused by name (#2251): removal would settle that row by deletion, an ask the update path refuses to unbind, complete, or drop without a reason. The check walks the whole cascade, so deleting the epic above the row is no way around it.

Source

pub(crate) fn announce_tasks_changed(&self, project_name: &str)

Tell every view the project’s task set moved, as the set the write just left. It routes on the project’s lead seat, which is the seat a project-scoped section belongs to; a name no project carries has no seat to route on and announces nothing.

Source

pub fn tasks_for_project(&self, project_name: &str) -> Vec<Task>

The tasks registered for project_name. Backs tasks__list and the Inspector TASKS snapshot.

Source§

impl Workspace

Source

pub fn seed_test_task(&self, task: Task)

Register a task directly, bypassing the MCP create path. Cross-crate test access to the otherwise pub(crate) task store so a view can exercise the Inspector’s refresh_tasks resolution against a seeded task.

The store write without the announcement: a fixture seeds state, it does not perform the write a view is owed a frame for.

Source§

impl Workspace

Source

pub(crate) fn terminal_for( self: &Arc<Self>, slot: &SessionSlot, ) -> Result<Arc<TerminalEntry>, String>

The seat’s terminal, spawned on first use.

One per seat, and the slot is the key. A /new or /resume swapping the occupant never reaches here, which is what makes the PTY outlive it.

Source

pub(crate) fn spawn_terminal_at( self: &Arc<Self>, slot: &SessionSlot, cwd: Option<PathBuf>, ) -> Result<Arc<TerminalEntry>, String>

The seat’s terminal, spawned in cwd the first time; an existing terminal is returned as it is.

A shell that exited without a close is replaced, so the seat’s next open starts a fresh shell rather than handing back a dead PTY.

Source

pub fn open_terminal( self: &Arc<Self>, slot: &SessionSlot, ) -> Result<Arc<TerminalEntry>, String>

The seat’s terminal, spawned on first use. The person’s door: a view can open one on a seat no agent has touched.

The door follows the seat read. A declared seat answers a read before anything starts in it (its directory resolves from the declaration), so its door opens and the terminal it starts appears in the record. A seat with no working tree - a worker never spawned, or a slot naming no project - has its read refused, and the door refuses with the same sentence rather than starting a shell nobody can draw.

Source

pub(crate) fn spawn_terminal_for_run( &self, slot: &SessionSlot, cwd: Option<PathBuf>, ) -> Result<Arc<TerminalEntry>, String>

A fresh terminal for terminal__run, not registered: the command’s own terminal, closed when the call returns.

Source

pub fn terminal_if_any(&self, slot: &SessionSlot) -> Option<Arc<TerminalEntry>>

The seat’s terminal, when it has one.

Source

pub fn terminal_by_id( &self, slot: &SessionSlot, id: u16, ) -> Result<Arc<TerminalEntry>, String>

The seat’s terminal, refused unless id names it.

Source

pub fn terminal_held(&self, slot: &SessionSlot) -> bool

Whether a terminal hand-off is parked on the seat.

Source

pub fn terminal_controlled(&self, slot: &SessionSlot) -> bool

Whether the person holds the seat’s terminal through a client’s take-control, rather than through an agent’s hand-off.

Source

pub fn take_terminal_control( &self, slot: &SessionSlot, id: u16, viewer: u64, ) -> Result<(), String>

The person’s own take: always explicit, and always landing.

A take is allowed while a hand-off parks, because a park opens the surface rather than handing out the keyboard: the person reads it, takes control, and types. The id is the terminal the caller is viewing: a replaced shell has a new id, and a take that landed on it would hand a role no push could reach.

Source

pub fn close_terminal(&self, slot: &SessionSlot)

Close the seat’s terminal, if it has one.

Source

pub(crate) fn sweep_idle_terminals(&self)

Close every terminal an idle clock may close: untouched by any agent call, unattached, and unheld by a hand-off.

Source

pub(crate) fn announce_terminal( &self, slot: &SessionSlot, terminal: Option<TerminalSummary>, )

Tell the views a terminal opened, changed or closed.

Source

pub(crate) fn register_terminal_hand_off( &self, caller: &SessionSlot, terminal: u16, reason: &str, ) -> Result<(Uuid, Receiver<HandOffEnding>), String>

Hold a terminal hand-off for the person at a client, and hand back its id plus the receiver the caller awaits.

No timeout rides the wait. The ask sits until an answer or the caller’s own drop. The one case answered up front is a stream nobody can answer on, which fails closed rather than parking the session on a prompt no view can draw.

Source

pub(crate) fn terminal_handoff_waiting( &self, id: Uuid, caller: &SessionSlot, ) -> bool

Whether a hand-off with this id is still registered to caller, without removing it.

Source

pub(crate) fn resolve_terminal_hand_off( &self, id: Uuid, caller: &SessionSlot, answer: TerminalHandOffAnswer, ) -> bool

Remove a parked hand-off, answer its waiter, and tell every view it is gone. false when the id is not this caller’s to resolve.

Source

pub(crate) fn abandon_terminal_hand_offs(&self, slot: &SessionSlot)

A seat ended - or /new//resume replaced the occupant that was waiting - with a hand-off still parked: resolve it as abandoned so no dock outlives its asker.

Source

fn spawn_terminal( &self, slot: &SessionSlot, cwd: Option<PathBuf>, ) -> Result<Arc<TerminalEntry>, String>

Spawn a terminal for slot, with its cwd and the environment its claude child gets.

Source

fn watch_terminal_exit( self: &Arc<Self>, slot: &SessionSlot, entry: &Arc<TerminalEntry>, )

Tell the strip when the shell exits, so “the shell exited” is drawn without waiting for a record read.

Source

fn watch_terminal_state( self: &Arc<Self>, slot: &SessionSlot, entry: &Arc<TerminalEntry>, )

A state change is a push, not a poll. The strip’s words - a command running, a password wanted - come from the record, and a record that only changed on open, close and exit would say “idle” through a whole command.

Source

fn ensure_terminal_clock(self: &Arc<Self>)

Start the idle clock, once. The registry makes a second call a no-op.

Source§

impl Workspace

Source

pub fn speaking_installs(&self) -> Vec<SpeakingInstall>

Every speaking install this machine has recorded.

Source

pub fn speaking_install(&self, variant: &str) -> Option<SpeakingInstall>

One installed variant, when this machine has it.

Source

pub fn speaking_variant(&self) -> (SpeakingVariant, SpeakingFrom)

The variant the role runs: the configured name, else the runtime pick, else the compiled pin.

Source

pub fn speaking_files(&self) -> Option<ModelFiles>

The files the active variant loads, when this machine has them.

Source

pub fn files_for_install(&self, install: &SpeakingInstall) -> Option<ModelFiles>

The files one installed variant is made of.

Source

pub fn voices(&self) -> TtsView

The models page’s and the settings card’s read.

Source§

impl Workspace

Source

pub(crate) fn vault_enrolment_material(&self) -> Option<VaultReleased>

The material a client wraps to enrol this device, from the shared session - or None when it is locked or no [vault] is configured.

Source

pub fn vault_state(&self) -> Option<VaultState>

The shared vault session as a view draws it - None when no [vault] is configured.

Source§

impl Workspace

Source

pub(crate) fn register_vault_ask( &self, caller: &SessionSlot, draft: VaultDraft, needs_unlock: bool, ) -> (Uuid, Receiver<VaultAskAnswer>, bool)

Hold a vault call and hand back its id plus the receiver the caller awaits. Addressed to caller, so the card surfaces in the session that will read the reply rather than in whichever session is focused. needs_unlock is the live session state at park time, used only for the announcement; every later read restamps it.

Source

pub(crate) fn vault_ask_waiting(&self, id: Uuid, caller: &SessionSlot) -> bool

Whether a vault call with this id is still registered to caller, without removing it - the read the dispatch guard makes first.

Source

pub(crate) fn resolve_vault_ask( &self, id: Uuid, caller: &SessionSlot, answer: VaultAskAnswer, ) -> bool

Answer a held vault call, returning whether one was waiting for THIS caller. True exactly once, for the entry that was there; a call held for another session is refused rather than answered.

Source

pub(crate) fn settle_vault_ask( &self, id: Uuid, caller: &SessionSlot, ending: VaultAskEnding, ) -> bool

Settle an answered or abandoned call: remove the entry and emit the ending every view reads. Idempotent, because the caller’s drop guard and the answer path both reach it and only the first has an entry.

Source

pub(crate) fn abandon_vault_asks(&self, slot: &SessionSlot)

End every vault call slot still holds as abandoned.

A parked call outlives its own future. The blocked MCP handler’s future is dropped when the session’s task goes, but a detached control task is not aborted by the client’s disconnect, so the registry sweep is what tells every other view the dock is over.

Source

pub(crate) fn rebase_vault_asks(&self, needs_unlock: bool)

Re-stamp every call still parked after the shared session’s lock state changed: each is now an ordinary approve card when needs_unlock is false, and asks for the password again when it is true. A fold that already holds that id merges the restamped frame in place instead of ignoring it. The answered call is already settled and gone from the registry.

Source

pub(crate) fn vault_asks( &self, slot: &SessionSlot, needs_unlock: bool, ) -> Vec<PendingAsk>

Every held vault call this seat owns, stamped with the live session’s lock state. A HashMap keeps no arrival order to offer, the same exception the settings writes already have.

Source§

impl Workspace

Source

pub(crate) fn voice_service(self: &Arc<Self>) -> Arc<VoiceService> ⓘ

The voice service, built on first use - the boot prewarm is the first caller when [tts] is on. Its sink and note dispatch hold a Weak<Workspace>, which is why this waits for an Arc<Self> rather than being built with the rest of the value.

Source

fn load_speaking_engine(&self) -> Result<Arc<dyn Engine>, String>

Load the speaking engine for the active variant.

The configured voice is checked against the installed set before the model loads, so a bad id is refused by name at warm rather than silently replaced by the engine’s own single-voice fallback.

Source

pub fn start_voice_prewarm(self: &Arc<Self>)

Load the speaking engine beside boot, so the first say pays no model load. One task per forge run, spawned rather than awaited; a failed load leaves the slot empty, which the first say fills.

The slot is only ever filled, never emptied: a loaded engine is never taken away, and a say that arrives while this load is still running waits on the same lock rather than answering SayError::NotReady.

Source

pub(crate) fn voice_enabled(&self) -> bool

Whether speech is on, as the running system answers it: the built service’s own flag when it exists, else the config that would build one. A save marked restart cannot move one side without the other.

Source

pub fn voice_listeners(&self, seat: &SessionSlot) -> usize

The clients with voice on for seat. Zero before the service has been built, which is the truth: nobody could have toggled it yet.

Source

pub fn voice_occupant_changed(&self, seat: &SessionSlot)

The seat’s occupant changed under a listening client: tell the new session it is heard. A no-op before the service exists, because nobody could have toggled voice on.

Source

pub fn voice_release(&self, client: u64) -> Vec<SessionSlot>

A connection has left: take it off every seat it was listening to and tell each seat’s subscribers the count it left behind.

Source

pub fn install_voice_engine_for_test(self: &Arc<Self>, engine: Arc<dyn Engine>)

Put an already-built engine behind the service, so a test can drive a render without loading a model.

Source

pub async fn voice_say_for_test( self: &Arc<Self>, seat: &SessionSlot, text: String, ) -> Result<(), SayError>

Speak one utterance the way voice__say does, so a cross-crate test can drive a render without an agent calling the tool.

Source

fn build_voice_service( self: &Arc<Self>, loader: Arc<dyn Fn() -> Result<Arc<dyn Engine>, String> + Send + Sync>, ) -> VoiceService

Source§

impl Workspace

Source

pub fn hold_seat(self: &Arc<Self>, slot: &SessionSlot)

Show slot, which is what starts its working tree being watched.

A hold is what “somebody is looking” means, and it is what keeps an idle seat from being scanned at all.

The hold does not wait for its reads. The hold starts the seat’s watch and answers; the record’s own read follows it straight away (the subscribe path encodes from the stores), and the watch’s first pass starts from what that read left. Those reads ran before the hold answered until #2506, so a warm open waited on every one of them.

The caller owes the read’s mark. Self::note_record_read is what releases the watch’s first pass; a hold without it has a watch that waits and scans nothing.

A held seat waits for its session. A seat with no session has no store for a read, so the watch’s own loop goes quiet on it and wakes when the session appears - the same rule that covers a session ending under a running loop.

What the old refusal cost was the viewer that never comes back. A subscribe to a sessionless seat was refused its hold, and most viewers recovered by accident: a session starting is a REPLACES frame, the reread around it unsubscribe/subscribes, and the next subscribe took the hold - the work_watch_quiet / session_gone line in the live log is one of those later holds. A viewer that nothing drove to re-subscribe had no watch for as long as it showed the seat, and its pushed rows stood still; the hold is unconditional now, so the FIRST subscribe is the one that counts (#1706).

Source

pub fn note_record_read(&self, slot: &SessionSlot)

Mark the seat’s record read: the snapshot has taken the stores, so the watch’s first pass may start from them. The subscribe path calls this once its record is built; a seat nobody encodes has no watch reading.

Source

pub(crate) fn note_repository_activity(&self, slot: &SessionSlot)

The session reported a repository mutation. Re-read the tree on the frame rather than at the next staleness window: a commit or a stage touches only .git, which the file watch filters out, so nothing else would notice for up to SNAPSHOT_STALENESS.

Source

pub(crate) fn note_code_change_published(&self, slot: &SessionSlot)

The session reported a published change. The held PR block was looked up before the PR existed and its cache window would serve that answer, so the next pass looks again.

Source

fn take_pr_stale(&self, slot: &SessionSlot) -> bool

Whether a nudge asked for a PR lookup, clearing the mark.

Source

async fn pass_test_scan_gate(&self)

Wait at the fixture’s gate when one is seeded. Test-only: production reads run as they always do.

Source

pub fn release_seat(&self, slot: &SessionSlot)

Stop showing slot. The last hold stops its loop and its watch.

Source

pub fn work_snapshot(&self, slot: &SessionSlot) -> Option<WorkSnapshot>

The seat’s scan, when one has been taken.

Source

pub(crate) async fn walk_processes_if_stale( &self, slot: &SessionSlot, pid: Option<u32>, ) -> bool

Walk slot’s process tree when the snapshot held is missing or older than SCAN_STALENESS, and store what the walk found.

The held seat’s own loop is the walker - the socket’s reads used to walk the seat a client read. This is that role moved to the loop, so a seat nobody holds is not walked at all and the walk costs what a held seat’s tree costs rather than a whole record per read.

The session’s live backgrounded local_bash commands ride along, from the registry the core holds: a backgrounded bash is setsid-detached and sits outside claude’s tree, so without them the walk misses the very processes the feed leads with.

Source

pub(crate) async fn walk_files_if_stale( &self, slot: &SessionSlot, dirty: bool, ) -> bool

Walk slot’s tree into a file index when the one held is missing, the tree moved, or the answer is past INDEX_STALENESS, and store what the walk found.

The held seat’s own loop is the walker, the role the process walk took: the socket’s reads used to walk their seat, and this walks only the seats somebody is showing. The walk honours the user’s own gitignore preference, read per walk so a flip reaches the next one.

The record’s read takes the same gate, so a snapshot’s first walk and the loop’s first pass are one walk rather than two of the same tree at once.

Source

async fn walk_files_locked(&self, slot: &SessionSlot, dirty: bool) -> bool

Self::walk_files_if_stale with the seat’s gate already held.

Source

pub async fn walk_files_for_record(&self, slot: &SessionSlot) -> Arc<FileIndex> ⓘ

The record’s walk of slot’s tree: the store when there is one, else the walk the loop would take, under the same gate - so the snapshot’s fallback and the first pass never walk at once. The pass walks again only when the tree moved.

Source

async fn walk_files_now(&self, slot: &SessionSlot) -> Option<Arc<FileIndex>>

Walk slot’s tree now and store it, answering what it found. A seat with no session has nowhere to keep it, and the walk is answered anyway - a record still draws the tree it stands in.

Source

pub fn store_file_index(&self, slot: &SessionSlot, index: Arc<FileIndex>)

Store a walked file index for slot, as the seat’s loop does.

Source

pub fn file_index(&self, slot: &SessionSlot) -> Option<HeldFileIndex>

The seat’s walked file index, when one has been taken.

Source

pub fn hold_work_gate(&self, slot: &SessionSlot) -> Option<Arc<Mutex<()>>>

The seat’s work gate, for a test that has to hold it across a read. None when the seat is not held.

Source

pub fn store_work_snapshot(&self, slot: &SessionSlot, snapshot: WorkSnapshot)

Store a scan for slot, as the seat’s loop and a fixture both do.

Source

pub async fn scan_work_if_stale( &self, slot: &SessionSlot, dirty: bool, pr: PrRefresh, ) -> Result<WorkSnapshot, String>

Answer the seat’s scan, reading the tree first when the rule says to.

The rule is checked under the seat’s own gate, so the record’s read, the loop’s read and a fixture’s read cannot both decide a stale tree needs two scans: the second one finds what the first stored.

§Errors

A seat with no session behind it has no directory to read, and forge answers that rather than scanning the process’s own directory - the binary test every read here owes.

Source

pub(crate) async fn scan_work_now( &self, slot: &SessionSlot, pr_force: bool, ) -> Result<WorkSnapshot, String>

The seat’s scan, taken because the session reported a repository mutation rather than because its stored read aged out. pr_force re-runs the PR lookup even inside the cache window the previous attempt left behind.

Source

async fn scan_work_locked( &self, slot: &SessionSlot, dirty: bool, pr: PrRefresh, force_pr: bool, ) -> Result<WorkSnapshot, String>

Self::scan_work_if_stale with the seat’s gate already held.

Source

pub async fn scan_work_at( &self, slot: &SessionSlot, cwd: &Path, pr: PrRefresh, ) -> WorkSnapshot

Read cwd as slot’s working tree, store what it answered, and answer it.

One body for both entry points - the record’s fallback and the loop - because a second scan path is a second answer about one tree.

Source

pub async fn scan_work_at_without_base_fetch( &self, slot: &SessionSlot, cwd: &Path, ) -> WorkSnapshot

A sessionless seat’s fetching scan with no background kick: the read that draws the diff kicks the base refresh itself a moment later, so its kick is the only one and nothing of forge’s is queued ahead of it on the shared per-repository gate.

Source

pub async fn scan_work_at_pinned_local( &self, slot: &SessionSlot, cwd: &Path, base: Option<&str>, measured: Option<BaseMeasurement>, ) -> WorkSnapshot

A local scan pinned to base, taken without the seat’s gate: the drawing read’s fallback when the gate is held past its bound. It writes the store like any scan, and the caller has already refreshed the base.

Source

async fn scan_work_at_with_base( &self, slot: &SessionSlot, cwd: &Path, pr: PrRefresh, base: Option<&str>, measured: Option<BaseMeasurement>, quiet: bool, force_pr: bool, ) -> WorkSnapshot

Self::scan_work_at pinned to one base revision: the branch-ahead layer is computed against base (a sha a drawing read resolved) rather than the ref’s name.

measured is what the checkout’s last completed base refresh measured, with the instant it did and the words a failure left; None keeps the state the store last carried, so a scan that did not refresh does not erase it. A measurement older than the base the store carries is ignored: a read that found the base unchecked has already superseded it. quiet suppresses the background kick on a Fetch scan.

Source

pub async fn scan_work_pinned( &self, slot: &SessionSlot, cwd: &Path, base: Option<&str>, measured: Option<BaseMeasurement>, ) -> Result<WorkSnapshot, String>

A scan a drawing read must take, pinned to one base revision: it awaits the seat’s gate rather than serving the store a concurrent scan is replacing, so the count and chain it answers were read after the base moved. Local only - the caller has already refreshed the base.

Source

fn project_env(&self, project: &str) -> HashMap<String, String>

project’s own declared environment, env_file included, which is the identity a gh spawn on its behalf carries: the scan’s PR row must read as the project, not as the server’s ambient login. A name no project has declares none, so gh falls back to its own login.

Source§

impl Workspace

Source

pub fn testing_stub_handle() -> (AgentHandle, UnboundedReceiver<AgentCommand>)

Construct a stub AgentHandle plus the matching Receiver<forge_primitives::AgentCommand> that drains every command dispatched to it. Tests use this to wire a stub connection without spinning up a real subprocess; the bridge underneath is forge_agent::Agent::testing_stub - same shape as before, now reachable from a view via forge_workspace::Workspace::* so a view crate needs no direct forge-agent dep.

The returned Receiver carries forge_primitives::AgentCommand because that’s what the bridge’s dispatcher accepts; this is distinct from crate::protocol::Command (the workspace’s outer envelope) that wraps these primitives under a SessionSlot.

Source

pub fn record_spawn_failure_for_test(&self, slot: &SessionSlot, message: &str)

Record a slot’s failed spawn, as the connection-failure path does before it releases the session. Test-only.

Source

pub fn record_failed_turn_for_test(&self, slot: &SessionSlot, at: SystemTime)

Stamp a slot’s failed-turn record the way an errored Result does, so a cross-crate test can read the row the surface builds. Test-only.

Source

pub fn record_death_for_test( &self, slot: &SessionSlot, occupant: &str, message: &str, )

Record a slot’s death alone - the state a recovering connect leaves: the failure cleared, the death riding with the reason it left. Add Self::record_spawn_failure_for_test for the crashed-and-still- down shape, where no connect has cleared anything. Test-only.

Source

pub fn emit_for_test(&self, update: SessionUpdate)

Push one update onto the fan-out, so a test can watch a view react to the core without driving a session. Test-only.

Source

pub fn park_slack_draft_for_test(&self, key: &SessionSlot, draft: SlackDraft)

Park a Slack draft on key as a blocked Slack tool call would. The receiver is dropped on purpose: the tool call is the test’s to simulate, and the note’s own path is covered where a handler awaits it. Test-only.

Source

pub fn emit_for_test_reported(&self, update: SessionUpdate) -> bool

Self::emit_for_test, reporting whether a subscriber was there to take it.

A dead subscriber is dropped from the registry on the next send, so the answer is false once nothing is listening - which is how a test tells a subscription still attached from one that went with its socket.

Source

pub fn test_subscriber_count(&self) -> usize

How many subscribers are attached right now. Test-only: a test that watches a subscription arrive and leave reads this rather than asking whether an emit landed, which a permanently attached listener answers for everything.

Source

pub fn test_answering_count(&self) -> usize

How many subscribers could answer a prompt right now. Test-only: the role decides whether the core parks a turn on a reply, and a test that watches a client declare it has nothing else to read.

Source

pub fn test_usage_poller_started(&self) -> bool

Whether the usage poller is registered and running. Test-only: the poller’s first tick is a minute out and its pass reaches the network, so a cross-crate boot test reads the registry rather than watching it poll.

Source

pub fn test_dictate_preflight_started(&self) -> bool

Whether the dictate prewarm’s start has run. Test-only: the start reaches the network for models this fixture has none of, so a cross-crate boot test reads the guard rather than watching it fetch.

Source

pub fn test_voice_prewarm_started(&self) -> bool

Whether the voice prewarm’s start has run. Test-only: the start loads multi-gigabyte weights this fixture has none of, so a cross-crate boot test reads the guard rather than watching it load.

Source

pub fn seed_test_cli_version( &self, installed: Option<&str>, latest: Option<&str>, )

Hold a claude version snapshot, so a cross-crate test can render the version line without a real claude --version and npm probe. Test-only.

Source

pub fn seed_test_fatal_error(&self, error: AppError)

Hold the last fatal, as the boot spawn’s failure records it, so a cross-crate test can read the home’s encoding without a dying spawn. Test-only.

Source

pub fn install_testing_stub( &self, key: &SessionSlot, ) -> UnboundedReceiver<AgentCommand>

Register a fresh testing-stub agent against key’s DomainSession. Returns the matching forge_primitives::AgentCommand receiver so tests can assert on the commands the workspace routes through it (the same shape as testing_stub_handle(), but with the handle installed in one step so a view’s test code doesn’t have to touch AgentHandle directly).

Auto-creates a DomainSession for key when none is registered yet; otherwise overwrites the existing DomainSession.conn slot.

Source

pub fn seed_queued_prompt( &self, key: &SessionSlot, uuid: &str, source: PromptSource, text: &str, )

Put a prompt in a seat’s queue exactly as a dispatch leaves it: the row a client attaching mid-queue reads, with no dispatch to drive.

Source

pub fn testing_stub() -> (Arc<Self>, UnboundedReceiver<SeatUpdate>)

Construct an empty Workspace for use in unit tests. Skips the on-disk forge.toml load + catalog scan that Workspace::new performs; the returned workspace carries an empty project list and an empty pool. Tests register a domain session via Self::register_domain_session before exercising any code path that needs one.

Returns the workspace alongside the SeatUpdate receiver this constructor subscribed. The workspace’s subscribe() slot is None, so a later subscribe() mints a stream of its own rather than handing back this one.

Source

pub fn testing_stub_with_config_dir( config_dir: PathBuf, ) -> (Arc<Self>, UnboundedReceiver<SeatUpdate>)

Like testing_stub but with a caller-supplied config_dir so tests that probe disk-side APIs (catalog scan, JSONL reads) can point the stub at a tempdir of their own.

Source

pub(crate) fn testing_stub_with_slack( config_dir: PathBuf, config: LoadedConfig, slack: Arc<SlackWorkspaces>, systemone: Option<Arc<SystemOneClient>>, ) -> (Arc<Self>, UnboundedReceiver<SeatUpdate>)

Self::testing_stub_with_config with the Slack clients supplied by the caller, so a test can drive a real facade against a double rather than a live workspace.

Source

pub fn seed_test_user_preferences(&self, preferences: Value)

Hold the CLI’s per-user preferences document, so a cross-crate test reads an ignore preference without the machine’s own $HOME/.claude.json. Test-only.

Source

pub fn seed_test_claude_pid(&self, key: &SessionSlot, pid: u32)

Give a fixture seat a live claude pid, so its held loop walks a real process tree with no CLI behind it. Test-only.

Source

pub fn seed_test_scan_gate(&self, gate: Arc<Semaphore>)

Hold every stale read at gate until the fixture adds permits, so a test can say what a hold answers while its scans are still in flight. Test-only.

Source

pub fn note_tree_moved_for_test(&self, key: &SessionSlot)

Report the seat’s tree moved, as the file watch’s own mark does, so a cross-crate test drives the watch’s pass instead of waiting on the OS daemon’s delivery, which has no budget to derive a wait from. Test-only.

Source

pub fn watch_pass_done_for_test(&self, key: &SessionSlot) -> Option<Arc<Notify>>

The mark a held seat’s watch raises at the end of every pass, so a test waits for a pass rather than for a clock. None for a seat held by nobody. Test-only.

Source

pub fn pause_next_live_seats_capture(&self) -> (Receiver<()>, Sender<()>)

Hold the next Self::live_seats capture between its mint and its reads: the returned receiver fires once it is held, and a message on the returned sender releases it. Test-only.

Source

pub(crate) fn pass_live_seats_gate(&self)

Hold this capture where a test armed Self::pause_next_live_seats_capture. Test-only: a production capture never meets a gate.

Source§

impl Workspace

Test-mode command interception. Feature-gated and pub for the same reason the seeds below are: a view’s tests reach for these to assert what boot WOULD have spawned, without starting a subprocess.

Source

pub fn enable_test_dispatch_intercept(&self)

Enable test-mode app-level command interception. After this call, every Command routed through the app-level branch of dispatch is buffered in lieu of running the spawn handler; drain via drain_test_dispatch_buffer. No-op if already enabled. Test-only - tests use this to assert what would have been dispatched without spinning up real subprocesses.

Source

pub fn test_rotation_announcer_started(&self) -> bool

Whether the rotation announcer is registered and running, so a test pins that the boot call actually reaches the one watcher.

Source

pub fn drain_test_dispatch_buffer(&self) -> Vec<Command>

Drain every app-level Command captured since the last call. Returns empty when no intercept was enabled or no commands were dispatched. Test-only.

Source§

impl Workspace

Source

pub fn seed_seat_state_for_test( &self, org: &str, project: &str, label: &str, state: SeatState, ) -> Result<()>

Plant a session row in the seat state a boot test derives from, so the wave can be driven without running a real spawn first. Test-only.

Source

pub fn park_terminal_hand_off( &self, slot: &SessionSlot, terminal: u16, ) -> Result<String, String>

Park a terminal hand-off on slot as a blocked terminal__hand_off call would, so a transport test can type through the hand-off’s door. Test-only.

Source

pub fn answer_terminal_hand_off(&self, id: &str, slot: &SessionSlot)

Answer a parked terminal hand-off with Done, as the dock’s button would. Test-only.

Source

pub fn seed_test_project(&self, name: &str, path: &str)

Append a synthetic project to the test overlay searched first by find_project_view_by_name. Used by respawn tests to drive the Connected-hook worker-spawn trigger without writing a real forge.toml. Test-only.

Source

pub fn remove_test_project(&self, name: &str)

Drop a project seeded by Self::seed_test_project, so a test can exercise the paths that react to a project leaving the config.

Source

pub fn seed_test_project_with_slack_workspace( &self, name: &str, path: &str, workspace: &str, )

Workspace::seed_test_project with a Slack binding, so a facade test can drive the slot-to-workspace resolution. Test-only.

Source

pub fn seed_test_project_with_max_workers( &self, name: &str, path: &str, max_workers: usize, )

Workspace::seed_test_project with the project’s max_workers override, so facade tests can exercise the override arm of the capacity read. Test-only.

Source

fn seed_test_project_inner( &self, name: &str, path: &str, max_workers: Option<usize>, slack_workspace: Option<&str>, )

Source

pub fn seed_test_gateway_bind_error(&self, error: Option<String>)

Set the gateway bind error, so a cross-crate test can render the failed state of the preflight gateway row. Test-only.

Source

pub fn install_test_spawn_handle(&self, handle: AgentHandle)

Set the gateway listener’s ready flag, so a cross-crate test can render each of the preflight gateway row’s states. Test-only. Stand in a stub handle for the next cold spawn, so a test can read the launch settings the spawn hands its child. Consumed by that spawn: a test that asserts on one installs one.

Source

pub(crate) fn take_test_spawn_handle(&self) -> Option<AgentHandle>

The installed stand-in, taken so one spawn cannot use it twice.

Source

pub fn test_spawn_server(&self) -> Option<McpServer>

The forge MCP server the last spawn composed, so a test can drive a tool through the spawn’s OWN wiring rather than a copy of it.

Source

pub fn seed_test_gateway_ready(&self, ready: bool)

Source

pub fn seed_test_gotify_connected(&self, connected: bool)

Set the Gotify stream’s connection flag, so a cross-crate test can give the two connectors’ liveness flags different values. Two false-valued bools read as agreement, which lets a read wired to the wrong connector pass unnoticed. Test-only.

Source

pub fn seed_test_ready_account(&self, account: &str)

Mark account Ready, so a cross-crate test can render chip-bearing rows without driving the real account loader. Test-only.

Source

pub fn seed_test_account_state(&self, account: &str, state: LoadingState)

Drive account to state directly, so a cross-crate test can render a mid-flight or bailed preflight screen without the real loader. Test-only.

Source

pub fn seed_test_account_failure(&self, account: &str, status: UsageFetchStatus)

Record a probe failure on account, so a cross-crate test can render bailed-row copy keyed on why the account bailed. Test-only.

Source

pub fn seed_test_dictate_snapshot(&self, snapshot: DictateSnapshot)

Replace the dictation preflight snapshot, so a cross-crate test can render any of its states without fetching 3 GB. Test-only.

Source

pub fn install_test_dictate_engine( &self, models_dir: &Path, ) -> Result<(), Error>

Arm dictation with an engine over models_dir: its weights are absent, so a take registers, meters and resolves without a model. The caller holds the directory for as long as the engine is wanted. Test-only.

The read-aloud set moves here too, because its own resolution falls back to the machine’s app-support dir whatever config dir a fixture gave the workspace: a test that kept a recording would write into the real one and the next run would read it back.

Source

pub fn mark_test_session_live(&self, key: &SessionSlot)

Mark key live the way a started session is - a command sender with nothing behind it - so a liveness gate reads true without a CLI. Test-only.

Source

pub fn testing_stub_with_dictate_enabled() -> (Arc<Self>, UnboundedReceiver<SeatUpdate>)

A testing_stub whose [dictate] enabled is true, so a cross-crate test exercises the key handler’s enabled path without a model download. Test-only.

Source

pub fn seed_test_bound_session(&self, key: &SessionSlot, account: &str)

Pool key as a live session and bind account to it, so a cross-crate test can render the account panel against a real binding without spawning a CLI. Test-only.

Source

pub fn seed_test_running_session_id(&self, key: &SessionSlot, session_id: &str)

Pool key under session_id, so a cross-crate test can read the transcript that id names. Test-only.

Source

pub fn seed_test_stored_session_id(&self, key: &SessionSlot, session_id: &str)

Record session_id as the store’s row for key with nothing live behind it: the seat a wake resumes, whose transcript is the last session it ran. Test-only.

Source

pub fn seed_test_usage(&self, account: &str, snapshot: UsageSnapshot)

Source

pub fn seed_test_advertised_catalogues( &self, slot: &SessionSlot, commands: Vec<AvailableCommand>, agents: Vec<AvailableAgent>, )

Advertise commands and agents for slot, registering its domain if it has none, so a cross-crate test can read what the CLI’s init frame would have left behind without driving the wire. Test-only.

Source

pub fn seed_test_pending_interaction( &self, slot: &SessionSlot, kind: PendingInteractionKind, )

Park one ask of kind on slot, so a test can read the state a held turn produces without driving the wire. A prompt’s domain is registered when it has none; a registry ask parks in its own registry and needs no domain. Test-only.

Source

pub fn clear_test_pending(&self, slot: &SessionSlot)

Let go of everything parked on slot, which is half of what answering does: the other half is the update that says so. Test-only.

Source

pub fn seed_test_awaiting_login(&self, slot: &SessionSlot)

Hold slot waiting to be let in, the state a session that cannot authenticate reads as. Test-only.

Source

pub fn seed_test_prompt_queue( &self, slot: &SessionSlot, kind: PendingInteractionKind, count: usize, )

Park count unanswered asks of kind on slot under distinct ids, the way a session holding a queue of them reads. Test-only.

Source

fn seed_test_asks( &self, slot: &SessionSlot, kind: PendingInteractionKind, count: usize, )

Park count asks of kind in the place that kind really lives - the session’s pending set for a prompt, its own registry for everything else - so the fixture is the state, not a stand-in. A registry ask is inserted directly: the hand-offs and the draft carry a sender whose receiver nobody kept, and the settings and vault asks, whose sender is optional, carry None. Each parked ask then takes its number, the way every production registration does, so a seeded ask reads the identity the app would give it. It is the parking that makes the seat need a person, and no test answers a seeded ask through its own oneshot.

Source

pub fn seed_test_worker_row(&self, project_key: &ProjectKey, label: &str)

Persist a dynamic-worker row directly, bypassing agents__spawn. Cross-crate test access to the otherwise pub(crate) store write so a view can render launchpad worker rows against a seeded row.

Source

pub fn seed_test_worker_row_at( &self, project_key: &ProjectKey, label: &str, cwd: &str, is_git_repo: bool, )

Self::seed_test_worker_row with the worker pointed at its own tree, the way a spawn with cwd records it.

Source

pub fn seed_test_git_worker_row(&self, project_key: &ProjectKey, label: &str)

Self::seed_test_worker_row for a worker that was spawned in a git repo, so its tree is the worktree under the project rather than the project root. The flag is what crate::Workspace::cwd_for_session composes the directory from, so a fixture that seeds the row without it hands the test the project’s own tree.

Source

pub fn attach_store_for_test(&self, db: Db)

Attach a store to the stub. Every other testing constructor leaves db as None so nothing touches the real machine store; this one opens the caller’s own redb, for the tests that read persisted rows (the worker-seat set, the launchpad’s labels). Test-only.

Source

pub fn seed_test_session_charter( &self, slot: &SessionSlot, charter: &str, interactive: bool, )

Write a session row for slot carrying charter, bypassing a spawn. A test that drives /new needs the store to already hold the worker’s mission, which is what the re-delivery reads; the interactive flag is what its respawn reads to decide whether the worker may put a question to its user.

Source§

impl Workspace

Source

const RESUME_CANDIDATES: usize = 50

The newest sessions a /resume picker offers: a reader picks from the top, and the record is re-encoded on every seat open and reread, so the answer cannot grow with the project’s history.

Source

pub fn new(config_dir: PathBuf) -> Result<Self, WorkspaceError>

Builds a Workspace, kicks off the background catalog scan and the claude version probe, and loads <config_dir>/forge.toml. Errors if forge.toml is missing or malformed (e.g. no [[orgs]] entries, no [[orgs.projects]] entries, unknown account references). No Agents are spawned on success. The session catalog starts empty and fills when the scan lands - see Workspace::start_catalog_scan.

Source

pub fn new_for_test(config_dir: PathBuf) -> Result<Self, WorkspaceError>

Like Workspace::new but puts forge’s whole app-support base - the redb store and the single-instance lock - under a tempdir inside config_dir rather than the real machine app_support_dir, so tests never touch the user’s durable store or contend for their live lock. The catalog scan does NOT auto-start; tests opt in via Workspace::start_catalog_scan so the catalog stays empty until a fixture asks for it. Neither boot probe starts either: a test workspace reaches no network.

Source

pub fn new_for_test_with_pricing_fetcher( config_dir: PathBuf, fetcher: PricingFetcher, ) -> Result<Self, WorkspaceError>

Workspace::new_for_test with the pricing refill driven by fetcher rather than by the inert default, so the refill’s fetch-to-rescan path is exercisable without the network.

Source

pub fn new_for_test_with_cli_version_prober( config_dir: PathBuf, prober: Box<dyn Fn() -> Pin<Box<dyn Future<Output = CliVersionInfo> + Send>> + Send + Sync>, ) -> Result<Self, WorkspaceError>

Workspace::new_for_test with prober driving the claude version probe in place of the real one, so a test drives the constructor’s own launch without spawning claude --version or reaching npm.

Source

fn new_for_test_impl( config_dir: PathBuf, cli_version_prober: Option<Box<dyn Fn() -> Pin<Box<dyn Future<Output = CliVersionInfo> + Send>> + Send + Sync>>, pricing_fetcher: Option<PricingFetcher>, ) -> Result<Self, WorkspaceError>

Source

pub fn start_catalog_scan(&self)

Run the catalog scan in the background and signal readiness when it lands. [Workspace::new] calls this during construction; tests built on new_for_test` opt in explicitly so the catalog stays empty until a fixture asks for it. Idempotent - a second call is a no-op.

Source

pub fn catalog_ready(&self) -> bool

Whether the background catalog scan has populated the catalog. False from construction until the scan lands; always true when constructed without a tokio runtime (nothing would run it).

Self::resumable_sessions reads it: a lead’s list is the catalog’s group once the scan has landed, and the directory’s own read until then, so the two sources are told apart rather than an unscanned catalog being read as a project with no sessions. Tests also await it to sequence a fixture against the scan.

Source

pub fn supervisor(&self) -> &Arc<Supervisor> ⓘ

The process’s subsystem supervisor. Every long-lived background task registers here, so a death is named and a restart is bounded.

Source

fn start_cli_version_probe( &self, prober: Box<dyn Fn() -> Pin<Box<dyn Future<Output = CliVersionInfo> + Send>> + Send + Sync>, )

Kick off the claude version probe on the tokio runtime: one fetch at boot, then a re-probe every CLI_VERSION_REFRESH_INTERVAL. The constructor is the only production caller, and the prober it takes is what lets a test drive this same launch. Idempotent - a second call is a no-op.

Source

pub fn cli_version(&self) -> Option<CliVersionInfo>

The claude CLI versions the core holds: what is installed on this machine and what npm publishes, both None until the boot probe resolves them. The same answer for every viewer, so a view reads it here rather than probing for itself. A change arrives as SessionUpdate::CliVersionChanged.

Source

fn new_impl( config_dir: PathBuf, app_support: Option<PathBuf>, catalog_scan: bool, cli_version_prober: Option<Box<dyn Fn() -> Pin<Box<dyn Future<Output = CliVersionInfo> + Send>> + Send + Sync>>, service_status_prober: Option<Box<dyn Fn() -> Pin<Box<dyn Future<Output = Option<ServiceIssue>> + Send>> + Send + Sync>>, pricing_fetcher: Option<PricingFetcher>, ) -> Result<Self, WorkspaceError>

Shared constructor body. app_support supplies the app-support base dir; None resolves the real machine app_support_dir and degrades to no lock and no durable store on failure (hard rule #14: no cwd fallback).

cli_version_prober and service_status_prober are None for a workspace built by a test. Both probes fetch over the network at boot and announce the answer on the core’s own stream, so a test workspace that ran them would both depend on a remote service’s health and hand every test a slot-less update it never emitted.

Source

pub fn server_config(&self) -> ServerConfig

Effective [server] settings: whether the socket’s listener starts and where it binds. Read by the binary entry point, which serves the socket - the workspace never does.

Source

pub(crate) fn config_arc(&self) -> Arc<LoadedConfig> ⓘ

The live config as one owned snapshot. A reload swaps the whole value, so every reader takes the snapshot it will use rather than a reference that a reload could pull out from under it.

Source

fn resolution_held(generation: u64, settled: u64) -> bool

Whether a spawn’s resolution ran entirely inside one settled config: the generation is odd while a reload swaps state, so the start must be even and equal to the end.

Source

pub fn reload_config(self: &Arc<Self>) -> Result<ConfigReload, WorkspaceError>

Re-read <config_dir>/forge.toml and apply what can change without a restart. A reload is a whole-config replacement: the parsed value goes through the same loader a boot uses, and only a valid file replaces the live config. An invalid file returns its error and leaves the previous config untouched, so a half-written save never loses a good config.

What a reader sees after this returns equals what a boot on the same file would load. Running sessions keep what they spawned with; only their next use reads the new values.

§Errors

The file could not be read, parsed or validated, or the systemone client could not be built. Nothing was applied.

Source

pub fn client_config(&self) -> ClientConfig

Effective [client] settings: which of the sets forge ships a client draws with. Carried by the greeting, so a client never reads forge.toml itself.

Source

pub fn set_settings_version(&self, version: &str)

Name the build the settings frame carries, and publish the first document. The server calls this once at boot, before the socket serves; a second call keeps the first name.

Source

pub fn subscribe_settings(&self) -> Receiver<Option<Arc<SettingsDocument>>>

The effective-config document as it stood at the last load. Every connection takes its own receiver; a reload that changed a drawn value sends a new one to all of them.

Source

pub fn save_settings( &self, fingerprint: &str, edits: &[SettingsEditRequest], ) -> Result<(), SettingsSaveError>

Apply a batched settings save to forge.toml.

The file is read under the write lock, the page’s fingerprint is compared with it, the edits are applied to its text, and the candidate is validated with the boot loader before an atomic replace. A save that fails validation writes nothing; the reload and its push are the watcher’s, so every client learns the outcome from one frame.

§Errors

The file moved since the page loaded it, an edit or the candidate is not valid, a guard refused it, or the write failed.

Source

pub(crate) fn validate_settings( &self, fingerprint: &str, edits: &[SettingsEditRequest], ) -> Result<(), SettingsSaveError>

Validate a save’s candidate without writing it.

The same checks the save runs, over the whole batch, so a caller can refuse a write the file would never accept before asking the reader about it. Not the authority: Self::save_settings repeats every check under its write lock.

Source

fn settings_candidate( &self, path: &Path, fingerprint: &str, edits: &[SettingsEditRequest], ) -> Result<String, SettingsSaveError>

Build the save’s candidate text and run it through every check short of the write.

Source

fn refuse_departures( &self, before: &LoadedConfig, after_projects: &HashSet<(String, String)>, after_accounts: &HashSet<String>, ) -> Result<(), SettingsSaveError>

Refuse a save that drops an entry something live depends on.

Source

fn live_sessions_of(&self, org: &str, project: &str) -> Vec<String>

The sessions a project currently has live, by their slots’ display names. A removal refuses while any exists: the reload leaves a live session running but invisible.

Source

pub(crate) fn settings_snapshot(&self) -> Arc<SettingsDocument> ⓘ

The effective-config document as the socket holds it, built from the live config where the server has not published one yet (a test workspace, or the first moments before the greeting). The settings MCP tools read this one document, so a key they report and the value the page draws cannot disagree.

Source

fn publish_settings(&self)

Build the document from the live config and publish it when it differs from what the socket already holds.

Source

pub fn browser_settings(&self) -> BrowserSettings

The server browser’s own knobs: [browser]’s encoding rate and quality, and its idle release’s two bounds. Absent section keeps the defaults the struct carries.

Source

pub fn browser_profile_config( &self, ) -> (HashMap<String, String>, HashMap<(String, String), String>)

The profile names forge.toml resolved: each org’s, and each project’s override keyed by (org, project). Project names are unique inside an org, so the pair is the key the fleet resolves a seat against.

Source

pub fn document_max_bytes(&self) -> u64

The ceiling on one handed document, in bytes: [documents] max_size_mib over the crate’s own default. Read by the document read, which refuses a document over it.

Source

pub fn note_client_attached(&self)

Note a client connection attached, and give it back when it goes. The transport’s per-connection guard calls these.

Source

pub fn note_client_detached(&self)

The counterpart of Self::note_client_attached.

Source

pub(crate) fn client_attached(&self) -> bool

Whether any client connection is attached. Only a client that draws a document viewer counts: a hand-off’s ask needs one that does.

Source

pub fn install_browser_host(&self, host: Arc<dyn BrowserHost>)

Install the server’s browser fleet. Boot-time and once: the fleet outlives every session, and a second install would be two browsers answering one tool surface.

Source

pub fn browser_host(&self) -> Option<Arc<dyn BrowserHost>>

The browser fleet, when this workspace has one.

Source

pub fn dictate_bind(&self) -> DictateBind

The push-to-talk key from forge.toml [dictate] bind. The value rides the seat record a client last read, so a reload reaches the key handler on the next seat read rather than the next event.

Source

pub fn dictate_mode(&self) -> DictateMode

How press/release maps onto starting and stopping a take, from forge.toml [dictate] mode. It rides the seat record like bind, so a reload reaches the key handler on the next seat read.

Source

pub fn dictate_enabled(&self) -> bool

Whether dictation is on at all. The key handler reads this so a press with [dictate] disabled is dead rather than a refusal: the box doc’s S0 is “nothing at all”, and with the section absent every Cmd chord would otherwise pop an error.

Source

pub fn plugin_settings(&self) -> PluginSettings

The [plugins] auto-update policy from forge.toml. Read by the plugins pane for the boot auto-update gate and the pane’s auto-update state display.

Source

pub fn record_plugin_updates(&self, records: &[PluginUpdateRecord])

Remember plugin updates applied by a pane run or by boot auto-update: the visible record of what moved, from where, and the ref a rollback restores. One transaction for the whole batch. A no-op with a warn when the store is closed.

Source

pub fn plugin_update_records(&self) -> Vec<PluginUpdateRecord>

Every remembered plugin update, latest write per installed entry. Empty when the store is closed or unreadable.

Source

pub fn clear_plugin_update_record(&self, plugin_id: &str, scope: &str)

Forget the record for one installed entry after its rollback ran: the previous version it named is now current.

Source

pub fn boot_wave_targets(&self, only: Option<&str>) -> Vec<BootWaveTarget>

The projects a boot opens, in forge.toml order.

With no project named, that is every configured project whose lead row is open (idle or in_turn) - the seats the user last had open. A closed lead is left shut, and a project whose lead never spawned has no row to open. A seat whose turn was cut off carries the restart note; a parked one is resumed with nothing.

only names one project explicitly, which opens whether its row is open or closed: naming it is the user’s own act, and closing is not forgetting. A name no project declares is returned as itself so the spawn handler reports it like any other unknown project.

A project renamed in forge.toml starts with no row, because the row is keyed by its org and name: the rename is the same act that loses the stored session id, and there is nothing stable underneath to key by instead.

Source

pub fn list_projects(&self) -> Vec<ProjectView>

Every project listed in forge.toml, each carrying its catalog sessions sorted by last-activity descending - sessions[0] is the lead. Empty sessions means the project has nothing on disk yet; the project still surfaces in the returned Vec.

Source

pub(crate) fn handoff_policy_for(&self, slot: &SessionSlot) -> HandoffPolicy

The handoff trigger this seat runs under: the project’s table over the top-level one, with the defaults under both.

Source

pub fn validate_project_name(&self, name: &str) -> Result<(), WorkspaceError>

Validate that name matches a project in forge.toml. Used by callers (e.g. the server’s boot) to fail fast on an unknown CLI positional arg before setup, rather than surface the same error later via Self::get_agent_handle.

Source

fn apply_lead_delegation( settings: &mut SessionLaunchSettings, kind: SessionKind, )

Stamp LEAD_DELEGATION_PREAMBLE onto a Lead session’s launch settings. No-op for a worker.

Source

fn slot_for_spawn(role: &SpawnRole, project: &LoadedProject) -> SessionSlot

The slot a spawn fills, from the role its caller stated. The label is used as given: a worker must never be resolved through the registry, which answers with the lead’s slot once its entry is gone, and nothing recovers either answer from the key’s shape - a key that looks like a worker is not evidence, and a project named worker_foo once classified as a worker for exactly that reason. The slot a spawn’s role states under project. The one source for both a session’s tool surface and its address, so the two cannot disagree.

Source

pub fn get_agent_handle( self: &Arc<Self>, target: SessionTarget, settings: SessionLaunchSettings, role: &SpawnRole, ) -> Result<Arc<AgentHandle>>

Hands out the Arc<AgentHandle> for the requested session, spawning the underlying Agent lazily if it isn’t already pooled. Idempotent - repeated calls for the same target return the same handle (no second subprocess). settings only apply to the spawn; subsequent calls reuse the existing Agent and ignore the parameter.

Each fresh spawn runs the gateway’s account selection and stamps the chosen account’s env onto the spawned claude subprocess, so it talks to the gateway listener rather than upstream. Selection state lives in the in-memory usage cache; nothing about account choice is persisted across forge launches.

Workspace does not track which handle the caller is “using” - that’s the caller’s concern. role is what the caller states this session to be. There is no keyless form: a role forge cannot state is one it would have to guess from the live-worker registry, which answers with the lead’s slot for a worker whose entry is gone.

Source

pub(crate) fn get_agent_handle_at_key( self: &Arc<Self>, target: SessionTarget, settings: SessionLaunchSettings, resolved_key: Option<SessionSlot>, role: &SpawnRole, reopen: bool, ) -> Result<Arc<AgentHandle>>

Like Self::get_agent_handle but takes the key the caller already resolved, so the pool entry lands under the same key the caller announced. None for callers that have not resolved one.

role is what the caller states this spawn to be. It is the one source for both the tool surface and the slot’s label, so a worker cannot be handed a lead’s tool surface or a lead’s address.

reopen is the opening door’s word: a user’s click, the boot wave and a cron wake open the seat they name, while the heal’s recovery of a death must leave a seat the reader closed shut.

Source

pub(crate) fn account_pool(&self) -> &AccountPool

Crate-internal accessor for the boot-time loading task to drive LoadingState transitions on the workspace’s account map. Not exposed beyond the crate - the field stays private so only intentional callers reach in.

Source

pub fn gateway_bind_error(&self) -> Option<String>

The gateway’s bind failure, if the listener could not start. The launchpad surfaces this instead of “loading accounts” so a dead bind does not read as an endless wait.

Source

pub fn gateway_ready(&self) -> bool

Whether the gateway listener has bound its port. The preflight screen’s gateway row renders this.

Source

pub fn gateway_port(&self) -> u16

The port the gateway’s listener binds.

Source

fn gateway_listener_url(&self) -> String

The base URL children are stamped with: the listener’s BOUND address, not the config-derived one - the config names the port that was asked for, the listener names the port that was bound, and the stamp must carry the one that answers.

Source

pub fn all_accounts_loaded(&self) -> bool

true when every [[accounts]] entry has reached a terminal LoadingState AND the gateway’s listener is bound. The boot wave and the cron spawn path gate on this - spawning while an account is still resolving means the walk runs over a partial map, and spawning before the listener is bound stamps a base URL nothing answers - and a view reads it for its accounts card. Public so a view can read the pool’s settledness without reaching into the crate-private account map.

Source

pub fn project_would_bind(&self, project_key: &ProjectKey) -> bool

Whether a spawn in project_key would find an account: the gateway’s walk over the project’s declared model, which is what a spawn runs and what a project row’s refusal reads. false for a project without a model, or one whose org serves nothing it declares, which is the refusal a row draws.

Source

pub(crate) fn project_walk_cooling( &self, project_key: &ProjectKey, ) -> Option<ProjectCooling>

The cooling that would refuse a spawn in project_key, or None when a spawn would find an account.

Matches the walk’s budget failure alone, rather than taking any error, because that failure is the one carrying a reset to wait for - and only a refusal that clears on its own is owed a deferral. The walk’s other failure is the config-level one, and forge.toml refuses a project whose model no account in its org serves (ProjectModelUndeclared), so it is not reachable from a loaded config today. Keep the match narrow anyway: an empty walk with nothing to wait for must fail as before rather than retry forever, and this is the line that says so.

Source

pub fn account_loading_snapshot(&self) -> Vec<AccountLoadingRow>

Snapshot of (AccountKey display name, LoadingState) pairs in declaration order, read by the accounts surface. The order matches forge.toml’s [[accounts]] declarations so the rows sit next to the user’s mental model of which-account-is-which.

Source

pub fn account_pool_view(&self) -> AccountPoolView

The account pool as a view draws it: the rows the home snapshot carries, and the payload SessionUpdate::AccountsChanged pushes.

One builder for both paths, so a client folding the frame and a client reading the snapshot draw the same pool.

Source

pub fn account_auth_for(&self, display_name: &str) -> Option<AccountAuth>

How the account called display_name authenticates - the input the auth-repair hints branch on. None when the name isn’t a configured account.

Source

pub(crate) fn degraded_account_name(&self, account: &str) -> Option<String>

The account name back, when that account is saturated or bailed: the walk takes one of those only when nothing else in the pin declares the project’s model, so a spawn landing there is worth saying out loud. None when it has room, and for an unknown name.

Source

pub fn config_path(&self) -> PathBuf

The forge.toml this workspace loaded. Preflight names it as one of the two ways past an account that will not authenticate; an edit applies to new work as the file changes. The other is repairing the account’s own credentials, which a poller picks up in place without one.

Source

pub fn config_dir(&self) -> &Path

The config dir this workspace owns: forge.toml’s parent dir, and the dir every session’s child runs under, so a session’s transcript lives in its projects/ tree.

Source

pub fn dictate_snapshot(&self) -> DictateSnapshot

Per-model dictation progress for the preflight screen. Empty models means [dictate] enabled is false and preflight has no Dictation section to draw.

Source

pub fn dictate_models_dir(&self) -> Option<PathBuf>

Where the dictation models land. None when the platform has no usable cache directory and none was configured.

Source

pub fn dictate_close(&self, key: &SessionSlot, initiator: u64) -> bool

The connection that was streaming a take for key has gone: the take is DROPPED, not submitted - its reader is gone, so nothing it produced would land anywhere, and stopping it now spends nothing further on it.

Only a take by THIS connection is closed: an entry with no sink is not a connection’s to close, and a take that ended with another one started since is not this connection’s to close. The seat is free the moment this returns, so a client that reconnects and starts again is not refused by the take it left behind. Answers whether there was one to close.

Source

pub fn dictate_axes(&self) -> DictateAxes

The dictate axes in force: forge.toml over the crate’s own defaults. What a capturing client starts on and resets to.

Source

pub fn dictate_push( &self, key: &SessionSlot, samples: &[f32], initiator: Option<u64>, ) -> bool

Push one frame of client-captured audio into the seat’s live take, answering whether the samples were kept.

The take must be initiator’s own. A frame carries no seat, and a seat’s live take can be another connection’s - decision 3’s own scenario, where a client whose start was refused still has its microphone open for the frames already on the wire - so a push from any other connection is dropped rather than landing in someone else’s dictation. false also means the seat has no live take (never started, refused, or already resolved) or the take has stopped (the cap, or the speaker let go). A caller drops the frame rather than holding it, because the take’s own answer is what a reader sees either way.

Source

pub fn cancel_dictate_preflight(&self)

Stop the in-flight model fetch. Whatever reached the disk stays there as a .part, so the next run resumes; the snapshot then carries crate::DictateFailure::Cancelled and forge quits, because there is no dictation-less runtime to fall back to.

Source

pub fn start_dictate_preflight(self: &Arc<Self>)

Fetch, verify and load the dictation models. One task per forge run, started beside the account loaders; a no-op when dictation is switched off.

This is the prewarm the run keeps resident: the engine it parks in the state serves every take, so a press pays no model load, and the task it runs on is spawned rather than awaited, so none of the fetch, the verify or the load is on the boot path. The one event it writes says which of the two ended it.

The active models are resolved first - the config key, the runtime pick, or the compiled pin, per role - and the resolution’s failure is the same stop a failed fetch is, naming the config key that could not be answered.

Source

pub fn session_chip_for( &self, project_key: &ProjectKey, ) -> Option<SessionChipInfo>

Resolve project_key to the per-session chip the Projects pane renders next to each row: the account a spawn in it would land on, and that account’s current visual-state category. Returns None when the project is unknown, declares no model, or its org serves nothing it declares.

The walk is per org and model, so every row in a project shows the same account - which is what a spawn does.

State derivation:

  • LoadingState::Bailed splits by the recorded failure class, the same split the account rows render: auth failures (Unauthorized / Expired) -> SessionChipState::Bailed (red with a ⚠ prefix); transient classes and an unrecorded bail -> Degraded (warning yellow, the pollers heal it).
  • Ready + any usage window at the cap (the same is_saturated signal the walk demotes an account for) -> AtCap (yellow; the session still spawns but will throttle).
  • Every account serving the model inside a rotation cooldown -> RateLimited, naming the account whose cooldown lifts first: the one the first spawn after the thaw lands on. The walk still refuses a spawn; only this read looks past the cooldown.
  • Otherwise -> Normal (DIM; default chip).
Source

fn select_account_for_project( &self, project: &LoadedProject, model: &str, ) -> Result<AccountKey, Error>

The account the gateway walks for a project’s declared model, with the walk’s failures turned into the vocabulary a spawn reports.

Source

pub fn start_account_loading_tasks(self: &Arc<Self>)

Spawn one boot-time loading task per [[accounts]] entry in forge.toml. Each task runs the per-account loading state machine (in the crate-private account_loader module) until the account reaches a terminal LoadingState (Ready or Bailed). Called once at startup, replacing the older “single-probe-then-poll” boot model with the explicit loading gate that the launchpad now consults via AccountStateMap::all_loaded().

Source

pub(crate) fn enqueue_kick(&self, request: KickRequest)

Enqueue a worker kick on the dispatcher channel (#259). The drainer task fires one Command::Prompt per KICK_DISPATCH_INTERVAL, so simultaneous boot-time kicks across a team of N workers spread out as N × INTERVAL instead of all hitting Anthropic’s per-IP burst limit in the same tick.

Send errors (channel closed - workspace has shut down) are logged at error because they signal a kick was dropped after maybe_kick_worker_on_connected already decided to send. The worker stays idle in that case; this is rare in practice (workspace shutdown drains all sessions before the kick path could fire), but the log line preserves diagnosability.

Source

pub fn start_kick_dispatcher(self: &Arc<Self>)

Spawn the worker-kick drainer task (#259). Takes the receiver out of kick_dispatcher_rx_slot and starts a tokio task that loops on recv(), calls Self::dispatch_forged_prompt for each request, then sleeps KICK_DISPATCH_INTERVAL before the next pull.

Call once at construction, AFTER Workspace::new returns and the result is Arc-wrapped (mirrors start_account_loading_tasks / start_usage_poller shape). Subsequent calls find the slot empty and no-op, so a caller’s programming error can’t duplicate the drainer.

The drainer holds an Arc::downgrade(self) so the task exits cleanly when the workspace is dropped: each upgrade() returns None and the loop breaks. No explicit shutdown signal needed.

Source

pub fn start_gateway_listener(self: &Arc<Self>)

Bind the gateway’s inference listener and open the boot gate once the port is ours. Called once at boot, from the server’s startup. On success the launchpad’s gate opens; on failure the gate STAYS SHUT and every spawn attempt are refused at the spawn entries with the reason attached - the refusal is the protection, not the log line.

Source

pub fn sole_instance(&self) -> bool

Whether this is the only live forge on the machine: no other config dir’s lock is held.

A boot may change shared app-support state only when this is true. The test beside the store tests pins the peer case.

The app-support tree is machine-wide, so a boot that changes something shared in it - the browser host’s per-version prune - has to establish this first, or it deletes a peer’s state.

Source

pub fn start_rotation_announcer(self: &Arc<Self>)

Announce the roster on every rotation change: a cooldown a live 429 starts (a write the gateway publishes) and the clock-driven thaw after it (which no write announces, so this task sleeps to it).

A project row’s would_bind is derived from the selection walk, so both moments change what the home draws with nothing else to hang an update on. Started once at boot; the registry makes a second call a no-op, since a second watcher would double every frame.

Source

pub fn start_usage_poller(self: &Arc<Self>)

Spawn the background account-usage poller on the interval [usage] poll_seconds resolves to (60 s by default). Fetches OAuth usage for every [[accounts]] entry via the per- account config-dir’s credentials file (no Agent spawn required), writes each result into AccountStateMap.by_key. The session page draws its 5h/7d utilization from that cache, and the gateway’s account selection reads it too.

Called once at boot, AFTER start_account_loading_tasks (which subsumed the old spawn_initial_account_probe in #246). The supervisor’s registry is the idempotency: a second call while the poller is running is a no-op, so a caller’s programming error can’t multiply the poll rate.

Source

pub async fn refresh_account_usage_once(self: &Arc<Self>)

One pass of the usage poller: fetch OAuth usage for every configured account in parallel, write each result back to AccountStateMap. Per-account fetch errors are logged at warn so persistent auth failures (revoked token, expired refresh, missing credentials file) surface in default log output. Snapshot-mapping errors stay at debug because they indicate a response-shape drift rather than something the user needs to act on. Public so tests can drive a deterministic refresh without waiting for the poll tick.

Source

pub(crate) fn record_usage_success( &self, key: &AccountKey, snapshot: UsageSnapshot, )

Write one successful poll result. A healed account is immediately eligible for new spawns: the walk reads the state map directly, so there is nothing to recompute. Existing sessions keep the account they bound to.

Source

pub fn usage_for(&self, display_name: &str) -> Option<UsageSnapshot>

The newest usage reading the pool holds for an account, by display name: a rate-limited response’s windows when one landed after the last successful probe, else that probe’s snapshot. None while the account has reported nothing. A view renders the 5h / 7d bars from this reading.

Source

pub fn bound_account_for(&self, key: &SessionSlot) -> Option<AccountKey>

The account the gateway is serving key’s session with right now, which a re-selection may have moved off the spawn-time pick. The lookup goes through the pooled registration: bindings are keyed by the segment the child’s base URL was stamped with, which is the id the occupant runs under.

Source

pub(crate) fn stamp_respawn_overrides( &self, slot: &SessionSlot, session_id: &str, launch_settings: &mut SessionLaunchSettings, )

Stamp a respawn’s gateway registration for a child that runs under session_id: the four gateway-owned keys ride launch_settings as overrides, the binding moves to that segment, and the one it replaces is dropped. The pool entry records the same id, so bound_account_for reads the binding the child actually answers to. The gateway half is a no-op when the slot is not pooled or carries no registration: the launch then keeps the account env the original spawn laid down.

The tools forge has replaced are denied unconditionally, because a respawn’s settings are built by a view and carry no spawn-time flags - so /new and /resume are exactly where those denials would otherwise come back. A worker’s own denials come back with them: the kind decides the worktree pins, and the row’s own interactive flag decides the question.

Source

pub fn usage_error_for(&self, display_name: &str) -> Option<UsageFetchStatus>

Read the last poll-attempt failure for an account, if any. None when the most recent poll succeeded (or no attempt has been made yet). The class is what tells an empty bar apart from an upstream failure (the HTTP 429 case is especially common when multiple forge instances poll the same Anthropic account).

Source

pub(crate) fn resolve_slot( &self, target: &SessionTarget, ) -> Result<SessionSlot, WorkspaceError>

Resolves a SessionTarget to the slot it names. A project-rooted target resolves to that project’s lead slot; a specific session names its own. For Named with no matching project, returns WorkspaceError::ProjectNotFound.

Source

fn project_for_target(&self, target: &SessionTarget) -> Option<LoadedProject>

The project a spawn under target belongs to. A project-rooted target names it directly; a specific session slot names it by org and name.

Source

pub(crate) fn project_for_slot( &self, slot: &SessionSlot, ) -> Option<LoadedProject>

The project slot names, if forge.toml still declares it.

Source

pub(crate) fn slack_workspace_for_slot( &self, slot: &SessionSlot, ) -> Option<String>

The Slack workspace bound to slot’s project: the project’s own binding, else its org’s. None when the slot names no configured project or nothing binds one - both leave the Slack tools refusing with the key to add.

Source

fn find_project_by_name( &self, name: &str, ) -> Result<LoadedProject, WorkspaceError>

Look up a project by name from forge.toml. Returns WorkspaceError::ProjectNotFound when no project carries that name.

Source

pub(crate) fn stored_charter_for(&self, slot: &SessionSlot) -> Option<String>

The charter the store holds for slot, if any. A worker’s mission lives in its conversation, so a /new that emptied it has to re-deliver this or the worker comes back with no idea what it is for. Only a worker has one, and a lead’s absence is the ordinary answer here.

Source

pub(crate) fn stored_interactive_for(&self, slot: &SessionSlot) -> Option<bool>

Whether the store’s row for slot says its session may ask its user a question. None when the store holds no row: a lead has none, and a worker without one was never spawned.

Source

fn stored_resume_id( &self, slot: &SessionSlot, force_new: bool, ) -> Result<Option<String>, WorkspaceError>

The id the store holds for slot, unless --new (force_new) forces a fresh session. Ok(Some(id)) => resume that id; Ok(None) => start fresh under a minted one. force_new overrides a stored id - that is what makes the boot wave’s leads come up fresh under --new, while every non-boot spawn leaves force_new false and resumes.

The store is the only source. A row with no id is a session that has never run, so the caller mints rather than re-deriving one from the transcripts. A store that cannot be read is an error rather than a mint: the session it names would be forked, and the new id written over the row that could not be read.

Source

fn record_spawn_id( &self, slot: &SessionSlot, id: Option<String>, reopen: bool, ) -> String

Record id as the session for slot, minting a fresh one when the caller has none to hand in. A spawn leaves the seat open with no turn in flight; the boot wave read its pre-spawn state already, and the turn its note starts flips it back.

Source

pub(crate) fn record_seat_state(&self, slot: &SessionSlot, state: SeatState)

Move slot’s row to state. A write that cannot land is warned and not retried: the row is what the next boot derives from, and a missing write reads as the state it held before. A closed row is left closed; only the spawn path reopens it.

Source

fn write_seat_state(&self, slot: &SessionSlot, state: SeatState, reopen: bool)

The one write behind Self::record_seat_state and the spawn’s reopen. reopen is the opening door’s word, not a turn’s: a spawn the user or the boot wave asked for may open a closed seat, while a settle or a swap landing late must not.

Source

fn restore_seat_state( &self, slot: &SessionSlot, state: Option<SeatState>, expected: SeatState, )

Put slot’s row back to the state it held before a refused spawn. Not a transition: the value is written verbatim, None included.

Source

pub(crate) fn stored_seat_state(&self, slot: &SessionSlot) -> Option<SeatState>

The state slot’s row holds, or None when the row is absent, was written before the field existed, or could not be read.

Source

fn forget_spawn_id(&self, slot: &SessionSlot, previous: Option<&str>)

Undo a spawn’s recorded id after a bind refuses: the store keeps the id it held before, or none at all, so a boot never resumes onto a transcript that never existed.

Source

pub(crate) fn note_running_session_id( &self, slot: &SessionSlot, session_id: &str, )

Record the id the CLI adopted for a live session, so the store stops disagreeing with what is running. An in-session /resume, a /clear, a login or a logout can move a session’s id without forge choosing it, and a boot resolves a session from this row.

slot is the one the task is registered under, so the row is named rather than derived: a session’s role is what its spawn stated and nothing re-derives it from the live-worker registry.

Source

fn note_worker_session_id(&self, slot: &SessionSlot, session_id: &str)

Mirror the id a live worker adopted onto its registry entry, so the status echo and agents__list name the occupant that is running rather than the one it was spawned under.

Source

pub fn running_session_id_for(&self, slot: &SessionSlot) -> Option<String>

The id the session at slot runs under, when this process holds it live. None for a slot with no pooled session. This is the slot’s occupant rather than a snapshot of when a caller last heard about it, so a read that has to name the id now - a transcript path, a CLI argument - asks here.

Source

pub fn transcript_session_id_for(&self, slot: &SessionSlot) -> Option<String>

The id a transcript read for slot is looked up under: the live occupant’s when this process holds one, and the store’s own row - the session a wake resumes - otherwise.

A sleeping seat still has a session. The store records the id under the slot before the child starts and the CLI writes its transcript under it, so a sleeping seat’s history is readable without a spawn; a read that named only pooled sessions drew one empty. None for a slot with no session at all: nothing live, and no stored id.

Source

pub(crate) fn session_is_pooled(&self, slot: &SessionSlot) -> bool

Whether an agent is currently pooled for slot.

Source

fn pooled_handle_has_task(&self, slot: &SessionSlot) -> bool

Whether the slot’s pooled handle still has a session task consuming its commands. The teardown signal is the task’s own record of itself: unanswered while it runs, answered as it drops. A pooled handle with no such task is one no spawn may hand back (#2683).

Source

pub fn session_last_activity(&self, slot: &SessionSlot) -> Option<SystemTime>

When the transcript of the session at slot was last written, read from the catalog row naming the id it currently runs under. None for a slot with no live session, or one whose transcript is not on disk yet.

Source

pub async fn resumable_sessions( &self, slot: &SessionSlot, ) -> Vec<SDKSessionInfo>

The sessions the seat at slot can jump back to with /resume, newest first, and never the one it is already on.

The set is what claude --resume can find from the directory the seat’s own resume runs in (Self::cwd_for_session, which is what --resume is handed): a lead’s project root, a worker’s worktree, or the project root for a worker with none. A lead gets its project’s own sessions and never its workers’ - Workers::Hidden, which is what keeps a lead’s turns out of a transcript tagged for a worker - while a worker gets only the transcripts carrying its own label.

A lead is answered from the boot catalog’s group for its project, which the boot scan already read from disk and holds in memory, with each row kept only if its transcript is still there: the catalog also mirrors every Connected, including a session that never wrote a turn, and --resume cannot find that one. A worker’s runs are not in the catalog, so its own directory is scanned - its worktree for a git worker, the project root otherwise. A seat whose project forge has not loaded has no directory to resume from, and answers empty.

Source

fn catalog_sessions_for(&self, cwd: &str) -> Option<Vec<SDKSessionInfo>>

The boot catalog’s group for the project at cwd, or None while the boot scan has not landed: a group missing because nothing has scanned is not a project with no sessions.

Source

fn transcript_dir_for(&self, cwd: &str) -> PathBuf

The directory claude writes cwd’s transcripts in, under this workspace’s config dir.

Source

fn session_scan_root(&self) -> PathBuf

Where this workspace’s projects/ tree resolves, with the boot scan’s own fallbacks.

Source

async fn scan_sessions( &self, cwd: &str, workers: Workers, ) -> Vec<SDKSessionInfo>

One directory scanned for the sessions a seat may resume - a worker’s own runs, and a lead’s before the boot catalog has landed - through the store’s tag cache like the boot scan: an unchanged transcript is not re-read end to end for a list a reader opens for a moment.

The cap bounds the reads as well as the answer: the scan reads the newest transcripts first and stops once the rows it serves are in hand, filling from the next batch when one comes back short - and a transcript a worker’s cached tag rules out is not read at all (#2567), so a non-git worker sharing the project root no longer pays the project’s whole scan. A lead takes the catalog above for a list that costs no read at all.

Source

pub(crate) fn fresh_session_id_for(&self, slot: &SessionSlot) -> String

A fresh id for a session starting now, recorded against slot’s row.

Source

fn stored_session_id( &self, org: &str, project: &str, label: &str, ) -> Result<Option<String>, Error>

The session id the store holds for (org, project, label).

Ok(None) means no id: the row is absent, or it carries none. That is a session which has never run, and a caller may mint for it. Err means the row could not be read, which is a different answer and must never be treated as absence: a caller that minted there would fork the session and write the new id over the row it failed to read.

Source

pub(crate) fn record_session_id( &self, org: &str, project: &str, label: &str, id: &str, )

Record id as the session for (org, project, label), keeping the fields a worker’s row already carries. A write that cannot land is warned and not retried: the session still runs under id, and the next boot reads whatever the row held.

Source

pub(crate) fn find_project_view_by_name( &self, name: &str, ) -> Option<LoadedProject>

Locate a ProjectView-like (LoadedProject) by name from forge.toml. Returns None when no project carries that name. Used by the spawn handlers to resolve the project’s path / cwd before emitting SessionUpdate::Spawning.

Source

pub(crate) fn lead_slot_for_project( &self, project_name: &str, ) -> Option<SessionSlot>

The seat a project-scoped section update routes on: the project’s lead, which exists for every declared project. None for a name no project carries, the shape a project that left forge.toml leaves.

Source

pub(crate) fn update_tx(&self) -> &UpdateFanout

Internal accessor for the SessionUpdate fan-out sender. Used by spawn.rs to emit Spawning / ConnectionFailed / FatalError from the App-level handlers.

Source

pub(crate) fn announce_accounts_changed(&self)

Tell every view the account pool moved.

Called from the writes that move it - a boot probe settling, the usage poller landing or failing, and the listener binding - because none of them belongs to a seat. The frame carries the pool’s drawn state, built by Self::account_pool_view, so a subscriber applies a usage-only move without a home re-read; a move in what a project row binds on still asks for the snapshot that refreshes it.

Source

fn session_cwd_for(&self, slot: &SessionSlot) -> Option<String>

The cwd a project lead’s slot runs in: its project’s path. claude --resume indexes by the project key derived from the subprocess’s working directory, so every explicit-resume code path must spawn the subprocess in the session’s original cwd or the resume hits “No conversation found with session ID …” even when the .jsonl exists.

Read from the slot rather than from the catalog: a catalog row is keyed by the id the CLI adopted, which /new replaces, so the lookup answered nothing for the session that had just started and resume was handed an empty working directory.

A WORKER answers nothing here, deliberately. Its cwd is its worktree, which only the live-worker registry can compose; the project root is a plausible-looking wrong answer that would shadow the registry arm in Self::cwd_for_session and send a git worker’s resume to the wrong directory.

Source

pub fn record_connected_session( &self, cwd: &str, session_id: &str, summary: Option<String>, )

Insert (or update) a session entry under the project that owns cwd. Called after every Connected migration so newly spawned sessions surface in the Projects-pane drilldown immediately, without forcing a full disk re-scan. The entry is placed at the head of the project’s session list to match the most-recent-first ordering the scan produces.

Source

pub fn subscribe_mirror(&self) -> UnboundedReceiver<SeatUpdate>

Subscribe without answering the workspace’s prompts: a logger, a mirror, anything that reads the stream but renders no prompt.

Every caller gets its own stream, so a second view attaches beside the first rather than being refused, and the workspace drops the matching subscription once its receiver goes. A stream carries what is emitted after this call; a subscriber reads what it missed from the subject’s snapshot instead.

The distinction from Self::subscribe_answerer is load-bearing rather than descriptive. A permission, question or Slack-draft request is parked on a reply, and the paths that raise one resolve it Cancelled instead when no subscriber can answer, so that a request nobody will reply to fails the turn rather than hanging it. A subscriber that declares itself an observer is not counted as an answer.

A browser hand-off fails closed differently: nothing parks a decision for it to cancel, so its registration hands the blocked call a dead receiver instead and the tool answers the reason - no attached client can show the hand-off - rather than holding a session on a prompt no view can draw.

Every frame arrives as a SeatUpdate, carrying the number its seat minted for it.

Source

pub fn subscribe_answerer(&self) -> UnboundedReceiver<SeatUpdate>

Subscribe as an answerer: the same stream, for a client that renders prompts and can answer them.

The answering half of Self::subscribe_mirror. The role decides one thing: whether the core may park a prompt on this subscriber’s reply.

Source

pub(crate) fn update_sender(&self) -> UpdateFanout

Clone the workspace’s SessionUpdate sender. Internal to this crate: a view’s own async work belongs on a channel of its own, so that the subscription methods above are the whole of what the core owes a frontend.

Source

pub fn seat_next_seq(&self, key: &SessionSlot) -> u64

The number key’s next frame will be minted under: what a client reading the seat expects next, beside the occupant that names the epoch it belongs to.

Source

pub fn seat_frames_between( &self, key: &SessionSlot, from: u64, to: u64, ) -> Vec<SeatUpdate>

The frames key’s stream emitted under the numbers in from..=to, oldest first: the answer to a client’s range ask while the seat’s own window holds them.

Source

pub fn seat_oldest_kept_seq(&self, key: &SessionSlot) -> Option<u64>

The oldest number key’s window still holds, None for a seat that has emitted nothing: what tells a range answered from memory from one that has to be read off the transcript.

Source

pub fn seat_unringed_between( &self, key: &SessionSlot, from: u64, to: u64, ) -> u64

How many of the numbers in from..=to on key’s stream no window holds: the frames that close an epoch are numbered but kept out of the ring, so a reader measuring whether a copy covers a stretch leaves them out of what it must account for.

Source

pub fn seat_epoch_opened_with_rows(&self, key: &SessionSlot) -> bool

Whether key’s latest epoch-opening seal carried rows, which no ring holds: what tells a copy filled from the window that it cannot reach the rows the epoch began with.

Source

pub fn domain_session_for( &self, key: &SessionSlot, ) -> Option<Arc<Mutex<DomainSession>>>

Borrow the workspace-side DomainSession for key.

Returns the Arc-cloned mutex protecting the domain bucket. Callers .lock() to read or mutate. None when no SessionTask is registered for key (e.g., the session was closed or hasn’t been spawned yet).

Callers should hold the lock for the shortest scope possible; concurrent reducers and the per-session SessionTask share this mutex.

Source

pub fn available_commands_for( &self, slot: &SessionSlot, ) -> Vec<AvailableCommand>

The slash commands the CLI last advertised for the session at slot. Empty for a slot whose session has not connected yet, or whose CLI advertised none.

Source

pub fn available_agents_for(&self, slot: &SessionSlot) -> Vec<AvailableAgent>

The subagents the CLI last advertised for the session at slot. Empty in the same two cases as Self::available_commands_for.

Source

pub fn subagent_cards_for(&self, slot: &SessionSlot) -> Vec<SubagentCard>

The sub-agent instances the session at slot has folded, as the list the session task pushes when it moves.

A record reads this rather than folding the conversation itself. The task folds a card per frame and announces the list moving, so a second fold over the held conversation would be a second answer to a question one fold already answers - and one that disagrees with the frames as soon as the conversation a record can see is shorter than the one the session has run.

Source

pub fn has_agent_for(&self, key: &SessionSlot) -> bool

Whether the session at key currently has a live agent handle stamped onto its DomainSession. Encapsulates the presence check so callers don’t need to peek at DomainSession.conn directly - the field layout is a workspace internal.

Source

fn apply_dictate_override( self: &Arc<Self>, key: &SessionSlot, update: DictateOverrideUpdate, ) -> Result<(), DispatchError>

Apply a /dictate override edit to the session’s DomainSession and echo the full set back. An unknown session is refused the same way the per-session commands are: there is nothing to edit.

§Errors

Returns DispatchError::UnknownSession when no DomainSession is registered for the key.

Source

pub fn dispatch_workspace_prompt( self: &Arc<Self>, key: &SessionSlot, text: String, ) -> Result<(), DispatchError>

Dispatch a workspace-originated plain prompt (cron fire, peer, gotify or slack delivery, kick, notices).

A delivery drawn in a view needs an update here, paired with an arm in forge-server’s delivery_turn. This path emits no frame of its own - the forge for one is delivery_turn, downstream, and it ends in _ => return None. So a prompt sent from here whose typed update has no arm there draws nothing in any view, silently: a prompt with no envelope of its own takes Self::dispatch_forged_prompt instead, which forges the frame here.

Source

pub fn dispatch_workspace_prompt_from( self: &Arc<Self>, key: &SessionSlot, text: String, source: PromptSource, ) -> Result<(), DispatchError>

Self::dispatch_workspace_prompt, with the row’s source label named by the caller: a delivery knows what it is (a cron fire, a Gotify notification, a peer comm) and the queued row has to say so.

Source

pub fn dispatch_workspace_prompt_under( self: &Arc<Self>, key: &SessionSlot, text: String, source: PromptSource, uuid: String, ) -> Result<(), DispatchError>

Self::dispatch_workspace_prompt_from with the id supplied by the caller, for a site that already minted one: a delivery that draws its own envelope row passes the same id to both, so the row a view holds while the prompt queues and the prompt’s lifecycle frames are one thing by id.

Source

pub(crate) fn fire_auto_continue(self: &Arc<Self>, key: &SessionSlot)

Fire the continuation for a seat whose turn just failed: one prompt of forge’s own, the moment the failure lands (#2146).

Every non-cancelled failed turn is this one’s to answer, whatever its class - a transient server error included, which nobody’s retry ladder throws back at the CLI once the turn has died. The decision is taken under the lock; the dispatch is not.

Source

pub fn dispatch_forged_prompt( self: &Arc<Self>, key: &SessionSlot, text: String, ) -> Result<(), DispatchError>

Dispatch a workspace-originated prompt whose words no view has drawn: a worker kick, an auto-continue. Self::dispatch_workspace_prompt exactly, plus the frame - nothing else carries these words, where a delivery’s own envelope update is what draws a delivery, and routing one of those through here would draw its words twice.

Source

pub(crate) fn dispatch_forged_prompt_under( self: &Arc<Self>, key: &SessionSlot, text: String, uuid: String, ) -> Result<(), DispatchError>

Self::dispatch_forged_prompt with the id supplied by the caller, for a forged prompt an association has to outlive the dispatch: the auto-continue records the id it sent under, so the prompt can be matched where it runs. The frame and the prompt carry one id either way - the row a view draws and the turn the model receives are one thing by id.

Source

pub(crate) fn dispatch_handoff_brief( self: &Arc<Self>, key: &SessionSlot, text: String, uuid: String, ) -> Result<(), DispatchError>

The brief a handoff’s successor runs as its first turn.

Its own row, forged like the machinery’s other prompts. The successor’s transcript opens with this prompt and the client draws it as the handoff procedure; the CLI never echoes a stdin prompt, so this frame is the page’s only carrier of it.

Source

pub(crate) fn dispatch_handoff_prompt( self: &Arc<Self>, key: &SessionSlot, text: String, uuid: String, ) -> Result<(), DispatchError>

The handoff’s own judgment prompt.

The one prompt a swap’s gate lets through. The gate opens when the handoff begins, and this is the prompt the handoff is built on: held like any other, it would wait for a successor that is waiting for it.

Source

fn dispatch_forged_prompt_held( self: &Arc<Self>, key: &SessionSlot, text: String, uuid: String, holdable: bool, ) -> Result<(), DispatchError>

Source

fn tell_row_owner(self: &Arc<Self>, task: &Task, text: String)

Tell a row’s live owner what the user just did to it, one line.

Forged, not plain. The board’s edits carry no envelope update of their own, so a prompt routed the plain way queues invisibly in a view - the forged frame is what draws the line the moment it lands. Silent for an ownerless row and a sleeping seat: the board is where that edit shows, and a prompt is a real turn.

Source

fn lane_words(to: TaskStatus) -> &'static str

What a board notice calls the lane a move landed in: the board’s own names, so Pending reads backlog as its lane does.

Source

pub fn dispatch(self: &Arc<Self>, cmd: Command) -> Result<(), DispatchError>

Route a Command. Per-session commands (cmd.key() == Some(key)) fan out to the matching SessionTask. App-level commands (cmd.key() == None - SpawnProject) route to the workspace’s own handler.

Test fallback: when no SessionTask is registered for key but a DomainSession carries a stub AgentHandle (e.g., the Workspace::testing_stub path), the command runs synchronously against that handle. This keeps #[test]-flavor unit tests (no tokio runtime) able to observe the forge_primitives::AgentCommand emitted on the stub’s channel without spinning up an async actor.

§Errors

Returns DispatchError::UnknownSession when no SessionTask is registered for the requested key (e.g., the session was just closed), with one exception: a ResumeSession starts the seat on the id it names, which is the recovery a failed start leaves. Returns DispatchError::SessionClosed when the task’s command receiver has been dropped.

Source

pub fn dispatch_from_view( self: &Arc<Self>, cmd: Command, ) -> Result<(), DispatchError>

Dispatch one command a view sent over the socket.

The same dispatch as Self::dispatch: the frame a prompt draws is the session task’s, emitted when the CLI starts it, so no entry needs to say which door the words came through.

§Errors

As Self::dispatch.

Source

fn dispatch_answered( self: &Arc<Self>, cmd: Command, ) -> Result<(), DispatchError>

Route a command, answering the prompts forge handles itself.

A prompt draws its turn in the session task, never here (#2816): the queue carries its words from dispatch and the page takes the row when the CLI starts it. Only the answer a forge command, a misuse or an unrunnable slash name owes is emitted from this path, because the task never sees those prompts.

NOT from dispatch_workspace_prompt, which is the delivery path: a delivery already draws as an envelope turn of its own.

Source

fn run_forge_prompt( self: &Arc<Self>, key: &SessionSlot, prompt: &ForgePrompt, ) -> (Result<(), DispatchError>, Option<(NoticeSeverity, String)>)

Run one of forge’s own commands against key’s seat.

The launch settings are built here rather than taken from the caller: a view that supplied its own would spawn a session with what its own snapshot happened to hold, and a client has no snapshot to supply.

The answer is returned rather than emitted, so the dispatcher can land it after the command has run.

Source

fn show_mode( &self, key: &SessionSlot, ) -> (Result<(), DispatchError>, Option<(NoticeSeverity, String)>)

A bare /mode answers what the session is on, not what the command’s usage is (#1626). The hook’s observation outranks the launch’s own setting - the precedence the inspector draws - because the CLI can change mode without re-emitting status. A session that has reported neither is answered with the usage line it answered before.

Source

fn show_effort( &self, key: &SessionSlot, ) -> (Result<(), DispatchError>, Option<(NoticeSeverity, String)>)

A bare /effort answers the level in force, on show_mode’s terms: the observed level, else the one the launch asked for (#1626).

Source

pub(crate) fn replace_browser_seat(&self, seat: &SessionSlot)

Tell the fleet a /new or /resume kept the slot and swapped the occupant, so the previous occupant’s tabs close. Best-effort, like the spawn’s own warm: a workspace with no fleet has no tabs to replace.

Source

fn release_browser_seat(&self, seat: &SessionSlot)

A session ended, told to the fleet as a fact rather than left to the next sweep: a sweep rebuilds its set from configuration, so a closed lead is still in it. The tombstone is synchronous, so it lands before the release work; the tab close is the fleet’s own async work.

Source

fn spawn_inputs(&self, key: &SessionSlot) -> (String, SessionLaunchSettings)

The cwd and settings a re-spawn on key carries.

Source

fn resume_without_task( self: &Arc<Self>, key: &SessionSlot, session_id: &str, launch_settings: SessionLaunchSettings, ) -> Result<(), DispatchError>

Start a seat on an id when no task is running for it: the /resume <old-id> recovery a failed handoff names.

A lead goes through the project wake, so a failed start surfaces the same ConnectionFailed and the same fallback a wake has. The id named is recorded first because that path resumes the slot’s stored id; it names a transcript that already exists, so a failed start leaves a real id rather than an unrun one. A worker goes through the spawn its boot re-spawn uses, with the row’s own charter, tree and family selection.

Source

fn fill_launch_settings( &self, launch_settings: &mut SessionLaunchSettings, cwd: Option<&str>, )

Fill in the launch settings a caller did not build.

A caller with its own snapshot of the documents builds them and hands them over; a client has no config to read, so a spawn it asks for arrives with none, and a spawn that ran on those alone would carry no language, no model and the CLI’s own defaults for permissions, effort and output style - a session launched differently from one started with the documents in hand.

Read from the workspace’s own config dir, which is what a session with no account-scoped dir of its own runs under. A caller bound to an account builds its own from that account, so the two can differ where an account names a dir of its own - and where they do, the caller that supplied settings keeps them.

Source

fn set_effort( self: &Arc<Self>, key: &SessionSlot, level: &str, ) -> (Result<(), DispatchError>, Option<(NoticeSeverity, String)>)

/effort <level>: write the level the next launch reads.

A settings write rather than a session command - the CLI carries no control request for effort - so it lands in the same document the launch builder reads and takes effect when the session next starts.

Source

fn unrunnable_slash_name(&self, key: &SessionSlot, text: &str) -> Option<String>

The line to answer a slash name this session cannot run, or None when the text is the CLI’s to read.

What a session can run is what its CLI advertised, or one of the names the CLI resolves without advertising (crate::prompt::FORWARDED). A session that has advertised nothing refuses nothing: an empty catalogue is not knowing, and refusing on ignorance would drop names the CLI has.

Source

pub(crate) fn notice( &self, key: &SessionSlot, severity: NoticeSeverity, text: &str, )

Emit one line the core has for a view about key.

Source

pub(crate) fn launch_settings_for( &self, key: &SessionSlot, cwd: &str, ) -> SessionLaunchSettings

The settings a launch on key carries, read from the same documents the CLI reads.

Source

pub(crate) fn route(self: &Arc<Self>, cmd: Command) -> Result<(), DispatchError>

Route a command that carries no frame of its own.

Source

fn board_edit_refused(&self, what: &str, why: &str)

Say on the service line why a board edit did not land: a refusal a reader cannot see is indistinguishable from a press that did nothing.

Source

pub(crate) fn dispatch_worker_respawns( self: &Arc<Self>, lead: &SessionSlot, project_key: &ProjectKey, dynamic: &[SessionRecord], force_new: bool, )

Re-spawn this project’s persisted workers on lead reconnect, resuming by label off the id the store holds for that label. Charter and kick come from the DB row.

Since this holds the worker set, it decides the kick directly: a resume of a worker whose row says in_turn takes the row’s own resume_kick and falls back to the forge restart note, telling it to continue rather than restart; a resume of a parked worker gets nothing, and a fresh re-spawn (never prompted, so no store row) re-delivers the stored kick. maybe_kick_worker_on_connected then just delivers whatever this put on the WorkerEntry.

Source

pub(crate) fn respawn_workers_for_lead( self: &Arc<Self>, lead: &SessionSlot, project_key: ProjectKey, force_new: bool, )

Lead Connected-hook entry point. Synchronously claims a per-project in-flight guard, then dispatches one Command::SpawnWorker per persisted row, resuming by the id the store holds for that row’s label. The guard is released after the dispatches go out so a fast double-Connected can’t slip a second wave through.

No-op when the per-project guard is already claimed (another wave is in flight). The first-pass live_workers.is_empty() gate in session_task::maybe_respawn_workers_on_connected catches the post-dispatch case; this guard covers the in-flight window.

Source

fn try_claim_respawn(&self, project_key: &ProjectKey) -> bool

Claim the per-project respawn in-flight guard. Returns true if the guard was acquired (entry was absent), false if another wave was already in flight.

Source

pub(crate) fn cron_unwakeable_is_new(&self, id: &CronId) -> bool

Whether this fire pass is the FIRST to find id unwakeable, recording it as seen if so. True is the pass that warns; false is every later pass while the condition holds.

Source

pub(crate) fn cron_unwakeable_commit(&self, ids: HashSet<CronId>)

Close a fire pass: ids is what it found unwakeable, and it replaces the previous pass’s set wholesale. That replacement is the only clearing this state needs - an entry cannot survive a pass that did not find its cron unwakeable, whether the cron fired, advanced, was deleted, or simply was not due.

Source

pub(crate) fn cron_cooling_warned_at(&self, id: &CronId) -> Option<SystemTime>

When the previous fire pass last warned about this cron’s cooling, or None when that pass did not find it cooling.

Source

pub(crate) fn cron_cooling_commit(&self, warned_at: HashMap<CronId, SystemTime>)

Close a fire pass with the crons it still found cooling and the time each last warned, replacing the previous pass’s map wholesale for the same reason Self::cron_unwakeable_commit does.

Source

pub(crate) fn resolve_worker_resume_session( &self, org: &str, project: &str, run_dir: &Path, label: &str, ) -> Result<ResumeTarget, Error>

The session a worker label resumes onto.

The row the label is registered under is authoritative while it is there: a worker that has one opens as it always has. The transcripts are the recovery path for a row that is gone - a despawn deleted it, or this install never had it - and there the newest transcript in run_dir’s directory carrying the label’s worker tag wins.

Ok(ResumeTarget::None) is a label with nothing in either, and Err is a store, directory or transcript that is there but could not be read. Only the first is a fallback: a caller that started a fresh session on the second would be answering a question the lookup never answered.

Source

pub(crate) fn stamp_adopted_worker_transcript( &self, project_key: &ProjectKey, label: &str, session_id: &str, run_dir: &Path, ) -> Result<()>

Stamp an adopted pre-field transcript with the adopting project, so a second project can never adopt it.

Source

fn projects_composing( &self, run_dir: &Path, label: &str, ) -> Vec<(String, String)>

The declared projects whose transcript lookup for label opens run_dir: the directory their worker runs in, or the sanitised key that directory is stored under.

Source

fn release_respawn(&self, project_key: &ProjectKey)

Release the per-project respawn in-flight guard. Paired with try_claim_respawn; called once the dispatches have gone out, on each path that can issue them.

Source

pub fn set_session_id_in_domain( &self, key: &SessionSlot, value: Option<SessionId>, )

Set the session_id field on the workspace’s DomainSession for key. No-op when no domain handle is registered for key. Stamps the claude-issued UUID once the first Connected event fires - the workspace consults this when routing AgentHandle calls that take a session_id.

Source

pub async fn shutdown(&self) -> Vec<SessionSlot>

Graceful shutdown of every pooled Agent. Drains the pool, then drops each Arc<AgentHandle>.

The subprocess kill is asynchronous through each SessionTask: dropping the command senders closes every task’s command channel, each task’s run loop exits, and its exit path awaits AgentHandle::disconnect, which takes the bridge’s client slot and runs the SDK’s graceful shutdown (signal reader task, drain, close the child). Client has no Drop of its own, so without that disconnect the child would survive the pool drain. A well-behaved caller releases its handle reference before calling shutdown, so Workspace is the sole owner of every pool entry. Callers that hold cloned handles across shutdown keep the AgentHandle’s task alive until they release them.

The return waits for those exit paths, because the runtime a caller is about to drop does not poll them: each child sits in its own process group and only its own close signals that group, so a shutdown that returned early would leave the tree running. No session may start once it begins - the gate goes up before the first drain and the mint reads it under the same lock - and the drain is repeated until nothing is registered. The wait is bounded by the same TEARDOWN_WAIT the despawn’s removal holds to.

Returns the slots that had not answered; empty is a clean stop. The caller turns a non-empty answer into a non-zero exit, so a supervisor can tell a clean stop from one that left a tree.

Source

pub(crate) async fn shutdown_within(&self, wait: Duration) -> Vec<SessionSlot>

Self::shutdown with the budget injected, so a case can drive the expiry without sitting out the production wait. Returns the slots that had not answered by the deadline; empty is a clean stop.

Source

pub fn release_session_with_cascade(self: &Arc<Self>, session_key: &SessionSlot)

Release a single session’s pool entry: drops the workspace’s Arc<AgentHandle> for that key so the underlying claude subprocess exits once the consumer (a view’s bucket) also drops its reference.

Cascade-aware lead release. Use this when closing a project’s lead session from a view: the lead-row × click, the launchpad’s per-row close on a failed lead bucket, etc.

When session_key is a project’s lead - it appears in the project’s catalog AND is NOT in live_workers[project] - every live worker under that project is released first via the non-cascading release_session primitive. Workers’ JSONLs persist on disk; only the in-memory live state + the running claude subprocesses are torn down.

The “in-catalog AND not-in-live_workers” rule is the discriminator (rather than sessions.first()): the catalog also indexes worker sessions once their Connected fires, so sessions[0] is not a reliable lead marker after a worker reaches Running. live_workers is the authoritative “this session is a child agent” registry.

Source

pub(crate) fn announce_release(self: &Arc<Self>, key: &SessionSlot)

Announce that a seat’s release has begun.

One place for the close doors to say it, so a viewer draws the seat as going to sleep from the gesture instead of keeping the state it last read. Each door calls it where the close is certain and the teardown has not started: a lead before its cascade, a worker before its own kill - where a despawn’s removal frame only follows the worktree cleanup, minutes later (#1930).

The seat’s failure record and any standing death go with the announcement, before the frame that makes a view re-read: a close is the reader acting on the row, and a trace that outlived the seat kept it in the needs-you group with nothing left to act on.

Source

pub(crate) fn release_session(self: &Arc<Self>, session_key: &SessionSlot)

Non-cascading single-session release - the primitive. Drops the pool entry, command sender, and domain handle for session_key. No side effects beyond that single session.

Use this for ANY session close where cascade semantics are undesirable or undefined:

  • handle_close_worker (per-row worker X click) - the close must affect only the worker being closed.
  • The lead-cascade loop inside release_session_with_cascade itself, when releasing each worker.

Use release_session_with_cascade instead when the caller is the lead-row close gesture.

Source

pub(crate) fn session_is_live(&self, key: &SessionSlot) -> bool

Whether a session task still exists for key, which is what makes it live: its command channel is registered and routable.

Source

pub fn is_shutting_down(&self) -> bool

Whether a shutdown has begun: no new session may start.

Source

pub(crate) fn mint_session_teardown( &self, key: &SessionSlot, ) -> Option<Sender<bool>>

Mint the slot’s teardown signal, or None when a shutdown is already underway and no session may start. The refusal is read under the same lock a shutdown drains with, so a spawn can never register behind the drain.

Source

pub(crate) fn session_teardown( &self, key: &SessionSlot, ) -> Option<Receiver<bool>>

Subscribe to the slot’s teardown signal, if a session task is registered behind it and has not answered yet. None for a slot with no task - a stranded row - and for an entry a finished task already answered: either way the caller has nothing to wait for, and the despawn’s mid-spawn guard reads None exactly that way.

The entry itself stays in the map, unlike the one receiver it replaced: more than one waiter may need the same answer, and a shutdown’s drain reads every entry’s value rather than subscribing blind.

Source

pub(crate) fn release_session_if_current( &self, session_key: &SessionSlot, handle: &Arc<AgentHandle>, )

Supersession-safe release: drop session_key’s pool entry, command sender, and domain handle ONLY when the pooled agent is still handle (by Arc identity). A SessionTask whose session was re-spawned under the same key is a stale predecessor - when it exits and runs this cleanup, the pool already holds the successor’s handle, so the guard no-ops and the live re-spawned session is left intact. Gating all three maps on the one identity check keeps a superseded task from half-cleaning its successor.

Source

pub fn live_seats(&self) -> Option<LiveSeats>

Every seat that exists right now: each configured project’s lead, and every worker the store holds - running or not, because a durable worker’s seat exists until it is despawned.

The browser sweep reads this: a tab whose owning seat is not here is an orphan and closes. No profile is deleted by that sweep - a profile’s logins are the org’s and stay until the idle clock closes the host - so the cost of a wrong answer is a closed tab, not a lost login. Captures are serialised, the lock spanning the mint and the reads (crate::browser::LiveSeats): a higher generation is a later seat set, which is what lets the fleet drop a stale one - including for a seat released without a sweep of its own, since only a capture that read after the release can drop its tombstone and every snapshot still listing the seat is older than that capture. It still holds two rules, because a closed tab is a page an agent was working in:

  • It errs toward INCLUSION: a seat wrongly present leaves one tab to the next sweep, while a seat wrongly absent would close a live session’s page.
  • It fails closed: None when the store cannot be read, so no caller can mistake an unread store for “no seats exist” and sweep everything. Nothing sends a set it could not read.
Source

pub fn list_live_workers(&self, project_key: &ProjectKey) -> Vec<WorkerEntry>

Snapshot the live workers for project_key. Returns an empty Vec when no workers exist (rather than None) so a view’s tree- child render can branch only on is_empty.

Source

pub fn live_worker_states_by_project( &self, ) -> HashMap<ProjectKey, Vec<LiveWorkerState>>

Every live worker’s liveness, keyed by project.

The launchpad’s worker rows read this once per frame, which is what the shape is for: one lock answers every project row, and the projection leaves each worker’s charter in the registry instead of copying it, which Self::list_live_workers does on every call.

Source

pub fn has_background_work(&self, slot: &SessionSlot) -> bool

Whether slot’s session has live background work. A fact about the session rather than about a viewer, so the core holds one answer for every view.

Source

pub fn session_failed_turn(&self, slot: &SessionSlot) -> Option<SystemTime>

When slot’s newest turn ended in failure, for the rail’s mark. None for a slot whose newest turn succeeded, never ran, was cancelled by the reader, or has since been moved past with a new turn.

Source

pub fn pending_interaction( &self, slot: &SessionSlot, ) -> Option<PendingInteractionKind>

What the session at slot is waiting on a person for, or None when it can advance on its own.

The one predicate every needs-you surface reads. A session activity, a roster row, a rail mark and a notification all ask this rather than each keeping a list of sources: the lists drifted, and a terminal hand-off drew in the dock while its seat read as running (#2954).

The order is fixed. The hand-offs lead because each parks in a registry of the workspace’s own rather than in the session’s pending set, so a seat with no domain can still be holding one; then the question outranks the permission prompt beside it, because both hold a turn and the question is the one a person has to read; then the remaining registries, the Slack draft first.

The row names the strongest ask; the dock draws its own queue. Self::pending_asks fronts a draft because a card is what a person reads first, while this answers one kind - so a seat holding a Slack draft and a document hand-off names the hand-off on its row and shows the draft at the front of the dock.

Source

pub fn pending_interaction_depth(&self, slot: &SessionSlot) -> usize

How many asks the session at slot is holding, zero when none. The same ledger Self::pending_interaction reads, summed, so a view showing one of them can say how many wait behind it.

Source

fn pending_ask_ledger( &self, slot: &SessionSlot, ) -> [(PendingInteractionKind, usize); 8]

Every ask source, in Self::pending_interaction’s precedence order, with the number of asks each holds.

One list feeds the kind and the depth, so a source cannot be counted without being answered or answered without being counted. The guard test parks one ask of every kind and reads both back.

Lock order: each registry read takes and releases its own lock, and the domain guard is off before the registry reads after it, so no path here holds a registry lock and the domain lock together.

Source

pub(crate) fn note_ask(&self, slot: &SessionSlot)

Note that an ask arrived for slot.

The number is the ask’s identity on the wire. A client announces a new ask by it, so a re-read of the roster is not news, an ask replacing another at the same depth is, and an answer that shrinks a batch is not.

Source

pub fn pending_request_seq(&self, slot: &SessionSlot) -> u64

How many asks have ever arrived for slot, zero when none has.

Source

pub(crate) fn record_fatal_error(&self, error: AppError)

Record a fatal error, so a view that was not attached when it fired can still read it.

Source

pub fn last_fatal_error(&self) -> Option<AppError>

The last fatal error, or None when nothing has failed fatally.

A fatal error is App-level: it names the startup that could not happen rather than a seat, and it arrives once. Without this the only way to know is to have been subscribed when it went out.

Source

pub fn service_status(&self) -> Option<ServiceIssue>

The statuspage’s last answer, or None.

None covers both “every relevant component is operational” and “the statuspage could not be reached”, which is the fetch’s own contract rather than something invented here - and a view that draws nothing for it is drawing the answer, not a gap.

The core probes rather than the view because the answer is the same for every viewer and because it must outlive any one of them: a client that attaches later would otherwise never see a service status at all.

Source

fn start_service_status_probe( &self, prober: Box<dyn Fn() -> Pin<Box<dyn Future<Output = Option<ServiceIssue>> + Send>> + Send + Sync>, )

Kick off the statuspage probe. Idempotent, and off the boot path.

Source

pub fn pending_asks(&self, slot: &SessionSlot) -> Vec<PendingAsk>

What the seat at slot is holding - a draft leads, then arrival order - empty when it is holding nothing. Every hop keeps that order, so a re-read and the live folds cannot disagree about the front, with one exception: two drafts held at once - and two configuration writes held at once - come back in the registry’s own order, because the registry keeps none to offer.

The list rather than one ask, because a parallel batch parks several at once and a view that attached mid-batch has no other way to read the ones behind the front: the stream is a mirror with no backlog, and the request beside the answer’s oneshot is what is left. A view draws the front.

A draft leads because it is parked in its own registry rather than in the session’s pending set, so a seat with no domain can still hold one.

Source

pub fn worker_activity(&self, entry: &WorkerEntry) -> SessionLifecycleState

What entry’s session is doing right now. The two liveness states that need no session of their own answer here; everything else is Self::session_activity.

Call this with no worker lock held - it reaches for domain_handles and then the DomainSession.

Source

pub fn spawn_failure(&self, slot: &SessionSlot) -> Option<String>

Why the last spawn or connection for slot failed, when it has not come up since. None for a slot that is running, was never tried, or recovered.

Source

pub(crate) fn record_spawn_failure(&self, slot: &SessionSlot, message: &str)

Record that slot’s spawn or connection failed, and why. Called before the failure releases the session, because the release is what takes the rest of the evidence with it.

Source

pub(crate) fn clear_spawn_failure(&self, slot: &SessionSlot)

Clear slot’s recorded failure: a session for it came up. Called from the Connected arm, which is the path production takes - the spawn entries hoist a domain straight into domain_handles rather than registering one, so clearing on registration would never run.

Source

pub(crate) fn record_death( &self, slot: &SessionSlot, occupant: SessionId, reason: &str, )

Record that the occupant running on slot died, with the reason it left, keeping both until a reader has been shown the seat and the recovery has announced itself. The lifecycle is NOT held down by this: a recovering connect clears the failure as it always did, and what rides on until shown is the death - died on the row, with the reason and the mark the client draws from it.

Source

pub(crate) fn unannounced_death(&self, slot: &SessionSlot) -> Option<SessionId>

The died occupant recorded for slot when the recovery has not announced it yet, consuming that one announcement: the notice fires on the first connect that answers the death and stays quiet after.

The spend does not consume it. A reader who opens the crashed seat’s page spends the row’s trace and then wakes the seat, so the connect that answers their own click is the first one to see the death - and it is owed the sentence like any other.

Source

pub fn death_stands(&self, slot: &SessionSlot) -> bool

Whether a death stands unshown on slot: the row carries died with its reason while one does, so a crash nobody watched is still readable when someone looks. pub because the row a view reads carries it.

Source

pub fn death_reason(&self, slot: &SessionSlot) -> Option<String>

The reason a standing death left, for the row to draw beside its mark. None once a reader has been shown the seat.

Source

pub fn set_heal_progress( &self, slot: &SessionSlot, attempt: u32, next_at: SystemTime, )

Record that slot is between heal attempts: which attempt is next and when it fires. Written by the healing supervisor and read by every row that draws the seat.

Source

pub fn clear_heal_progress(&self, slot: &SessionSlot)

Forget slot’s heal progress: an occupant connected, or the seat gave up and its row now draws the failure instead.

Source

pub fn heal_progress(&self, slot: &SessionSlot) -> Option<HealProgress>

Where slot’s heal has got to, or None when the seat is not restarting.

Source

pub fn heal_attempts(&self) -> Option<u32>

How many restarts a heal may make, from the current config: the row says restarting (attempt 1 of 3) from this.

Source

fn healing(self: &Arc<Self>) -> Arc<Healing> ⓘ

The supervisor, built on first use.

Source

pub(crate) fn note_seat_death( self: &Arc<Self>, slot: &SessionSlot, diagnostic: &str, )

A seat that had been up died: hand the death to the supervisor. A resume’s new occupant that never came up is a death here too - the attempt failed rather than a first spawn being surfaced.

Source

pub(crate) fn note_seat_connected(self: &Arc<Self>, slot: &SessionSlot)

A session came up on slot, which cancels any scheduled heal.

Source

pub(crate) fn mark_worker_restarting( &self, project_key: &ProjectKey, slot: &SessionSlot, )

slot’s worker is between heal attempts: its row draws Restarting, and the label is free for the resume.

Source

pub fn spend_death(&self, slot: &SessionSlot) -> bool

A reader has been shown the death on slot - the seat’s page was opened: the row’s trace goes (whether or not the recovery has announced itself yet), and the answer says whether there was one to spend. pub because the seat being shown is the server’s to see, and this is the spend the server makes on the view’s behalf.

Source

pub fn session_activity(&self, slot: &SessionSlot) -> SessionLifecycleState

What the session at slot is doing right now - the axis WorkerLiveness does not answer, since it stops moving once the worker connects. Reachable without a WorkerEntry, which a project lead has none of.

A pending interaction outranks the turn it is blocking: Attention is the state a lead has to act on, and reporting the blocked worker as Running is what let the deadlock stay invisible.

Call this with no worker lock held - it reaches for domain_handles and then the DomainSession.

Source

pub fn worker_status_snapshot(&self, entry: &WorkerEntry) -> WorkerStatus

entry projected to the wire shape with activity derived. This is the agents__list projection; WorkerEntry::to_status is the event-path one that leaves activity unset.

Source

pub fn project_name_for_path(&self, cwd: &str) -> Option<String>

The forge.toml project NAME that owns cwd, matched by the project whose expanded path is an ancestor-or-equal of cwd (component-aware, longest wins), so a worker in a <project>/.claude/worktrees/<label> worktree resolves to its parent project. cwd is ~-expanded before the lexical match so a tilde form can’t miss the already-expanded ProjectView.path. None when cwd is blank or under no configured project. The Inspector stamps this NAME onto a tab’s UI bucket once (at Connect), then scopes SCHEDULES / GOTIFY by name rather than re-deriving the project every render tick from a stale cwd.

Source

pub fn project_name_for_resolved_path(&self, cwd: &str) -> Option<String>

The forge.toml project NAME that owns the RESOLVED cwd: the match Self::project_name_for_path makes, retried with both sides canonicalized when the lexical one misses. A spawn’s tree is canonical - the CLI derives its state directory from the canonical path - while a declared path may reach the same directory through an alias (/tmp for /private/tmp, a symlinked parent), and the lexical rule alone would miss: a crossing-into-another-project check that misses is the rule the check exists for.

Both sides resolve together or not at all, the way the sibling’s lexical match keeps them in one form - a per-side canonicalize would put an existing root on one side of the comparison and an uncanonicalized child on the other.

Source

fn project_identity_for_key( &self, project_key: &ProjectKey, ) -> Option<(String, String)>

The (org, project name) a project key names, when forge.toml still declares it. The sessions table is keyed by the pair while every worker site holds the key, so a worker’s row cannot be written or found without this translation.

Source

pub(crate) fn record_worker_row( &self, project_key: &ProjectKey, label: &str, id: &str, charter: &str, kick: Option<&str>, resume_kick: Option<&str>, interactive: bool, tree: WorkerTree, mcp_families: Option<&[String]>, ) -> Result<()>

Write label’s worker row: the id it runs under plus the re-spawn arguments its spawn stated. The row is the only thing a boot re-spawns from, so the charter, the kick, the interactive flag and the gitness its worktree derives from land here rather than staying in memory.

Err when the store is closed, the project is no longer configured, or the write failed, so the caller can warn the lead that this worker will not survive a restart.

Source

pub(crate) fn recorded_worker_is_git_repo( &self, project_key: &ProjectKey, label: &str, ) -> Result<Option<bool>>

The git flag the worker’s row records, or Ok(None) when there is no row to read or its row predates the field. The spawn reads it in preference to probing the project path, so the cwd it builds is the one the launchpad already checked.

A read failure is an Err, not an absent row: an absent row means the caller may probe, while an unreadable one means the answer is unknown and the caller should say so rather than quietly compose a different directory. Distinct from Self::stored_worker_row, which reports both the same way because its callers act on the row itself rather than on a field of it.

Source

pub(crate) fn recorded_worker_mcp_families( &self, project_key: &ProjectKey, label: &str, ) -> Result<Option<Vec<String>>>

The MCP-family selection the worker’s row records, or Ok(None) when there is no row or its row names none (which resolves to every family). Same failure contract as Self::recorded_worker_is_git_repo: an unreadable row is an Err, not an absence.

Source

pub(crate) fn delete_worker_row( &self, project_key: &ProjectKey, label: &str, ) -> Result<bool>

Delete a worker’s persisted row so it never re-spawns. The row is the only thing that brings one back, so a delete that cannot land is an error rather than a warn.

Ok reports whether a row was there to remove. Err means the row could not be addressed or the delete failed - which is not the same answer as an absent row, so spawn::handle_despawn_worker reports the failure rather than NotFound when it gets one.

Source

pub(crate) fn worker_rows_for_project( &self, project_key: &ProjectKey, ) -> Vec<SessionRecord>

Every persisted worker row for project_key, the lead’s own row excluded. Empty when the DB isn’t open or the read fails. Backs the lead-reconnect re-spawn merge and the durable-identity read.

Source

pub(crate) fn worker_rows_for_project_strict( &self, project_key: &ProjectKey, ) -> Result<Vec<SessionRecord>>

The project’s durable worker rows, with a store failure surfaced rather than swallowed: a caller deciding whether a worktree path is free refuses on an unknown.

Source

pub fn worker_labels_by_project(&self) -> HashMap<ProjectKey, Vec<String>>

Every persisted worker label the launchpad still offers, keyed by the project key it holds. A row the boot wave would not start is not among them, unless a live worker holds it as Failed: that row is the failure’s own record, and its reason has to be drawable.

The launchpad’s worker rows read this once per frame, which is what the shape is for: one read transaction answers every project row, over a projection that skips each row’s charter. Unlike Self::list_live_workers it answers before the project has launched, which is when the launchpad renders.

Empty on a read failure rather than surfacing it, deliberately unlike the sibling stored_worker_row: the caller is a render path, where a warn plus a bare row beats failing the frame.

Source

pub(crate) fn stored_worker_row( &self, project_key: &ProjectKey, label: &str, ) -> Result<Option<SessionRecord>>

The row label holds in project_key. Distinct from Self::worker_rows_for_project, which swallows a read failure as empty: this surfaces the error (and treats a missing store as one) so the cron fire router can tell “conclusively absent” from “could not read” and leave the cron on a hiccup.

Source

pub(crate) fn update_worker_row( &self, project_key: &ProjectKey, label: &str, charter: Option<String>, kick: Option<String>, resume_kick: Option<String>, mcp_families: Option<Vec<String>>, ) -> Result<bool>

Merge the supplied fields onto the worker row for (project_key, label), leaving a None field at its stored value. Returns whether a row existed; this never creates one, because a row is what makes a worker re-spawn on the next lead connect. Read and write share one store lock so a concurrent despawn cannot land between them and resurrect the row.

Source

pub fn scan_usage(&self) -> UsageReport

Scan the shared session-JSONL pool into a UsageReport for the usage subject. Query-style (a direct method, not a Command): reads the one real projects dir, refreshes the incremental per-file cache, and rolls the deduped summaries up into the four windows priced from the bundled table. Does blocking file IO, so callers run it off the UI thread.

Source

fn usage_summary_for(&self, path: &Path, tz: &Tz) -> Option<FileUsageSummary>

Cached summary for path when its mtime and size still match, otherwise re-parse (bucketing by tz) and refresh the cache. None when the file vanished between listing and parsing.

Source

fn load_usage_summary(&self, path: &str) -> Option<FileUsageSummary>

Source

fn store_usage_summary(&self, path: &str, summary: &FileUsageSummary)

Source

pub async fn refresh_pricing(self: &Arc<Self>) -> bool

Refresh the LiteLLM pricing cache when it is absent or older than a day, re-fetching immediately on a missed day. Fire-and-forget from a view: it fetches through the proxy-aware client and is a no-op on any network failure, so the last-good cache is kept. Returns whether a new price table was stored (so a caller can re-price without a redundant scan when nothing changed). The redb read and the ~1.6 MB write run on the blocking pool so the once-a-day fsync can’t stall a UI frame.

Source

fn store_fresh_pricing(&self, json: String) -> bool

Store freshly-fetched pricing json unless it parses to an empty table - a garbage 200 must not wipe a good cache. Returns whether it stored.

Source

fn load_pricing(&self) -> PricingTable

The cached pricing, or an empty table before the first fetch lands (a scan answered in the meantime reports tokens with a blank cost).

Source

fn load_cached_pricing(&self) -> Option<CachedPricing>

Read the cached pricing snapshot, warning (not swallowing) on a redb or decode error so a corrupt cache is diagnosable.

Source

fn pricing_is_fresh(&self) -> bool

Whether the cached pricing is younger than the daily refresh window; a missing or older cache is stale and re-fetched.

Source

fn store_pricing(&self, entry: &CachedPricing)

Source

pub(crate) fn canonical_model_for_session( &self, key: &SessionSlot, ) -> Option<String>

The canonical model forge stamped for key’s session: the project’s declared model. None when the session has no project registration or the project declares no model.

Source

pub(crate) fn declared_models_for_session( &self, key: &SessionSlot, ) -> Vec<AvailableModel>

The /model picker rows for a session: the declared models of the session’s org’s accounts, in pin order, deduped.

Source

pub fn install_db_for_test(&self, db: Db)

Install a redb store into a test workspace so the durable-vs- ephemeral persistence path is exercisable without Workspace::new.

Source

pub fn all_live_worker_session_keys(&self) -> Vec<SessionSlot>

Snapshot every live worker’s session_key across every project. Used for worker-key routing that excludes worker buckets from project-row click routing without depending on list_projects() for enumeration.

Source

pub fn insert_live_worker(&self, project_key: &ProjectKey, entry: WorkerEntry)

Insert a worker entry into live_workers[project_key]. remove_latest_worker resolves the single live match. The one-live-worker-per-label invariant is enforced by Self::insert_live_worker_if_label_absent; this raw push is for callers (tests, re-tag) that already own that guarantee.

Source

pub fn insert_live_worker_if_label_absent( &self, project_key: &ProjectKey, entry: WorkerEntry, cap: Option<usize>, ) -> Result<(), LiveWorkerRefusal>

Insert entry only if no live (non-Failed) worker already holds its label in project_key, and - when cap is Some - only if that project’s live worker count is under it. Holds live_workers.lock() across the label-check, the cap-check AND the push, so two genuinely-concurrent SpawnWorker dispatches (a reconnect re-spawn racing a manual agents__spawn, say) can’t both pass a check-then-insert window and fork two subprocesses onto one worktree or overshoot the cap. The label check precedes the cap check, so a duplicate-label spawn at the cap reports the collision. cap: None is the boot re-spawn exemption. Returns Ok(()) on insert. This is the sole enforcement point for the at-most-one-live-worker-per-label invariant and the per-project worker cap.

Source

pub(crate) fn mark_despawn_cleanup_pending( self: &Arc<Self>, project_key: &ProjectKey, label: &str, ) -> DespawnCleanupPending

Mark label’s despawn cleanup as in flight in project_key, and hand back the guard that releases it.

A despawn takes this BEFORE its teardown, under the live registry’s lock: from here until the cleanup ends the label must not read as free to a spawn, because the worktree that cleanup will delete is still on disk. Drop does the releasing, so a cleanup that returns early or panics cannot leave the label unspawnable for the run.

Source

pub(crate) fn despawn_cleanup_pending( &self, project_key: &ProjectKey, label: &str, ) -> bool

Whether label’s despawn cleanup is still running in project_key.

Source

pub fn count_live_workers(&self, project_key: &ProjectKey) -> usize

The project’s cap-relevant worker count: the same number insert_live_worker_if_label_absent enforces against, read for WorkerFacade::capacity.

Source

pub(crate) fn clear_failed_worker(&self, project_key: &ProjectKey, label: &str)

Drop any failed tombstone for label before a restore appends its own entry.

A failed spawn keeps its entry for a worker that has no worktree, so the failure stays on screen. That entry is not live, so the insert would append a second entry for the same slot, and the by-slot reads take the first match: the tombstone would move as the new session connects while the new entry stayed behind as a phantom. A restore supersedes the failure it replaces.

Source

pub fn remove_latest_worker( &self, project_key: &ProjectKey, label: &str, ) -> Option<WorkerEntry>

Remove the latest-spawned worker matching label from live_workers[project_key]. Returns the removed entry, or None when no match exists.

Source

pub fn remove_worker_by_session_key( &self, session_key: &SessionSlot, ) -> Option<(ProjectKey, WorkerEntry)>

Remove the worker whose session_key exactly matches across any project’s live_workers. Used by the async-spawn-failure path so concurrent same-label spawns don’t accidentally roll back the wrong entry (remove_latest_worker would peek the wrong one when two workers share a label). Returns the matched (project_key, entry) pair, or None when no entry matches.

Source

pub fn drain_live_workers(&self, project_key: &ProjectKey) -> Vec<WorkerEntry>

Drain every worker entry for project_key and return them in insertion order.

Source

pub fn worker_lookup_for_session( &self, session_key: &SessionSlot, ) -> Option<(ProjectKey, String, bool, bool, Option<PathBuf>)>

Locate (project_key, label, is_git_repo_at_spawn, needs_tag, cwd) for any worker matching session_key across every project’s live_workers. Used by the Connected handler in SessionTask::translate_event to decide whether a just- connected session is a worker (and what tag to write). is_git_repo_at_spawn lets the tag-write path route to the worktree-derived JSONL via worker_tag_dir in crate::mcp::workers::types, and needs_tag says whether that write has ever landed - the rollback reads it to tell a worker that never established itself from one that is simply re-tagging. cwd is the tree the spawn resolved, which every composed directory reads instead of the project’s path. None when the session is a lead (or not a worker at all).

Source

pub fn worker_tree_for_session( &self, session_key: &SessionSlot, ) -> Option<PathBuf>

The tree a live worker runs in when that is NOT its project’s own directory: the root its spawn resolved. None for a lead, for a worker in its project’s directory, and for a worker this cannot place - so a caller that gets one has a tree to name.

Source

pub(crate) fn worktree_collision( &self, path: &Path, own: &ProjectKey, own_label: &str, own_op: WorktreeOp, ) -> Option<String>

What a worker’s composed worktree path collides with, when it does: a declared project’s own checkout, or the worktree of a worker under another project with the same label.

The base is repository-global (#2460), so two projects of one repository - the main checkout and a linked worktree of it, or two linked worktrees - compose the same <main>/.claude/worktrees/ <label> and the same worktree-<label> branch for one label. The one-live-worker guard is per project, so without this the second spawn would run in the first worker’s tree and a despawn would remove it; a label naming a checkout parked under .claude/worktrees would do the same to that project. A crashed worker’s row still claims its path until the row is closed - refusing is the safe direction - and a durable row claims it even with no in-memory entry (after a restart, before that project’s lead connects). Callers refuse rather than reuse or remove. A separate forge process is not visible here.

Source

pub(crate) fn claim_worktree( &self, path: &Path, project_key: &ProjectKey, label: &str, op: WorktreeOp, ) -> Result<WorktreeClaimGuard, String>

Claim the composed worktree path for this (project, label) while a spawn’s ensure/insert or a despawn’s removal acts on it.

Re-entrant for the same owner: a resume’s pre-ensure claim and the core handler’s claim are one spawn. Refused when another project or label holds it, which is the repository-wide half of the one-live rule - the live registry is per project, and two projects of one repository compose the same path.

Source

fn claim_conflict( &self, composed: &Path, own: &ProjectKey, own_label: &str, own_op: WorktreeOp, ) -> Option<String>

What a composed path is claimed by, when a seat other than own/own_label on the SAME operation holds it. A different operation on the same owner and label is a conflict: a despawn must not enter a resume’s claim.

Source

fn claim_message(&self, claim: &WorktreeClaim, path: &Path) -> String

Source

pub(crate) fn worktree_held_elsewhere( &self, path: &Path, own: &ProjectKey, own_label: &str, ) -> Option<String>

What a composed path is held by, when it is held by anything other than a spawn of own/own_label: an in-flight claim of another owner or operation, or a live seat of ANY project (a same-project winner of a spawn race included). Used by a rollback deciding whether a tree it minted is still its own.

Source

fn live_worker_root( &self, project_key: &ProjectKey, cwd: Option<&Path>, ) -> Option<PathBuf>

The tree a live worker’s directory composes against: the one its spawn resolved, or the project’s own forge.toml path when the spawn stated none. None when the session is not a live worker, or its project is no longer loaded.

Source

pub fn cwd_for_session(&self, session_key: &SessionSlot) -> Option<String>

The working directory forge holds for session_key. Two sources, in order:

  1. The sessions catalog (session_cwd_for). Leads only: the boot scan hides worker-tagged sessions and the Connected handler skips the catalog mirror for workers, so a worker never has a row to read.
  2. The worker registry (live_workers via worker_lookup_for_session), composed by worker_tag_dir against the tree the worker’s spawn resolved - its own root when it has one, the project’s forge.toml path otherwise - the worktree for a git worker, that root itself otherwise. This is the authoritative source for every worker.

None leaves the caller to decide what an unknown cwd means: resume_cwd_for_slot hands claude an empty cwd, while the review MCP reports SessionCwdUnknown to the caller.

Source

pub fn document_roots_for_session( &self, session_key: &SessionSlot, ) -> Vec<PathBuf>

The extra document roots the seat’s project declares: its document_roots entries, ~ expanded at load. Empty for a project that declares none, and for a slot whose project is not loaded.

Source

pub(crate) fn resume_cwd_for_slot(&self, session_key: &SessionSlot) -> String

The cwd to pass claude --resume for the session at session_key: Self::cwd_for_session, or an empty string when forge holds none (pass through and let the bridge surface ConnectionFailed - the session can’t be resumed cleanly anyway).

claude --resume does NOT receive a --worktree flag (see SessionLaunchSettings::extra_args in forge-agent/src/client.rs - lead/resume paths leave extra_args empty), so the subprocess cwd is the ONLY signal claude uses to derive the JSONL location. Handing a git worker just its project root makes claude look under the project’s sanitised dir, miss the worker JSONL (which lives under the worktree’s sanitised dir), and exit with “No conversation found with session ID:” (#245 Layer B).

Source

pub(crate) fn project_root_for_key( &self, target: &ProjectKey, ) -> Option<PathBuf>

Look up a project root path by its ProjectKey. Searches the test overlay first (under cfg(test) / feature = "testing"), then config.projects. Returns None when no loaded project’s path canonicalises to the given key.

Used by Self::git_scan_cwd_for_session and Self::resume_cwd_for_slot to derive the project root from a worker’s project_key without depending on the worker’s cwd_raw value (which carries the project root for fresh spawns and the worktree path for resumed sessions - the two cases would otherwise need different composition logic).

Source

pub(crate) fn project_key_for_name(&self, name: &str) -> Option<ProjectKey>

The project whose path canonicalises to target. Consults the test overlay, like every other project lookup - a resolution that saw only config.projects would return None for a seeded project and make an absence assertion pass vacuously. The registry key for a project, resolved from its forge.toml name. None when the config no longer names it.

Source

pub(crate) fn project_for_key( &self, target: &ProjectKey, ) -> Option<LoadedProject>

Source

pub fn git_scan_cwd_for_session( &self, session_key: &SessionSlot, cwd_raw: &Path, ) -> PathBuf

Resolve the cwd a git-diff scan should run against for the session at session_key. Workers spawned in a git tree run inside claude’s --worktree <label> fork at <base>/.claude/worktrees/<label>/ (base the canonical root of the tree the worker’s spawn resolved - its own when it has one, the project’s forge.toml path otherwise), and the worker’s cwd_raw carries different values depending on lifecycle:

  • fresh spawn -> cwd_raw = <root> (the value claude sends in AgentEvent::Connected.cwd before it chdirs into the worktree),
  • resumed session -> cwd_raw = <base>/.claude/worktrees/<label> (claude chdirs before writing the first catalog row, so the resume path reads the worktree path back as cwd).

Anchor the composition on the worker’s own tree (via Self::worker_lookup_for_session + the internal live_worker_root lookup) rather than cwd_raw so both lifecycle states resolve to the same final path. For non- worker sessions (project leads), non-git workers, or workers whose root can’t be resolved, returns cwd_raw unchanged.

Source

pub(crate) fn handle_async_worker_spawn_failure( self: &Arc<Self>, session_key: &SessionSlot, message: &str, kind: SpawnFailureKind, ) -> bool

Handle an async worker-spawn failure. Two paths, branching on the classifier outcome (#245 Layer C):

  • Worktree-creation failure (the worker never actually started because git worktree setup failed): roll back the WorkerEntry (the worker doesn’t exist; the user-visible signal is a typed notice routed to the lead’s chat) and emit WorkerStatusChanged { Removed }. Mirrors the sync rollback in handle_spawn_worker.
  • Any other failure (resume against missing JSONL, generic dispatch error, claude subprocess exit, etc.): keep the WorkerEntry and transition it to WorkerLiveness::Failed with the first line of the message as the diagnostic. The Projects pane renders the worker as a red ✕ with a DIM sub-row carrying the diagnostic, so a stuck-Spawning-forever case becomes visible instead of silently disappearing.

The classifier uses is_git_repo_at_spawn + substring match against the bridge-wrapped message; see classify_worker_spawn_failure for the routing rules and the bridge-prefix contract.

Returns true when the caller IS a worker (so it knows the failure was consumed here - the existing ConnectionFailed emission still fires for the view side). false for lead sessions or any other non-worker, in which case the caller’s existing behaviour proceeds unchanged.

Source

pub(crate) fn fail_worker_without_worktree( self: &Arc<Self>, slot: &SessionSlot, ) -> bool

Mark a git worker Failed and release its session when the worktree its connect is about to run in is gone or unusable. Answers whether it did, so the first-connect branch can stop before it kicks the worker, announces it live or hands it parked prompts (#1593).

The path composes from the row’s own root, never the connect cwd: a resumed worker connects with its worktree as cwd, and composing from that doubled the path in the reason.

Source

pub(crate) fn apply_worker_tag_or_rollback( self: &Arc<Self>, session_key: &SessionSlot, session_id: &str, cwd: &str, minted_row: bool, )

Tag a worker session’s JSONL with forge:worker:<label> and transition its WorkerEntry from Spawning to Running. Called from SessionTask::translate_event on the worker’s first Connected.

cwd is the tree the worker runs in (the directory discriminator tag_session uses to find the right .jsonl when CONFIG_DIR hosts multiple project directories).

The tag-write races against claude CLI’s first JSONL write. For workers spawned with an initial_prompt, the file lands within ~100 ms-2 s of Connected, so a 30 x 100 ms retry loop reliably catches it. For idle-spawned workers (no prompt) claude doesn’t create the JSONL at all until the first user turn arrives later, so the retry will exhaust with Io(NotFound). That’s NOT a rollback condition - we keep the worker live (it’s fully functional from live_workers), transition to Running, mark the entry needs_tag = true, and emit StatusChanged. The opportunistic retry on first DeliverWorkerPrompt (see spawn::handle_deliver_worker_prompt) catches it once claude is processing the turn.

Other errors (permission denied, disk full, invalid UUID) still indicate the worker can’t be properly tracked on disk, so we roll back: remove from live_workers, release the session, emit a Removed status event.

The tag-write runs in a detached tokio task to keep translate_event synchronous.

Idempotent - calling twice for the same session_key is a no-op if the entry is already Running.

Source

pub(crate) fn apply_worker_tag_or_rollback_with_config_dir( self: &Arc<Self>, session_key: &SessionSlot, session_id: &str, project_key: &ProjectKey, label: &str, cwd: &str, is_git_repo_at_spawn: bool, needs_tag: bool, minted_row: bool, config_dir: &Path, )

Testable inner of Self::apply_worker_tag_or_rollback that takes the resolved config_dir directly. The production caller resolves via config_dir_for; unit tests pass a tempdir to exercise the retry / rollback / deferred branches without having to register a full AgentHandle.

cwd is the tree this worker runs in - the root its spawn resolved, which is the project’s forge.toml path for a worker that stated none. For git-repo workers (is_git_repo_at_spawn = true) the tag-write is routed to <base>/.claude/worktrees/<label> via crate::mcp::workers::types::worker_tag_dir, base the CLI’s main working tree, so the lookup matches where claude’s --worktree <label> actually wrote the JSONL.

needs_tag and minted_row together decide whether a rollback takes the worker’s durable row with it; see the arm below.

Source

pub(crate) fn apply_worker_tag_or_rollback_with_budget( self: &Arc<Self>, session_key: &SessionSlot, session_id: &str, project_key: &ProjectKey, label: &str, effective_cwd: &Path, facts: WorkerTagFacts, config_dir: &Path, budget: WorkerTagRetryBudget, )

Self::apply_worker_tag_or_rollback_with_config_dir with the worktree-aware tag directory and the retry budget both injected, so a test can shrink the detached task’s wall time and derive its own wait from the same numbers the task runs on.

Source

pub(crate) fn retry_worker_tag_opportunistic( self: &Arc<Self>, project_key: &ProjectKey, session_key: &SessionSlot, session_id: &str, label: &str, cwd: &str, is_git_repo_at_spawn: bool, )

Opportunistically retry the JSONL tag-write for a worker whose needs_tag flag is set. Called by spawn::handle_deliver_worker_prompt when a DeliverWorkerPrompt arrives for a worker that was spawned idle (no initial_prompt) and therefore had no JSONL at Connected. By the time the first turn fires, claude is writing the JSONL, so the tag-write should succeed within a turn or two.

On success: clear needs_tag and emit WorkerStatusChanged { StatusChanged }.

On failure (any kind): log warn, leave needs_tag = true so the next turn retries again. Never rolls back - the worker stays functional regardless of the tag’s on-disk state.

Source

pub(crate) fn retry_worker_tag_opportunistic_with_config_dir( self: &Arc<Self>, project_key: &ProjectKey, session_key: &SessionSlot, session_id: &str, label: &str, cwd: &str, is_git_repo_at_spawn: bool, config_dir: &Path, )

Testable inner of Self::retry_worker_tag_opportunistic that takes the resolved config_dir directly. Same shape as the _with_config_dir variant of apply_worker_tag_or_rollback, including the worktree-aware cwd routing for git-repo workers.

Source

pub fn refresh_context_usage( &self, key: &SessionSlot, ) -> Result<(), DispatchError>

Request a fresh context-usage snapshot for key. The bridge replies via SessionUpdate::ContextUsageSnapshot.

§Errors

Returns DispatchError::UnknownSession when no agent is registered for key (e.g., the session was just closed) or when the bridge hasn’t stamped a session_id yet.

Source

pub fn refresh_mcp_snapshot( &self, key: &SessionSlot, ) -> Result<(), DispatchError>

Request a fresh MCP server snapshot for key. The bridge replies via SessionUpdate::McpSnapshot.

§Errors

See Self::refresh_context_usage.

Source

pub fn store_process_snapshot( &self, key: &SessionSlot, snapshot: Option<ProcessSnapshot>, )

Record the OS walk of key’s process tree for views to read.

The walk itself belongs to whoever owns the tick that drives it, because a walk is a sysinfo refresh that costs tens of milliseconds and no read should pay for one. The answer belongs here, so a second view reads the tree rather than walking the OS again. None clears it: the walk described a subprocess tree that is gone.

No-op for a slot with no session: minting a domain for whoever asked would leave the workspace routing to a session nobody runs.

Source

pub fn process_snapshot(&self, key: &SessionSlot) -> Option<ProcessSnapshot>

The last OS walk of key’s process tree, or None when nothing has walked it or the tree it described is gone.

Source

pub fn project_memory_path( &self, key: &SessionSlot, cwd: &Path, ) -> Option<PathBuf>

Resolve the auto-memory path the bridge would consult for cwd, scoped to key’s configured account. Returns None when no agent is registered for key.

Source

pub fn settings_documents( &self, key: &SessionSlot, cwd: Option<&Path>, ) -> Option<SettingsDocuments>

Snapshot the bridge’s settings documents for key at cwd. Returns None when no agent is registered for key.

Source

pub fn user_preferences(&self) -> Option<Value>

The CLI’s per-user preferences document, from $HOME/.claude.json.

Read on each call rather than held, because the user can change a preference while forge runs.

Source

pub fn config_dir_for(&self, key: &SessionSlot) -> Option<PathBuf>

Resolve the agent’s configured config_dir for key. Returns None when no agent is registered for key.

Source

pub fn claude_pid(&self, key: &SessionSlot) -> Option<u32>

OS PID of the claude subprocess bound to key. Returns None when the session has no live client (pre-spawn / post-disconnect). The PID is stable for the lifetime of the subprocess, so consumers (e.g. the Inspector pane’s PROCESSES OS walk) can cache snapshots keyed off this value.

Source

fn agent_handle_for(&self, key: &SessionSlot) -> Option<Arc<AgentHandle>>

Borrow the Arc<AgentHandle> registered against key. Workspace-internal helper - surfaces a sometimes-None to keep the early-init / disconnected branches explicit.

Source

fn handle_and_session_id( &self, key: &SessionSlot, ) -> Result<(Arc<AgentHandle>, String), DispatchError>

Resolve (handle, session_id_string) for key. Both must be available; missing either surfaces as UnknownSession for uniform error handling.

Source§

impl Workspace

Source

pub fn register_domain_session( &self, key: SessionSlot, handle: Option<Arc<AgentHandle>>, ) -> Arc<Mutex<DomainSession>> ⓘ

Register a fresh DomainSession for key under this workspace. handle is None for pre-spawn / pre-Connect domains (filled in later when the spawn handler runs); Some for test fixtures that wire a stub handle up front.

DomainSession carries workspace-internal routing metadata (conn / session_id / pending_interactions). A view’s per- session operational state lives on the view side; the workspace does not read or write those fields.

Returns the inserted Arc<Mutex<DomainSession>> so callers can seed fields on the same handle they just registered.

Source

pub(crate) fn notice_undelivered_message( self: &Arc<Self>, sender: &SessionSlot, target: &SessionSlot, reason: PeerFailureReason, )

Tell a sender its message never landed.

The one delivery-ack path, and it is NOT outstanding state coming back: nothing is tracked between the send and this notice. A message parked for a sleeping project is dropped when the spawn it waited on fails, and the parked entry itself carries the sender, so the sender is told rather than left believing the words arrived.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
§

impl<T> Any for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

§

fn type_name(&self) -> &'static str

§

impl<T> AnySync for T
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send> ⓘ

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<S> FromSample<S> for S

§

fn from_sample_(s: S) -> S

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more