Skip to content

fix(orchestration): reliable subagent launches, prompts, waits, answers and exits - #160

Open
BIackFIame wants to merge 24 commits into
howdeploy:mainfrom
BIackFIame:pr/orchestration-reliability-v2
Open

BIackFIame wants to merge 24 commits into
howdeploy:mainfrom
BIackFIame:pr/orchestration-reliability-v2

Conversation

@BIackFIame

@BIackFIame BIackFIame commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

This replaces #147 (reworked on top of current main after #158 and alongside #159; the old PR is closed with a link here).

Branch pr/orchestration-reliability-v2 (head f12fbb5a, 9 commits), stacked on #159 (pr/bug-fixes, 93cc2bb4), which is on main 0fa6895d. No UI.

Merge order: #159 first, then this PR, then the isolation/safety PR (#161), then the subagent worktrees PR (#162). Until #159 is merged, this PR also shows its 15 commits.

Already in v1.8.2-test.2, and what this PR adds

All nine commits of #147 shipped in the prerelease v1.8.2-test.2 ("subagent account/model inheritance and input delivery from PR #147"). That build was cut from 1.8.1 on a hotfix line, so none of it is on main. This PR brings the same nine fixes to main, reworked for what main has gained since:

The prerelease also carries a maintainer follow-up (1eb22fde: a MODEL_INHERITED code on the inherited-model refusal, and Claude draft-key changes). It is not part of this PR.

Why

In the review of the closed #132/#134 you asked for narrow core fixes, each with a test and no UI, for bugs in the existing orchestration that showed up in live runs. These were:

  • a subagent silently leaving its orchestrator's account (OpenCode then falls back to hosted OpenCode Zen, which is a privacy issue);
  • SIGKILL reported as "done, exit 0";
  • a double --model crash;
  • OpenCode losing its first prompt before its TUI is ready;
  • wait_for_agent outliving the MCP client's tool timeout.

This PR has those fixes, plus two more found the same way: launch-file cleanup order and plain-text subagent output. There is one commit per fix.

What changes for users

  • Account and model: a subagent runs on its orchestrator's model and account (the plugin launch options it inherits), not on the CLI's default. A choice that would give it a second model is refused with a reason. spawn_agent reports servedBy. Launches the person starts are unchanged.
  • Signals: an agent killed by a signal shows as failed with exit 128+n (SIGKILL → 137), the way a shell reports it.
  • First prompt: OpenCode gets its first prompt and its follow-ups only once its input field is up, so the prompt is no longer lost. Codex waits for its composer, as on main since Fix Codex prompt delivery and account-scoped quota status #158. Every send has a 45 s limit with a clear error. Windows keeps the current behaviour for OpenCode.
  • Waiting: wait_for_agent returns within 50 s, under the ~60 s at which MCP clients end a call. On a timeout it returns the progress and asks to call again. A needs_approval prompt is returned once, as with fix: fifteen bug fixes in isolation, orchestration, UI, plugins, GitHub sign-in and tests #159. A later wait waits for the person's answer.
  • Answers: a subagent's answer belongs to the turn that answered the latest prompt, never to an earlier one. With status hooks off, the tools say answerUnavailable: status_hooks_off. Every write of person or agent input goes through one method, writeInput. It keeps this bookkeeping and Fix Codex prompt delivery and account-scoped quota status #158's Codex draft state. A later feature that holds input and replays it therefore counts a replayed Enter as a prompt.
  • Hooks: a turn whose start hook was lost still ends. A failed approved tool no longer leaves the card at "needs approval".
  • Plain-text output: wait_for_agent, get_agent_result, observe_agent and exitLines return the subagent's screen as plain text, not raw TUI escape sequences.
  • Cleanup and quit: closing a card removes its run folder only after its launch files are gone. Quitting runs every shutdown step even when one fails, so PTYs are always hung up and sessions are always saved.

