feat: terminal rebind hardening — metadata re-keying, stale-bounce suppression, plugin heartbeat, rebind e2e - #573
Merged
Merged
Conversation
Load-bearing assumption pass (6 verified, 4 falsified) drove: - new Task 0: mandatory merge of origin/main @ e1f4d4c (#571) before implementation; anchors refreshed to the post-merge tree - Task 4 identity guard scoped to opencode (claude/codex have identical swap windows; all-provider guard would bounce legitimate input) - fail-open coverage claim rewritten (REST/fresh-agent panes are permanently unresolved, not a ~2s spawn window) - Task 5 wiring test re-keyed on the '[Resume blocked]' xterm write (expected!=current takes the notice-only branch; old RED was invalid) - documented accepted residual: pre-fold bounces are visibly refused (no cross-channel ordering guarantee exists) - Task 8 never-steal negative gains a WsCapture liveness sentinel (bus lag closes captures with 4008, making negatives vacuous)
…nalDurableSessionRefUpdate
…fold - Extend createState test fixture to accept tabOverrides for metadata setup - Add updateTab to imports for asserting on tab update actions - Wire caller in reconcileTerminalSessionAssociation to pass tab.sessionMetadataByKey to buildTerminalDurableSessionRefUpdate - Verify metadata is re-keyed (ses_old → ses_new) on server-authoritative rebind - Test verifies: result='reconciled', updateTab dispatch, re-keyed metadata, and flushPersistedLayoutNow dispatch Generated with Amplifier Co-Authored-By: Amplifier <[email protected]>
…association fold Add two characterization guards for the duplicate rebind broadcast path: 1. Stale repeat whose previousSessionId no longer matches the pane current ref 2. Identical repeat that neither re-dispatches updateTab nor flushes layout again Both tests verify idempotent behavior on repeat broadcasts when the pane and tab have already been updated to the new session reference. Generated with Amplifier (https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <[email protected]>
…MISMATCH carrying actualSessionRef 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <[email protected]>
…bounces When the server bounces an in-flight input frame carrying the OLD sessionRef (Task 4's guard) but the pane has already folded the new ref, the echoed actualSessionRef equals the pane's current ref -- the race is already self-healed and the '[Resume blocked]' notice is spurious. Add an exported pure helper isStaleSessionIdentityMismatch (suppress iff BOTH refs sanitize to full refs AND are equal; absent/partial refs fail toward the visible path) and an early return at the top of TerminalView's SESSION_IDENTITY_MISMATCH handler. Real mismatches still notice/repair. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <[email protected]>
…hen a pane never says hello
Teach the opencode signal drain to discriminate {"hello":true,...}
plugin-alive heartbeats (delete-on-read) from rebind signals
(act-then-delete, unchanged), returning both via the new
OpencodeDrainOutcome. A HelloTracker behind Arc<Mutex<..>> inside the
watcher keeps drain_and_rebind_opencode's public signature unchanged,
and a once-per-terminal opencode_rebind_heartbeat_missing WARN fires
for RUNNING opencode panes older than OPENCODE_HELLO_GRACE_MS (120s)
that never said hello, suppressed when the server's own env shows
injection was deliberately skipped.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <[email protected]>
- Add resolveSignalTarget() helper to extract home/terminalId resolution
- Implement emitHello() that writes a one-time hello signal at TUI startup
- Refactor createEmitter() to use the shared resolveSignalTarget()
- Hello signal: filename <terminalId>__<nonce>.json (14-digit ts, 6-digit seq, pid)
- Body: {"hello":true,"source":"opencode-tui-plugin"}
- Never throws into TUI (writer exceptions swallowed)
- Add unit tests for emitHello() with DI pattern
- Add integration test with real temp home and hostile API surface
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <[email protected]>
First browser-level proof that the full signal-driven rebind stack composes: a Playwright spec drives a real browser against a real Rust server, writing atomic signal files into the swept opencode signal dir. Legs: rebind broadcast with exact [sesA, sesB] sequence, client Redux fold, reload persistence (freshell.layout.v3), post-restart respawn argv (--session sesB, never sesA), never-steal refusal of a pane-2-owned id, and a same-capture positive liveness control (LB9) that keeps the negative window non-vacuous. Deviation from the task brief (spec-realism fix, no production change): signal-minted ids are seeded as session rows into the fake's opencode.db first. The reconcile existence probe correctly adjudicates a diskless id dead_session/session_not_on_disk on the post-restart respawn path; in production the TUI plugin only signals ids opencode has already persisted, so the seed models the real drifted-session shape. Registered rust-only (RUST_ONLY_SPECS + rust-chromium testMatch): the signal-file rebind lane exists only on the Rust server (opencode_signal.rs). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <[email protected]>
pull Bot
pushed a commit
to HinchK/freshell
that referenced
this pull request
Jul 29, 2026
…danshapiro#574 codex-lane-self-healing) into rebind-review-polish Semantic merge of the opencode signal lane, preserving both sides: - main's OpencodeDrainOutcome split (rebinds + hellos), plugin-alive hello delete-on-read, HelloTracker with once-per-pane grace-window warning - branch's SignalDisposition ladder (Acted/Retain/Discard) with explicit foreign-provider signal discard, stale .tmp staging reap, and all other polish items (deterministic claude drain ordering, portable timestamp-first nonce, sidecar guard parity, warn-once fork-ambiguity, percent-encoded plugin URL, codex_locator test split) Conflicts (crates/freshell-ws/src/opencode_signal.rs): - drain() doc + signature: kept OpencodeDrainOutcome return with combined doc covering hellos AND stale-.tmp reaping - drain_and_rebind_opencode(): kept main's hello-tracker pass, folded the rebind loop onto the branch's SignalDisposition match - adapted branch test drain_reaps_stale_tmp_staging_files_but_keeps_fresh_ones to the OpencodeDrainOutcome shape Hello files are discriminated at parse time (before the rebind ladder), so the foreign-signal Discard path can never consume a hello as "foreign". Verified: cargo fmt --check, clippy -D warnings, cargo test --workspace (2198 passed / 0 failed), contract freeze (test:port + regen idempotency + freshell-protocol --locked), coordinated npm run check. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardens the recently-merged terminal session rebind lanes (#567/#568) with four vetted salvage ports from a discarded parallel implementation plus one new improvement:
(1) Client metadata re-keying on rebind: Client fold now re-keys tab.sessionMetadataByKey old→new on terminal rebind, fixing stranded tab metadata across all three providers.
(2) In-flight terminal.input verification + stale-bounce suppression: Server now verifies in-flight terminal.input against the session the client believes it targets. Client suppresses stale SESSION_IDENTITY_MISMATCH bounces that are mere echoes of an already-folded rebind (opencode-scoped; a keystroke inside the sub-second swap window may still show one truthful "[Resume blocked]" notice — documented trade-off).
(3) Duplicate-rebind idempotence: Client tests verify no double persistence flush on rebind.
(4) End-to-end browser testing: New rust-only browser e2e proving the full opencode rebind stack end to end — signal → broadcast sequence → client fold → localStorage persistence → restart respawns --session → never-steal refusal with liveness control.
(5) Plugin heartbeat and observable degradation: Opencode rebind plugin now emits a plugin-alive hello at TUI startup. Server WARNs once (rate-limited) if an opencode pane never says hello — makes silent rebind degradation (plugin API drift after opencode self-update) observable.
Testing: All gates green:
Reviews: Both independent cross-model reviews (plan and delta) passed first-round with zero blockers.
Co-Authored-By: Amplifier [email protected]