Repository navigation
Conversation
…te boundary fish toggles DECSET 2031 around every prompt repaint — five toggles in ~5ms at command accept, each in its own PTY chunk. Every responder decides from the chunk it observed (correctly, per the stablyai#10817 chunk-final contract), but the reply then crosses a scheduling boundary (renderer IPC, remote protocol, fact relay) before reaching the PTY. By arrival, a later chunk has withdrawn the subscription and fish has handed stdin to a child: the reply lands as literal text or as the child's stdin, corrupting interactive prompts (brew/npx [y/N] confirmations). In-process terminals answer in microseconds and never lose this race; Orca's multi-process pipeline loses it on every prompt accept, doubled because two repaint subscribes are answered late. Byte-level ground truth captured from fish 4.7.1 + Tide behind a nested PTY on v1.4.168 is pinned in the repro test. Main ingests PTY output ahead of every reply route, so it always holds fresher subscription state than the responder that produced a reply. Gate exact CSI 997 report writes at the two write funnels (renderer pty:write provider path and the runtime controller carrying remote-client, CLI, and main-emulator replies) against ingestion-fresh state: - The gate keeps its own bounded raw-byte scan fed from onPtyData, so its state does not depend on fact consumers or per-pane reply ownership. - While a PTY's scan authority is delegated to the daemon the delivered bytes may be gapped: the raw scan stands down and the daemon's relayed 2031 facts feed the state instead. Any signal that can trigger a reply also feeds the gate first, so the gate can only be stricter than the responder that fired. - A ?996n DSR answer reuses the CSI 997 bytes and must never be dropped: observed queries bank one pass each (bounded). - Never-observed PTYs, gaps, and teardown all fail open — no behavior change where main has no state. Refs stablyai#9993
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdded a per-PTY gate for CSI 997 color-scheme replies. The gate tracks mode-2031 subscriptions, delegated daemon decisions, scan gaps, and bounded 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/runtime/pty-color-scheme-reply-write-gate.ts (1)
1-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCondense the module comment.
Lines 1-23 repeat the boundary rationale and timing detail. Keep the protocol contract and issue reference. Move the detailed scenario to a design note or issue.
As per coding guidelines, comments must be concise, non-obvious, and preferably one line.
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f0ecf80a-492d-4f07-b267-7019f40a59f8
📒 Files selected for processing (5)
src/main/ipc/pty.tssrc/main/runtime/issue-9993-fish-prompt-accept-stale-mode2031-reply.repro.test.tssrc/main/runtime/orca-runtime.tssrc/main/runtime/pty-color-scheme-reply-write-gate.test.tssrc/main/runtime/pty-color-scheme-reply-write-gate.ts
|
@coderabbitai Fixing. Will be in the next commit |
|
Tip For best results, initiate chat on the files or code changes.
You are interacting with an AI system. |
…n queries A daemon keep-tail gap can hide a ?2031h, so a raw-scan unsubscribed verdict must not outlive the gap; and a ?996n query split across PTY chunks must still bank its reply slot, so the query scan now carries a bounded incomplete-prefix tail reset on the same delegation/gap boundaries as the 2031 scan.
|
@ferpart Thanks for staying on this, and sorry for the long wait. I picked #9993 up today and used your byte-level trace as the starting point — it's accurate, and the timing capture is what made it possible to test the competing hypotheses instead of arguing about them. Two of your conclusions turned out to be right and load-bearing:
I've opened #13904 with a different fix, and I want to be straight about why rather than just superseding this quietly. Where the write-boundary gate doesn't holdI built a harness around real fish 4.7.1 + node-pty and instrumented whether each reply was stale by main's own view at the moment it was written. It usually isn't: At a 1 ms reply delay the leak is deterministic (5/5) while Two smaller things, for completeness: the predicate is an exact whole-payload match against a 2-element set, so a coalesced What #13904 does insteadStops emitting the reply at all. Replying to Your repro is now a test: The part of your report I under-weightedYour original comment said the bytes are "read as stdin by whatever program runs next" — framed around 997, but the mechanism you described is more general than that, and you were right. With 997 replies fully disabled, fish still leaks: CPR and OSC 11, into the same child stdin. Different cause — #13309's echo-safe path defers OSC and private-DSR replies 21–200 ms while CPR and DA1 go out immediately, and fish uses DA1 as its read sentinel, so anything landing after it is stranded. Deterministic, not racy, on every prompt paint on every POSIX host. Filed as #13892 with a validated fix sketch. So #13904 will not fully close the class of failure you reported — #13892 is the other half, and I'd genuinely value your eyes on it given how well you characterised the first one. Closing this in favor of #13904 — but the diagnosis work here was good and it shortened the path considerably. |
|
Superseded by #13904, now merged as Closing this properly; my earlier comment said I was closing it and I only left the comment, sorry for the noise. Reasoning is in that comment, but the short version: the reply usually isn't stale by main's own view at write time ( The other half of what you originally reported — replies landing in the next child's stdin generally, not just 997 — is #13892, if you want to follow it. |
Refs #9993 (still reproducible on v1.4.168 after #8206 / #8214 / #10817)
Summary
^[[?997;1n^[[?997;1nstill lands in the stdin of whatever runs next after a fish prompt-accept, corrupting interactive prompts (brew/npx[y/N]confirmations,git add -p, any keystroke reader). Reproduced deterministically on v1.4.168.Root cause (byte-level ground truth)
Captured from fish 4.7.1 + Tide behind a nested PTY inside an Orca 1.4.168 terminal. At command accept, fish toggles mode 2031 once per repaint, each toggle its own PTY chunk, five chunks in ~5 ms:
Every responder decision here is correct for the chunk it observed — the #10817 chunk-final contract holds. The replies lose to the next chunk: each one crosses a scheduling boundary (renderer IPC, remote protocol, fact relay) before reaching the PTY, and by arrival fish has withdrawn in a later chunk and handed stdin to the command. In-process terminals answer in microseconds and never lose this race; a multi-process pipeline loses it on every prompt accept, doubled because two repaint subscribes get answered late. No amount of per-chunk decision hardening at the responder sites can close this — it is a race between reply latency and the next chunk.
Fix
Main ingests PTY output ahead of every reply route, so at the moment a reply write reaches the PTY, main always holds fresher subscription state than the responder that produced it. This PR adds a last-moment gate at the write boundary (
pty-color-scheme-reply-write-gate.ts), checked at both write funnels:pty:writeprovider path (writePtyProviderInput)terminal.send, main-emulator query replies)A write that is exactly a standalone
CSI ?997;1n/CSI ?997;2nreport is dropped iff main's ingestion-fresh state says the subscription was withdrawn.Design points:
scanMode2031ReplyDecision) fed fromonPtyData, so its state does not depend on fact consumers or per-pane reply ownership.2031-subscribe/2031-unsubscribefacts feed the state instead. Any signal that can trigger a reply also feeds the gate first, so the gate can only ever be stricter than the responder that fired.?996nDSR answer reuses the CSI 997 bytes and must never be dropped — observed queries bank one pass each (bounded at 4).Reproduction
On v1.4.168, fish 4.7.1 + Tide, in a regular pane or the floating terminal:
The repro test replays the captured chunk stream verbatim through
OrcaRuntimeService.onPtyDataand pins both drops, the pass-while-subscribed case, the daemon fact path, and the?996npass.Also verified live on a dev build: a program that subscribes and stays subscribed still receives exactly one
CSI ?997;1n(gate pass), and the doubled leak is gone.Screenshots
No visual change.
Testing
oxlint(also runs in the pre-commit hook)pnpm typecheck:nodepnpm test(focused): new gate unit suite (15) + repro suite (4), plusorca-runtime(1023),pty(327),terminal-query-responder(66),daemon-background-transient-facts,terminal-output-side-effects,terminal-mode-2031-final-state,terminal-color-scheme-protocol— all passingpnpm check:max-lines-ratchet,pnpm check:reliability-gates,git diff --checkpty-color-scheme-reply-write-gate.test.ts(unit) andissue-9993-fish-prompt-accept-stale-mode2031-reply.repro.test.ts(end-to-end through the runtime ingest path, chunk stream pinned from a real byte trace)AI Review Report
Reviewed with an AI coding agent. Main risks checked and addressed:
?996nanswers share the CSI 997 bytes — covered by pending-query slots; subscribe-and-hold seeds and theme-flip pushes pass because the gate state issubscribed; never-observed PTYs fail open.onPtyData, strictly before renderer delivery and any reply route; delegated (gapped) streams switch to the daemon fact feed, and both boundaries reset the scan carry (mirroringsetTransientFactScanningSuppressedsemantics).clearProviderPtyStatealongside the Phase-5 ConPTY spawn record.Security Audit
No new command execution, filesystem access, network surface, or IPC endpoints. The change only inspects bytes already flowing through existing write/ingest paths and can only drop two exact 9-byte sequences, never inject or transform input. Bracketed paste and batched user input cannot match the exact-write predicate.
Notes
observePtyMode2031Decision— see the module doc.