This document describes the current implementation, including waiting, retries,
follow-up rounds, and recovery. Mermaid nodes use the actual persisted phase
and status names where applicable. done means publication finished; it does
not mean the PR has merged. These diagrams describe the code on this branch;
features under Unreleased are available in a build of this branch and enter a
published package through the release process.
Read the diagrams together: sections 1–3 cover scheduling and admission, section 4
covers the saved main session, sections 5–7 cover helpers and publication, and
section 8 covers recovery and local task closure. Model planning, subagents and review
happen inside running; they are not additional persisted phases.
flowchart TD
Activate[Open TUI or call plugin.list for the owner location] --> Load[Load combined automation plugin]
Load --> Owner{Primary Git checkout root?}
Owner -->|No or outside Git| Inactive[Plugin stays inactive]
Owner -->|Yes| Config[Use nonempty plugin options or read .opencode/automation.json]
Config -->|No project config| Inactive
Config --> Register[Register primary checkout in local user inventory without activating other owners]
Register --> Resolve[Validate repository file policy and resolve GitHub auth, routes and defaults]
Resolve --> Metadata[Register resolved repositories and base branches]
Metadata --> GH[Acquire github lock and load queue.json]
GH --> RPC[Register runtime bridge and dispatcher RPC]
RPC --> Worker[Immediate worker tick, then every workerEverySeconds]
RPC --> Scheduler[Start scheduler after GitHub setup succeeds]
Scheduler --> State[Acquire scheduler lock, load scheduler.json and register RPC]
State --> Clock[Immediate scheduler tick, then every second]
Clock --> Due{Job due, unpaused and not already running?}
Due -->|Yes| Scan[Invoke configured RPC, normally automation.github.scan]
Scan --> Save[Persist result, failures and nextAt]
Save --> Clock
Due -->|No| Clock
Worker --> Closing[Resume due closing and cancelling requests independently of active worker]
Closing --> Clear{Any closure or cancellation pending?}
Clear -->|Yes| Worker
Clear -->|No| Recover[Probe eligible stopped sessions and recover unpublished questions]
Recover --> Round[Promote one done or watching task with pending feedback to a new round]
Round --> Select[Choose ready or retry_wait task, saved running session first]
Select --> Candidate{Candidate exists?}
Candidate -->|No| Merge[Check eligible merges]
Candidate -->|Yes| TaskDue{Candidate nextAt elapsed?}
TaskDue -->|No| Worker
TaskDue -->|Yes| Dispatch[Advance saved phase]
Dispatch --> Worker
Merge --> Worker
RPC -.-> Keepalive[Each component touches the same empty owner session every ten minutes]
State -.-> Keepalive
Keepalive --> Discover[Discover service through /api/info]
Discover --> PID{server.info PID matches this process?}
PID -->|Yes| Touch[Create or reuse maintenance session, then session.update its title]
PID -->|No| Skip[Skip keepalive]
Stop[Owner reload or shutdown] --> Cleanup[Stop timers, save stopped inventory snapshots, settle writes, dispose RPC, release locks]
Cleanup --> Preserve[Preserve durable queue and healthy worktree execution]
Preserve --> Load
View[Runtime sidebar or botstatus] -.-> Monitor[Read dispatcher monitor and scheduler status every five seconds]
Monitor -.-> RPC
Monitor -.-> State
Monitor --> Fresh{Both readings available and fresh?}
Fresh -->|Yes| Display[Show live operations, queue and selected task]
Fresh -->|No| Stale[Mark unavailable or retained stale readings]
RPC -.-> DS[Publish dispatcher snapshot every five seconds]
State -.-> SS[Publish scheduler snapshot every five seconds]
DS --> Inventory[Per-user registry and separate atomic component snapshots]
SS --> Inventory
Register --> Inventory
Metadata --> Inventory
Init[CLI init or explicit list --discover] --> Inventory
List[CLI list or bot Repositories via current owner RPC] --> Read[Read local inventory without activating owners]
Inventory -.-> Read
Read --> Check[Check paths, config, PID and 15-second freshness]
Check --> Report[Report repository status, scan timing, task counts and issue errors]
- Registration and component snapshots are observational, best-effort writes.
Registry errors do not stop the bot.
initregisters after configuration succeeds; explicit discovery imports old standard configs without auth or service startup. The CLI and TUI report share a per-user host registry, not the task queue. Missing paths and stale/dead processes are shown explicitly. See inventory behavior and limits. - Easy configuration puts state under the shared Git directory at
opencode2-automation/. Worker worktrees do not start another scheduler. - Default discovery interval: 60 seconds. Default worker interval: 5 seconds. These are separate loops: pausing scheduled scans does not cancel accepted work or active sessions.
- A scan cannot overlap another scan in the same dispatcher. Only one worker invocation runs at a time; one scheduler job cannot overlap itself.
- Each component renews one empty owner maintenance session at startup and every
ten minutes. Durable session events refresh OpenCode's inactivity timer; listing
plugins does not. No model is prompted. A PID check prevents touching a different
service, using
server.infoafter discovery through/api/infoin SDK 2.0.6. The title update usessession.update. Requests do not overlap and have a 15-second deadline. Pausing issue scans does not pause keepalive. Standalone servers without a matching registered service skip it. - State is schema-validated and saved through a temporary file, file sync, and rename. A corrupt state file fails to load rather than resetting the queue. Local locks prevent duplicate owners sharing this state directory; independent machines do not share ownership.
- Shutdown clears timers and aborts local SDK waits even if the adapter ignores cancellation. It settles local writes, bounds RPC disposal to five seconds per component, and attempts all cleanup steps before releasing ownership. A replacement waits up to 15 seconds for locks without removing them. Healthy worktree sessions continue; the next owner reconciles the saved session before verifying and publishing.
Sources: index.ts, easy.ts, GitHub plugin, scheduler plugin, lifecycle.ts, dispatcher.ts — workOnce, state.ts, repositories.ts, repository-report.ts.
flowchart TD
Scan[Scan each configured repository] --> PRs[Refresh tracked PRs excluding merged and locally closing, closed or cancelling tasks]
PRs --> Issues[List open issues and fetch missing actively tracked issues]
Issues --> Skip{PR entry, locally closing, closed or cancelling task, or closed untracked issue?}
Skip -->|Yes| Ignore[Ignore entry]
Skip -->|No| Comments[Read comments and filter authorized human comments without bot markers]
Comments --> Tracked{Task already exists?}
Tracked -->|Yes| Answer{Open issue with unanswered published question and eligible reply?}
Answer -->|Yes| Accept[Save first eligible answer and any permission decision]
Accept --> Ready[Only waiting status becomes ready]
Ready --> Remaining[Remove answer from fresh and previously queued feedback]
Answer -->|No| Feedback[Append remaining fresh comments to pendingFeedback]
Remaining --> Feedback
Feedback --> Cursor[Persist cursor from all observed comments and save queue]
Cursor --> Gate{Task done or watching?}
Gate -->|Yes| Later[Next available worker pass may start a follow-up round]
Gate -->|No| Retain[Keep feedback until publication or cancellation completes]
Tracked -->|No| Body[Match body route only for an authorized issue author]
Body --> Found{Body route found?}
Found -->|Yes| Queue[Persist queued / ready with initial authorized feedback]
Found -->|No| Route[Try authorized comments, keeping the last matching route]
Route -->|Route found| Queue
Route -->|No route| Ignore
Body -->|Multiple matching tags| Block[Persist queued / blocked task]
Route -->|Multiple matching tags| Block
Authorized comment filtering requires an author in the configured allowlist
(case-insensitive), a nonempty body, a non-Bot account type, and no
<!-- opencode2: marker. A human may use the same account as the bot: the login
itself is not excluded. Initial issue-body routing checks the issue author's
allowlist membership; the issue schema does not include an account-type check.
Routing tags are case-insensitive configured @mentions. If the issue body
selects a route, comments do not replace it. Otherwise the last matching
authorized comment encountered selects the route, unless matching throws for
multiple tags. An authorized comment can trigger work on another author's issue.
Each task is keyed by lowercase owner/repository#issueNumber. Its branch is
automation/issue-N-DIGEST, where DIGEST is the first 12 hex characters of the
SHA-256 of that key. Initial feedback contains the authorized comments already
seen. Later comments are tracked by increasing comment ID; edits do not create
new feedback. PR review comments do not drive implementation rounds.
Discovery and execution are separate: saving pendingFeedback does not itself
clear a blocked task or interrupt its current session. Only done tasks start a
new round. A session-stop block can first reconcile successful manual continuation
as described in section 8. A pending question consumes its first eligible reply
instead of also treating that reply as follow-up work. The comment cursor includes
all observed comments, while only authorized, unmarked comments become inputs.
Source: dispatcher.ts — scanOnce, config.ts — matchRoute.
flowchart TD
Q[queued / ready] --> Guard[Re-fetch issue and validate route, authorization and follow-up PR]
Guard --> A[analyzing: generate or reuse structured decision without tools]
A --> Decision{Decision kind?}
Decision -->|question| AQ[Persist proposals and question, publish one signed comment]
AQ --> AW[analyzing / waiting]
AW -->|Authorized reply| Dialogue[Save dialogue and invalidate prior decision]
Dialogue --> Guard
Decision -->|proceed| Ack[Publish or reconcile signed analysis acknowledgement]
Ack --> C[commented with confirmed commentID]
C --> Pinned{Base already pinned?}
Pinned -->|No| Base[Interpret authorized branch discussion with main model]
Base --> Choice{Valid unambiguous branch exists on origin?}
Choice -->|No| BQ[Publish base question, commented / waiting]
BQ -->|Authorized reply| Base
Choice -->|Yes| Pin[Persist baseBranch]
Pinned -->|Yes| Prepare[Validate repository and reuse saved worktree or create a new one]
Pin --> Prepare
Prepare --> R[running: save workspace, install runtime and execute saved session]
R -->|Question| RW[running / waiting]
RW -->|Authorized reply| R
R -->|Timeout or unsuccessful final result| Stopped[running / blocked with sessionStopped]
Stopped -->|Manual continuation succeeds and probe passes| R
Stopped -->|Explicit restartworkflow| Recover[Persist recovery intent, rejoin the same session]
Recover --> R
R -->|Validated success, no unresolved question| Report[Persist final public assistant summary with session and round]
Report --> V[verifying: configured checks and commit]
V --> P[publishing: reconcile or create PR, push when required]
V -->|Failed check or Git consistency guard| VB[verifying / blocked]
VB -->|Operator retries saved stage| V
P --> Done[pr_opened / done with PR and publishedAt]
P -->|Branch matches, PR head not yet updated| Lag[publishing / retry_wait within attempt limit]
Lag -->|Backoff elapsed, reuse saved push| P
P -->|Publication failure| PB[Retain publishing phase and apply error policy]
PB -->|Eligible retry| P
Done -->|Pending authorized feedback| Round[Increment round, move feedback and reset per-round state]
Round --> Q
Cancel[Operator confirms Cancel current round] --> Cancelling[Persist cancelling and block new execution]
Cancelling --> Archive[Interrupt and drain, archive round and preserve worktree]
Archive --> Watching[watching: no replay or publication]
Watching -->|New authorized feedback| Fresh[New local branch and worktree from published PR or pinned base]
Fresh --> Round
Closed -->|Explicit Resume issue tracking| Resume[Validate open GitHub objects, skip observed backlog]
Resume --> Cancelling
Operator[Operator confirms Stop and close task] --> Closing[Persist closing at any saved phase]
Closing --> Drain[Interrupt known sessions and drain current operation]
Drain --> Closed[Persist closed and retain work and history]
Closing -.-> Guards[Reject new checkpoints, runtime hooks and publication]
Before queued, analyzing, or commented work advances, the dispatcher checks
that the issue is still open. Follow-ups additionally require an open original
PR and still-authorized feedback authors. A changed issue title, body, or route
after saved analysis blocks progress. Removing configuration or leaving no
unambiguous route also blocks work. These guards are phase-specific; closing an
issue does not immediately cancel an already-running session.
Analysis has no tools and does not inspect code. It returns validated JSON:
proceed with an English understanding and plan, or question with proposals
and a clarification. Invalid output retries. Requests for proposals or a choice
before implementation must wait for a reply, then run analysis again. An unclear
reply can generate another question. Persisted older analyses without a decision
are reassessed before implementation.
Base selection follows the acknowledgement. It considers authorized issue text,
comments, and clarification dialogues, excluding quoted lines and fenced code.
The model must substantiate an explicit branch using text from those inputs.
A selected branch, including the configured default, is checked on origin; ambiguity or absence
produces a question. Invalid model output or Git connection failure retries.
No preference uses the configured base, which easy configuration defaults to the
GitHub default branch. Fetch still has to succeed during preparation. Once saved,
the base stays pinned across retries and rounds; later comments do not rebase work.
Preparation validates the checkout root, origin repository, and branch name.
A new worktree is created from the fetched base commit under
stateDirectory/worktrees/BRANCH-WITH-SLASHES-REPLACED-BY-DASHES.
For a saved worktree, preparation uses that exact path even if the branch was
renamed during recovery. Its canonical directory must be a direct child of the
managed worktree folder and the exact Git worktree root, with the expected branch
and shared Git directory. Its pinned baseSha is retained. A missing saved path,
or a branch already existing without its expected worktree, blocks work rather
than creating a replacement. The worker runtime is installed before execution.
Failures retain their current phase. A stopped running session can return to
that phase through automatic reconciliation or explicit workflow recovery; neither
path skips the session checks or jumps straight to done. Recovery of verifying
or publishing retries that saved stage. The full error and command rules are in
section 8.
Sources: dispatcher.ts — workOnce, resolveAnalysis, resolveBase, executor.ts — analyze, selectBase, GitWorkspace.prepare, branch.ts.
sequenceDiagram
participant D as Dispatcher / executor
participant S as Saved OpenCode main session
participant R as Worker runtime
participant G as GitHub issue
participant U as Authorized user / operator
opt No saved session ID
D->>D: Persist sessionID before contacting OpenCode
end
D->>S: Get session, create only on explicit not-found
D->>D: Validate worktree location and save sessionReady
opt Initial prompt not attempted
D->>D: Persist promptAttempted
D->>S: Implement agreed scope with task marker
end
opt Explicit recovery queued, continuation not attempted
D->>S: Wait until current execution is idle
D->>S: Read saved outcome
alt Outcome is not succeeded
D->>S: Confirm original task marker in context
D->>D: Persist recovery.attempted before sending
D->>S: Continue same task with recovery marker and deterministic message ID
else Already succeeded
Note over D,S: Do not send another continuation
end
end
D->>S: Wait for completion
opt OpenCode permission evaluation
S->>R: Permission action, resources and current effect
alt Explicit OpenCode allow or deny
R-->>S: Preserve effect
else Exact saved decision for this main session
R-->>S: Apply saved allow or deny
else Repo file policy enabled, eligible action and all paths inside repo or task worktree
Note over R: Resolve canonical paths, exclude media helpers and unanswered questions
R-->>S: Allow file access without an issue question
else Permission still needs approval
R-->>S: Deny this attempt and use the question flow below
end
end
opt Clarification or remaining permission question
S->>R: ask_issue / intercepted question / permission ask
R->>D: Register against main task session
D->>D: Persist pending question
D->>G: Publish signed question with stable marker
R-->>S: Stop work and finish turn
D->>D: Preserve running phase, set waiting
U->>G: Reply in the same issue
D->>G: Scan reads eligible answer
D->>D: Persist answer and set ready
D->>S: Resume same session with deterministic answer message ID
D->>S: Wait for completion
end
alt Operator closes task
U->>D: Confirm Stop and close task in bot menu
D->>D: Persist closing and reject new checkpoints and prompts
D->>S: Interrupt known task sessions with resume false and wait for idleness
D->>D: Drain in-flight worker and persist closed
Note over D,G: Preserve work and history, no GitHub closure request
else Operator cancels the round
U->>D: Confirm Cancel current round
D->>D: Persist cancelling and reject new execution checkpoints
D->>S: Interrupt saved sessions with resume false and wait for idleness
D->>D: Drain worker and question posts, archive round, enter watching
Note over D,G: Keep PR tracking and future feedback, preserve cancelled worktree
else Owner is disposed
Note over D,S: Release local wait without interrupting healthy execution
Note over D: Replacement owner loads queue and rejoins saved session
else Session deadline expires
D->>S: Interrupt execution
D->>D: Save running / blocked with sessionStopped
else Wait completes
D->>S: Read context and final outcome
alt Valid task marker, admitted recovery marker if required, and successful final assistant
D->>D: Save final public assistant text with session and round
D->>D: Clear recovery state and advance to verifying
else Unsuccessful final outcome or assistant
D->>D: Save session-stop block for later reconciliation
else Missing marker or wrong location
D->>D: Block for inspection, no automatic prompt replay
end
end
- A saved
sessionIDis reused after transport failure. A network error when looking up a session never authorizes creating a duplicate. The initial prompt is sent only whenpromptAttemptedis false. - The implementation prompt instructs the agent to edit only its worktree and leave pushes, PR creation, comments, and branch changes to the dispatcher. These publication restrictions are prompt instructions; verification provides the subsequent Git consistency checks.
- Runtime context injects bundled bot instructions and optional project prompt instructions on each agent loop, including after compaction. A missing or empty configured prompt file fails execution.
- Within
running, the bundled prompt guides project inspection, planning, use of available workflows and native subagents, implementation, verification, and final review. These are model instructions, not persisted dispatcher phases or enforced review gates. The executor'sverifyingphase remains separate. A prose blocker in the final summary does not setblockedstatus; user-input blockers must go throughask_issue. autoApproveRepositoryFilesis a repository opt-in, carried in the generated worktree runtime settings. It handlesexternal_directory,read, andeditrequests for canonical paths within the configured checkout or assigned worktree. Native workers inherit task association through parent lookup; new rounds use the same repository policy. Explicit denials, pending questions, media-helper restrictions, and shell rules are unchanged. See configuration and reload behavior.- One unresolved question is retained at a time. Runtime hooks remove tools and reject non-question tool execution while a question is pending. Native subagent questions are attached to the main task; the reply resumes the main session.
- The first authorized comment after the published question is accepted without another mention. Other fresh comments remain feedback for a later round. Replies require the issue to be open. Question state survives restarts.
- Permission questions require the entire trimmed reply to be exactly
/allow QUESTION_IDor/deny QUESTION_ID. The stored decision is scoped to the main session, action, and sorted resource set, including native workers. Until answered, the requested operation is denied. Explicit OpenCode deny rules are not overridden: the hook only handles permissions with effectask. - Replies to analysis questions rerun analysis; base replies rerun selection; implementation replies resume execution. Answer delivery is checkpointed and uses a deterministic message ID for retry reconciliation.
- A pending question prevents verification and PR publication. Waiting tasks release worker selection so other queued tasks can proceed.
- A session wait deadline attempts to interrupt the server session and records
a
SessionStoppedblock. A final outcome other thansucceeded, a missing final assistant, an assistant error, or a finish other thanstopalso records a session stop. A successful manual continuation can be discovered automatically. - Explicit workflow recovery waits for existing execution before deciding whether
to send a continuation. It sends nothing if the saved outcome is already
succeeded; normal final-message validation still applies. Otherwise it checks the original task marker, persistsrecovery.attempted, and sends the recovery marker with a deterministic message ID. A later retry never blindly resends that attempted prompt. Missing recovery evidence blocks for inspection. - Wrong session location and uncertain original prompt delivery are ordinary
Blockederrors, not session-stop eligibility. A pending question still prevents publication. Successful execution clearssessionStoppedandrecoveryas the dispatcher advances toverifying.
Sources: executor.ts — runSession, runtime.ts, repository-permissions.ts, prompt.ts, dispatcher.ts — workOnce, question, publishQuestion, restartWorkflow.
flowchart TD
Call[inspect_media request] --> MainTask{Owning main task has route and worktree?}
MainTask -->|No| Error[Return tool error]
MainTask -->|Yes| Cap{Main model supports requested vision or audio input?}
Cap -->|Yes| Main[Select main model for a separate helper session]
Cap -->|No| Other{Configured mediaModel supports input?}
Other -->|Yes| Helper[Select configured helper model]
Other -->|No| Ask[Post issue question for configuration or text description and wait]
Main --> Files[Validate 1 to 8 HTTPS URLs or real files inside worktree]
Helper --> Files
Files -->|Invalid input| Error
Files --> Guard{Task running with no unresolved question?}
Guard -->|No| Error
Guard -->|Yes| ID[Persist deterministic helper ID for main session and tool call]
ID --> Session[Get saved helper or create only on explicit not-found]
Session --> Prompt[Send deterministic attachment prompt, hooks disable all tools]
Prompt --> Wait[Wait with session deadline]
Wait -->|Timeout| Interrupt[Interrupt helper with resume false and return error]
Wait -->|Other failure| Error
Wait -->|Completed| Result{Succeeded outcome and non-error final assistant with finish stop?}
Result -->|No| Error
Result -->|Yes| Return[Return findings to main session, keep main model unchanged]
Closing[Local task closing, closed, cancelling or watching] --> Deny[Reject helper registration and runtime lookup]
Only the owning main bot session can delegate media; the helper-registration
step also requires running, active tracking and no unresolved question. Helpers have no tools.
URLs cannot contain credentials; local paths are resolved and must remain inside
the worktree. GitHub credentials are not forwarded to media URLs. A helper uses
stable session and prompt IDs for a given call. Helper failures return errors;
a helper timeout attempts interruption. Native implementation subagents are a
separate mechanism: they may use permitted tools, while their questions route back
to the main task through parent-session lookup. Neither kind of helper creates
another dispatcher round or publishes its own PR.
Source: runtime.ts — inspect_media.
flowchart TD
Start[Validated session success or retry of verifying phase] --> Report[Reuse saved completion summary or recover from saved session]
Report --> Identity[Require saved workspace and base, exact managed root, branch and shared repository]
Identity --> Base[Require baseSha ancestor of HEAD and no unresolved conflicts]
Base --> Checks[Run configured checks sequentially, or none if list empty]
Checks -->|Configured check fails| Block[blocked at saved phase, retain work]
Checks -->|Pass| Diff[Recheck identity and git diff --check]
Diff --> Stage[git add --all, check staged diff and record staged tree]
Stage --> Commit[Commit staged changes if any]
Commit --> Validate[Require committed tree matches, changes versus base and clean worktree]
Identity -->|Explicit consistency guard fails| Block
Base -->|Unresolved conflicts| Block
Validate -->|Explicit consistency guard fails| Block
Validate -->|Pass| Save[Persist summary, checks and exact commit SHA, phase publishing]
Retry[Retry saved publishing phase] --> Body
Save --> Body[Recover missing legacy summary, save original report and render body]
Body --> Find[Find branch PR including closed PRs]
Find --> Follow{Follow-up round?}
Follow -->|Yes| Open{Existing PR open?}
Open -->|No| Block
Open -->|Yes| Pushed{Saved pushedCommit matches verified SHA?}
Pushed -->|Yes| Description
Pushed -->|No| Push[Validate origin, workspace, saved HEAD and clean tree, push exact SHA]
Follow -->|No| Exists{PR already exists?}
Exists -->|Yes| Closed{PR closed?}
Closed -->|Yes| Done[Record PR and publication time, pr_opened / done]
Closed -->|No| Description
Exists -->|No| Title[Generate title only if no saved prTitle]
Title --> Issue{Issue still open?}
Issue -->|No| Block
Issue -->|Yes| NewPushed{Saved pushedCommit matches verified SHA?}
NewPushed -->|Yes| Create
NewPushed -->|No| PushNew[Validate origin and workspace, push exact verified SHA]
PushNew --> SaveNew[Persist pushedCommit]
SaveNew --> Create[Create or reconcile signed PR against pinned base]
Create --> Description[Read open PR and remote branch, reconcile managed description]
Push --> SavePush[Persist pushedCommit]
SavePush --> Description
Description -->|Branch matches, PR head differs| Pending[Retry saved publication with backoff and attempt limit]
Pending --> Retry
Description -->|Closed PR, changed branch, edited managed block or oversized body| Block
Description -->|Unchanged or update succeeds| Acknowledge[Persist published body checkpoint]
Acknowledge --> Done
Failure[Other command, model or transport error] --> Policy[Keep current phase and apply retry policy in section 8]
Close[Operator closes task before publishing starts] --> Drain[Finish in-flight local operation, reject next checkpoint]
Drain --> Preserve[Do not publish, preserve existing local changes]
Cancel[Cancel round before publication starts] --> DrainRound[Persist cancelling, drain local work and reject publication]
DrainRound --> Watch[Archive work and watch for new comments]
InFlight[Publication already in flight] --> Refuse[Reject close or cancel request and retry after completion]
Resuming running validates the saved session first; retrying verifying runs
checks again. Retrying publishing uses the saved verified SHA without rerunning
checks implicitly. If a push is still required, the worktree must still match
that SHA. After a checkpointed push, description reconciliation checks GitHub's
branch and PR instead; it does not publish later local edits. An already pushed
branch does not by itself make a task complete.
The configured checks are command argument arrays. A failing configured check
produces blocked. With no configured checks, only Git consistency checks run;
the PR explicitly says the dispatcher did not independently rerun agent-reported
tests. Commit hooks changing
the recorded tree, a dirty worktree after commit, or no diff from the base block
publication. Other command failures use the general error policy below.
The PR body uses the final public text of the successful assistant response, with its Markdown preserved, rather than the pre-work analysis acknowledgement. The executor saves it with the session and round before verification; verification adds the checks and exact commit to the same snapshot. No extra model call rewrites the report. Reasoning, tools, and failed or unfinished responses are excluded. Missing legacy snapshots are read from the saved session; a missing session or empty response produces an explicit summary-unavailable notice, never analysis as a fallback. Transport errors retain the stage for retry.
The body keeps the original report and replaces one Latest update section on
follow-ups. Dispatcher checks appear separately from agent-reported tests, followed
by Closes #N, session, round and verified commit. Push uses
COMMIT:refs/heads/TASK_BRANCH without force. The first publication reconciles an
existing branch PR without another push; follow-ups require an open PR and push
the new verified commit before updating its description. A successful push saves
pushedCommit; retries skip that push when it matches the verified SHA, including
after an owner restart. New rounds clear this checkpoint. If push succeeded but
its response or checkpoint was lost, normal non-force push reconciliation still
applies. The title is retained.
An already closed first-round PR is recorded without editing its description.
Before reconciling the description and again before PATCH, compare the remote
branch ref with the verified SHA. If the branch matches but the PR head is stale,
PullHeadPending uses the normal backoff and maxAttempts policy at publishing.
It does not rerun implementation, verification, or a checkpointed push. A closed
PR or different branch SHA blocks with a distinct error; a stale PR view is not
permission to overwrite a changed branch. The guard also checks the branch when
the PR view already reports the expected SHA.
A stable HTML marker pair encloses the bot-managed description. Notes outside it are preserved; an edited or removed managed section blocks publication rather than overwriting it. The last acknowledged body is checkpointed, so a lost update response can be reconciled without duplicate sections. Exact legacy descriptions can be replaced; otherwise unmarked content is retained and the managed section appended. GitHub is reread before writing to detect concurrent edits, although there is no atomic compare-and-swap across that read and write. See publication recovery and limits for details.
Signed comments use stable opencode2 markers; reconciliation looks for a marker
posted by the authenticated account. This covers analysis acknowledgements,
questions, and merge acknowledgements after a lost response.
Sources: executor.ts — GitWorkspace.verify, push, title, dispatcher.ts — publishing, github.ts — ensureComment, ensurePull, updatePullBody, pr-description.ts — extraction, rendering and reconciliation.
flowchart TD
Pending[Authorized comment enters pendingFeedback] --> Done{Current task done or watching?}
Done -->|No| Keep[Retain comment while running, waiting or blocked]
Keep --> Recovery[Session recovery and publication must finish first]
Recovery --> Done
Done -->|Yes| Round[Next worker pass starts a new round, isolating worktree after cancellation]
Round --> Snapshot[Retain original report and published body, reset current completion]
Snapshot --> Guard[Require open issue, open original PR and authorized feedback, then analyze again]
Idle[Worker has no eligible execution task] --> Eligible{Auto-merge enabled and done or watching task with published head eligible?}
Eligible -->|No| Later[Wait for a later worker pass]
Eligible -->|Yes| Since{publishedAt exists?}
Since -->|No| Window[Record current time as fresh merge-comment window]
Window --> Later
Since -->|Yes| Scan[Scan again before considering merge]
Scan --> Fresh{Pending feedback or closed PR?}
Fresh -->|Yes| Later
Fresh -->|No| Detail[Read GitHub PR details]
Detail --> Already{Already merged?}
Already -->|Yes| Ack[Post or reconcile signed merge acknowledgement, persist merged and closed PR]
Already -->|No| Head{Open, non-draft PR with saved published head?}
Head -->|No| Poll[Clear mergeError, set mergeNextAt at least 60 seconds later]
Head -->|Yes| Review[Match latest decisive reviews to exact SHA, match merge comments after publication]
Review --> Author{No outstanding changes request and eligible approver has write, maintain or admin access?}
Author -->|No| Poll
Author -->|Yes| Ready{mergeable and mergeable_state clean?}
Ready -->|No| Error[Record mergeError and delayed retry, preserve task status]
Ready -->|Yes| Merge[Request GitHub merge with exact SHA and configured method]
Merge -->|Merged| Ack
Merge -->|Rejected or request fails| Error
Poll --> Later
Error --> Later
Manual[Manual PR close or merge] --> Refresh[Repository scan refreshes tracked PR state]
Ack --> UI[Activity events and TUI polling every 10 seconds]
Refresh --> UI
Local[Task closure finishes with status closed] --> UI
Menu[bot menu: select issue or Repositories] --> Action[Open session, details, close tabs, restart, cancel round, resume tracking, stop and close]
Menu -->|Repositories| Repos[Read connected server inventory, choose repository, show timestamped details]
Repos --> Observe[No task or scheduler mutation, no activation of other owners]
Action -->|Cancel current round| CancelRound[Confirm, stop round, retain PR and issue tracking]
Action -->|Resume issue tracking| Resume[Validate closed task, skip backlog and watch future comments]
CancelRound --> UI
Resume --> UI
Action -->|Stop and close task| Confirm[Confirm stop and close, queue durable closing request]
UI --> Busy{Associated tab busy?}
Busy -->|Yes| Defer[Retry closure on a later snapshot]
Busy -->|No| Tabs[Close known task and helper tabs once, preserve sessions and worktrees]
Status[Independent monitor polling every five seconds] --> Sidebar[Append BOT RUNTIME to existing sidebar]
Selected[Selected session changes] --> Sidebar
Sidebar --> Details[Show owner operations and matching task, or active task fallback]
Status --> Missing[On failure retain last readings and mark stale]
Missing --> Sidebar
Merge eligibility requires done, or watching with a saved publishedHead,
a tracked nonclosed PR, a saved published commit, no
merged flag, no pending feedback, and an elapsed mergeNextAt. Missing
publishedAt in an older queue starts a fresh window for merge comments, which
have no commit binding. Formal reviews still require the exact verified SHA.
Merge checks run when the worker has no execution task to
advance, rather than immediately after every publication.
For each reviewer, the latest APPROVED, CHANGES_REQUESTED, or DISMISSED
review is decisive. Any outstanding changes request suppresses all approval
candidates, including comment approvals. An approval review must reference the
current verified SHA and have a valid submission timestamp; it may precede
publishedAt. Finishing or retrying a PR description update does not invalidate
approval of unchanged code. An approval comment must have been created after
publishedAt and match a configured phrase
as a whole message after case, whitespace, and trailing ./! normalization.
Bot comments and marked automation comments are excluded. Default phrases are
/merge, lgtm, merge, and approved, merge; default merge method is squash.
The permission check then requires an allowlisted candidate with repository write,
maintain, or admin access. GitHub still enforces merge requirements.
Every successful round updates publishedAt, so old merge comments cannot
authorize the next published round. Formal reviews remain valid for the same
commit only; a different published SHA requires a matching review. When the approval method returns false (for example,
no eligible approval or a mismatched head), the dispatcher clears mergeError and schedules another check
after 60 seconds. An approved PR that GitHub says is not ready, a rejected merge,
or a request failure records mergeError; error retries also respect GitHub timing.
An already-merged response can reconcile a previously lost merge response.
Follow-up rounds reset analysis, question, current session, session-stop/recovery state, current completion summary, checks, and commit; they retain the original report, last published body, branch, worktree, pinned base, and previous session reference. After cancellation, the next round preserves that worktree as history and creates a new local branch/worktree from the remote PR head (or pinned base without a PR). Other preparation reuses the saved worktree path rather than deriving a new path from the branch name. A renamed branch can therefore retain its original directory. Preparation, verification, and push all check the managed path, exact Git root, local branch, and shared repository. The remote publication branch stays unchanged. A missing checkpoint directory blocks the task without creating a replacement worktree. A follow-up creates a new main session, whereas an implementation-question reply or workflow recovery retains the current one. Comments received while working, waiting or blocked stay queued until publication of the current round completes. Feedback after closure can still be queued, but the next round's guards block it.
PR-state scanning is independent of auto-merge and issue openness. The TUI
subscribes to activity and polls every 10 seconds, including recovery on startup.
It opens background task tabs when enabled and exposes /bot for task management
and /restartworkflow for operator recovery in the owner project. Commands use
owner-scoped RPC; they are not GitHub comment commands. Activity phases merged
and pr_closed are display values, not new persisted execution phases.
Local statuses closing/closed end tracking; cancelling/watching skip a round
while preserving tracking. They are durable and separate from GitHub PR state.
Closure cleanup includes known earlier-round sessions and media helpers. Busy
tabs wait until idle; cleanup does not delete sessions, interrupt work, or remove
worktrees. A manually reopened tab is not repeatedly closed in the same TUI instance.
Sources: dispatcher.ts — workOnce, mergeOnce, scanOnce, approval.ts, github.ts — mergeApproved, ui.ts, activity.ts, sidebar.ts, runtime-panel.ts.
The runtime sidebar has an independent five-second observation loop with a
four-second request bound and one-second local countdown updates. It combines
automation.github.monitor with scheduler status; requests never advance a
phase. It reports actual in-process worker/scan activity rather than deriving it
from ready. Last successful readings remain visible with stale warnings after
errors or 15 seconds without fresh data. /botstatus exposes a text report even
without a sidebar. See runtime panel details
for task selection, cache observations and display limits.
Phase records where execution stopped. Status records whether it may run. An error normally preserves the phase so retry continues from its checkpoint.
| Status | Meaning and next action |
|---|---|
ready |
Eligible for worker selection when due. |
waiting |
Awaiting an issue answer; no implementation or publication while unresolved. |
retry_wait |
Transient failure; automatic retry after nextAt. |
blocked |
Explicit Blocked or PR-description conflict, or GitHub HTTP 401, 404, or 422; requires inspection/retry, except a stopped session completed manually is reconciled automatically. |
failed |
Other errors reached maxAttempts; operator recovery/retry required unless the checkpoint also qualifies as a stopped-session recovery candidate. |
done |
PR publication/reconciliation completed; feedback and merge monitoring remain possible. |
closing |
Operator requested end of tracking; interrupt sessions and drain in-flight work, retaining errors for retry. |
closed |
Tracking ended locally; preserve history and work, exclude discovery, runtime hooks, execution and merge monitoring until explicit resumption. |
cancelling |
Stop saved sessions and drain the selected round; retry interruption failure without publishing. |
watching |
Round cancelled; no automatic execution replay. Track PR state and new feedback, merge only against a saved published head. |
flowchart TD
Work[Execute saved phase] --> Result{Result?}
Result -->|WaitingForAnswer| Wait[waiting, or ready if answer already arrived]
Result -->|SessionStopped| Stop[running / blocked, sessionStopped true]
Result -->|Other Blocked, description conflict or GitHub 401, 404, 422| Block[blocked at saved phase]
Result -->|PR head propagation or other failure below attempt limit| Retry[retry_wait at saved phase]
Retry -->|nextAt elapsed| Work
Result -->|PR head propagation or other failure at limit| Fail[failed at saved phase]
Stop --> Probe[On available worker pass, probe due saved session without unresolved question]
Legacy[Recognized legacy timeout or outcome block] --> Probe
Probe --> Complete{Matching location and task marker, succeeded outcome and valid final assistant?}
Complete -->|Yes| Rejoin[ready at running, run full session validation again]
Rejoin --> Work
Complete -->|No or probe fails| Retain[Retain block and feedback, probe no sooner than 30 seconds later]
Retain --> Probe
Command[Operator uses restartworkflow] --> Guards{Known actively tracked task, no unresolved question and no closed or merged PR?}
Guards -->|No| Reject[Return actionable error, preserve checkpoint]
Guards -->|Yes| Eligible{Status blocked or failed?}
Eligible -->|No| Noop[accepted false, do not duplicate scheduled or completed work]
Eligible -->|Yes| Safe{Route exists, and running phase is a recognized session stop?}
Safe -->|No| Reject
Safe -->|Yes| Recover[Persist recovery ID for running phase, clear error and attempts, ready at saved phase]
Recover --> Work
Block --> Manual[Operator uses retry while worker and maintenance idle]
Stop --> Manual
Fail --> Manual
Manual --> Restart{restartSession requested?}
Restart -->|No| Reset[Clear error and attempts, ready at saved phase]
Restart -->|Yes| Cancel[Interrupt old session, clear sessionID and promptAttempted]
Cancel --> Earlier[Return to commented if commentID exists, otherwise queued]
Earlier --> Reset
Reset --> Work
Close[bot menu: Stop and close task] --> Flight{Publication or merge already in flight?}
Flight -->|Yes| RejectClose[Reject closure, wait and try again]
Flight -->|No| SaveClose[Persist closing before interruption]
SaveClose --> Interrupt[Interrupt with resume false, missing sessions count as stopped]
Interrupt --> Drain[Wait for current worker and pending question posts]
Drain --> Again[Interrupt again to cover in-flight session creation]
Again --> Closed[Persist closed, preserve history and all local work]
Interrupt -->|Failure| CloseError[Retain closing with error, retry after 30 seconds]
Again -->|Failure| CloseError
CloseError --> Interrupt
Restart[Owner restart with saved closing request] --> Interrupt
CancelRound[Cancel current round] --> Publish{Publication or merge in flight?}
Publish -->|Yes| RejectClose
Publish -->|No| SaveCancel[Persist cancelling, block prompts, hooks and checkpoints]
SaveCancel --> DrainCancel[Interrupt with resume false, drain worker and questions, interrupt again]
DrainCancel -->|Success| Watch[Archive round, clear live errors, enter watching]
DrainCancel -->|Failure| RetryCancel[Keep cancelling and error, retry after 30 seconds or owner restart]
RetryCancel --> DrainCancel
Watch -->|New feedback| NewRound[New round in fresh worktree, retain archived work]
NewRound --> Work
Closed -->|Resume issue tracking| Validate[Require open issue and any known PR, skip observed backlog]
Validate --> SaveCancel
- Task backoff is
min(3600, 5 * 2^attempts)seconds, with the incremented attempt count: the first retry is after 10 seconds. GitHub retry headers can extend it. Default maximum attempts: 5. Successful phase transitions reset attempts, so the limit is not a lifetime cap across all phases. - Scheduler failures use a separate backoff: 5, 10, 20 seconds, and so on, capped at one hour. Successful scans return to the configured scan interval.
- A resumable
runningtask with a saved session takes priority over other work. If its retry time is still in the future, the worker waits rather than starting another issue that could overlap an unreconciled session. - A waiting question whose POST response was lost is republished/reconciled by its marker. Failures in that recovery path retry after 60 seconds.
retryaccepts only blocked or failed tasks and is rejected while the worker or maintenance is busy.restartSessiondoes not delete the worktree or changes; it restarts session execution from the appropriate earlier phase.- Automatic probes select only
runningtasks with a saved session, statusblockedorfailed, a recognized session stop, elapsednextAt, and no unresolved question. Probes run when the worker can begin another pass, not concurrently with an already-running worker invocation. An unsuccessful probe delays the next one by at least 30 seconds. Successful saved sessions re-enterrunningvalidation, then configured checks and publication. This recognizes legacy timeout/outcome errors as well as the persistedsessionStoppedclassification. It never infers success from a clean worktree or an already-pushed commit. /restartworkflowqueues a durable recovery request for a stopped task without resetting its phase, worktree, branch, PR, or feedback. It can be queued while another task works. For a stopped execution, the executor waits for idleness, verifies the original task marker, and sends a checkpointed continuation only if still incomplete. A lost response never replays that prompt blindly. Admission does not interrupt active sessions; normal session deadlines still apply. Unresolved questions and unsafe errors remain blocked. A missing task, pending question, or closed/merged PR produces an error before the status check. Other statuses returnaccepted: false; this means no recovery was queued, not that a running session was stopped. Eligible tasks need a route, andrunningadditionally needs a recognized session-stop checkpoint.- Merge errors use
mergeErrorandmergeNextAt; they do not turn a published task into an implementation failure. - Reloading the owner project after restart restores polling from durable state. Activity events are notifications, not the durable queue.
Sources: dispatcher.ts — workOnce, restartWorkflow, retryOnce, scheduler.ts, state.ts.
Run CLI commands from the primary owner checkout, not a task worktree.
restartworkflow changes dispatcher state; it does not restart the OpenCode
service, resume a paused scheduler, or perform a scan itself.
| Action | Saved phase and session | Effect |
|---|---|---|
| Continue a stopped session in the TUI | Same session, running phase |
Once successful and recognized by the probe, normal session validation, checks and publication resume automatically. |
/bot → Cancel current round, or cancelround KEY |
Archive round and retain PR tracking | Persist cancelling, drain work, then watch new feedback. Next round uses a fresh worktree. |
/bot → Resume issue tracking, or resumetracking KEY |
Preserve closed history and work | Validate GitHub objects, skip old backlog, stop saved sessions and watch future comments. |
/bot, select an issue, then Stop and close task |
Keep phase, sessions, worktree, branch and PR | Persist closing, interrupt saved sessions and drain work, then close local tracking. No GitHub issue/PR close or deletion. |
/bot, select an issue, then Close session tabs |
No checkpoint change | Close idle local tabs only, continue tracking. |
/restartworkflow, then select an issue |
Same phase, session, worktree, branch and PR | Queue recovery for an eligible blocked/failed task. A stopped session may receive one continuation; verification/publication retries its saved stage. |
opencode2-automation restartworkflow 'owner/repository#123' |
Same as the TUI command | Calls automation.github.restartworkflow with { key }, returning { accepted }. |
opencode2-automation retry 'owner/repository#123' |
Same saved phase and session | Clear blocked/failed status while worker and maintenance are idle; it does not send a continuation merely because a session was stopped. |
opencode2-automation retry 'owner/repository#123' --restart-session |
Earlier phase, new session identity on execution | Interrupt the old session and clear its ID and initial-prompt flag; preserve the worktree. Use after inspecting uncertain delivery, not as a routine publication shortcut. |
opencode2-automation resume |
No task checkpoint reset | Unpause the scheduler; accepted task execution has its own loop. |
| Restart service, then activate the owner | Reload durable state | Restore polling and worker selection; preserve unresolved questions and nonrecoverable blocks. |
The queue stores sessionStopped to distinguish execution stops from other
blocks. recovery.id identifies an explicit continuation request and
recovery.attempted records the decision to send it before calling OpenCode.
Both are cleared after successful execution and when the next feedback round
starts. Worktree, branch, pinned base, session history and queued comments remain
separate durable checkpoints. A failed test is never treated as session success.
Regression evidence: core.test.ts, executor.test.ts, runtime.test.ts, lifecycle.test.ts, ui.test.ts.
/bot also exposes the saved error and task identity before any operator action.
Closing is independent of GitHub availability, issue state, PR state, route validity
and pending questions. The durable closed record prevents the same issue key
from being rediscovered until explicit Resume issue tracking. Scans skip closing/closed/cancelling records before PR, missing-issue
and comment reads; late checkpoints and errors cannot reactivate them. There is
no automatic deletion based on an ambiguous GitHub 404 response.
Closing preserves pending feedback and questions as history, but does not process
them. The main runtime and media helper registration reject further task activity.
The task's in-flight worker operation may finish local work before closure completes;
no subsequent verification/publication phase starts. Already-started publication
or merge refuses closure admission. In-flight comments cannot be recalled. Errors
while stopping sessions remain visible as closing, retried after 30 seconds or
from the menu. accepted acknowledges the request, not finished interruption.
The sidebar names up to three blocked/failed/closing/cancelling tasks with their saved errors,
excludes locally closed tasks from live queue counts, and shows a separate closing
count. /bot retains all task records and their actions, including opening the
saved conversation after closure. See runtime management.
While a closure is pending, the dispatcher does not start another worker pass. An unrelated already-running task can finish; scanning continues for other tasks. The monitor reports task maintenance until closure completes.
Round cancellation uses the same interrupt/drain discipline as closure but ends
in watching. It retains pending new feedback, clears the cancelled question and
live error, and archives the stopped round. Exact permission replies for archived
question IDs are ignored. Resume issue tracking is a separate explicit action for
closed tasks: validate the open issue/PR, skip already-observed backlog, then
cancel any saved execution and watch future comments. Historic errors remain in
Show details; closed/watching sidebar snapshots do not display them as live failures.
controlVersion and round ordering prevent late TUI events from reviving old work.
See cancellation behavior
and persistence details.