Commits

  1. 94b4e479 fix(terminal): remove a closed card's run folder only after its launch files
  2. d4235812 fix(main): run every shutdown step even when one fails
  3. 9bc86fc1 fix: report an agent killed by a signal as failed with exit 128+n
  4. d34c8505 fix(orchestration): keep the orchestrator's model choice for subagents
  5. 709534b8 fix(agents): type OpenCode prompts only once its input field is up
  6. 6aacab69 fix(orchestration): return wait_for_agent before the MCP client gives up
  7. 48fc8c83 fix(agents): tie subagent answers to their turn and say when hooks are off
  8. fc13bd88 fix(runtime): keep a turn whose start hook was lost, and map PostToolUseFailure
  9. f12fbb5a fix(orchestration): return a subagent's screen as plain text

The messages, authors and dates are the same as in #147. Each commit type-checks on its own, so you can drop any one of them. Commits 5–9 touch the same files, so dropping one of those needs a small rebase.

Verification

Risks

  • Same-CLI subagents now inherit all of the orchestrator's plugin launch options, not only the account. An orchestrator can still name a plugin to choose differently.
  • Any process killed by a signal, the person's own shell included, now shows as failed instead of done.
  • OpenCode's readiness signal is the terminal cursor sequence. If a future OpenCode never shows it, its first prompt fails with a reason after 45 s instead of being lost silently.
  • Orchestrators make more wait_for_agent calls (at most 50 s each, instead of up to 600 s).
  • Rendering the output costs one headless xterm parse of up to 64 KiB per call, which takes milliseconds.

Depends on

#159 (pr/bug-fixes). Both PRs change the same orchestration tool texts (wait_for_agent and send_to_agent rows, catalog descriptions and the orchestrator skill), the same wait loop and PreparedLaunch. Those are the same lines, so the overlap cannot be split, and this PR is stacked on #159 instead of conflicting with it.

If closing the Even G2 companion threw, quitting skipped terminalManager.shutdown(), so PTYs were not hung up and sessions were not saved. Shutdown now runs its steps in order through runShutdownSteps, which logs a failed step and continues.
…e off

A Codex or OpenCode subagent's final answer could belong to an earlier
turn: OpenCode reused its session id as the turn id and reported idle
before its asynchronous answer read finished, and recordAnswer accepted
any answer. OpenCode now names each turn, reports no idle before the
answer is read and drops a read a newer turn superseded; retry counts as
a turn start. A submitted input (not an approval answer, not an empty
Enter in an idle card) starts a new task, and only an answer of the turn
that answers it is kept. A signal of that turn which predates the input
still moves the status, so wait_for_agent ends instead of hanging, but
brings no answer. An answer reported as the agent exits still counts.
main's signal glue is now applyRuntimeSignal, shared with the tests.

With CanvasTTY status hooks off a launch gets no hooks, so no answer can
be captured. Whether a launch has them is decided when it is planned
(deferred plugin launches included). wait_for_agent and get_agent_result
then return answerUnavailable { reason: "status_hooks_off", message } so
the orchestrator reads the output instead.

Every write of person or agent input now goes through one method,
writeInput, which does this bookkeeping; input() and deliverInput call it,
and a later writer that holds input and replays it must call it too, so a
replayed Enter counts as a submitted prompt. It writes nothing into a
process the card no longer runs.
…UseFailure

Once a turn id was active, the gateway dropped every non-start event with
a new turn id. Each hook runs on its own connection and can time out, so
when a turn's start hook was lost the whole turn, including its Stop, was
ignored and the card kept the previous turn's status. The gateway now
tracks whether a turn is open. After an idle, an unseen turn id opens the
next turn; during an open turn, or for an id already seen, the event is
still dropped as out of order.

When a tool the person approved fails, the agent CLI runs
PostToolUseFailure instead of PostToolUse. CanvasTTY did not register it,
so the card stayed needs_approval until the next hook although the turn
went on. It now maps to working.
node-pty reports a process killed by a signal as exitCode 0 plus the
signal. The signal was dropped, so an agent killed with SIGKILL (OOM,
kill -9) was recorded as "done, exit 0": wait_for_agent returned done,
get_agent_result said done, and the saved session restored as exited.

