feat: add readable duplex messaging
Some checks failed
tamq-ci / test (push) Failing after 6s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
tegwick 2026-08-25 19:26:27 +02:00
parent 788eb8e2ed
commit 92881b6b56
35 changed files with 1206 additions and 806 deletions

163
README.md
View file

@ -4,7 +4,7 @@ Tmux Agentic Message Queueing (`tamq`) provides repository-aware tmux sessions
and a local, durable message queue. It does not assume who or what uses a pane.
The local alpha provides tmux endpoint lifecycle, local SQLite history and
leases, direct `@repo:` routing, JSONL export/replay, and a Unix-socket protocol
leases, readable `To:` routing, JSONL export/replay, and a Unix-socket protocol
for a later coordination-engine adapter.
## Install and start a local session
@ -27,8 +27,8 @@ tamq flex-auth audit-core
```
This creates or reuses the managed `tamq` session, opens same-named windows in
the exact gita paths, leaves tmux's ordinary interactive shells untouched, and
attaches to the first window. The explicit form is equivalent:
the exact gita paths, starts an ordinary shell behind tamq's transparent PTY
observer, and attaches to the first window. The explicit form is equivalent:
```bash
tamq start flex-auth audit-core
@ -64,111 +64,80 @@ tamq --mode inbox flex-auth audit-core
`--no-display` remains a compatibility alias for `--mode inbox`.
Experimental `pushy` mode submits each routed message to the target pane as
input. It is intended for coding-agent interfaces that queue user prompts:
Experimental `pushy` mode places a routed message in the target input buffer
without Enter. `trigger` places the same input and submits it exactly once:
```bash
tamq --mode pushy --command codex flex-auth audit-core
tamq --mode trigger --command codex flex-auth audit-core
```
For newly created windows with an explicit command, pushy mode automatically
places that command behind tamq's transparent PTY tap. Inside the coding-agent
input, use `#repo:` to avoid interfaces that reserve `@` for file mentions or
templates:
All messaging-enabled windows use the same terminal-neutral PTY observer,
whether they contain a shell, a tool, or an explicitly selected coding agent.
An operator or a worker can emit the same readable line:
```text
#audit-core: Please review the authentication change.
To:audit-core: Please review the authentication change.
```
The tap observes this complete line and queues the same durable route as
`@audit-core: ...`; it forwards the source input unchanged to its occupant.
`#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:
Operator input is forwarded unchanged and is delivered with `/o`; a line
originating in worker output has no suffix:
```text
# from flex-auth: please review the auth boundary [m-...]
From:flex-auth/o: Please review the authentication change.
From:flex-auth: Worker-generated message.
```
The `# from` shape cannot match the outbound `#repo:` grammar, preventing a
tapped recipient from bouncing the message back. Its leading `#` also makes an
empty ordinary shell prompt treat the line as a comment. Pushy mode cannot
determine whether a pane is an agent, a shell, or whether someone is already
editing input: it can append to that input and submit the combined line. Use it
only for panes whose occupant is known to accept or queue asynchronous prompts.
Switching an existing endpoint to pushy may also submit pending messages that
have never been displayed.
Pushy startup requires a capability for this non-routable framing and restarts
an older broker that only advertised generic pushy input. As a second circuit
breaker, the tap refuses a legacy `#sender: body [message-id]` line when that
identifier belongs to the corresponding durable inbound delivery. This keeps
mixed-version local processes from reflecting a message between panes.
`From:` is never routable. The observer suppresses recent exact operator lines
when a TUI echoes or redraws them, so the echo cannot become a second worker
message. Ambiguous exact echoes fail closed. Pushy and trigger still cannot
infer the foreground program or protect input already being edited, so both
remain explicit experiments. Startup requires the readable-duplex, trigger,
and line-limit capabilities and replaces an incompatible old broker.
## Exchange messages manually
Each managed shell exports its own repository slug as `TAMQ_REPO`. From the
`flex-auth` window, queue a message using the repository command installed for
the session:
Each managed window has its own repository identity. From operator input in the
`flex-auth` window, queue a message with:
```bash
@audit-core: please review the auth boundary
To:audit-core: please review the auth boundary
```
The spelling without the trailing colon is equivalent:
The same line at the start of worker output queues a worker-originated message.
The `@`, `#`, reply shorthand, and interactive recipient composer from earlier
alphas have been removed.
```bash
@audit-core please review the auth boundary
```
Run bare `@` to open the inline composer. It shows the latest sender as the
recipient, and ordinary prose is read directly rather than parsed by the shell:
Operator input can change allowlisted runtime state:
```text
$ @
@audit-core: What's up?
Cmd: mode=trigger
Cmd: maxmsg=16
Cmd: maxin=2048
Cmd: maxout=65536
Cmd: reset-limits
```
Press Tab to cycle through the other repository windows while preserving the
draft, Enter to send, or Ctrl-C to cancel without queuing. The initial recipient
is the sender of the latest durable inbound message, including an already
acknowledged message; without history, it is the first peer window.
Self-addressed messages are ignored.
The one-line fast path remains available, but its text is parsed by the shell
and therefore follows ordinary shell quoting rules:
`Cmd:` is recognized only on the operator-input path; identical worker output
is inert. The transparent input contract means the line also remains visible
to the foreground program. Small tamq-owned shell absorbers prevent ordinary
shells from reporting `To:repo:` and `Cmd:` as missing commands; tamq refuses
to overwrite unrelated files. The explicit CLI send form is:
```bash
@ "What's up?"
tamq reply "What's up?"
```
These are tamq-owned executable commands beside the installed `tamq` command,
not shell-specific aliases. Set `TAMQ_COMMAND_DIR` before startup to select a
different writable command directory already present on your shell's `PATH`.
Tamq refuses to overwrite unrelated commands. The shims only use tamq's durable
send operation. The long form remains available:
```bash
tamq send '@audit-core: please review the auth boundary'
tamq send 'To:audit-core: please review the auth boundary'
```
With normal `output` delivery, the target pane visibly receives:
```text
#flex-auth: please review the auth boundary [m-...]
From:flex-auth/o: please review the auth boundary
```
This uses the pane's tmux-reported `/dev/pts/<number>` device—the same Unix
terminal-output mechanism underlying tools such as `write(1)`. For an ordinary
shell with screen rows above its cursor, tamq confines scrolling to those rows,
writes the comment immediately above the input row, and restores the cursor.
writes the delivery immediately above the input row, and restores the cursor.
Thus a partially typed command remains in place. If no safe row exists, tamq
uses ordinary line output; alternate-screen programs receive the conservative
fallback and may redraw over it. Neither path uses `send-keys`, sends Enter, or
@ -182,11 +151,11 @@ tamq inbox
tamq ack <message-id>
```
Human inbox output is safe to paste into an ordinary shell because every line
is a comment. It includes the durable id needed by `ack`:
Human inbox output uses the same framing. Use `--json` or `tamq history` to get
the durable id needed by `ack`:
```text
#flex-auth: please review the auth boundary [m-...]
From:flex-auth/o: please review the auth boundary
```
To explicitly consume pending messages through a command, use an inbox filter:
@ -195,7 +164,7 @@ To explicitly consume pending messages through a command, use an inbox filter:
tamq inbox --filter 'cat >> msg.log'
```
The command runs once per pending message with the comment form on standard
The command runs once per pending message with the readable `From:` form on standard
input. `TAMQ_MESSAGE_ID`, `TAMQ_SENDER_REPO`, and `TAMQ_TARGET_REPO` are set in
its environment. A zero exit acknowledges that message; a non-zero exit leaves
it and all later messages pending. Filters never run in the background and
@ -203,10 +172,23 @@ cannot be combined with `--all` or `--json`.
Outside a managed window, use `tamq inbox --repo audit-core` and optionally
`--json`. Output-displayed messages remain durable and pending until
acknowledged. Neither `output` nor `inbox` mode injects terminal keystrokes, so
they cannot execute or alter a command being typed in the target pane. Pushy
mode intentionally crosses that boundary and records accepted submissions as
`injected`.
acknowledged. Neither `output` nor `inbox` mode injects terminal keystrokes.
Pushy places input without Enter; trigger adds exactly one Enter. Both input
modes record accepted delivery as `injected`.
Every session window has independent, session-lifetime running counters for
accepted outbound messages, operator input lines, and normalized worker output
lines. Defaults are 8, 1024, and 32768 respectively:
```bash
tamq --maxmsg 16 --maxin 2048 --maxout 65536 flex-auth audit-core
```
The same defaults can be set as `maxmsg`, `maxin`, and `maxout` under `[tamq]`
in the configuration file. A message is rejected once any counter is equal to
its limit. Tamq displays the applicable count and `Cmd: reset-limits` resets all
three counters for the current window. Counters survive tap and broker restarts
within the same managed-session generation and are visible in `tamq status`.
After upgrading from an earlier alpha, recreate the managed session once so
existing panes inherit the neutral shell contract, repository command `PATH`,
@ -237,8 +219,9 @@ tamq cleanup --yes
Confirmed cleanup stops only the verified tamq broker, closes only a tmux
session carrying tamq's management marker, disconnects transient endpoints,
clears leases, removes the configured socket/PID/lock files, deletes only
tamq-generated `@repo` shims, and removes owned stale `tamq-*` tmux sockets.
clears leases and line counters, removes the configured socket/PID/lock files,
deletes only tamq-generated protocol absorbers (including legacy `@repo`
shims), and removes owned stale `tamq-*` tmux sockets.
Durable message history and unrelated tmux sessions or files are preserved.
The command is idempotent; an ownership mismatch is reported instead of being
removed.
@ -276,9 +259,10 @@ later phase.
1. tmux control mode is the topology and output/control stream;
2. the local broker assigns endpoint/source identity and durably queues intent;
3. Explicit `tamq tap` mode is a full-duplex PTY proxy around a command. It forwards
bytes unchanged in raw terminal mode, propagates terminal resize and lifecycle
signals, and observes complete input lines for `@repo:`/`#repo:` routing.
3. `tamq tap` is a full-duplex PTY proxy around every messaging-enabled shell
or explicit command. It forwards bytes unchanged in raw terminal mode,
propagates resize and lifecycle signals, observes operator input, and
normalizes worker output for start-of-line `To:` routing.
This keeps tmux-specific topology concerns separate from reusable terminal I/O
observation and message identity.
@ -286,10 +270,10 @@ observation and message identity.
Normal endpoints use terminal-output delivery: each message is written once to
the target pane's PTY output and stays pending in the durable inbox until
acknowledged. `--mode inbox` selects inbox-only manual mode. Neither becomes
pane input. Experimental `--mode pushy` sends one checked tmux command list
containing sanitized literal input followed by Enter, then records the message
as `injected`. When pushy mode creates explicitly commanded windows, the same
PTY tap observes outbound `@repo:` and `#repo:` lines without rewriting them.
pane input. Experimental `--mode pushy` places one sanitized `From:` line
without Enter; `--mode trigger` performs the same placement and submits once.
The PTY observer records explicit operator/worker provenance and suppresses
echoed operator lines before they can route again.
The older pane-delivery experiment remains available only with the explicit
`tamq start --tap --command ...` opt-in. Both input paths remain subject to the
retry and acknowledgement limitations tracked by `TAMQ-WP-0003`.
@ -310,6 +294,9 @@ purge_before = "365d"
purge_max_size = "100MB"
history_max_size = "100MB"
delivery_poll_interval = "0.5"
maxmsg = 8
maxin = 1024
maxout = 32768
[policy.profiles.diagnostics]
safety_gated_max_attempts = 2