Skip to content

feat(agent): put inline completion through the admission gates - #56

Merged
rrrodzilla merged 1 commit into
mainfrom
worktree-completion-admission
Aug 31, 2026
Merged

feat(agent): put inline completion through the admission gates#56
rrrodzilla merged 1 commit into
mainfrom
worktree-completion-admission

Conversation

@rrrodzilla

Copy link
Copy Markdown
Contributor

Closes #22, the known gap named in both the 1.1 and 1.2 release notes.

_garrison/complete resolved the session that owns it and then made a paid model call. Ownership proves the client holds the session; it says nothing about whether this install may spend.

One finding changed the issue's framing

The issue called policy "unclear" for completions because a completion runs no tools. But policy/agent.rs doesn't gate tools at all — it refuses when there's no policy state, when the install is Ungoverned, and when the configured provider isn't an approved endpoint in the bundle. Its own comment is that refusing late means an operator "has already sent it their code", which is exactly what a completion does on every typing pause. Policy turned out to be the gate that mattered most here, not the least.

What this does

  • Every gate but the session keeper answers a completion as it answers a turn. AdmitTurn carries an admission::Work, so each gate decides for itself rather than a caller filtering opaque handles. A gate added later cannot compile without answering for both.
  • The session keeper admits completions without asking. Both its rules are about a stored record a completion never writes, and it's the only gate that reaches the store, so skipping that read is also what keeps the gates inside the budget.
  • A refused completion is sealed; an admitted one is not. audit::seal_refusal is the sealing thread.rs already did, lifted so both paths record the same shape.
  • Gates get 250ms here rather than 5s. They all answer from state they already hold, and the completion is abandoned at 2s regardless.
  • Concurrency capped at 8 in flight daemon-wide (the issue's "Related"). Dropped, not queued.
  • Refusals stay silent to the editor, as every other failure on this path already is. The trail is where they aren't silent.

Verification

The session_persistence assertion has teeth, checked rather than assumed: with the keeper carve-out removed the test fails, having sealed a second turn_interrupted refusal (left: 2, right: 1). That matters because the keeper is last in gate order, so the assertion could otherwise have passed vacuously.

Workspace clippy clean at -D warnings; 1297/1297 tests passing.

Closes #22.

`_garrison/complete` resolved the session that owns it and then made a paid
model call. Ownership proves the client holds the session; it says nothing
about whether this install may spend. An install refused from running a turn
kept spending on every typing pause, and the number of completions in flight
against the model was bounded only by a two-second deadline.

Running completions off the session actor was a deliberate latency decision
and is still the right one. Losing admission along with it was not a decision
at all, and that is what this fixes.

Every gate but one now answers a completion exactly as it answers a turn. The
request carries an `admission::Work` saying which kind of work it is, so each
gate decides for itself what its own rule means. The alternative was a second,
shorter gate list for the completion path, and it was rejected: it puts the
judgment about completions in the code that assembles the list rather than in
the gate that holds the rule, and a gate added later would join that list by
being forgotten rather than by being considered.

Policy is the gate that matters most here, against first appearances. The
issue called it unclear on the grounds that a completion runs no tools, but
this gate does not gate tools at all: it refuses when there is no policy
state, when the install is Ungoverned, and when the configured provider is not
an approved endpoint in the bundle. Its own comment says refusing late means
an operator "has already sent it their code" — which is precisely what a
completion does, on every typing pause, to whatever endpoint the daemon points
at. It was the least gated path to the least approved provider.

The session keeper is the exception and admits a completion without asking.
Both of its rules are about a session's stored record, and a completion writes
none; refusing one because an earlier turn was left open would stop a
developer's editor for a reason that is not about them. It is also the only
gate that reaches the store to answer, so skipping that read is what keeps the
gates inside a completion's budget. `session_persistence` proves it with a
real daemon and a real trail rather than by assertion: with the carve-out
removed the test fails, having sealed a second `turn_interrupted` refusal.

A refused completion is sealed, an admitted one is not. The refusal is the
governance-relevant event, and a debounce timer decides how often this path is
entered — a trail written by a timer rather than by a person is noise wearing
the shape of evidence. `audit::seal_refusal` is the sealing `thread.rs` already
did, lifted so both paths record the same shape.

Two smaller decisions the issue raised:

The gates get 250ms on this path rather than the turn path's five seconds.
Every gate a completion crosses answers from state it already holds, so one
that has not replied in that long is wedged rather than working, and a
completion is abandoned two seconds from now regardless. Waiting the full five
would spend the budget on the gates and leave nothing for the model.

Concurrency is capped at eight in flight daemon-wide, which the issue filed
under "Related" and which nothing bounded before. A completion that cannot get
a permit is dropped rather than queued, for the same reason this path does not
go through the session actor: a suggestion that waits for a slot arrives after
the keystroke that made it interesting.

A refusal stays silent to the editor. Every other failure on this path answers
"no suggestion" and this is no different; raising an error per keystroke would
bury a developer in dialogs over a state one glance at `_garrison/status`
explains. The trail is where a refusal is not silent.
@rrrodzilla
rrrodzilla merged commit 9001eec into main Aug 31, 2026
3 checks passed
@rrrodzilla rrrodzilla mentioned this pull request Aug 31, 2026
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.

Inline completion bypasses admission: seat, policy and audit gates do not apply to _garrison/complete

1 participant