Skip to content

feat(infinity-daemon): add input-less Interrupt for in-flight generation - #165

Closed
akainth015 wants to merge 1 commit into
pr/interrupt-history-kindfrom
pr/daemon-interrupt
Closed

akainth015 wants to merge 1 commit into
pr/interrupt-history-kindfrom
pr/daemon-interrupt

Conversation

@akainth015

@akainth015 akainth015 commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Adds ClientMessage::Interrupt { thread_id }: cancels the in-flight completion on the target thread and its active descendants without queuing input or shutting the session down. Old daemons skip the unknown frame.

  • The last decisive input of a batch (user text or interrupt marker, found by last_decisive_input) decides what happens: [interrupt] and [text, interrupt] cancel without a new completion; [interrupt, text] and [text] cancel and answer the text.
  • Thread::record_interrupt records an interrupt-ending batch without a completion: a trailing unanswered tool call gets a synthetic "interrupted" result (late real results are dropped, which stops multi-step loops), and the pending choice is dismissed. One ResponseDone is sent per interrupted round.
  • Harness-generated entries (markers, synthetic results) are persisted via promote_harness_settlements. Without this, a late tool result could restart generation once the driver idles out. Unanswered user text stays unpersisted.

Stacked on #170.

Replaces #142, which GitHub auto-marked merged when the stack was reordered. Nothing reached main.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Deploying infinity with  Cloudflare Pages  Cloudflare Pages

Latest commit: 65776c2
Status: ✅  Deploy successful!
Preview URL: https://b1495459.infinity-dc7.pages.dev
Branch Preview URL: https://pr-daemon-interrupt.infinity-dc7.pages.dev

View logs

@shadaj shadaj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

First pass.

// Input-less interrupts cancel the in-flight completion
// just like user text; the markers themselves are consumed
// by the shared stripping below (never stepped).
if has_user_text || batch.iter().any(|(msg, _)| is_interrupt_input(msg)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we do the || inside to avoid a double iteration?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Folded into a single backwards scan, last_decisive_input. It answers both whether the batch interrupts and which decisive input came last.

// User text supersedes an interrupt marker in the
// same batch: folding the text already settles a
// trailing tool call, so the marker is redundant.
batch.retain(|(msg, _)| !is_interrupt_input(msg));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

?? If there is an interrupt after user text that would mean we should not trigger the model, right? Should think through the semantics carefully.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, the order now decides: the last decisive input wins. [text, interrupt] records the text but does not trigger the model; [interrupt, text] cancels and answers; [interrupt] just cancels. One choice to check: the unanswered text in [text, interrupt] is not persisted, since no completion validated it. That matches input cancelled before output on main, and the #159 stance on untrusted input. The tests pin this choice.

if let InputMessageContent::User(UserContent::Text(text)) =
&mut user_inputs[0].0.content
{
text.text = format!("<interrupt>{}", text.text);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would love to get rid of this code and instead have interrupts be their own message kind in the agent history and derive the <interrupt> tokens at runtime dynamically.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done, in a new preparatory PR, #170 (now the base of this one): interrupts are InfinityMessage::Interrupt history entries, and <interrupt> is derived when history is rendered (message::render_history). User text is never rewritten. The provider sees byte-identical output for interrupt+text, and legacy prefixed rows render unchanged with no migration. While restructuring I also fixed a latent bug here: the synthetic interrupted result was never persisted, so a late tool result could restart generation after the driver idled out.

@akainth015
akainth015 removed this pull request from stack #167 October 19, 2026 23:51
@akainth015
akainth015 changed the base branch from main to pr/interrupt-history-kind October 19, 2026 23:51
@akainth015
akainth015 added this pull request to stack #171 October 19, 2026 23:51
@akainth015
akainth015 force-pushed the pr/interrupt-history-kind branch from d7c64e1 to b2c57e5 Compare October 20, 2026 00:16
@akainth015
akainth015 marked this pull request as draft October 20, 2026 02:52
@akainth015
akainth015 force-pushed the pr/interrupt-history-kind branch from b2c57e5 to d2870d3 Compare October 20, 2026 17:42
…ation

Clients can stop a thread's in-flight work without queuing input and
without shutting the session down. Builds on interrupts being recorded as
history entries (`InfinityMessage::Interrupt`, previous commit).

* New `ClientMessage::Interrupt { thread_id }`: interrupts the thread and
  its descendant subtree. Old daemons skip unparsable frames, so sending it
  to one is a no-op.

* User text and interrupt markers are a batch's *decisive* inputs. One
  backwards scan (`last_decisive_input`) decides whether the batch
  interrupts the in-flight completion (any decisive input) and what happens
  next (the last one):

  | batch                   | in-flight work | new completion?       |
  |-------------------------|----------------|-----------------------|
  | `[interrupt]`           | cancelled      | no                    |
  | `[text, interrupt]`     | cancelled      | no                    |
  | `[interrupt, text]`     | cancelled      | yes, answers the text |
  | `[text]`                | cancelled      | yes (implicit marker) |

* A batch ending on an interrupt is recorded by `Thread::record_interrupt()`
  without a completion: inputs are folded in order, a trailing unanswered
  tool call is settled as interrupted (RAP cancellation notified, pending
  choice dismissed; this settlement is factored out of `step_no_defer`), and
  history is persisted. Exactly one `CompletionFinished` (→ `ResponseDone`)
  is emitted per interrupted round.
* `HistoryManager::promote_harness_settlements()` persists the leading
  interrupt entries that no completion will validate. History then ends on
  the interrupt, so the driver idles out and a late tool result is dropped
  instead of restarting generation. User text stays unvalidated and
  unpersisted, like any input cancelled before model output.

* `SessionManager::interrupt_thread()` delivers interrupt markers to the
  target and its active descendants; idle descendants are skipped.
* `ClientMessage::Interrupt` is handled in the client handler, with thread
  refs stripped on the remote path like `UserInput`.
@akainth015
akainth015 force-pushed the pr/interrupt-history-kind branch from d2870d3 to ae72ccf Compare October 22, 2026 01:18
@akainth015
akainth015 removed this pull request from stack #171 October 29, 2026 22:53
@akainth015 akainth015 closed this Oct 29, 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.

2 participants