diff --git a/README.md b/README.md index e2e6e03..ff9d307 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ The tap observes this complete line and queues the same durable route as `#repo:` is therefore an agent/tap convention, not a shell command—at an ordinary shell it remains a comment. Existing windows are never respawned, so recreate a session that was originally started without pushy observation. +Terminal protocol replies and local backspace editing are removed only from +tamq's observation copy, so strict routing remains reliable in TUIs such as +Codex while the wrapped program still receives the original byte stream. The submitted target line is sanitized, sender-labelled, deliberately non-routable, and then followed by exactly one Enter key: diff --git a/SCOPE.md b/SCOPE.md index 45b1646..1a0b40e 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -98,7 +98,7 @@ Not yet suitable: and stronger process-supervision evidence. - Cross-host messaging or use as a general-purpose broker. -The suite currently has 120 passing tests and 77% statement coverage. Coverage +The suite currently has 123 passing tests and 77% statement coverage. Coverage is strongest in durable storage and registry handling, and weakest in the PTY tap and CLI orchestration; PTY statement coverage increased from 23% to 33%, while subprocess behavior is primarily proven by the real-tmux test. The diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 778c123..0293ed9 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -9,6 +9,7 @@ | Kind | ID | Status | Lane | Source | | --- | --- | --- | --- | --- | | workplan | TAMQ-WP-ADHOC-2026-08-24 | finished | — | workplans/ADHOC-2026-08-24.md | +| workplan | TAMQ-WP-ADHOC-2026-08-25 | active | — | workplans/ADHOC-2026-08-25.md | | workplan | TAMQ-WP-0001 | finished | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | | workplan | TAMQ-WP-0002 | active | — | workplans/TAMQ-WP-0002-coordination-engine-adapter.md | | workplan | TAMQ-WP-0003 | active | — | workplans/TAMQ-WP-0003-delivery-reliability.md | @@ -21,6 +22,7 @@ | workplan | TAMQ-WP-0010 | finished | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md | | workplan | TAMQ-WP-0011 | finished | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | | task | TAMQ-WP-ADHOC-2026-08-24-T01 | done | — | workplans/ADHOC-2026-08-24.md | +| task | TAMQ-WP-ADHOC-2026-08-25-T01 | progress | — | workplans/ADHOC-2026-08-25.md | | task | TAMQ-WP-0001-T01 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | | task | TAMQ-WP-0001-T02 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | | task | TAMQ-WP-0001-T03 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | diff --git a/src/tamq/ptytap.py b/src/tamq/ptytap.py index 15ac5a3..d89c299 100644 --- a/src/tamq/ptytap.py +++ b/src/tamq/ptytap.py @@ -3,6 +3,7 @@ from __future__ import annotations import fcntl import os import pty +import re import select import signal import sys @@ -13,6 +14,35 @@ from collections.abc import Callable, Sequence from .broker import InputBroker +ENHANCED_ENTER = re.compile( + rb"\x1b(?:\[13(?:;[0-9:]+)*u|\[27;[0-9:]+;13~|OM)" +) +TERMINAL_INPUT_SEQUENCE = re.compile( + rb"\x1b(?:" + rb"\[[0-?]*[ -/]*[@-~]" + rb"|O." + rb"|P.*?(?:\x1b\\)" + rb"|\].*?(?:\x07|\x1b\\)" + rb")", + re.DOTALL, +) + + +def observed_line(raw: bytes) -> str: + """Return typed text without terminal replies or local editing controls.""" + decoded = TERMINAL_INPUT_SEQUENCE.sub(b"", raw).decode( + "utf-8", errors="replace" + ) + text: list[str] = [] + for character in decoded: + if character in ("\b", "\x7f"): + if text: + text.pop() + elif character == "\t" or character >= " ": + text.append(character) + return "".join(text) + + class PtyTap: """Full-duplex PTY proxy. Input is observed, never rewritten.""" @@ -24,13 +54,19 @@ class PtyTap: def _observe_input(self, buffer: bytearray, data: bytes) -> None: buffer.extend(data) while True: - delimiters = [index for marker in (b"\r", b"\n") if (index := buffer.find(marker)) >= 0] - if not delimiters: + boundaries = [ + (index, index + 1) + for marker in (b"\r", b"\n") + if (index := buffer.find(marker)) >= 0 + ] + if match := ENHANCED_ENTER.search(buffer): + boundaries.append(match.span()) + if not boundaries: return - index = min(delimiters) - raw = bytes(buffer[:index]) - del buffer[: index + 1] - line = raw.decode("utf-8", errors="replace") + start, end = min(boundaries) + raw = bytes(buffer[:start]) + del buffer[:end] + line = observed_line(raw) routed = self.broker.inspect_line(line) if routed and self.on_line: self.on_line(line) diff --git a/tests/test_ptytap.py b/tests/test_ptytap.py index 8b99e91..e1100d9 100644 --- a/tests/test_ptytap.py +++ b/tests/test_ptytap.py @@ -4,7 +4,7 @@ import pty import struct import termios -from tamq.ptytap import PtyTap, copy_winsize, write_all +from tamq.ptytap import PtyTap, copy_winsize, observed_line, write_all class RecordingBroker: @@ -29,6 +29,48 @@ def test_input_observer_accepts_raw_terminal_carriage_returns(): assert observed == ["#activity-core: hello"] +def test_input_observer_accepts_enhanced_terminal_enter_sequences(): + broker = RecordingBroker() + observed = [] + tap = PtyTap(["true"], broker, on_line=observed.append) + buffer = bytearray() + + tap._observe_input(buffer, b"#activity-core: CSI-u\x1b[13;1") + assert broker.lines == [] + tap._observe_input(buffer, b":1u#other: modify\x1b[27;1;13~") + tap._observe_input(buffer, b"plain\x1bOM") + + assert broker.lines == [ + "#activity-core: CSI-u", + "#other: modify", + "plain", + ] + assert observed == ["#activity-core: CSI-u", "#other: modify"] + + +def test_input_observer_ignores_terminal_replies_before_typed_line(): + broker = RecordingBroker() + observed = [] + tap = PtyTap(["true"], broker, on_line=observed.append) + buffer = bytearray() + + tap._observe_input(buffer, b"\x1b[9;1R\x1b[?1;2;4c") + tap._observe_input(buffer, b"#activity-core: Helol\x7f\x7flo\r") + + assert broker.lines == ["#activity-core: Hello"] + assert observed == ["#activity-core: Hello"] + + +def test_observed_line_removes_common_terminal_protocol_wrappers(): + raw = ( + b"\x1b[200~#target: pasted\x1b[201~" + b"\x1b]10;rgb:ffff/ffff/ffff\x07" + b"\x1bP1$r0m\x1b\\" + ) + + assert observed_line(raw) == "#target: pasted" + + def test_copy_winsize_preserves_rows_columns_and_pixels(): source_master, source_slave = pty.openpty() target_master, target_slave = pty.openpty() diff --git a/workplans/ADHOC-2026-08-25.md b/workplans/ADHOC-2026-08-25.md new file mode 100644 index 0000000..bca0a8e --- /dev/null +++ b/workplans/ADHOC-2026-08-25.md @@ -0,0 +1,42 @@ +--- +id: TAMQ-WP-ADHOC-2026-08-25 +type: workplan +title: "Recognize enhanced terminal submission keys" +domain: communication +repo: tmux-amq +status: finished +owner: codex +topic_slug: coulomb-social +created: "2026-08-25" +updated: "2026-08-25" +--- + +# Recognize enhanced terminal submission keys + +## Decode Codex TUI line submission in the PTY tap + +```task +id: TAMQ-WP-ADHOC-2026-08-25-T01 +status: done +priority: high +``` + +Reproduce the missing `#repo:` route with a real isolated Codex TUI, extend +input observation to recognize enhanced terminal Enter encodings without +rewriting pane input, add regression and real-Codex smoke evidence, install the +fix, and update the operator documentation. + +## Completion evidence + +- Reproduced the failure with two isolated real Codex 0.149.1 panes: Codex + submitted the source line but no durable message was created. +- Raw input capture showed CSI cursor/device replies preceding the typed route; + these contaminated the tap's observation buffer before the terminating CR. +- The observer now removes terminal protocol sequences and applies local + backspace editing only to its parsing copy while forwarding source bytes + unchanged. +- A network-disabled two-Codex rerun created one durable `#audit-core:` message, + transitioned it to `injected`, and showed the `# from flex-auth:` prompt in + the target's queued follow-up inputs. +- Full checks and installed-package verification passed. No user session or + durable user message was changed during diagnosis.