Record such an exit the way a shell does, as 128+signal (SIGKILL -> 137),
mark the session failed, and put the signal name first in the failure
details. Every reader of the exit (wait_for_agent, get_agent_result, the
session store, the HOME failure summary) already derives its state from
the exit code, so they now agree. Exits CanvasTTY causes itself (closing a
card, quitting) are unaffected: those sessions are removed before
the kill, and the exit handler ignores them.
A subagent spawned without launch options ran on its CLI's own default
model and sign-in (for OpenCode the hosted OpenCode Zen model), even when
its orchestrator ran on a model the person picked through a plugin's
launch options. A plugin the spawn does not name now keeps the
orchestrator's own values for a subagent of the same CLI; repeating the
person's choice needs no delegable declaration, and then the normalized
values must equal the orchestrator's exactly (missing keys come from the
orchestrator, never from the plugin's defaults). A subagent of another
CLI must name a plugin whose contribution set the orchestrator's model,
and spawn_agent reports servedBy (CLI, requested model, inherited or
explicit options).

- Recognise every form a CLI takes for its model: --model and --model=,
  -m and -mX for OpenCode, Codex, Qwen, Kimi and Grok, and for Codex also
  --profile/-p and -c/--config setting model, model_provider or profile.
- Refuse a second model (an explicit model next to a plugin that sets it,
  or two plugins that both set it; OpenCode crashes at start on two
  --model values) only for a subagent's launch. A person's own launches
  and restores start as before.
- Inherit an empty choice (a plugin without fields) as chosen. Leave out
  an inherited plugin that was turned off since, unless it set the model
  (then refuse); refuse with a clear reason when its fields changed.
- Reset modelSetBy at the start of each launch.
- Describe inheritance, servedBy and the refusals in the orchestrator
  skill and docs.

The core stays plugin-agnostic: it reads the plugin from the launch
options and the contribution, never from a fixed plugin id.
OpenCode drops every key that arrives while its full-screen view starts
(a few seconds). CanvasTTY typed spawn_agent's first prompt, send_to_agent
and plugin sessions.send as soon as the PTY existed, so an OpenCode
subagent sat at an empty prompt until the orchestrator gave up.

deliverInput now waits, within the same bounded wait it already had for
the launch, until OpenCode can take input: the terminal control sequence
that shows its text cursor on the alternate screen once its prompt has
focus, or a lifecycle hook from its plugin, counted only 2 s after the
launch's first one (the plugin can report before the prompt takes keys).
Only DEC private mode sequences are read; human-readable screen text is
never parsed and no status changes. Other CLIs keep getting input when
the PTY exists.

- The wait is used on macOS and Linux. Windows' pseudo console may not
  pass those sequences, so there input is typed when the PTY exists, as
  before.
- spawn_agent (checks, launch and readiness together) and send_to_agent
  wait at most 45 s, so the orchestrator gets the reason and the session
  id, not an MCP client timeout. Nothing is typed then.
- A handoff waits up to 15 s for the CLI to take input instead of
  failing at once; a caller that does not wait is told the state.
- Sends to one card are delivered in the order they were made, a queued
  send never reaches a later launch, and a send cancelled while it waits
  behind another settles at once and types nothing.
wait_for_agent accepted up to 600 seconds, but the MCP clients of several
supported CLIs (OpenCode among them) end a tool call after about 60
seconds, so an orchestrator that asked for a long wait got a failed call
instead of a result.

A call now asks for at most 100 seconds (default 50). Unless the
orchestrator's CLI is known to let a tool call run longer, a call returns
after at most 50 seconds. A timeout carries the status and the masked
output as progress plus a message to call again, and the tool
description in both catalogs, the orchestrator skill and the docs say
that a timeout means the subagent is still running.
wait_for_agent, get_agent_result and observe_agent returned the raw PTY
tail, which for a full-screen TUI such as OpenCode is cursor moves,
colours and spinner redraws, often starting mid-sequence. They now render
the masked scrollback window (the last 65,536 characters) through the
existing headless xterm at the session's size and return the visible
text, masked again and capped as before (observe_agent within its
maxChars). A cut window starts at its first escape or line break, so the
rest of a cut sequence ("18;9H") is never printed as text.

