Skip to content

Stop the gate from ending every Thread at its first tool call - #36

Merged
QuintinBotes merged 2 commits into
mainfrom
gate-defer-fix
Aug 3, 2026
Merged

Stop the gate from ending every Thread at its first tool call#36
QuintinBotes merged 2 commits into
mainfrom
gate-defer-fix

Conversation

@QuintinBotes

Copy link
Copy Markdown
Owner

Stacked on #35. Retarget to main once that lands.

The gate was killing every Thread it protected. An agent states its plan, requests one tool, and the Thread reports Completed having done nothing — in under two seconds, every time.

Cause

defer is Tervin's word for "no objection". It is not a word the runtime knows. The protocol accepts allow, deny and ask; handed anything else it ends the turn immediately and reports success — no tool result, no continuation, no error. Because this gate matches every tool call, that landed on the first one, so the agent stopped at its first action while the panel above it said Tervin Rules were protecting the session.

The hook now says nothing at all when Rules do not object, which is how this protocol spells "no opinion". Denials are unchanged: a reason on stderr, exit 2.

Established by elimination, because every suspect had an alibi

Configuration Result
Same prompt/profile/model/flags straight at the CLI works
With a deliberately dead gate socket works
Through Tervin's own runtime, no arbiter wired works
Audit trail allowed — "no policy rule matched and the action is low risk", 87ms before the Thread ended

What every working case had in common was a hook that printed nothing. Substituting a hook that printed only the defer line reproduced it exactly.

Why the suite missed it

The same reason as the last gate bug. The tests that drive the real client over the real socket captured its exit code and its stderr and threw its stdout away. Every assertion about this path was correct; the fatal byte was in the stream nobody looked at.

run_client_capturing now returns stdout and two tests assert it is empty — verified by reintroducing the println! and watching them fail with the exact payload from the timeline.

Two things found while chasing it, both kept

  • Raw payloads were never stored. take_raw_stream was defined and called from nowhere, so every event wrote a pointer to a body that was never saved: raw_payloads held zero rows while the channel grew unbounded for the life of each session. It is now drained alongside the events it belongs to. This is what made the bug diagnosable — "what did the runtime actually send?" had no answer when it was first asked.
  • Every protocol line is traceable behind TERVIN_LOG=trace. Needing a code change to see the stream is how a one-off becomes unreproducible.

Also in here: Block output is stripped of escape sequences server-side, using the same routine as the preview and the search index. Three views of one output that previously disagreed — the one you actually read was showing [1m[7m%[27m.

rust 682 to 683 on this branch, clippy and fmt clean.

The gate was killing every Thread it protected. An agent would state its plan,
request one tool, and the Thread would report Completed having done nothing —
twice in a row, on two different prompts, in under two seconds each.

`defer` is Tervin's word for "no objection". It is not a word the runtime knows.
The protocol accepts `allow`, `deny` and `ask`; handed anything else it ends the
turn immediately and reports success. No tool result, no continuation, no error.
Because this gate matches every tool call, that landed on the *first* one, so
the agent stopped at its first action while the panel above it said Tervin Rules
were protecting the session.

The hook now says nothing at all when Rules do not object, which is how this
protocol spells "no opinion". Denials are unchanged: a reason on stderr, exit 2.

Established by elimination rather than inspection, because every plausible
culprit had an alibi. The same prompt, profile, model and flags driven straight
at the CLI worked. With a deliberately dead gate socket it worked. Driven
through Tervin's own runtime with no arbiter wired, it worked. The audit trail
showed the action allowed, "no policy rule matched and the action is low risk",
87ms before the Thread ended. What all the working cases had in common was a
hook that printed nothing; substituting a hook that printed only the `defer`
line reproduced it exactly.

Why the suite missed it, and the same reason as the last gate bug: the tests
that drive the real client over the real socket captured its exit code and its
stderr and threw its stdout away. Every assertion about this path was correct.
The fatal byte was in the stream nobody looked at. `run_client_capturing` now
returns stdout and two tests assert it is empty, verified by reintroducing the
`println!` and watching them fail with the exact payload from the timeline.

Two things found while chasing it, both kept:

- **Raw payloads were never stored.** `take_raw_stream` was defined and called
  from nowhere, so every event wrote a pointer to a body that was never saved:
  `raw_payloads` held zero rows while the channel grew unbounded for the life of
  each session. It is now drained alongside the events it belongs to, which is
  what made this bug diagnosable at all — "what did the runtime actually send?"
  had no answer when it was first asked.
- **Every protocol line is traceable** behind `TERVIN_LOG=trace`. Needing a code
  change to see the stream is how a one-off becomes unreproducible.

rust 691 to 692.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@QuintinBotes
QuintinBotes changed the base branch from pty-input-gate to main August 3, 2026 15:56
@QuintinBotes
QuintinBotes enabled auto-merge (squash) August 3, 2026 15:56
@QuintinBotes
QuintinBotes merged commit ffaf3bc into main Aug 3, 2026
3 checks passed
@QuintinBotes
QuintinBotes deleted the gate-defer-fix branch August 3, 2026 16:53
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