Skip to content

test(opencode): start the Tab agent-switch checks from a closed autocomplete - #2223

Merged
devswha merged 1 commit into
mainfrom
fix/flaky-opencode-tab-switch
Oct 28, 2026
Merged

devswha merged 1 commit into
mainfrom
fix/flaky-opencode-tab-switch

Conversation

@devswha

@devswha devswha commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Found by the main-integration lane: Integration failed on main run 40643422650 (7dc187a).

Cause (test flake from #1991's test order, not a regression from #2185 or #2200)

refuses a confirmed Tab agent switch during immediate delivery without Enter runs right after delivers a queued message completely when Tab leaves the skill unconfirmed, and that test leaves @effect's autocomplete row selected (the recorded screen whose status row names no agent). The stand-in redraws only when it is typed to. If herdr has not yet drawn the screen that the next send's own @effect produces, typeWithSkills (server/opencode-skill-attach.ts) reads the old box. The agent before Tab is then unknown, and the Build to Plan switch cannot be seen, so the send succeeds. The queue case then fails on the outcome the immediate case left behind.

Reproduced: delaying the stand-in's @effect redraw by 400 ms fails both cases 3 of 3, with CI's ok: true and the recorded outcome.

Change

  • The stand-in draws the recorded closed-autocomplete screen on Ctrl-L.
  • closedPrompt() sends Ctrl-L, then waits until the stand-in has logged it and herdr shows no selected @effect row. Both Tab-switch cases call it before they note what was typed.

Proof

Test-only. Splits: #2222 and #2224.

Reviews

  • omo-native-code-reviewer, round 1 on the combined change: BLOCK with two HIGHs, both fixed. One was a loop shape that scripts/conventions.test.ts counts as a fixed sleep; the other was the OpenCode helper not waiting for its own Ctrl-L. Round 2 on these heads: APPROVE, no findings above LOW.
  • Test-only, so no Codex review (notes/pr-policy.md: the test-only gate is omo alone).

…omplete

The test before them leaves @effect's row selected on screen. A send that read that old screen before the
stand-in's own redraw had no agent to compare after Tab, so the switch went unseen (main run 40643422650).
The stand-in draws its closed prompt on Ctrl-L, and the checks wait for it.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between b257be8 and 31f4d5f.

Files selected for processing: 2
  • changelog.d/integration-flake-opencode-tab-switch.md
  • server/opencode.contract.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devswha added a commit that referenced this pull request Oct 28, 2026
…e, after an unobserved switch-back (#2224)

Found by the main-integration lane: Integration failed on run
40646297534 (ci/self-hosted-runner-cache, main 70e19f4).

## Cause (test flake)

The Copilot stand-in writes a `:switch` as two log lines,
`Unregistering` and then `Registering`. A conversation read between them
rightly fails closed: scrollback, no turns, and another `history_id`.
The test's wait for the unobserved switch-back stopped at any
`history_id` change, so it could stop on that read and then fail with
`turns[0]...text` `undefined`.

**Reproduced:** with 400 ms between the two writes, the old test fails 3
of 3 with CI's `Expected: "...a03" Received: undefined`, and the fixed
one passes 3 of 3.

## Change

The loop waits for the Copilot transcript itself: source
`copilot-transcript`, a new `history_id`, and the resumed session's
first turn. It still throws after 5 s if the binding never advances.

## Proof

- **Stress:** `server/copilot.contract.test.ts` passed 10 of 10 on CI's
Node 22 and herdr 0.9.3.
- **Local note:** this file cannot run on herdr 0.9.5 with Node 24,
because the stand-in's process shows as `MainThread` and herdr never
names it Copilot. The runs above use CI's versions.
- **Local checks:** `bun run check fast` ok.

Test-only. Splits: #2222 and #2223.

## Reviews

- **omo-native-code-reviewer**, round 1 on the combined change: BLOCK
with two HIGHs, both fixed. One was a loop shape that
`scripts/conventions.test.ts` counts as a fixed sleep; the other was the
OpenCode helper not waiting for its own Ctrl-L. Round 2 on these heads:
APPROVE, no findings above LOW.
- Test-only, so no Codex review (notes/pr-policy.md: the test-only gate
is omo alone).
devswha added a commit that referenced this pull request Oct 28, 2026
…he first agent ends (#2222)

Found by the main-integration lane: Integration failed on main run
40644839354 (30cd84f) and on #2176's run 40643516119.

## Cause (test flake, not a product regression)

`a create request's first message > is bound to the agent process it was
queued under: a replacement of the same kind ...` started SESSION2 from
the shell (`recorder SESSION1; exec recorder SESSION2`). Between SIGKILL
of SESSION1 and the `exec`, bash alone is in front. A pane event in that
gap makes `launchGone` (shell back in front, `server/core/pending.ts`)
hold the message as `first_message_launch_ended`. That is the documented
behaviour from #1969: the launched agent did end, and no agent was in
front. The test expected `pending_target_changed`, so it failed whenever
a read landed in the gap.

**Reproduced:** keeping the shell busy for a moment before the `exec` (a
builtin loop, so the shell itself stays in front) fails the old test 3
of 3, with exactly CI's `Received: "first_message_launch_ended"`. A
`sleep 1` there does not reproduce it, because `sleep` is another
process and reads as a changed target.

## Change

- The npm-style launcher fixture now also handles SIGUSR1: it starts
SESSION2, and ends SESSION1 once SESSION2 has spawned, so an agent is in
front throughout. Its exit-driven path (used by the #1545 test) is
unchanged.
- The test uses that launcher and waits until herdr no longer lists
SESSION1 before it reports idle.

## Proof

- **Mutation:** with `samePendingIdentity` ignoring process pid and
birth, the new test fails 2 of 2 (the message is typed into SESSION2).
- **Stress:** the first-message block plus the two replacement tests (21
tests) passed 10 of 10 on CI's Node 22 and herdr 0.9.3, through `dori
heavy` on a loaded host.
- **Local checks:** `bun run check fast` ok.

Test-only: no app code changes. Splits from the same lane: #2223 and
#2224.

## Reviews

- **omo-native-code-reviewer**, round 1 on the combined change: BLOCK
with two HIGHs, both fixed. One was a loop shape that
`scripts/conventions.test.ts` counts as a fixed sleep; the other was the
OpenCode helper not waiting for its own Ctrl-L. Round 2 on these heads:
APPROVE, no findings above LOW.
- Test-only, so no Codex review (notes/pr-policy.md: the test-only gate
is omo alone).
@devswha

devswha commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

Merging 31f4d5f, a test-only fix from lane main-integration for the OpenCode v2 native Tab switch Integration failure seen on main. The Tab agent-switch checks now start from a closed autocomplete. Codex reviewed it and found no issues; its virtual-time probes support the stated failure mechanism and the fix. No production change and no fixed sleep were added. The required checks are green, and CodeRabbit did not review this head.

@devswha
devswha merged commit da41b2c into main Oct 28, 2026
20 checks passed
@devswha
devswha deleted the fix/flaky-opencode-tab-switch branch October 28, 2026 08:24
devswha added a commit that referenced this pull request Oct 29, 2026
…s last pane stopped being OpenCode (#2389)

Found by the flake-hunt lane: Integration failed once on 2026-10-28
12:54Z (job 122825435909), after #2223, at
`server/opencode.contract.test.ts` "OpenCode v2 native service > closes
the shared stream when its last verified pane ends or stops being
OpenCode". The error was "the stream closing after the last verified
pane stopped being OpenCode did not happen (native
{"stream":true,"panes":["w8:p1"]})". A rerun passed.

## Cause (product race)

Native OpenCode ownership (`verifiedPanes` in
`server/opencode-native.ts`, and the shared event stream to `opencode
serve`) was released only by `onStatus` naming another agent and by pane
end (`server/core/status.ts`). The status collector
(`server/collector.ts`) does not always call `onStatus`:
- It stores a first-seen pane as a baseline without one.
- It replays a gap by comparing status, not agent.
- It recovers a lost subscription through `onResync`.

So when a pane stopped being OpenCode and the bridge learned it only
from a snapshot, the stream stayed open with nothing left to release it:
an event stream to `opencode serve` held for good. The CI failure is one
such ordering.

**Reproduced:** with the real collector and the status hub's callbacks
(`server/core/status.test.ts`, "native OpenCode ownership
reconciliation"), a first baseline, an equal-status gap and a resync
that each show the pane no longer OpenCode all leave `{ stream: true }`
on origin/main: 6 cases fail.

## Change (`server/core/status.ts`, `server/opencode-native.ts`)

- **When a snapshot releases:** a reconciled snapshot releases a
verified pane when it names another agent, or a known status without an
agent.
- **Handover kept:** a pane with no agent and status `unknown`, as
during a server handover, is kept. The exception is when
`pane.process_info` proves the shell alone is in front: one foreground
process that is the shell by name and argv, not just by pid, since `exec
opencode` keeps the shell's pid. That read happens only for verified
panes in that state, and not on Windows, which reports only the shell.
- **Older snapshots lose:** a verification generation read before each
snapshot keeps ownership verified since then. Only verified panes are
released, so an older snapshot cancels neither a newer verification nor
one still in flight. Replay releases go through the same guarded path.

## Proof

- **Regression cases:** 21 passed and 6 failed before the fix. All 28
pass after, including exec, handover, newer verification and in-flight
verification. Each guard's cases fail when that guard is removed
(checked by both reviewers).
- **Related suites:** `server/core/status.test.ts`,
`server/collector.test.ts` and `server/opencode-native.test.ts` pass
149/0.
- **Contract:** `bun run check run bun test --timeout 30000
./server/opencode.contract.test.ts` passed 24/24, three times in a row.
- **Local checks:** `bun run check fast` ok.

## Reviews

- **Codex Sol 6.1 (fast, xhigh):**
  - Round 1: BLOCK, with two MEDIUMs.
- The shell was proven by pid alone; after `exec opencode` that released
a live pane.
    - An older snapshot released a newer verification.
  - Both fixed.
- Round 2: APPROVE with no findings. It removed each guard in turn and
saw its cases fail.
- **omo-native-code-reviewer:**
- REQUEST_CHANGES on one MEDIUM: snapshots also released never-verified
panes, cancelling a verification still in flight and its send watches.
Fixed: only verified panes are released, with a regression case.
  - Delta re-review: APPROVE.

## Known limits (LOW, not fixed here)

- **Late verification:** a verification that starts while the pane is
labelled OpenCode, during an event gap, can still complete after a newer
snapshot shows another agent. The pane then holds the stream until the
next release trigger: an agent-change event, pane end, a failed
re-verification, or the next snapshot. A precise fix would compare a
verification's start with the snapshot's read time.
- **Live events:** a live event with no agent and status `unknown` still
releases without shell proof, as before this change.
- **Shell list:** it lacks pwsh, xonsh and elvish. That fails safe: the
stream stays open.


## Second commit: a re-verification begun after a snapshot's read

The merge pass blocked 0c9ceca on one reproduced MEDIUM: an older
status snapshot could cancel a newer in-flight re-verification of an
already-verified pane. The checkpoint guarded only completed
verifications, so the successful session lookup was discarded and the
shared stream closed under a pane that was still OpenCode.

**Change:** each verification is stamped when it starts, from the same
counter. `releaseOpenCodePane(pane, checkpoint)` stands down while the
pane has a verification begun after that checkpoint; the newer attempt
settles the pane itself. Releases without a checkpoint (status events,
pane end) still cancel everything in flight.

**Proof:**
- **Regression:** a new test with prior ownership in
`server/opencode-native.test.ts` failed before the fix (the
re-verification resolved `null` and the stream closed) and passes after.
- **Related suites:** 150/0.
- **Contract:** the opencode contract file passed 24/24 three times.
- **Local checks:** `bun run check fast` ok.

**Reviews:**
- **Codex Sol 6.1 (fast, xhigh):** APPROVE, no findings. It ran 100
repetitions in a source harness.
- **omo-native-code-reviewer:** APPROVE, with LOW notes:
- With an older and a newer verification overlapping, the early return
spares the older one too. Two overlapping verifications can already race
this way.
- Two early exits of a newer attempt (process roots, a missing session
id) return without releasing, so the stream waits for the next trigger.
- No test covers a checkpoint release cancelling a verification begun
before it.
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