Runtime Flow
This guide documents Agentty's runtime data flows end to end: the foreground event loop, reducer/event buses, session-worker turn execution, merge/rebase/sync orchestration, and every background task with trigger points and side effects.
Architecture Goals🔗
Agentty runtime design is built around these constraints:
- Keep domain logic independent from infrastructure and UI.
- Keep long-running or external operations behind trait boundaries for testability.
- Keep runtime event handling responsive by offloading background work to async tasks.
- Keep AI-session changes isolated in git worktrees and reviewable as diffs.
- Decouple agent transport (CLI subprocess vs app-server RPC) behind a unified channel abstraction.
Workspace Map🔗
| Path | Responsibility |
|---|---|
crates/ag-forge/ | Shared forge review-request library crate for normalized review-request types, GitHub remote detection, and gh adapter orchestration. |
crates/agentty/ | Main TUI application crate (agentty) with runtime, app orchestration, domain, infrastructure, and UI modules. |
crates/ag-xtask/ | Workspace maintenance commands (migration checks, workspace-map generation, automation helpers). |
docs/site/content/docs/ | End-user and contributor documentation published at /docs/. |
Main Runtime Flow🔗
Primary foreground path from process start to one event-loop cycle:
flowchart TD
main["main.rs"]
db["Database::open()<br/>sqlite open + WAL + foreign keys + migrations"]
app_new["App::new()"]
scan["Startup-only home-directory project scan<br/>then project/session snapshot load"]
fail_ops["Fail unfinished operations from previous run"]
background["Spawn app background tasks"]
runtime["runtime::run(&mut app)"]
terminal["terminal::setup_terminal()"]
event_reader["event::spawn_event_reader()<br/>dedicated OS thread"]
main_loop["run_main_loop()"]
drain["process_pending_app_events()<br/>reduce queued AppEvent values"]
draw["ui::render::draw()"]
process["event::process_events()"]
key_events["Key events<br/>mode handlers -> app/session orchestration"]
app_events["App events<br/>App::apply_app_events reducer"]
tick["Tick<br/>refresh_sessions_if_needed safety poll"]
main --> db
main --> app_new
app_new --> scan
app_new --> fail_ops
app_new --> background
main --> runtime
runtime --> terminal
runtime --> event_reader
runtime --> main_loop
main_loop --> drain
main_loop --> draw
main_loop --> process
process --> key_events
process --> app_events
process --> tickrun_main_loop()drains queued app events before draw so touched sessions sync from their live handles without a full list-wide sweep every frame.process_events()waits on terminal events, app events, or tick (tokio::select!).- After one event, it drains queued terminal events immediately to avoid one-key-per-frame lag.
- Tick interval is
50ms; metadata-based session reload fallback is5s(SESSION_REFRESH_INTERVAL).
Data Channels🔗
Agentty uses four primary runtime data channels:
Terminal Event channel (runtime/event.rs)🔗
- Producer(s): Event-reader thread
- Consumer(s):
runtime::process_events() - Payload:
crossterm::Event - Purpose: User input and terminal events.
App event bus (AppEvent)🔗
- Producer(s): App background tasks, workers, and task helpers
- Consumer(s):
App::apply_app_events()reducer - Payload:
AppEventvariants - Purpose: Safe cross-task app-state mutation.
Turn event stream (TurnEvent)🔗
- Producer(s):
AgentChannelimplementations - Consumer(s): Session worker
consume_turn_events() - Payload: Loader-thought and pid updates
- Purpose: Transient loader updates and PID updates while final transcript output waits for the completed turn result.
Session handles (SessionHandles)🔗
- Producer(s): Workers and session task helpers
- Consumer(s):
App::apply_app_events()viaSessionUpdated-drivensync_session_from_handle()calls - Payload: Shared
Arc<Mutex<...>>output, status, and pid handles - Purpose: Fast targeted snapshot sync without a full DB reload.
App Event Reducer Flow🔗
App::apply_app_events() is the single reducer path for async app events.
Flow:
- Drain queued events (
first_event+try_recvloop). - Reduce into
AppEventBatch(coalesces refresh, git status, model, and loader-thinking updates). - Apply side effects in stable order.
Reducer behaviors that matter for data flow:
RefreshSessionssetsshould_force_reload, which triggersrefresh_sessions_now()andreload_projects().reload_projects()now reloads only persisted project rows; the expensive home-directory repository discovery pass runs only duringApp::new().BranchPublishActionCompletedswaps the session-view popup from loading to success or blocked/failure copy after the session-viewpreview-request publish flow finishes.ReviewRequestStatusUpdatedpersists refreshed forge summaries for review-ready sessions and silently transitions externally merged sessions toDoneor externally closed sessions toCanceled.SessionUpdatedmarks touched sessions so reducer can callsync_session_from_handle()selectively.SessionProgressUpdatedrefreshes transient loader text used by the session view.AgentResponseReceivedroutes question-mode transitions for active view sessions and applies the worker's reducer-ready turn projection (summary, questions, token deltas) to the currently loaded session.PublishedBranchSyncUpdatedtracks detached post-turn auto-push state for already-published session branches so stale background completions do not overwrite newer sync attempts in the active session view.- After touched-session sync, terminal statuses (
Done,Canceled) drop per-session worker senders so workers can shut down runtimes.
Session Chat Rendering Flow🔗
The session chat panel is rendered by crates/agentty/src/ui/page/session_chat.rs and crates/agentty/src/ui/component/session_output.rs. The page chooses which session is visible and which auxiliary view state applies; the component turns that state into the exact lines printed inside the bordered output panel.
Data Origins🔗
The printed session-chat data comes from these sources:
session.outputLoaded with the session row incrates/agentty/src/app/session/workflow/load.rs, then kept hot from the per-session handle viacrates/agentty/src/app/session_state.rs. Runtime workers append new transcript text throughSessionTaskService::append_session_output()incrates/agentty/src/app/session/workflow/task.rs, which updates both the in-memory handle buffer and the persisted database row.active_prompt_outputCached bySessionManager::set_active_prompt_output()incrates/agentty/src/app/session/core.rswhen a start or reply prompt is submitted. This stores the exact prompt-shaped transcript block that was just appended tosession.outputsoSessionOutputcan split the transcript into completed-turn content and the currently active turn without reparsing generic prompt-looking lines from assistant output.session.summaryPersisted by the turn worker incrates/agentty/src/app/session/workflow/worker.rsas the raw protocolsummarypayload.App::apply_agent_response_received()incrates/agentty/src/app/core.rsnow applies that same raw payload to the in-memory session snapshot immediately, andcrates/agentty/src/ui/component/session_output.rsrenders the synthetic summary block fromsession.summaryinstead of storing a second markdown copy insidesession.output. For merged/done flows, merge helpers can rewrite the stored value into a display-oriented markdown form before the next reload.session.questionsPersisted by the worker alongside the latest turn metadata and applied immediately by the reducer, but these items do not render insideSessionOutput. Instead they driveAppMode::Question, where the bottom question panel renders the current question and its options/input while the transcript panel remains visible above it.review_status_messageandreview_textStored onAppMode::Viewand its restore-view variants. Review mode is opened fromcrates/agentty/src/runtime/mode/session_view.rs, which either reuses a cached review, shows a loading message, or starts a review-assist task. That task emitsReviewPrepared/ReviewPreparationFailed, andApp::apply_review_update()incrates/agentty/src/app/core.rswrites the resulting text or error/status message back into the active view mode.active_progressSourced fromApp::session_progress_message()incrates/agentty/src/app/core.rs. Session task helpers emitAppEvent::SessionProgressUpdatedfromcrates/agentty/src/app/session/workflow/task.rs, the reducer batches those updates, and session view mode reads the latest message before rendering.done_session_output_modeStored onAppMode::Viewand related restore-view snapshots incrates/agentty/src/ui/state/app_mode.rs. This mode does not change what is persisted; it only changes whether the panel uses the summary as its primary body (Summary) or uses transcript output as the primary body (OutputandReview).
Output Assembly Diagram🔗
SessionOutput does not render one flat stored message list. Instead it assembles the panel from the persisted transcript plus synthetic metadata and transient view state:
flowchart TD
render["SessionChatPage render_session()"]
lines["SessionOutput output_lines()"]
render --> lines
base["Choose base body text"]
lines --> base
base --> draft["Draft preview<br/>(draft New session)"]
base --> done_summary["Rendered summary markdown<br/>(Done + Summary mode)"]
base --> transcript["session.output<br/>(all other modes and statuses)"]
split["Split transcript with active_prompt_output"]
completed["Completed-turn text"]
active["Active-turn text"]
lines --> split
split --> completed
split --> active
completed --> spacing["Normalize prompt spacing"]
spacing --> footer_split["Peel trailing footer block"]
footer_split --> footer_kind["Commit footer or commit error footer"]
footer_split --> completed_md["Render completed-turn markdown"]
completed_md --> summary["Append synthetic session.summary block"]
summary --> footer["Append trailing commit footer"]
footer --> active_prompt["Append active-turn prompt block"]
active_prompt --> review["Append focused review markdown<br/>(review_text)"]
review --> branch_sync["Append published-branch sync row<br/>(auto-push started, completed, or failed)"]
branch_sync --> final_row["Append final status row<br/>or t toggle hint"]This means session.output stays the durable transcript, while summary and focused review are layered on during render instead of being appended back into that transcript string.
App owns one shared MarkdownRenderCache and threads it through RenderContext, the router, overlay restore state, and SessionChatPage so repeated transcript blocks can reuse rendered markdown between frames. Changes in this area should keep caches bounded and avoid introducing separate layout-only render passes that bypass the shared cache.
Render Path🔗
The exact session-chat render path is:
crates/agentty/src/runtime/mode/session_view.rscalculates the visible output height by callingSessionChatPage::rendered_output_line_count(...)with the selectedSession, the currentDoneSessionOutputMode,review_status_message,review_text, and the latestactive_progress.crates/agentty/src/ui/page/session_chat.rsbuildsSessionOutputinsiderender_session(), forwarding the same render inputs plus scroll offset.SessionChatPage::render_session_header()prints the single-line session header above the bordered output region.SessionOutput::output_text()incrates/agentty/src/ui/component/session_output.rsselects the base text: staged draft preview for draftNewsessions, rendered summary text forStatus::Donesummary mode, otherwisesession.output.SessionOutput::output_lines()converts that source text into final panel lines: it optionally splits the transcript usingactive_prompt_output, normalizes prompt spacing, splits any trailing commit footer, renders the completed-turn markdown, appends the synthetic summary block fromsession.summarywhen the current status/mode allows it, reattaches the trailing commit footer, appends the active prompt block, appends focused review markdown fromreview_text, appends the published-branch sync row when a detached auto-push starts, completes, or fails, and finally adds the loader row orttoggle hint when the current status requires it.SessionOutput::render()writes the finalLinelist into aratatuiParagraph, which is the exact widget printed in the session chat output area.
Print Timing🔗
The session output panel shows different data at different lifecycle points:
flowchart TD
submit["Start or reply submitted"]
submit --> append_prompt["Append prompt block to session.output"]
submit --> cache_prompt["Cache same block as active_prompt_output"]
submit --> progress["Render loader row from active_progress"]
turn_done["Turn completes"]
append_prompt --> turn_done
cache_prompt --> turn_done
progress --> turn_done
turn_done --> answer["Append assistant transcript chunk to session.output"]
answer --> answer_kind["Protocol answer<br/>or joined clarification-question text"]
turn_done --> metadata["Persist and apply latest-turn metadata"]
metadata --> summary_meta["session.summary"]
metadata --> question_meta["session.questions"]
turn_done --> clear_active["Drop active_prompt_output"]
turn_done --> next_status["Move session to Review or Question"]
next_status --> review_run["Focused review runs"]
review_run --> review_loader["Render AgentReview loader row<br/>from review_status_message"]
review_run --> review_text["Append review_text<br/>after transcript content"]
question_meta --> clarifications["Clarification answers submitted"]
clarifications --> clarification_prompt["Runtime builds one normal reply prompt<br/>starting with Clarifications:"]
question_meta --> end_turn["Esc ends clarification turn<br/>and restores Review without a reply"]What Prints, When It Prints, and When It Stops Showing🔗
Use the artifact-by-artifact reference below instead of a wide comparison table. Each item keeps the same four questions grouped vertically so the page stays readable on narrow screens.
User prompt blocks🔗
- Comes from:
session.outputandactive_prompt_output - Prints: immediately after start or reply submission. The same block stays in the transcript after the turn finishes.
- Hidden or removed: the transcript entry is durable. Only the transient
active_prompt_outputcache is removed after turn metadata is applied or when the session is no longer active.
Assistant answer🔗
- Comes from:
session.output - Prints: after a successful turn when protocol
answeris non-empty. - Hidden or removed: durable transcript entry; not removed by
SessionOutput. It can be hidden from view only whenDoneSessionOutputMode::Summaryreplaces the base body with rendered summary text.
Clarification question text from the assistant🔗
- Comes from:
session.outputfallback when noanswerexists - Prints: after a successful turn with questions but without top-level
answertext. - Hidden or removed: durable transcript entry once appended. Pending structured
session.questionscontinue separately in question mode until answered.
Structured clarification questions🔗
- Comes from:
session.questions - Prints: in
AppMode::Question, inside the bottom question panel, not insideSessionOutput. - Hidden or removed: cleared when the resumed turn starts. The output panel never renders these as synthetic transcript rows.
Clarification answers🔗
- Comes from: a new reply prompt built by runtime and appended into
session.output - Prints: when the user finishes all questions and submits the generated
Clarifications:reply turn. - Hidden or removed: durable transcript entry.
SessionOutputonly adjusts spacing between numbered question groups for readability. If the user ends question mode withEsc, no clarification reply is built and the session returns toReview.
Summary block🔗
- Comes from:
session.summary - Prints: appended after transcript content for most statuses. In
Done + Summarymode it becomes the primary body instead. - Hidden or removed: hidden for
Canceledsessions. It is also not appended a second time whenDone + Summarymode already uses the summary as the base body.
Commit footer🔗
- Comes from: trailing lines in
session.outputthat begin with[Commit]or[Commit Error] - Prints: reattached after summary and follow-up sections so commit notes stay tied to the completed turn footer.
- Hidden or removed: durable transcript content; only moved later in render order.
Focused review loader🔗
- Comes from:
review_status_messagewithStatus::AgentReview - Prints: while review assist is running or when the latest review attempt failed.
- Hidden or removed: removed when a review result arrives, when session view leaves review state, or when a new reply clears the review cache.
Focused review text🔗
- Comes from:
review_textfrom review cache or view state - Prints: appended after transcript content once review assist succeeds.
- Hidden or removed: cleared when a new reply starts, when the session returns to
InProgress, or when a later review result fails and replaces it with an error status message.
In-progress loader🔗
- Comes from:
active_progress - Prints: while a turn is running in
InProgress. - Hidden or removed: removed when the turn finishes or the session leaves an active status.
Mode Rules🔗
DoneSessionOutputMode::SummaryUses rendered summary text as the primary body forStatus::Done. The panel still appends thettoggle hint.DoneSessionOutputMode::OutputUsessession.outputas the primary body, then appends the synthetic summary section.DoneSessionOutputMode::ReviewStill uses transcript output as the primary body. If focused review text is available, it is appended after the transcript; if not, the transcript remains visible. The mode mainly changes the view-state selection and thettoggle target back to summary.Status::QuestionKeeps the transcript panel visible above, while the question/options/input UI is rendered in the bottom panel rather than insideSessionOutput.Status::CanceledUses raw transcript only. Synthetic summary rendering is intentionally suppressed so interrupted sessions do not show finalized metadata they never completed.
Session Turn Data Flow🔗
From prompt submit to persisted result:
- Prompt mode submits:
start_session()for first prompt (AgentRequestKind::SessionStart) orreply()for follow-up (AgentRequestKind::SessionResume).- Shared prompt-composer helpers in
crates/agentty/src/domain/composer.rsderive slash-menu options, attachment-aware deletion ranges, and the drained prompt submission payload before runtime hands the turn to the app layer. The transcript keeps raw@pathlookups, while the later agent-facing prompt text quotes the repository-relative path without adding transport sentinels. - Session command is persisted in
session_operationbefore enqueue. SessionWorkerServicelazily creates or reuses a per-session worker queue.- Worker marks operation
running, checks cancel flags, then runs channel turn. - Worker creates
TurnRequest(reasoning level, model, prompt,request_kind, replay output, provider conversation id). - Worker spawns
consume_turn_events(). AgentChannel::run_turn()streamsTurnEventvalues and returnsTurnResult.- Worker applies final result:
- Append final assistant transcript output when no assistant chunks were already streamed (
answertext, fallbackquestiontext). TurnPersistence::apply(...)transactionally stores the canonical summary payload, question payload, token-usage deltas, and provider conversation markers, then returnsTurnAppliedState.- Emit
AppEvent::AgentResponseReceivedwith that reducer projection so the active session updates without a forced reload. - If canonical metadata persistence fails, append a recovery error to the transcript, trigger
RefreshSessions, and skip reducer projection emission so the UI falls back to durable state on reload. - Run auto-commit assistance path, which preserves a single evolving commit on the session branch: the first successful file-changing turn creates the commit, later turns regenerate the message from the cumulative diff with the active project's
Default Fast Model, auto-commit recovery prompts use that same fast-model selection, and the sessiontitleis synced from the rewritten commit after success while the structured responsesummarypayload remains unchanged. - Refresh persisted session size.
- Update final status (
RevieworQuestion; on failure ->Review).
Operation Lifecycle and Recovery🔗
Turn execution is durable and restart-safe:
- Before enqueue: insert
session_operationrow (queued). - Worker transitions:
queued -> running -> done/failed/canceled. - Cancel requests are persisted and checked before command execution.
- On startup, unfinished operations are failed with reason
Interrupted by app restart, and impacted sessions are reset toReview.
Status Transition Rules🔗
Runtime status transitions enforced by Status::can_transition_to():
New -> InProgress(first prompt)New draft -> Canceled(list-mode cancel before first turn)Review/Question -> InProgress(reply)Review -> Queued -> Merging -> Done(merge queue path)Review -> Rebasing -> Review/Question(rebase path)Review/Question -> CanceledInProgress/Rebasing -> Review/Question(post-turn or post-rebase)
Agent Channel Architecture🔗
Session workers are transport-agnostic through AgentChannel:
flowchart TD
worker["app/session/workflow/worker.rs"]
factory["create_agent_channel(kind, override)"]
provider["Provider registry<br/>infra/agent/provider.rs"]
cli_mode["transport_mode() -> Cli"]
cli_channel["CliAgentChannel<br/>Claude; subprocess per turn"]
app_server_mode["transport_mode() -> AppServer"]
app_server_client["create_app_server_client()"]
app_server_channel["AppServerAgentChannel<br/>Codex/Gemini; persistent runtime per session"]
client_trait["AppServerClient"]
codex_client["RealCodexAppServerClient"]
gemini_client["RealGeminiAcpClient"]
worker --> factory
factory --> provider
provider --> cli_mode
cli_mode --> cli_channel
provider --> app_server_mode
app_server_mode --> app_server_client
app_server_mode --> app_server_channel
app_server_channel --> client_trait
client_trait --> codex_client
client_trait --> gemini_client Key types (infra/channel/contract.rs, re-exported by infra/channel.rs):
| Type | Purpose |
|---|---|
TurnRequest | Input payload: reasoning_level, folder, live_session_output, model, request_kind, prompt, and provider_conversation_id. |
TurnEvent | Incremental stream events: ThoughtDelta, Completed, Failed, PidUpdate. |
TurnResult | Normalized output: assistant_message, token counts, provider_conversation_id. |
AgentRequestKind | SessionStart, SessionResume (with optional session output replay), or UtilityPrompt. |
Provider conversation id flow:
- App-server providers return
provider_conversation_idinTurnResult. - Worker persists it to DB (
update_session_provider_conversation_id). - Worker also persists the matching instruction-bootstrap marker so app-server follow-up turns know whether the active provider context already received Agentty's full prompt contract.
- Future
TurnRequestloads and forwards both values so runtime restarts can resume native provider context and decide between a full bootstrap and a compact reminder.
Agent Interaction Protocol Flow🔗
Provider output is normalized to one structured response protocol:
- Prompt builders choose among
BootstrapFull,DeltaOnly, andBootstrapWithReplay. CLI turns still use the full shared protocol preamble each turn, while persistent app-server turns reuse a compact reminder when the active provider context already matches the stored instruction bootstrap marker.crates/agentty/src/infra/agent/template/protocol_instruction_prompt.mdowns the normal request wrapper,crates/agentty/src/infra/agent/template/protocol_refresh_prompt.mdowns the compact reminder wrapper, the sibling profile-specific markdown templates supply the request-family instruction text,crates/agentty/src/infra/agent/prompt.rsowns shared prompt preparation, andcrates/agentty/src/infra/agent/protocol.rsroutes to the authoritative protocol model/schema/parse submodules. BootstrapFullandBootstrapWithReplaystill prepend the same self-descriptiveschemarsdocument, so every provider sees the sameanswer/questions/optional-summaryschema and transport-enforcedoutputSchemapaths can normalize that same contract separately.- The caller selects one canonical
AgentRequestKindbefore transport handoff, and the transport derives the matchingProtocolRequestProfilefrom it. Session turns useSessionStartorSessionResume, while isolated utility prompts useUtilityPrompt. - Session discussion turns typically populate
summary.turnandsummary.session, while one-shot prompts may leavesummaryunused. - Channels emit transient loader updates as
TurnEvent::ThoughtDeltavalues when providers surface thought or tool-status text during the turn. - Final output is parsed to protocol
answer,questions, and the optional structured summary. The final assistant payload itself must match the shared protocol JSON object, while direct deserialization into the shared wire type still accepts summary-only or otherwise defaulted top-level fields. If a provider prepends prose before one final schema object, parsing now recovers that trailing payload as long as nothing except whitespace follows it. Rejected payloads now surface parse diagnostics including response sizing, JSON parser location/category, and discovered top-level keys. - Worker persists final display text, then
TurnPersistence::apply(...)commits the canonical turn metadata and emitsAgentResponseReceivedwith the matching reducer projection. If that transaction fails, the worker requestsRefreshSessionsand does not emit the projection.
Streaming behavior differs by transport/provider:
- CLI channel (
CliAgentChannel): parses stdout lines into loader-onlyThoughtDeltaupdates for non-response progress/thought text and keeps raw output for final parse. Claude now uses its documentedstream-jsonoutput path here so compaction/tool-use progress can surface without waiting for a single final JSON payload. - CLI prompt submission can stream the fully rendered prompt through stdin for providers that would otherwise exceed argv limits on large diffs or one-shot utility prompts.
- Shared CLI subprocess helpers under
crates/agentty/src/infra/agent/cli/now own stdin piping and provider-aware exit guidance so session turns and one-shot prompts use the same subprocess behavior. - App-server channel (
AppServerAgentChannel): routes provider thought phases and progress updates to transient loader text, while withholding assistant transcript chunks until the completed turn result is parsed. - One-shot prompt submission asks the concrete backend for its transport path, so app-server providers (Codex and Gemini) resolve their own runtime client and Claude stays on direct CLI subprocess execution.
- Provider capabilities in
crates/agentty/src/infra/agent/provider.rscentralize strict final protocol validation, CLI stream classification, app-server thought-phase handling, and provider app-server client construction. - Gemini ACP still accumulates streamed assistant chunks internally for its final turn result, but the runtime now prefers the completed
session/promptpayload whenever that payload parses as protocol JSON and the streamed accumulation does not. - Worker persistence behavior:
ThoughtDeltaupdates refresh the loader only, while assistant transcript output is appended once from the final parsed turn result.
- Claude, Gemini, and Codex use strict protocol parsing and return an error immediately when invalid.
- One-shot agent submissions still surface schema errors directly to the caller whenever the shared parser rejects the final output, including plain text, blank utility responses, non-utility prompts that miss the schema, or any output that leaves trailing non-whitespace text after the recovered protocol payload. Those surfaced errors now also include the shared protocol parser's debug report.
- App-server restart retries preserve the original protocol profile and now compare the persisted instruction state against the runtime's actual
provider_conversation_idbefore choosing the prompt-delivery mode.
Clarification Question Loop🔗
- Worker receives final parsed response containing clarification questions in
questions. - Worker persists question list and sets session status
Question. - Reducer switches active view to
AppMode::Questionwhen that session is focused. - User answers each question. Submitting a blank free-text answer stores
no answer. - Runtime builds one follow-up prompt:
Clarifications:
1. Q: <question 1>
A: <response 1>
2. Q: <question 2>
A: <response 2>- Runtime submits this as a normal reply turn; flow returns to standard worker path.
Pressing Esc instead ends question mode immediately, restores the session to Review, and does not send the generated clarification reply.
Background Task Catalog🔗
Detached/background execution paths and their trigger conditions:
Terminal event reader thread🔗
- Trigger: Runtime startup
- Spawn site:
runtime/event::spawn_event_reader - Emits or writes: Terminal
Eventchannel - What it does: Polls crossterm and forwards terminal events into the runtime loop.
Git status poller loop🔗
- Trigger: App startup when the project has a git branch, project switch, and session refreshes that change active session branches
- Spawn site:
TaskService::spawn_git_status_task - Emits or writes:
AppEvent::GitStatusUpdated - What it does: Runs a periodic fetch plus one repo-level upstream snapshot for the active project branch, then combines each active session branch's base-branch comparison with any tracked-remote snapshot before emitting one combined update about every
30s.
Version check one-shot🔗
- Trigger: App startup
- Spawn site:
TaskService::spawn_version_check_task - Emits or writes:
AppEvent::VersionAvailabilityUpdated - What it does: Checks the latest npm version tag and reports update availability.
Per-session worker loop🔗
- Trigger: First command enqueue for a session
- Spawn site:
SessionWorkerService::spawn_session_worker - Emits or writes: DB
session_operationupdates plus app or session updates - What it does: Serializes all turn commands per session and manages channel lifecycle.
Per-turn turn-event consumer🔗
- Trigger: Every queued turn execution
- Spawn site:
run_channel_turn - Emits or writes: Loader updates and pid slot updates
- What it does: Consumes the
TurnEventstream and applies immediate side effects.
CLI stdout and stderr readers🔗
- Trigger: Every CLI-backed turn
- Spawn site:
CliAgentChannel::run_turn - Emits or writes:
TurnEventstream plus raw output buffers - What it does: Reads subprocess streams and emits transient loader updates while buffering final output.
App-server stream bridge🔗
- Trigger: Every app-server-backed turn
- Spawn site:
AppServerAgentChannel::run_turn - Emits or writes:
TurnEventstream - What it does: Bridges
AppServerStreamEventvalues into the unified turn event stream.
Clipboard image persistence🔗
- Trigger: Prompt input
Ctrl+VorAlt+V - Spawn site:
runtime/mode/prompt::handle_prompt_image_paste - Emits or writes: Temporary PNG files under
AGENTTY_ROOT/tmp/<session-id>/images/plus prompt attachment state - What it does: Reads a clipboard image or PNG path via
spawn_blocking, persists it, and inserts an inline[Image #n]placeholder.
Session title generation🔗
- Trigger: First
Startturn, before main turn execution - Spawn site:
spawn_start_turn_title_generation - Emits or writes: Database title update plus
AppEvent::RefreshSessions - What it does: Runs a one-shot title prompt in the background and persists the generated title when valid.
At-mention file indexing🔗
- Trigger: Prompt input or question free-text input activates
@mention mode - Spawn site:
runtime/mode/prompt::activate_at_mentionandruntime/mode/question::activate_question_at_mention - Emits or writes:
AppEvent::AtMentionEntriesLoaded - What it does: Lists session files via
spawn_blocking, falling back to the active project working directory when an unstarted draft session has not yet materialized its worktree, and updates mention picker entries for the active composer.
Background session-size refresh🔗
- Trigger:
Enteron a session in list mode - Spawn site:
App::refresh_session_size_in_background - Emits or writes: Database size update plus
AppEvent::RefreshSessions - What it does: Computes the diff-size bucket without blocking the key-handling path.
Session-view branch-publish action🔗
- Trigger: Session view
pinReview, then publish popupEnter - Spawn site:
App::start_publish_branch_action - Emits or writes:
AppEvent::BranchPublishActionCompleted - What it does: Collects an optional remote branch name before first publish, locks to the existing upstream after publish, then runs
git push --force-with-leasefor the session branch in the background and updates the session-view popup with success or recovery guidance.
Deferred session cleanup🔗
- Trigger: Delete with the deferred cleanup path
- Spawn site:
delete_selected_session_deferred_cleanup - Emits or writes: Filesystem and git side effects
- What it does: Removes the worktree folder and branch asynchronously after database deletion.
Focused review assist🔗
- Trigger: View mode focused-review open when the diff is reviewable
- Spawn site:
TaskService::spawn_review_assist_task - Emits or writes:
ReviewPreparedorReviewPreparationFailed - What it does: Runs the model review prompt and stores the final review text or error.
Sync-main workflow task🔗
- Trigger: List-mode sync action
s - Spawn site:
TokioSyncMainRunner::start_sync_main - Emits or writes:
AppEvent::SyncMainCompleted - What it does: Pulls, rebases, and pushes the selected project branch with the assisted conflict flow when needed.
Session merge task🔗
- Trigger: Merge confirmation accepted
- Spawn site:
SessionMergeService::merge_session - Emits or writes: Output append, status updates, and session metadata updates
- What it does: Runs rebase, reuses the single evolving session-branch
HEADcommit message for squash merge, then cleans up the worktree in the background.
Session rebase task🔗
- Trigger: Rebase action in view mode
- Spawn site:
SessionMergeService::rebase_session - Emits or writes: Output append and status updates
- What it does: Runs the assisted rebase flow and returns the session to
RevieworQuestion.
Sync, Merge, and Rebase Flows🔗
Project and session git workflows use shared boundaries (GitClient, FsClient, assist helpers) but have distinct orchestration paths:
sync main: selected project branch pull/rebase/push, optional assisted conflict resolution, popup result summary.- session merge: queue-aware workflow, assisted rebase first, reuse the single evolving session-branch
HEADcommit message for the squash commit into the base branch, then clean up the worktree and set statusDone. - session rebase: assisted rebase of session branch onto base branch, returns to
Reviewafter completion/failure reporting. - session review-request publish: review-ready sessions push the session branch through
GitClientwith--force-with-lease, then create or refresh the forge review request throughReviewRequestClient. - background review-request sync: review-ready sessions with a published branch or linked review request are polled through
ReviewRequestClient; merged requests move the session toDone, and closed requests move it toCanceled.
Persistence and Recovery Boundaries🔗
Persistence invariants that shape runtime flow:
- DB opens with SQLite WAL and
foreign_keys = ON, then embedded migrations run at startup. - Session snapshots in memory are authoritative for rendering; DB is authoritative for restart recovery.
- Shared session handles (
output,status,child_pid) provide low-latency updates between DB reloads. - Event-driven refresh is primary (
RefreshSessions); metadata polling is fallback safety only. - External integrations (
GitClient,ReviewRequestClient,AppServerClient,AgentChannel,EventSource,FsClient,TmuxClient) isolate side effects and enable deterministic tests.