Skip to main content

Module spawn

Module spawn 

Source
Expand description

App-level spawn command handlers. Called from crate::Workspace::dispatch when an App-level Command::SpawnProject arrives. Each handler resolves the id the session will run under, emits SessionUpdate::Spawning under it, and kicks off the agent spawn. The matching Connected arrives later under the same key.

Re-exports§

use std::sync::Arc; 🔒
use std::time::Duration; 🔒
use std::time::SystemTime; 🔒
use forge_agent::client::SessionLaunchSettings; 🔒
use crate::mcp::gotify::types::GotifyNotification; 🔒
use crate::mcp::peers::types::WrappedPrompt; 🔒
use crate::protocol::Command; 🔒
use crate::protocol::PromptSource; 🔒
use crate::protocol::SessionUpdate; 🔒
use crate::protocol::WorkerSpawnReply; 🔒
use crate::protocol::WorkerStatusAction; 🔒
use crate::protocol::WorktreeDisposition; 🔒
use crate::target::ProjectKey; 🔒
use crate::workspace::DespawnCleanupPending; 🔒
use crate::workspace::LiveWorkerRefusal; 🔒
use crate::workspace::Workspace; 🔒
use crate::SessionSlot; 🔒
use crate::SessionTarget; 🔒
use std::fmt::Write as _; 🔒
use forge_primitives::slack::SlackMessage; 🔒

Structs§

DespawnCleanup 🔒
The despawn’s filesystem half, handed to the blocking pool.
WorkerSpawnArgs 🔒
The per-worker half of a spawn request, bundled so a caller cannot transpose kick with resume_kick - both are Option<String> and the mistake is silent.

Enums§

CronFireOutcome 🔒
Outcome of a cron fire’s delivery, so fire_due_crons can decide the entry’s fate rather than treating every hand-off as a success.
CronOwnerCheck 🔒
Outcome of checking whether a cron’s owner still exists to be woken.

Constants§

DEFAULT_LEAD_CHARTER 🔒
Charter every lead session is launched with.
REPLACED_CLI_TOOLS 🔒
The claude CLI’s tools forge has replaced with its own surfaces: the task quartet (mcp__forge__tasks__* over forge’s own store), the cron trio (forge’s durable crons), SendMessage / ListAgents (the agents__* group), Workflow (forge’s workers) and RemoteTrigger (cloud routines, which forge has no equivalent for and nothing here reaches for).
SECONDS_PER_DAY 🔒
TEARDOWN_WAIT 🔒
How long the despawn holds its worktree removal for the session task’s teardown signal before giving up and removing anyway: the SDK’s close-wait budget (the child’s reap) plus a margin for the task to observe its closed channel, disconnect and drop. An expiry is logged - it is the one shape where the removal goes ahead unprotected.

Functions§

answer_delivery 🔒
Answer a delivery’s respond channel, if the caller asked for one: Ok(()) once the prompt was accepted - handed to the target’s session queue, held for its turn end, or parked for its connect - and Err(reason) when nothing could take it. The sender’s ack is built from this answer alone, so it never claims a delivery that did not happen.
apply_blocked_tools 🔒
Deny every session - lead, resumed or worker - the tools forge has replaced, merging into a --disallowedTools value already there, so the flag is emitted once with every name in it rather than twice. A worker’s spawn is the case that carries one: its own denials already hold that entry. Called from the one spawn path every session goes through and from the respawn stamp, so no launch can be assembled without it.
apply_lead_charter 🔒
Stamp DEFAULT_LEAD_CHARTER onto the launch settings so every lead session carries one. No-op when a charter is already set - worker spawns supply their own and we never overwrite it.
blocked_tools_for 🔒
The --disallowedTools value for a session, comma-separated - the form the CLI’s variadic <tools...> parser accepts.
buffer_prompt_until_connected 🔒
Handle a Command::DeliverWorkerPrompt: route a wrapped peer-style envelope to the worker matching target_label in the caller’s project. Latest-spawned-wins on duplicate labels. The typed PeerEnvelopeAppended echo follows the same pattern as handle_deliver_peer_prompt - workers reuse the peer envelope verbatim so a view’s chat render is identical between the two paths.
build_worker_extra_args 🔒
Build the list of (flag, value) extra CLI args specific to a worker spawn. When the project is a git repo, append ("worktree", Some(label)) so the spawned claude subprocess creates a worktree at <base>/.claude/worktrees/<label>/ - base the repo’s canonical root, the main checkout for a project that is itself a linked worktree - and runs the session inside it. In all cases, append the worker’s blocked_tools_for set as its --disallowedTools entry: workers are pinned to their spawn-time location (whether a worktree or the project cwd) and must not be able to call claude’s built-in worktree-hop tools to escape. Comma-separated value form is empirically accepted by the CLI’s variadic <tools...> parser.
charter_with_withheld_families 🔒
The worker’s prompt charter: the stored text plus one line per withheld MCP family. The row keeps the RAW charter - these lines are composed per spawn from the current selection, so they can neither duplicate across re-spawns nor outlive a revision.
cron_slot_exists 🔒
Whether a cron’s slot still has a session that can be woken. A lead’s does whenever its project does; a worker’s does while its label has a row AND that row can still start, since the boot wave is what re-spawns it. A read failure yields CronOwnerCheck::Unknown so the fire router leaves the cron rather than deleting a live slot’s cron on a transient hiccup.
deliver_cron_prompt 🔒
Deliver a due cron’s prompt into its OWNER’s session as a plain user turn AND echo it as a cron block. team_role None routes to the project lead, Some(label) to that worker. A live owner gets a Command::Prompt; an asleep-but-existing owner is woken by dispatching Command::SpawnProject (which resumes the lead and, through the lead’s respawn, the worker), the prompt buffered by (project, team_role) and drained on the owner’s connect. An owner that no longer exists (a gone project, or a worker label with no persisted row) yields TargetGone.
deliver_gotify_message 🔒
Deliver a matched Gotify notification into project as a plain user turn AND echo it into the target’s chat as a notification block. When team_role names a running team worker, deliver straight to it; otherwise deliver to the project lead - echo + dispatch a Command::Prompt if it’s running, else park it for the target’s slot and dispatch Command::SpawnProject (SessionTask drains + echoes on Connected via deliver_parked_gotify). A team-worker subscription with NO live entry falls through to lead delivery (spawning the project brings the team up); a live-but-not-yet-connected worker parks for its own label instead. A project no longer in forge.toml is logged and skipped.
deliver_slack_message 🔒
Deliver one conversation’s matched Slack messages to its subscriber’s session: dispatch them now when that session is running, buffer them on the session’s own domain when it is still spawning. Mirrors deliver_gotify_message, including its rule that a worker-owned subscription falls through to the lead only when the worker is gone entirely: the despawn race (teardown removed its subscriptions first), or a Failed entry, which is kept by design and is not live - a bucket parked for one would never drain. The fall-through commits under the worker’s own dedupe key, so the batch counts as delivered and stays with the lead: a durable worker that later respawns does not re-receive it.
delivery_hold_signal 🔒
The turn signal a delivery is held on: only what a Result clears.
dismiss_failed_worker 🔒
Close a failed worker, or one whose failure is all that is left of it: take the tombstone entry when one survives, clear the slot’s failure record, and announce the release so views re-read the row without it.
drain_turn_held_for_slot 🔒
Deliver everything held for slot while a turn was in flight, in the order it arrived. Called where the turn’s Result lands, so each held prompt starts a turn of its own instead of folding into the one that was running. A prompt the seat cannot take now - the session died between the hold and the turn end - is told to its sender rather than dropped, which is the whole point of holding it.
emit_worker_removed 🔒
Announce a torn-down worker to the views, carrying what became of its worktree.
handle_close_worker 🔒
Handle a Command::CloseWorker (a view’s per-row X-button): tear the worker down via teardown_worker. Does NOT touch the git worktree - that’s the agents__despawn path’s job; the X-button’s behavior is intentionally unchanged.
handle_deliver_peer_prompt 🔒
Handle a Command::DeliverPeerPrompt. Resolves the target project to a running SessionTask (deliver immediately) or a sleeping one (buffer + auto-spawn), then dispatches the wrapped prompt as a regular Command::Prompt.
handle_deliver_worker_prompt 🔒
handle_deliver_worker_prompt_to_lead 🔒
Handle a Command::DeliverWorkerPromptToLead: route a wrapped peer-style prompt from a worker back to its lead, addressed by the lead’s SessionSlot (resolved at Tool dispatch from the worker’s spawned_by_session_id). Same wire shape as DeliverWorkerPrompt - PeerEnvelopeAppended echo + Command::Prompt dispatch - so a view renders the lead’s message identically to a sibling-worker delivery.
handle_despawn_worker 🔒
Handle a Command::DespawnWorker (the agents__despawn MCP tool): the lead’s clean-close gesture. Unlike handle_close_worker it also cleans up the worker’s git worktree.
handle_despawn_worker_within 🔒
handle_despawn_worker with the teardown wait injected, so a case can drive the expiry without sitting out the production budget.
handle_heal_project_spawn 🔒
The heal’s resume: the same spawn as handle_spawn_project, but it cannot open a seat the reader closed. A heal recovers a death; it is never an opening door.
handle_spawn_project 🔒
Resolve the id the project’s lead will run under, emit SessionUpdate::Spawning under it, then spawn the agent. The Connected event from the resulting SessionTask lands under the same key, so the announced bucket is the one the child connects under. A spawn that fails names the reason, which the heal’s own supervisor counts as a failed attempt; a command dispatch drops it.
handle_spawn_worker 🔒
Handle a Command::SpawnWorker: insert a Spawning worker entry in live_workers[project_key], dispatch a spawn for the id the worker will run under (or the id being resumed) with the charter threaded onto SessionLaunchSettings, then reply on return_to with that id and the tag value. The Connected handler in session_task::translate_event writes the actual JSONL tag row and transitions the entry from Spawning to Running (or rolls back on tag-write failure).
in_thread_order 🔒
The bundle’s members in reading order: oldest first, with a thread’s replies directly under the parent they answer.
known_project_refusal 🔒
The refusal for a spawn whose tree resolves to a project forge.toml declares (D3). One source: the facade refuses with it before it ensures anything, and the handler refuses with it as the shared core both dispatch sources pass through.
live_cron_slot 🔒
The live session that fills the cron’s slot in view: for a lead cron the running lead (the first open session that is not a live worker); for a worker cron the live worker with that label. None when no session holds the slot.
member_line 🔒
One bundle member. The <author>: clause is what forge_server::envelope::detect_inbound keys on, so a member always carries one. A resolved name is preferred; the raw id is only a fallback the block drops, since an id is never a name to print.
missed_cron_text 🔒
Text delivered for a cron fire: the raw prompt, prefixed with a plain marker when the fire is overdue so the owner can tell a catch-up from an on-time fire.
push_cron_prompt_into_chat 🔒
Emit a typed CronPromptAppended so the target session’s chat buffer shows a cron block for the fired prompt. Mirrors push_gotify_notification_into_chat - the target’s claude subprocess still receives the raw prompt via a separate Command::Prompt; this only drives the visible chat echo.
push_gotify_notification_into_chat 🔒
Emit a typed GotifyNotificationAppended so the target session’s chat buffer shows the inbound notification block. Mirrors push_peer_user_turn_into_chat - the target’s claude subprocess still receives the prose via a separate Command::Prompt dispatch; this only drives the visible chat echo.
push_peer_user_turn_into_chat 🔒
Emit a typed PeerEnvelopeAppended so the target session’s chat buffer shows the inbound peer user-turn. A view’s reducer builds the chat-side echo directly from the WrappedPrompt’s typed fields - workspace no longer forges an SDK Message::User frame (audit I11).
push_slack_message_into_chat 🔒
Emit a typed SlackMessageAppended so the target session’s chat buffer shows the inbound Slack block. Mirrors push_gotify_notification_into_chat - the target’s claude subprocess still receives the prose via a separate Command::Prompt dispatch; this only drives the visible chat echo.
reap_branch_and_reviews 🔒
Reap the worker’s branch and, once that is gone, the review state keyed to it. Returns the warning a kept branch produces, which the caller’s result carries.
reap_worker_branch 🔒
Reap the worktree-<label> branch claude creates for a worker’s worktree, which outlives the worktree itself. Returns a warning when the branch was left in place, None when it was deleted or never existed.
send_dispatch_turn_error 🔒
A failed prompt dispatch to a running target unwinds the echo’s already-opened turn: without this its bar counts forever and the bucket spinner never drops (mirrors the typed-submit compensation). Per-site warns stay at the call sites.
slack_bundle_to_prose 🔒
The user-turn prose for one conversation’s delivered Slack messages. It carries the ids a reply needs - conversation, each member’s own ts, and its thread - because the only way an agent can answer in place is to feed those back to slack__post or slack__edit. forge_server::envelope::detect_inbound parses this shape into a Slack envelope and a view renders that as a block, so the bracketed header, its optional age clause, its member count and the one-line-per-member body are a contract with the parser.
spawn_project_inner 🔒
spawn_prompt_charter 🔒
The charter a spawned worker’s prompt carries: the stored text plus the withheld-family lines for the EFFECTIVE set - the same config gate the tool build applies, so the prompt and the tools cannot disagree about a family.
stale_age_clause 🔒
The , 8d old clause a header carries when its newest message is at or past the workspace’s stale age. Zero turns the mark off; a future or unparseable ts carries none.
stamp_permission_mode 🔒
Stamp the project’s permission_mode into the launch settings’ permissions.defaultMode, where the existing applied_permission_mode arm in forge_sdk_worker picks it up; overrides a launcher-supplied default.
sweep_browser_seats 🔒
Tell the browser fleet which seats remain: the seat is down, so its tabs go with it, except any a parked hand-off holds. The sweep runs off the teardown path, which is reaping a child and can take minutes; a fleet that is not installed is a workspace with no browsers to sweep. The snapshot carries its capture generation, so the fleet drops a sweep that arrives after a newer capture.
team_worker_key 🔒
The team worker labelled label in project, if a LIVE entry exists - regardless of whether it has finished connecting. The caller checks connectedness to decide dispatch-vs-buffer.
teardown_worker 🔒
Shared worker teardown used by both handle_close_worker (a view’s X-button) and handle_despawn_worker (the agents__despawn MCP tool): remove the latest-spawned worker matching label from live_workers[project_key] and release its session (terminates the claude subprocess on drop). Returns the removed WorkerEntry, or None when no live worker matched. JSONL on disk is NOT deleted - teardown only removes the in-memory live state.
try_emit 🔒
Emit a SessionUpdate and log at debug when no subscriber took it (the view has not attached yet, or has gone). The send is logically best-effort - no caller can act on the failure - but visibility in the log distinguishes “no view took it” from “the emit never happened” during diagnosis.
warm_browser_seat 🔒
Warm the seat’s browser profile: create the entry and start its host, with no tab. Best-effort, off the spawn’s own path: a profile whose host cannot start is logged by the fleet rather than failing the spawn.
warn_teardown_wait_expired 🔒
The removal went ahead without the ordering: the session’s teardown did not answer within the wait. Warned rather than silent - it is the one shape where the despawn’s protection is absent.
worker_limit_reached_message 🔒
The caller-facing refusal for an at-cap worker spawn. One source so the classifier pin in the facade tests tracks the real text.
worktree_dirty_verdict 🔒
The despawn’s dirty verdict, taken off the runtime’s worker: git status walks the tracked tree, and the verdict gates the teardown, so the caller cannot await a spawn_blocking handle. block_in_place hands the worker’s tasks to another thread for the call; a current-thread runtime has no other thread, and stays inline. A LocalSet is not detected - tokio still reports MultiThread there, where block_in_place panics - and the workspace runs none.