exitLines were the regex-stripped last 16 KiB, so a TUI that redraws only
the cells that changed read as its redraws joined together, and a line
drawn early and never redrawn (the model's status line) was lost. They
are now rendered by the same headless terminal from the same window,
masked before and after, and the three tools return the same exitLines.
The Kimi provider runs Kimi Code CLI, which keeps its configuration and
OAuth sign-in in KIMI_CODE_HOME (default ~/.kimi-code); the older kimi-cli
uses KIMI_SHARE_DIR (default ~/.kimi). The isolation layer only knew
KIMI_HOME, which neither CLI reads. So:

- a Kimi launch whose home was moved with KIMI_CODE_HOME (an account home a
  launch plugin hands it, e.g. one sign-in of an account pool) did not get
  that folder as its own writable state, and
- other CLIs inside the layer could read ~/.kimi-code, where Kimi Code's
  refresh token lives.

Kimi's own folders are now KIMI_CODE_HOME (or ~/.kimi-code), KIMI_SHARE_DIR
(or ~/.kimi), KIMI_HOME when set, and the usual named folders; all three
variables move Kimi's home. Nothing else changes.
…atus hooks and tools

Grok Build reads hooks only from GROK_HOME/hooks and Kimi Code only from
KIMI_CODE_HOME/config.toml; Kimi also reads its MCP servers (mcp.json)
and permission rules there. CanvasTTY wrote its lifecycle hook overlays
and Kimi's browser/agents configuration into the default homes only, so
a card a launch contributor started in its own account home (an account
pool) ran without working/idle/needs_approval status and without the
canvastty_browser / canvastty_agents tools.

When a launch contributor sets GROK_HOME / KIMI_CODE_HOME to an account
home below its own data folder (<userData>/plugin-data/<pluginId>, from
the host's record), the launch pipeline reports it as the launch's
account home and the same overlays are written there instead, shared by
the cards in that home and removed when the last one ends. A variable
pointing anywhere else, including another plugin's data, leaves the
launch as before. Inside plugin data every folder from plugin-data down
and every file CanvasTTY writes must be real (no symbolic link), or the
launch is refused. The overlays keep the existing lock, journal, backup
and marker rules, so the person's own settings are restored byte for
byte. The checks live in accountHomes.ts and are shared by both
overlays.

Claude Code, Codex, Qwen Code and OpenCode take their hooks from the
launch's arguments or environment and already work in any home.
…ts orchestrator

An OpenCode subagent on a launch contributor's model keeps bash on "ask"
(a launch contributor's other model keeps bash asking, like accept-edits
elsewhere). When no decision service decides, OpenCode's own prompt waits
in the subagent's card for the person. That part already works: the card
turns needs_approval, Needs attention lists it, and wait_for_agent
returns at once with the prompt in output. Two things stood in the way:

- send_to_agent still typed into that card. Any prompt text ends with
  Enter, which answers OpenCode's prompt, so the orchestrator (on the same
  third-party model) could approve the command for the person. It is now
  refused while the subagent is needs_approval, with a reason that tells
  the orchestrator to ask the person and wait.
- wait_for_agent returned the same pending prompt again on every call,
  so an orchestrator could only poll or give up. It now returns a prompt
  once; a later wait on the same prompt (the card has not changed since)
  waits for the person's answer and then for the subagent's turn, so the
  orchestrator carries on in the same turn once the person has answered.
  A new prompt is reported at once again.

Unchanged: bash stays "ask" for a third-party model in every card; a
decision service's deny still fails the call before OpenCode asks and its
allow (from a plugin the person let allow) still answers OpenCode's
prompt once; no verdict is never turned into an allow; person cards and
OpenCode on its own model are untouched; no new UI.

The test runs CanvasTTY's real OpenCode plugin, runtime gateway, decision
hooks, TerminalManager and orchestration tools: allow answers the prompt,
deny fails the call, no verdict reaches Needs attention and the
orchestrator, send_to_agent writes nothing, and the person's answer
resumes the subagent and the orchestrator's wait.
…s a subagent's approval prompt

Asks are answered by the person, or by a decision service the person
allowed, never by the orchestrator. The canvastty_agents tools already
refuse send_to_agent while a subagent is needs_approval. The control
endpoint did not: on an Orchestrator session's own connection, choose
answered Codex's approval prompt ("1. Yes, proceed") and dismiss answered
it with Escape ("No, and tell Codex what to do differently").

On an orchestrator's connection, choose, dismiss and send are now refused
with NEEDS_PERSON (not retryable, nothing is written) while the worker
is needs_approval, the status each CLI's own approval signal sets. For
Codex's choose and dismiss the menu is also checked: Codex can draw its
approval prompt before its PermissionRequest hook's report arrives, and
choose acts on that exact screen. codexApprovalPrompt matches Codex's
command, edits, permissions, network and MCP approval prompts, not hooks
review, folder trust, model or update menus.

Unchanged: menus that are not approvals stay the orchestrator's; status,
screen and result still read; interrupt still stops its own turn (the
asked call does not run); the person's own app-wide connection, the
card's input and decision services are untouched.

Tests run the real TerminalManager, AgentControlService, gateway and
control CLI request path with an orchestrator's session grant: approval
prompts refused before and after the hook report, non-approval menus
chosen and dismissed, the person's connection still choosing, and
send_to_agent refused at every CLI's approval signal and delivered once
the person answered.
…e UI scale

Some UI chrome kept fixed pixel sizes, so Settings -> Appearance -> UI
scale (0.85-1.25) did not reach it:

- The terminal card's options menu (Don't restore this window, plugin
  card actions) had a 12px font and pixel paddings while the card header
  around it scaled.
- The canvas shortcut-hints panel looked the same at 0.85 and 1.25.
- In Settings -> Plugins the plugin description scaled with the settings
  panel, but the permission list, the contribution rows ("App on
  canvas"), the plugin title, author, version, buttons, module and
  native-hook lists kept fixed 9-17px sizes, so at 1.25 one card mixed
  large and small text.

The menu and the hints panel now take their font size from
calc(Npx * var(--ui-scale, 1)) and size their paddings, gaps, radii and
minimum widths in em; the hints icon is sized in em too. The plugin
settings font sizes use the same calc. At scale 1.0 every size is the
same as before. UI_CONTRACT names the menu and the hints panel among the
chrome that follows --ui-scale.

The title bar is left as is: its height is shared with the native
window chrome (macOS traffic-light position, the startup page) and with
every viewport-bound overlay through --titlebar-height.
…like the reference

The Russian canvas hints and Settings still showed English: the
wheel/pinch hint, the hard-coded "Shift + drag" key cap in the hints
panel and the shortcut reference, the canvas navigation override
description ("wheel, pinch и drag ... Drag колёсиком") and the
Settings > Canvas wheel capture strings and Off/On/Key labels.

- Translate them, reusing the existing on/off keys and adding
  shortcutShiftDrag and canvasWheelCaptureKey; the hints use one word for
  the drag gesture ("перетаскивание"). The group-selection label no
  longer repeats its key cap (ru/en). A test checks that these ru strings
  hold no Latin words besides key names.
- The wheel/pinch and navigation override rows in Settings > Keyboard
  shortcuts wrap instead of cutting their longer Russian labels off.
- The hints panel showed raw bindings ("Alt+ArrowUp") while the shortcut
  reference showed "Alt+↑". Both use one formatter now. It maps key by
  key, so on macOS "Meta+Comma" reads "Command+," instead of the
  reference's ",nd+Comma" (the Comma key was replaced inside "Command").

The canvas wheel and navigation settings' text (only Settings shows it)
moves from the startup string table to canvasNavigationText.ts, next to
Settings, so it loads with the Settings chunk; renderer-bundle-splitting
checks it stays out of the startup bundle.
When GitHub returned a token but the /user request hit the request timeout,
the timeout's AbortError was taken for a cancellation: the poll stopped
silently, the flow was cleared and the status stayed "pending", so Cancel
did nothing. Only this flow's own abort now counts as a cancellation; a
timed-out profile request ends the sign-in as failed.
…or body

A transient 429/5xx from the token endpoint with a JSON body such as
{"error":"temporarily_unavailable"} skipped the transient-status check and
ended the sign-in as an unknown provider error after one answer. These
statuses are now retried with backoff whatever the body says; slow_down
keeps its own handling.
browser_click on an engine without layout returns when element.click()
answers; the fake engine reports the navigation it started afterwards.
Until Page.frameNavigated arrives the tab still shows the old document
as ready, so h.ready() could return before the revision advanced and
the test failed under load. h.ready() now takes the revision to wait
past.
"terminal window descriptors persist atomically without scrollback or
environment" saves a descriptor whose cwd is process.cwd() and then
checks the whole file against /buffer|environment|capability|token/.
Run from a checkout whose path contains one of those words (for example
a worktree named after an environments feature), the cwd alone matched
and the test failed on every run. The check now leaves the descriptor's
own cwd value out; everything else in the file is checked as before.
…h files

Closing a card started the run cleanup and the removal of runs/<id> at the same time, and a launch still being prepared (plugin prepare, environment wrap) could write run files after the folder was gone. A throw from the environment wrap also skipped the planned and contributed cleanup.

Contributed launches are now tracked. Closing waits for pending launches, then the serialized exit/restart cleanups, then removes the run folders each pipeline created for this card object, and the card folder only when it is empty, so a card brought back under the same id keeps its new run. An unadopted launch releases its planned spawn and contribution in a finally block, and a launch whose card was closed or restarted in the same tick never asks its plugins. Run folders are removed with retries for files Windows still holds open.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T14:49:49.348565Z
We couldn’t complete this review. Comment @codex review to try again.
9966061 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@BIackFIame

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@BIackFIame
BIackFIame force-pushed the pr/orchestration-reliability-v2 branch from 57fdc89 to 9966061 Compare October 22, 2026 14:43
@BIackFIame

Copy link
Copy Markdown
Contributor Author

Self-review while the bot review fails: fixed two bugs in their commits. A subagent that inherits the person's own pair of plugins (one routes the CLI to another service, one picks the model) is no longer refused as "both set the model". The exception holds only when both are repeated unchanged from the orchestrator; a pair the orchestrator picks or adds itself is still refused, and tests cover both. The plain-text screen now keeps a full-screen CLI's last screen when it leaves it on exit, so exitLines show the error again. Also fixed the wait_for_agent docs row.

@BIackFIame

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

… delegated cards

Agent cards inherited CanvasTTY's whole environment, so every agent, every
subagent and every plugin-started card, isolated or not, received provider
API keys, tokens and organization-level admin keys (OPENAI_ADMIN_KEY,
ANTHROPIC_ADMIN_KEY) exported where the app was started.

Now an agent card never inherits admin credentials (*_ADMIN_*KEY,
*_ADMIN_*TOKEN) or CanvasTTY's own (CANVASTTY_GITHUB_TOKEN and any
CANVASTTY_*TOKEN/KEY/SECRET/PASSWORD); the person's own top-level agent cards
otherwise keep their environment, so CLIs signed in by API key keep working.
Subagents and every card a plugin starts, a plain shell card included since
the plugin drives it, inherit only an allowlist (path, home, user, shell,
locale, terminal, temporary and XDG folders, Windows system folders, proxy
and certificate variables, the CLIs' home folders); what they need beyond
that comes explicitly through a launch contribution's secretEnv. The launch
builders (agent runtime hooks, browser and orchestration bridge, OpenCode
inline config) now read the card's filtered environment instead of the
app's, so they no longer hand the person's OPENCODE_CONFIG_CONTENT to a
delegated agent. A plain shell card the person starts keeps everything but
CanvasTTY's own credentials. CANVASTTY_AGENT_PASS_ENV in the app's
environment lists variables to keep anyway.
…'s shapes

OpenCode's decision bridge sent write, edit and multiedit all as toolName
"edit" with {file_path, content}. A decision service could not tell a
whole-file write from a partial edit, and an edit's replaced text was
dropped, so it could not see what the edit removes.

The bridge now uses Claude Code's names and shapes, which hooks and
decision services already read:
- write: Write {file_path, content}
- edit: Edit {file_path, old_string, new_string, replace_all}
- multiedit: MultiEdit {file_path, edits: [{old_string, new_string,
  replace_all}]}
- bash and apply_patch are unchanged.

Base protection reads file_path for Write, Edit and MultiEdit as before.
A test goes through DecisionHooks: the service gets kind "edit", the
path and both texts, and a MultiEdit outside the project is still
denied. The docs (en/ru/zh-CN, plugin-api.d.ts) list the shapes
OpenCode's tools arrive in.
The HOME limit row showed the used share as a bare "9%" with a bar and a
reset countdown; "used" was only in the hover/screen-reader text. Codex's own
status line shows "% left", so 9 % used read as 9 % left. The row now says
"9% used · 91% left" (ru «9% использовано · осталось 91%»), and the
hover/screen-reader line "9% used · 91% left" («использовано 9% · осталось
91%»). Usage history's latest observation says the same instead of a bare
percent.

The normalizers were right: Codex app-server usedPercent and Claude
utilization are the used share; a test pins both with fixture payloads.
…a way to the setting

A plugin page whose service could not run because the person had not trusted
its native code (or had its module turned off) only got "Plugin service is not
running.". The host now refuses those requests with a fixed text; frames show
it localized (ru/en), and the host notice carries an Open settings button that
opens Settings -> Agents -> Extension native code (or Settings -> Plugins for a
module). A trusted service that crashed or failed keeps the generic text, now
followed by its last error.
…led outcomes

A started GitHub device sign-in could not be cancelled, retried poll
failures until the code expired, and ended silently when GitHub answered
access_denied or expired_token. A sign-in that could not start showed
the raw IPC error ("Error invoking remote method 'github-auth:start':
...") at the top of the Plugins page, in English in the Russian UI.

- GithubAuthService.cancelDeviceFlow() is public and exposed through the
  existing github-auth IPC (main-renderer only) and preload.
- status() reports deviceFlowState: idle, pending, denied, expired,
  failed or cancelled.
- access_denied, expired_token, unknown provider errors and non-transient
  HTTP errors stop polling with that outcome; OAuth error bodies on HTTP
  400 are read as well.
- Network errors, timeouts, HTTP 408/429/5xx and unreadable bodies back
  off and retry, but five failures in a row end the flow as failed.
- slow_down keeps adding five seconds to every later poll.
- The host clock ends the flow as expired even if GitHub keeps answering
  authorization_pending.
- Settings shows a Cancel button next to the pending code and a localized
  status line (en, ru) for the outcome, using existing classes: denied,
  expired and failed use the Settings error style, cancelled is a plain
  status line. A start or cancel failure shows a localized line ("not
  configured" for a build without an OAuth client); the details go to
  the log.

The new sign-in lines live in PluginSettingsSection (the lazily loaded
Settings chunk), not in the startup string table; renderer-bundle-splitting
checks they stay out of the startup bundle.
@BIackFIame
BIackFIame force-pushed the pr/orchestration-reliability-v2 branch from 6e21564 to f12fbb5 Compare October 25, 2026 07:41
howdeploy added a commit that referenced this pull request Oct 26, 2026
Integrate #163 by @BIackFIame, #164 by @CLOSETTTY, and #165/#166 by @teo-nex, with separate maintainer integration fixes. Preserve original contributor commits and authorship. Include the already-merged #158 contribution by @pepechka-kun in the release credits. PRs #159 and #160 remain excluded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant