Skip to content

fix(pty): submit_confirmed false-positives on a busy receiver, suppressing the #259 retry #260

Description

@rdfitted

Summary

classify_submit_confirmation (src-tauri/src/http/handlers/inject.rs:68-76) returns
Some(true) / sustained-post-submit-activity whenever the PTY output ring changes after the
submit write. It cannot tell the composer reacted to Enter apart from the receiver was already
producing output for unrelated reasons
.

Against a busy receiver it therefore confirms submits that did not happen.

This is documented as a caveat in the receipt's own evidence_scope string ("An agent that was
already streaming output can produce a false positive"), but it is treated as a footnote. In
practice it is the common case, not the edge case.

Live evidence — session 7fb25633-4827-4ea1-9646-17d5c7852126, 2026-08-20

Four Queen-issued injects to three codex principals that were mid-indexing:

Target payload_bytes submit_bytes submit_confirmed basis
worker-1 284 1 true sustained-post-submit-activity
worker-2 408 1 true sustained-post-submit-activity
worker-3 390 1 true sustained-post-submit-activity
worker-2 (addendum) ~330 1 true sustained-post-submit-activity

All four payloads were still sitting unsubmitted in the composers. The operator observed this
directly and reported it. A follow-up {"message":"","submit":true} bare-Enter flush to each
target submitted all three.

Why this observation is admissible under #241's evidence rule

#241 bars verifying an inject by watching whether the agent acts, because an operator at the
terminal cannot distinguish an auto-submit from their own keypress. That rule constrains
positive observations only.
This is a negative observation — text visibly resident in a
composer — and no operator keypress can manufacture a non-submit. The asymmetry should be
written into docs/pty-submit-sweep.md: negative observations are valid evidence even when
uncontrolled; positive ones are not.

Why this matters more than a noisy field

The compensating retry in #259 is keyed on the signal this bug suppresses. That retry fires
strictly on Some(false) (a deliberate decision — the tri-state exists so an ambiguous buffer is
never upgraded). A false Some(true) bypasses it entirely.

So the two defects compose badly:

  • Idle receiver, Enter swallowed -> Some(false) -> retry fires. Handled.
  • Busy receiver, Enter swallowed -> false Some(true) -> no retry. Not handled.

And the busy case is the one that actually occurs, because inject exists precisely to interrupt an
agent that is working. An idle receiver is the rare state.

Secondary hypothesis worth measuring

A bracketed-paste payload followed by a discrete CR after the configured gap — the post-#256
design — still failed to submit against a busy codex composer. That points at a receiver state
problem (the TUI is not accepting keystrokes mid-generation) rather than the timing problem #256
addressed. If true, no sender-side gap tuning fixes it and the retry in #259 will also be
swallowed.

The busy-state column of the sweep matrix in docs/pty-submit-sweep.md is the right place to
settle this. Owned by #241.

Suggested directions

  1. Establish an activity baseline before the payload write, not just before the submit write,
    and require a change in character rate or a composer-specific signature rather than any byte
    delta. Downgrade to None when the receiver was already streaming.
  2. Consider whether a busy receiver should be classified None unconditionally — an honest
    "unknown" is more useful than a confident wrong answer, and None is already the value that
    correctly means "do not act on this."
  3. Do not let the retry key widen to None as a workaround. That reintroduces the
    double-execution risk that bounding it to the confident negative was chosen to avoid.

Related: #241 (owns the sweep matrix), #259 (the retry this suppresses), #256, #257

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions