Skip to content

Spec: mngr message delivery signals#2411

Draft
weishi-imbue wants to merge 2 commits into
mainfrom
mngr/compact
Draft

Spec: mngr message delivery signals#2411
weishi-imbue wants to merge 2 commits into
mainfrom
mngr/compact

Conversation

@weishi-imbue

Copy link
Copy Markdown
Contributor

Summary

mngr message drives Claude through a tmux pane, so it has no ACK and must reconstruct "did it land" from side effects. Almost every side effect Claude Code emits is semantic -- it records what the input meant, not that input arrived -- and the two signals mngr races (the UserPromptSubmit hook, the enqueue transcript event) are both model-bound. Input that never reaches the model satisfies neither.

That is why a typo, /login, and /cost each hang for the full 90s despite being delivered, and why /clear and /compact each needed a bespoke hook.

This PR adds a problem-statement spec. No production code changes.

What's in the spec

  • Overview of why this resists simplification.
  • Seven input categories: prompt, local command, modal local command, session-restarting command, invalid command, queued-while-busy, empty.
  • Signal inventory, split by producer (Claude Code vs mngr), with author and firing time for each.
  • Signal-to-case matrix, measured against a real local Claude agent (Claude Code 2.1.205, macOS).
  • Twelve issues, each reproduced unless marked otherwise.
  • A design direction (proposal, not a decision) and a test plan.

Headline findings

Two silent GNU-coreutils dependencies break mngr message on stock macOS, independent of any slash-command behavior:

  • timeout is absent, so timeout <n> tmux wait-for returns 127 and the waiter is never registered. When the UserPromptSubmit hook fires tmux wait-for -S, the signal wakes nobody. A plain prompt that Claude demonstrably answered was reported as Failed to send message after 93.6s.
  • tac is absent (mngr_transcript_lib.sh:105), so transcript offset reconciliation fails and its stderr is painted into Claude's input box.

Beyond those:

  • mngr message can return before Claude's input buffer is clear, so the next message is concatenated onto residue (/clear /zzzrace1, reproduced 1-in-3).
  • Modal local commands (/cost, /model, /login) leave no input row; every later send fails after a 30s generic timeout, and _preflight_send_message does not detect it.
  • Busy and idle disagree about the same input: a typo sent to a busy agent returns exit 0 in 2.6s (it enqueued); sent to an idle agent it fails after 93.7s.
  • The Slack thread's premise is wrong -- an invalid command does write a structured transcript entry (type=system, level=warning), already tailed into events.jsonl.
  • ~/.claude/history.jsonl is the only artifact written before interpretation, and the only one that fires for every input class. Its one deviation is a consecutive-duplicate dedup.

Also adds a top-level uncertainties.md recording two comments/docstrings that assert behavior which does not hold.

Review focus

The design section is deliberately a proposal. The open questions -- what a sound "settled" predicate is, which dedup guard to use, and whether modal commands should fail or warn -- are the parts most worth arguing about.

🤖 Generated with Claude Code

mngr drives Claude through a tmux pane, so `mngr message` has no ACK and must
reconstruct "did it land" from side effects. Almost every side effect Claude
Code emits is semantic -- it records what the input meant, not that input
arrived -- and the two signals mngr races (the UserPromptSubmit hook and the
enqueue transcript event) are both model-bound. Input that never reaches the
model satisfies neither, which is why a typo, /login, and /cost each hang for
the full 90s despite being delivered, and why /clear and /compact needed their
own bespoke hook.

The spec categorizes the seven input classes, inventories every signal with its
producer, maps signals to cases in a matrix measured against a real agent, and
records twelve issues. Two are silent GNU-coreutils dependencies that break
`mngr message` on stock macOS: `timeout` in the tmux wait-for waiter (so the
hook signal is fired into the void) and `tac` in transcript offset
reconciliation (whose stderr is painted into Claude's input box).

`~/.claude/history.jsonl` is the only artifact written before interpretation,
and so the only one that fires for every input class.

No production code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

1. Reject the agent if its lifecycle state is `STOPPED`.
2. Acquire an exclusive per-agent file lock.
3. `_preflight_send_message` -- check the `permissions_waiting` marker and match the pane against `_DIALOG_INDICATORS`. Fail fast on a hit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't we extend this to capture those modal commands like /cost and /login, so they can fail fast?

…nals

Three defects, all measured against a real Claude agent on macOS, where
`mngr message` previously failed after ~93s for EVERY message -- a plain
prompt included, not just slash commands.

1. `timeout` is GNU-only. The tmux submission waiter ran under it, got exit
   127, and was never registered, so the UserPromptSubmit hook fired
   `tmux wait-for -S` into the void. No portable `timeout` binary exists
   (`gtimeout` needs `brew install coreutils`), so the waiter is now bounded
   with plain bash, which these commands already run under.

2. tmux LATCHES `wait-for` signals: a `-S` sent with no waiter registered is
   remembered, and the next `wait-for` returns immediately. The code asserted
   the opposite ("a signal with none registered is lost"). Fixing (1) alone
   would expose this as a false success -- a stale signal confirming the next
   send before its Enter is processed. The channel is now drained first.

   Two traps found while fixing it: `tmux wait-for` handles SIGTERM and exits
   0, indistinguishable from being woken, so the deadline must SIGKILL; and
   wrapping the waiter in a subshell orphans the `tmux wait-for` client on
   kill, leaving it registered to steal a later submission's signal.

3. `tac` is GNU-only. Transcript offset reconciliation used it, and on macOS
   the resulting "command not found" was painted into Claude's input row,
   which then broke the next send's paste-visibility check (16.7s failure).

Measured on the affected host: normal prompt 93.6s/exit 1 -> 2.1s/exit 0;
`/clear` 93.7s/exit 1 -> 2.1s/exit 0; `/clear` followed by a prompt now
succeeds without concatenating.

Also records the full signal inventory and remaining issues in
specs/mngr-message-delivery-signals.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant