Skip to content

Orchestration v2: Pi steering, completion notices, limited chats, native forks - #26

Merged
hoangvu12 merged 148 commits into
mainfrom
feat/orchestration-v2
Oct 17, 2026
Merged

hoangvu12 merged 148 commits into
mainfrom
feat/orchestration-v2

Conversation

@hoangvu12

Copy link
Copy Markdown
Owner

Implements the Orchestration v2 spec (local tracker: .scratch/t3-orchestration-v2/spec.md): Pi steering, completion notices, limited chats with account failover, and native forks with harness switch.

Closes the spec t3-orchestration-v2 and tickets (local markdown under .scratch/t3-orchestration-v2/issues/):

  • 01 budgeted fork history
  • 02 Pi prompt delivery hardening
  • 03 Pi steers mid-turn
  • 04 no steer replay after restart
  • 05 structured limit and auth errors
  • 06 limited chats and resume
  • 07 resume at reset
  • 08 harness account sign-in state
  • 09 resume on another account
  • 10 web limited and accounts
  • 11 side-chat creator and message origin
  • 12 completion notice wakes idle parent
  • 13 completion notices: busy parent and restart
  • 14 completion notice ack, stop and cap
  • 15 completion notice rendering
  • 16 MCP orchestration instructions
  • 17 native fork: Codex
  • 18 native fork: Claude Code
  • 19 native fork: Pi
  • 20 native fork: OpenCode
  • 21 fork from any reply
  • 22 fork into another harness
  • 23 fork_chat MCP tool
  • 24 one-way harness switch
  • 25 continue limited chat on another harness
  • 26 web forks and harness switch
  • 27 live verification: ready-for-human, not part of this PR

No local builds or tests were run (user policy); CI on this PR is the verification signal.

A forked side chat's first prompt pasted the entire source transcript as
raw JSON with no size limit (t3-orchestration-v2 ticket 01, spec story 58):
long chats burned the new session's context before it started.

One reusable engine piece, crates/engine/src/transcript_budget.rs, now
builds what a fresh provider session receives instead:

- contents: user and assistant text, command runs (input, exit code,
  output), errors and file-change names; reasoning and attachments stay
  out; continuation fragments join into one message;
- selection: the latest user, latest assistant and first user messages,
  then the newest remainder, newest first, within 16k tokens counted at
  one token per UTF-8 byte; a transcript that fits whole rides complete
  with no note; long items are truncated with a visible marker;
- a coverage header names the omitted count and points at the read_chat
  MCP tool with the source chat's id;
- the new user message is never counted, never shortened.

fork_history_prompt (sessions.rs) routes through it for both the fresh
bootstrap and the owed-history warm/steer/resume paths; the native-command
gate is untouched. Tickets 09, 17-20, 22 and 24 reuse the same builder.

Tests: unit suite in the module (selection, truncation, contents policy,
budget) plus side-chat engine integration tests for a long fork, a short
fork, an oversized single message and the omission header. side_chats was
not run by any CI workflow; ui-tests.yml now includes it. Not run locally
per the verification policy; CI is the signal.

No wire or persisted-name changes (ADR 0005): the builder reads the doc
and writes only the delivered prompt.
…r kinds

AgentEvent::Error gains an optional structured kind (ADR 0005, additive on
the wire): usageLimit {window, resetsAt} or signedOut. The error message
stays the rendering source; older payloads and clients are unchanged.

- Claude Code: the blocking rate_limit_event now reads the CLI's resetsAt
  (unix millis) and classifies as a usage limit with the window label; the
  terse assistant rate_limit code classifies as a usage limit and
  authentication_failed as signed out (parent and subagent paths). Overage
  spending keeps its existing not-a-limit handling.
- Codex: turn-level usageLimitExceeded/rateLimitExceeded (turn/failed,
  turn/completed with an error, the bare error notification, and child
  threads) classify as a usage limit; unauthorized classifies as signed
  out. The reset time comes from the latest account/rateLimits/updated
  snapshot, which the driver now tracks (exhausted window, later reset
  binds). A classified turn failure moves its text onto the Error event so
  the transcript keeps exactly one error part while the kind reaches the
  engine; unclassified failures keep their historical Done-only shape.
- Mock harness's scripted usage-limit error carries the kind too, so
  engine-side Limited state can be tested without a real CLI.
- Wiregen: AgentErrorKind joins the export manifest; the generated web
  types are regenerated by hand to match ts-rs byte-for-byte (no local
  builds per the ticket override; the CI gate verifies).
- Tests: proto wire-shape pin (additivity both directions), Claude and
  Codex normalize unit tests, fake-CLI scenarios (usage-limit,
  rate-limit-completed, auth) and harness-seam assertions for every ticket
  criterion, including no-kind/no-text-change for other errors.
…robe

Orchestration v2 ticket 02 (.scratch/t3-orchestration-v2/issues/02):
make the Pi harness prompt path robust. Pi stays a turn-boundary harness
here; ticket 03 flips that.

- Every prompt - turn starts and mid-run steers alike - goes out as
  `prompt` with streamingBehavior "steer": pi starts a run when idle and
  queues the prompt when an extension run is streaming, atomically
  against settlement. The bare `steer` command and bare `prompt` are no
  longer used.
- The acknowledgement is read by disposition: started (a run began),
  queued (delivered at its user message_start echo - the consumed
  signal), handled (no run; the turn ends once pi reports it is not
  streaming, and any pending delivery record for it is retired). pi
  older than 0.99 reports none: inferred from whether a run starts (the
  settle probe).
- A message counts as delivered only after pi accepts it: a rejected
  prompt never ends the session. The message returns to the driver
  queue (the engine at-least-once ledger keeps its entry until the
  Steered boundary), the rejection surfaces, and the message
  re-delivers once the turn ends - with a bounded retry rate (one
  immediate at-rest re-delivery per acceptance cycle, then the probe
  path and its re-arm delay).
- No fixed acceptance timeout kills the run: pi preflight (extensions,
  the Roboco MCP bridge, compaction) may take arbitrarily long; Stop
  still interrupts it (one Interrupted Done, never an
  error-then-interrupted pair).
- Turn end is agent_settled, then a state probe requiring not
  streaming, not compacting and zero pending messages, guarded by a
  generation counter bumped by agent_start, compaction_start, every
  prompt or steer (send and acknowledgement) and every settle. The
  probe re-arms after every prompt acknowledgement racing it and after
  any busy report; a stranded steer is recovered through clear_queue;
  a probe that keeps failing ends the turn errored - never a silent
  kill, with the failure counter reset for the next turn (T3 bugs
  16846/19983 not copied).

Tests (fake-pi-rpc.sh prior art): handled, rejected-while-streaming,
slow preflight (31 s), stop-interrupts-slow-preflight, settle race,
busy probe re-arm, probe failure, old-pi dispositionless inference.
Not run locally (CI policy).
…classified Done

Review fixes on the previous commit:

- Claude Code's rate_limit_event resetsAt is unix SECONDS, not millis —
  the repo's own live 2.1.228 capture (fixtures/claude/
  live-2.1.228-background-subagent.jsonl) carries 1786985400 against a
  session at 1786971716 (a five-hour window resetting 3.8h out; as millis
  it would be January 1970). The parser now converts seconds → millis
  like the Codex snapshot path, and the fixture/tests feed seconds.
- turn/completed only empties the Done's error text when the classified
  Error event actually carried it (kind without a message no longer drops
  the text silently).
- Comments: the mock knob no longer names the not-yet-defined Limited
  state; the unknown-window "usage" label is documented on the proto
  variant; window_label wording follows the glossary.
- Claude's assistant rate_limit code names the plain "usage" label
  directly instead of fishing the fallback out of rate_window_label("").
Groundwork for completion notices (orchestration v2 ticket 11), all
additive on the wire (ADR 0005):

- a side chat created through the Roboco MCP create_chat / create_chats
  by an origin chat durably records the creating chat and a notify flag
  (default on; notify: false opts out per request). UI forks, blank side
  chats and human CLI creates never carry the mark, so they never notify;
- Run and Steer commands (QueueCommand originChatId), queued rows
  (QueueMessage originChatId) and the user message entries they write
  carry the originating chat as structured data - through run, steer,
  hold-for-turn-end, the Stop that cancels a held send, and the retry
  re-issue (attribution text is unchanged on top of it);
- wiregen: new AgentOrigin type; Chat.agentOrigin,
  SessionMessageEntry / SessionCommandEntry / QueuedMessage
  .originChatId, MutateParams createChat.agentOrigin;
- glossary: Side chat in CONTEXT.md;
- ui-tests.yml: the engine-local step runs --test agent_origin.
# Conflicts:
#	.github/workflows/ui-tests.yml
Orchestration v2 ticket 03 (.scratch/t3-orchestration-v2/issues/03):
messages sent to a working Pi chat reach pi at its next step boundary,
exactly as for Claude Code and Codex, instead of sitting in the engine
queue until the whole run ends.

- The harness declaration and the engine registry descriptor flip to
  StepBoundary together (the engine routes queue holds, the queued row
  action and MCP send_message auto mode off the descriptor; the queue row
  now reads "Steer" for pi). This reverses wave-4 ticket 30 items 8-9 -
  the reversal is recorded in that ticket decision record.
- set_steering_mode "all" is sent at session start unless the user
  configured a mode (pi agent-dir or project .pi/settings.json), so a
  burst of steers is delivered together, in order. Best-effort: an older
  pi without the command logs and runs on its own default. Title runs
  never touch the setting. PiHarness::with_agent_dir pins the settings
  dir for hermetic tests.
- Steers carry their attached images: the mid-run prompt stages the
  mailbox message attachments as inline ImageContent blocks, exactly like
  a turn-start prompt.
- Stop sends clear_queue before abort: steers pi accepted but never
  consumed are cancelled with the turn - the engine drops their
  at-least-once ledger entries on the interrupted exit and their user
  entries already sit in the transcript, the same outcome as a held send
  cancelled by Stop.
- A run that recovered from an error through a successful compaction
  retry ends completed: a successful compaction_end with willRetry clears
  the turn error that triggered it.

Tests (fake-pi-rpc.sh prior art, extended): steering-mode default and the
never-overwrite guard (agent-dir and project settings, session-file
markers); a steer burst delivered together in order; a steer carrying an
image (wire-level base64 assert); stop clearing pi queue before abort
(fixture asserts the wire order and the stranded text); a
compaction-recovered run completing. New engine integration suite
pi_steering.rs (message-queue prior art, but driving the real Pi harness
through the fixture): a mid-run message from a queued row's Steer action
is steered into the live turn - in the transcript, queue empty, turn
still working, one turn, no follow-up re-run - and a Stop with an
unconsumed steer leaves it in the transcript unrun with no re-dispatch.
Registry test: every descriptor mirrors its harness declaration.
ui-tests.yml runs the new suite beside pi_resume. Not run locally (CI
policy).

No wire changes (ADR 0005): SteeringMode already carried step-boundary;
nothing new crosses the wire.
The tracer bullet for native forks (t3-orchestration-v2 ticket 17,
spec stories 57, 58 and 66): forking a Codex chat now continues the
source's real harness session instead of pasting history into a fresh
one, with the budgeted transcript from ticket 01 as the fallback.

- Transcript entries record an additive provider turn reference
  (harness, provider session id, turn ref). A new AgentEvent::TurnRef
  carries it: the Codex driver emits it whenever the app-server adopts
  a turn id (start response, turn/started, steer-as-new-turn), and the
  engine stamps it on the assistant entry when its segment finishes.
- The ForkSideChat RPC records a native-fork intent on the new chat
  (doc meta, additive) when the boundary entry carries a ref, instead
  of only severing the session: source harness, provider session, and
  the inclusive boundary turn.
- The fork's first dispatch injects the intent into RunRequest
  (additive providerFork). The Codex driver calls thread/fork
  {threadId, lastTurnId} instead of thread/resume; on success nothing
  is pasted and the reported session counts as holding the history.
- Fallbacks: no ref (old chats), a changed harness, or a refused
  thread/fork all fall back to the budgeted history — the engine
  attaches the ticket-01 selection as the request's fallback prompt
  and the driver pastes it in front of the user message on the fresh
  thread it starts. The intent is consumed once.
- transcript_budget gains budgeted_transcript_prefix (the selection
  without the user message) for the fallback; the paste path is
  byte-identical.
- Wire: additive only (AgentEvent::TurnRef, RunRequest.providerFork,
  SessionMessageEntry.providerTurn; generated TS regenerated by hand
  in the wiregen layout). The codex fake app-server gains
  fork-success/fork-refused scenarios; the codex test suite is now
  wired into ui-tests.yml (it was not run by any workflow before).

Tests: proto wire round-trips, doc stamp/meta round-trips and salvage,
prefix composition, engine side-chat tests (ref-driven fork, consumed
once, no-refs and changed-harness fallbacks, entry stamping), Codex
harness tests (thread/fork params and no paste, refused-fork budgeted
paste, one TurnRef per turn). Not run locally per the verification
policy; CI is the signal.
The bulk field insertion anchored on the last field-looking line of
each SessionMessageEntry literal, which for the dictation test's
literal was the nested UserInputQuestion's options line — so
provider_turn landed inside UserInputQuestion (which has no such
field) instead of the entry.

Re-audited every RunRequest/SessionMessageEntry literal the insert
touched with a brace-depth-aware scanner (innermost enclosing literal
must be the owning type; every non-spread literal must carry its
field): this was the only misplacement.
Each harness account row gains two additive wire fields (ADR 0005):
sign_in_state (signed-in / signed-out / unknown) and limited_until (the
latest reset among exhausted windows). Older payloads parse as unknown.

Engine (agent_accounts):
- ProbeError::SignedOut is the hard-rejection signal: a saved Claude or
  Grok slot whose own refresh was refused (400 invalid_grant), a fresh
  token the provider still rejects, or a Devin key refusal (keys never
  lapse). It persists in the usage cache and marks the account signed
  out; every other failure (expired-but-CLI-refreshes, rate limit,
  network) leaves the state alone — ambiguous, never signed-out.
- UsageEntry records the sign-in evidence; a successful probe flips it
  to signed-in. A signed-out mark recorded against credentials the slot
  no longer holds reads back as unknown, and fresh credentials lift the
  30s freshness gate, so the reload right after a sign-in re-probes.
- mark_account_signed_out records a run's structured signed-out error
  (ticket 05's kind; the engine wiring calls this) durably, offline.
- The probe's failure reason stays on the row beside cached windows -
  stale meters no longer hide a sign-out (the display bug).

Desktop (Settings -> Accounts and the composer account popover):
- Signed-out rows carry a danger mark, show the reason under/beside
  their meters, and offer Sign in again, which runs the provider's own
  login flow (inline in the popover: browser + poll, or the paste-code
  fallback) for the account's provider group.
- Switching to a signed-out account confirms first.

Wiregen: AgentSignInState joins the export manifest; the generated web
types are hand-updated to match ts-rs byte-for-byte (no local builds per
the ticket override; the CI gate verifies).

Tests: proto wire-shape pin (additivity both directions), usage-entry
state transitions and staleness, the reason mapping, limited_until, and
mock-server flows for the refused refresh, the ambiguous active login,
the sign-in-again round trip, the run-error mark, and Devin revocation.
All not run locally per the ticket override; CI on the PR is the signal.
Brings tickets 02, 03, 05 and 11 (Pi prompt delivery, Pi mid-turn
steering, structured limit/auth error kinds, side-chat creators and
message origins) plus their CI fixes under the native-fork branch.

Three textual conflicts, resolved by keeping both sides:
- schema.rs / SessionMessageEntry.ts: the entry gained BOTH ticket 11's
  origin_chat_id and ticket 17's provider_turn (field order matches the
  merged Rust struct: durationMs, originChatId, providerTurn);
- AgentEvent.ts: ticket 05's error `kind` and ticket 17's `turnRef`
  variant (between userMessage and subagent, per declaration order).

Auto-merges audited and repaired where the two bulk field inserts met:
- my dispatch call site in the new ref test gained the 6th origin arg;
- transcript_budget's test helper gained origin_chat_id (ticket 11's
  insert never reached this branch's file);
