fix(connector): deliver peer messages as nextTurn, not steer - #8
Conversation
Hitting ESC to interrupt a running OMP turn while a cotal message was waiting bled the message text into the editable composer instead of holding it for redelivery. The connector delivered every inbound mesh message with deliverAs:"steer". When idle that wakes a fresh turn, but a steer arriving while OMP is still tearing down a user-interrupted (ESC) turn folds via agent.steer() into the editable pending- message UI = the composer (agent-session.ts:7466; the ESC path latches #advisorAutoResumeSuppressed at 7766-7768 and isStreaming stays true through the unwind). The connector cannot observe the interrupt to guard against it — AgentEndEvent carries no reason and ExtensionContext exposes no aborting signal — so it must deliver in a mode that is hidden-from-composer by contract. deliverAs:"nextTurn" is that mode (agent-session.ts:7458-7479): idle+triggerTurn routes to the same #promptAgentInitiatedMessage fresh turn as steer, while the still-unwinding case is parked in the hidden #queueHiddenNextTurnMessage queue and redelivered on the next clean turn — never the composer. The connector never intends a mid-turn fold (drive() early-returns while busy), so steer's only distinguishing behavior was exactly the misroute frame; nextTurn loses zero intended behavior and preserves ack-on-turn-end. Regression: interactive-loop.smoke.ts test 9 models OMP's routing in FakeHost (steer-during-interrupt -> composer; nextTurn -> hidden held queue) and asserts a DM queued mid-turn lands HELD not composer, then is delivered + acked on the next clean turn-end. Fails on the pre-fix steer envelope, passes after. Co-Authored-By: seal <noreply@sealedsecurity.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PeerHost.sendMessage contract and its drive() call site now use deliverAs: "nextTurn" with triggerTurn: true. The smoke harness updates its fake host routing to separate turns, held, and composer content, and adds an ESC-interrupt scenario that exercises held redelivery. ChangesnextTurn delivery mode
Estimated code review effort: 2 (Simple) | ~15 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR changes how the interactive OMP connector delivers peer messages.
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (3): Last reviewed commit: "docs(connector): explain why ESC-interru..." | Re-trigger Greptile |
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.
Fix all with cubic | Re-trigger cubic
cubic P3 on #8: the ESC regression asserted the inbox drains on the next clean turn-end, but FakeHost never modeled the held nextTurn content actually being delivered into a turn — so it proved held-AND-acked, not held-AND-delivered (the drain came purely from the connector's surfaced bookkeeping). A future regression where the hidden queue never redelivers would still pass. Model OMP's deferred continuation (#promptQueuedHiddenNextTurnMessages, agent-session.ts:7323-7346) with FakeHost.flushHeld(): it drains the hidden next-turn queue into consumed turn content, exactly as a clean redelivery does. Test 9 now flushes the held message into a real turn and asserts it was consumed BEFORE the clean turn-end acks it — so the ack is proven to follow an actual delivery, closing cubic's gap and mirroring the real deliver-then-ack ordering. Refs #3. Co-Authored-By: seal <noreply@sealedsecurity.com>
…sage Capture the coalescing invariant at the ack site (greptile P1 on #8): a future reader touching ackSurfaced must see WHY a nextTurn message parked during an ESC unwind can't be acked before it's consumed. OMP holds the wire-level agent_end while #promptInFlightCount > 0 and lets a later one supersede it (agent-session.ts:2787-2799), so a wire-level subscriber sees one agent_end at the true settle — the interrupted turn and the deferred continuation collapse into a single post-consume event. Backstop: ackSurfaced drains only front-matching ids, so an unconsumed survivor redelivers (fails safe). Comment only, no behavior change. Refs #3. Co-Authored-By: seal <noreply@sealedsecurity.com>
CodeRabbit flagged step 4 vs task B1 naming the same call two ways (`pi.sendMessage` vs `host.sendMessage`), both attaching a `<CotalInjectionDetails>` type-arg to the non-generic `PeerHost` seam. Name the single send path `host.sendMessage` (host binds to `pi` at `extension.ts:99`) and state the real mechanism: widening the seam's `details` field type-checks the payload; the seam stays non-generic (no type-arg at the call), and OMP's generic `sendMessage<T>` keeps it sound end-to-end. Also correct source line anchors that had drifted to a divergent local checkout: on PR #8, `text = override` is `:74`, the mention-recall caller `:127`, the `formatInjection` call `:79`, and `host` binds to `pi` at `extension.ts:99`. Co-Authored-By: seal <noreply@sealedsecurity.com>
6a9af43
into
cotal-connector-session-title
Structured per-item rendering of inbound peer messages (via sendMessage details + registerMessageRenderer) and renderCall/renderResult on the cotal_* tools to leave OMP's animated-spinner fallback. Two independent workstreams; implementation gated on #8's fork-base clearing, the design record is not. Co-Authored-By: seal <noreply@sealedsecurity.com>
CodeRabbit flagged step 4 vs task B1 naming the same call two ways (`pi.sendMessage` vs `host.sendMessage`), both attaching a `<CotalInjectionDetails>` type-arg to the non-generic `PeerHost` seam. Name the single send path `host.sendMessage` (host binds to `pi` at `extension.ts:99`) and state the real mechanism: widening the seam's `details` field type-checks the payload; the seam stays non-generic (no type-arg at the call), and OMP's generic `sendMessage<T>` keeps it sound end-to-end. Also correct source line anchors that had drifted to a divergent local checkout: on PR #8, `text = override` is `:74`, the mention-recall caller `:127`, the `formatInjection` call `:79`, and `host` binds to `pi` at `extension.ts:99`. Co-Authored-By: seal <noreply@sealedsecurity.com>
Problem
Hitting ESC to interrupt a running OMP turn while a cotal message is waiting bled the message text into Matt's editable composer/text input, instead of holding it and redelivering it as a peer message on the next turn.
Root cause
The interactive connector delivered every inbound mesh message with
deliverAs: "steer"(interactive-loop.ts). OMP routes a steer two ways (agent-session.tssendCustomMessage, 7457-7499):#promptAgentInitiatedMessage— a fresh turn (the happy path).this.agent.steer()(7466), which folds into the live turn and, during an ESC-abort teardown, surfaces into the editable pending-message UI = the composer. ESC latches#advisorAutoResumeSuppressed = true(7766-7768) andisStreamingstays true through the unwind (it also counts#promptInFlightCount, 5211-5213).The connector's no-interrupt gate (
busy, flipped by the externalagent_endnotification) can race ahead of OMP's abort-unwind window, so a flushed message is steered in while OMP is still unwinding → composer bleed.The connector cannot observe the interrupt to guard against it:
AgentEndEventcarries no reason/abort field (shared-events.d.ts:152-155), andExtensionContextexposesisIdle()/hasPendingMessages()but no aborting signal (extensions/types.d.ts:229-234). A "hold only when interrupted" guard would need an OMP-side signal (cross-repo). So the fix must be a delivery mode that is hidden-from-composer by contract, unconditionally.Fix
Deliver with
deliverAs: "nextTurn"instead of"steer"(the contract,types.d.ts:850-859; routing,agent-session.ts:7458-7479):#promptAgentInitiatedMessagefresh turn as steer — happy path byte-identical.#queueHiddenNextTurnMessagequeue and redelivered on the next clean turn — neverthis.agent.steer(), never the composer.The connector never intends a mid-turn fold (
drive()early-returns whilebusy), so steer's only distinguishing behavior was exactly the misroute frame.nextTurnloses zero intended behavior and preserves ack-on-turn-end. Two-line behavioral change (interface option type + the single call site).Regression test (red → green)
interactive-loop.smoke.tstest 9 models OMP's documented routing inFakeHost(steer-during-interrupt → composer; nextTurn → hidden held queue; cited toagent-session.ts:7458-7479so the fake can't silently drift). It asserts the user-visible landing zone, not the envelope string:RED on the pre-fix
steerenvelope (FAIL: 9) message must NOT bleed into the composer (got [...📨 DM from Alice...])), GREEN after the flip. Tests 1-8 unchanged.Verification
pnpm test(connector): peer 10/10, extension 4/4, interactive-loop 9/9 — all green.pnpm typecheck: clean.Stack
Stacked on #6 (session-title) — both are interactive-
loop.ts-side connector fixes, disjoint from the headlessloop.tspath. Kept as a separate PR (distinct fix, independent regression test, independent revertability).Co-Authored-By: seal noreply@sealedsecurity.com