- agent_origin.rs (ticket 11's new tests) gained provider_fork and
  provider_turn on its literals.

A brace-depth-aware audit (every SessionMessageEntry literal carries
both new fields, every RunRequest literal carries provider_fork, every
field sits at its literal's own depth) now reports clean; dispatch/
steer call sites all pass the origin argument.
…02/03 follow-ups

# Conflicts:
#	crates/proto/src/entities.rs
#	web/packages/proto/src/generated/index.ts
Completion notices, the tracer bullet (orchestration v2 ticket 12):

- a turn of an agent-created, notifying side chat is notifiable when its
  starting message or a steer within it came from the parent: the run
  handle carries the notifiable parent, armed at dispatch/steer time from
  the durable Chat.agentOrigin row (notify: false opts out; user-created
  side chats have no row and never notify; a user message starting a
  turn resets the claim, a mid-turn parent steer arms it);
- when a notifiable turn ends, the engine records an outcome from its own
  turn-end state - completed (parked session, quiesce watchdog or final
  Done, gated on the completion marker), errored with a one-line reason,
  stopped, or waiting for input with a one-line summary of the question -
  and wakes an idle parent with an engine-authored notice message that
  starts a parent turn on its own harness; a busy parent is left to
  ticket 13 (the notice is never steered mid-turn);
- the notice is a SYSTEM entry, never a user message, carrying a new
  MessagePart::CompletionNotice marker (per side chat: title, full id,
  outcome, one-line detail) with the notice text as a plain sibling part
  so older layouts stay readable; the text names each side chat by title
  and short id and points the agent at read_chat for the full result;
- wiregen: new NoticeOutcome / NoticeSide types (additive, ADR 0005),
  hand-generated TS alongside; ui-tests.yml's engine step now runs
  --test completion_notices;
- base repairs from ticket 11's fallout so the crate compiles: the two
  5-arg dispatch_with_source_context calls in sessions.rs (auto-resume
  now threads the revived user entry's origin; the orphaned-steer
  re-dispatch keeps the steer's origin via RoutedSteer.origin_chat_id)
  and the transcript_budget test helper's missing origin_chat_id.
Wire ticket 08's account state to ticket 05's structured error kind: when
a run's AgentEvent::Error carries kind signedOut (directly, or tagged
inside a subagent event — the child runs on the same login), the engine
resolves the harness's live login and marks its account signed out on the
harness-accounts store the RPCs list. The mark is best-effort (logged,
never failing the run), once per run, and lands before the run's terminal
state settles.

- SessionsEngine gains the accounts hook (set at assembly, replaceable so
  a test's isolated store swaps in over the detected one);
  Inner::note_run_signed_out drives AgentAccounts::mark_live_signed_out,
  which lists first so the live login's slot exists even when no accounts
  view ever ran, then marks.
- AgentAccounts::with_mocked_endpoints: doc-hidden test seam mirroring
  override_cli — every provider endpoint on one local mock server, the
  same set the in-crate provider tests build on (mocked_endpoints, moved
  up from provider_tests).
- Engine integration tests (account_sign_in_state.rs, on the ui-tests
  engine line): a scripted Claude-shaped harness emits the same
  classified error the Claude driver attaches to authentication_failed;
  the run marks the active account signed out through ListAgentAccounts,
  and a later successful probe against the mock usage endpoint flips it
  back to signed-in. A second test covers the subagent-tagged shape.

Not run locally per the ticket override; CI verifies.
Brings the PR branch's Pi mid-turn steering (03), the Codex native fork
with provider turn refs (17) and the CI fixes onto the completion-notice
branch.

Conflicts (both in crates/engine/src/sessions.rs, both the ticket-11
5-arg dispatch_with_source_context fallout the PR branch had also fixed
by passing None): resolved keeping this branch's origin-preserving calls
- the auto-resume re-dispatch threads the revived user entry's origin,
and the orphaned-steer re-dispatch keeps the steer's origin via
RoutedSteer.origin_chat_id, so re-issued parent messages keep their
turns notifiable.

Ticket 17 integration in the new ticket-12 code: SessionMessageEntry
literals gained provider_turn (the engine-authored notice carries none -
no provider turn backs it, a fork falls back to the budgeted paste like
the fork seam), the test RunRequest literals gained provider_fork, and
the transcript_budget test helper kept a single origin_chat_id after the
auto-merge had duplicated it. AgentEvent's TurnRef arm and the fold's
catch-all were already handled by the PR branch side.
# Conflicts:
#	.github/workflows/ui-tests.yml
#	crates/engine/src/sessions.rs
… limits

B1  pi caps a message it keeps rejecting (two deliveries): the turn ends
    errored, never an Error + Completed loop
B2  a failed settle probe clears stale delivery records and drains the
    driver-owned queue like a clean settle (no wedged mailbox)
B3  Stop only takes the already-settled shortcut when nothing is in
    flight; a probe armed mid-run still clears the queue and aborts
B4  a quiescent probe never ends the turn while a prompt or steer awaits
    its acknowledgement (re-arms after the delay)
B5  steer images load before the call is queued: bursts keep mailbox
    order on the wire
B6  a stranded steer recovers as its original message (attachments and
    config intact)
B7  the settle ref capture is bounded and a Stop mid-capture closes the
    turn interrupted at once
B8  the unbounded acceptance is documented as deliberate
B9  pi fork notice uses glossary wording (chat transcript, trimmed)
B10 claude resetsAt parses leniently (number, float, numeric string);
    a malformed value never fails the frame
B11 a claude fork whose first result errors before any assistant output
    retries once fresh with the budgeted transcript
B12 same for a failover resume carrying resume_fallback_history
B13 codex rateLimitExceeded is a usage limit only with a live exhausted
    window; past resets are ignored
B14 pi fork_boundary returns a named ForkBoundary
D   one prompt_with_history helper for every driver's fallback paste

Tests added (not run; CI): pi late-ack, mixed image burst, reject cap,
queued-behind-failing-probe, stranded image steer, Stop with armed probe;
claude fork bad uuid, resume refused after launch, lenient resetsAt;
codex limit classification (live vs lifted reset, bare 429).
No wire changes.
…eader

- NoticeOutcome::verb (crates/doc) is the one outcome vocabulary: the
  engine's notice text and the desktop notice card both read it (the
  card's "stopped"/"interrupted"/"waiting for input" drifted from the
  engine's "was stopped"/...); the web mirror follows.
- roboco_proto::SIDE_CHAT_MESSAGE_HEADER_TAIL: the notifying side-chat
  message header's tail, shared by the MCP server that writes it and the
  transcript that folds it (the MCP use lands with group C).
- agent_accounts: failover_candidates' doc moved off failover_signed_out;
  rank_failover_candidates' doc says the org penalty is the primary key.

Tests updated (not run; CI): desktop + web notice verb tables.
No wire changes.
C1  desktop failover candidates: a failed load is remembered per chat
    (no refetch every render) with a "Retry accounts" pill
C2  fork / continue dialogs take their stale-load guard from a monotonic
    Shell counter (a reopened dialog never receives an old load)
C3  the failover outcome notice is keyed by chat id
C4  cancel_sign_in's CancelAgentLogin is detached, never in action_task
C5  paste-code submit sets "submitting" only after the engine lookup
C6  a refused harness switch restores the previous config and reports
    the reason (desktop failure chip via HarnessSwitchFailed; web draft
    revert + failure notice)
C7  the harness switch has its own task slot (harness_switch_task)
C8  harness_switchable reads the staleness-checked indicator
C9  user-made side chats never inherit the parent's agent_origin
C10 web Resume sends only the continue prompt (bare send): staged files,
    review comments and the typed draft stay untouched
C11 web surface-registry: every hook above the picker early returns
C12 web banner Resume-at-reset / Cancel errors surface via the notice
C13 web fork dialog: a fork settling after close never writes into a
    reopened dialog
C14 web Limited banner gated by the send overlay (limitedBannerShows)
C15 RESUME_CONTINUE_PROMPT emitted by wiregen into @roboco/proto
    constants; the web re-exports it
C16 MCP instructions: the parent hears of a side chat's reply only when
    it did not opt out (notify: false)
C17 MCP fork_chat sends targetDeviceId like the desktop dialog;
    AgentOrigin serialized, not hand-built
C18 continue dialog default pick via resolve_default_harness (test calls
    it); the Limited strip renders from a LimitedAction model the test
    asserts; harness_switch test asserts no resumable session instead of
    the internal empty-string marker
Glossary: continue dialog/banner (harness, chat), fork dialog note
(transcript), MCP fork instructions (chat, harness).

Wire: additive generated constant RESUME_CONTINUE_PROMPT
(web/packages/proto/src/generated/constants.ts, wiregen constants_ts).

Tests added/updated (not run; CI): desktop refused-switch revert, stale
Working unlock, user side chat origin, Limited strip actions; MCP
instructions + fork payload; web bare Resume, fork close race,
limitedBannerShows.
The PR branch now forwards a result frame's text on Done: the two new
Claude launch-probe fallback tests (B11/B12) expect it. The short-id
helpers keep eight characters ("chat-sid"): the engine's char-boundary
test and the web notice tests asserted seven.
…elease under their row id; a refused switch drops the local picks
…fore restart, confirm steers

- user_created_chats_are_never_marked_agent_created seeded only a user
  entry; the fork RPC copies through the latest complete reply, so it
  refused. Seed a settled turn like the agent-fork test does.
- agent_created_side_chats_record_their_creator_and_notify_durably
  reassembled while the first EngineCore (and its data-dir instance lock)
  was still alive. Drop it first, as restart_resume does.
- steer_commands_stamp_their_origin_on_the_user_entry: HeldHarness drained
  steers without a Steered boundary, so the run exit re-dispatched the
  unconfirmed steer as a fresh turn that hung past the single finish.
  The harness now confirms each steer like a step-boundary agent.
…s tests match the engine

- continue_on_harness queue_run sends from the chat row's cwd (/tmp):
  queued rows dispatch from the row and a harness session only resumes
  from the cwd it was opened in, so the follow-up never resumed the
  continuation's session.
- The Limit stand-in steers at the turn boundary like the Claude driver
  and the opening turn runs the held Run's model, so the mid-turn Run is
  held instead of replacing the limited turn; the test waits for the hold
  before ending the turn.
- Waits for a new turn key on the mock holding the run (prompt recorded,
  after subscribing to finish) instead of Working, which publishes before
  run() subscribes and could drop the finish signal.
- The loop-cap test's last wait is for the 27th notice entry; 26 was
  already true before round 27.
@hoangvu12
hoangvu12 marked this pull request as ready for review October 17, 2026 11:10
push_part never wrote DocPartJson.harness, so every HarnessSwitch part
pushed into a session doc read back as an empty text part: the seam was
invisible to the switch tests and to the engine's history-seam gating
(is_harness_switch_seam), so a post-switch send carried no history.
RegistryDoc::upsert_session (and the legacy seed) never wrote
scheduledResume — only the old WorkspaceDoc writer did — so the armed
continuation never reached the workspace mirror: not on arm, not on the
boot republish. resume_at_reset: the scripted harness subscribes to
finish before logging the prompt, and the queued-row test releases the
continuation's turn so the row can follow it.
Scripted takes its turn before logging the prompt, so a seen prompt
means the run already holds its scripted turn and the next run cannot
take it. run_to_idle replaces queue_run + wait_idle: it waits for the
run's own prompt (its Working row is published by then) before the idle
wait, which otherwise passes on the chat's previous Idle row. The 25-round
loop-cap loops wait for each round's notice turn to start before waiting
the parent idle (the notice entry lands before that dispatch).
@hoangvu12
hoangvu12 merged commit 35f1a42 into main Oct 17, 2026
17 of 19 checks passed
hoangvu12 added a commit that referenced this pull request Oct 17, 2026
Orchestration v2 (PR #26):
- Pi steers at step boundaries: messages sent mid-run land in the
  running turn instead of waiting for it to end.
- Side chats report back: a parent chat gets one completion notice
  when its side chats finish, fail, stop or wait for input.
- Limited chats: a chat that hits a usage limit shows when it resets,
  can resume at the reset, switch account (Claude Code, Codex) or
  continue on another harness.
- Native forks for Codex, Claude Code, Pi and OpenCode; other harnesses
  fork by pasting the trimmed transcript.
- Switching a chat's harness carries the conversation over.
@hoangvu12
hoangvu12 deleted the feat/orchestration-v2 branch October 17, 2026 14:52
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