Skip to content

feat(infinity-slack-bot): dedicated Slack threads per subagent, stream fixes, native stop button - #141

Closed
akainth015 wants to merge 4 commits into
pr/daemon-rewindfrom
pr/slack-subagent-threads
Closed

akainth015 wants to merge 4 commits into
pr/daemon-rewindfrom
pr/slack-subagent-threads

Conversation

@akainth015

@akainth015 akainth015 commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Each subagent's output now streams into its own dedicated Slack thread (anchor message + permalink cross-link; replies/buttons inside a subagent thread route back to that subagent). Also fixes stream discontinuity around interposed blocks, frozen 'in progress' task cards, embedded-serialization bugs, and adds support for Slack's native stop button (agent_session_stopped).

Stacked on #140; part 2 of 6 (followed by #142 #143 #144 #145). See commit message for full details.

@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying infinity with  Cloudflare Pages  Cloudflare Pages

Latest commit: 790a43c
Status: ✅  Deploy successful!
Preview URL: https://e8d2f8a6.infinity-dc7.pages.dev
Branch Preview URL: https://pr-slack-subagent-threads.infinity-dc7.pages.dev

View logs

@shadaj

shadaj commented Oct 16, 2026

Copy link
Copy Markdown
Member

Can we move the stop button logic into #143? Since it only works properly post #142.

@akainth015

Copy link
Copy Markdown
Member Author

Done — the stop-button logic now ships in #143 alongside the daemon-interrupt wiring (where it actually works end-to-end, per your point about #142). This PR is now just the subagent threads + stream-correctness fixes (~380 lines lighter). #142's own diff is unchanged (interdiff-verified), and the trees from #143 up are byte-identical to before, so #144/#145/#150 diffs didn't churn.

@akainth015
akainth015 force-pushed the pr/slack-subagent-threads branch 2 times, most recently from 72afe98 to 4973257 Compare October 19, 2026 20:31
@akainth015
akainth015 removed this pull request from stack #149 October 19, 2026 22:08
@akainth015
akainth015 changed the base branch from main to pr/daemon-rewind October 19, 2026 22:08
@akainth015
akainth015 force-pushed the pr/slack-subagent-threads branch from 4973257 to 19a0968 Compare October 19, 2026 23:51
@akainth015
akainth015 force-pushed the pr/daemon-rewind branch 2 times, most recently from 1508d93 to 39a3427 Compare October 20, 2026 00:16
@akainth015
akainth015 force-pushed the pr/slack-subagent-threads branch from 19a0968 to 2a2b13c Compare October 20, 2026 00:17
@akainth015
akainth015 marked this pull request as draft October 20, 2026 02:52
@akainth015
akainth015 force-pushed the pr/slack-subagent-threads branch from 2a2b13c to 2001008 Compare October 20, 2026 17:42
…nstead of prefixing user text

When user text interrupted an in-flight completion, the driver rewrote the
text to `<interrupt>…` before folding it into history, so the token the
model sees was baked into the stored user message. Likewise, an interrupted
tool call was settled by storing a synthetic "interrupted" tool result.

Interrupts are now first-class history entries:

* `TaggedSyntheticKind::Interrupt` is an input marker; `prepare_input`
  folds it into `InfinityMessage::Interrupt` (settling a trailing
  unanswered tool call like any other non-result entry). The driver
  enqueues a marker ahead of the interrupting text instead of mutating it.
* The model-facing `<interrupt>` token is derived at render time by
  `message::render_history` (used by `HistoryManager::get_history`): it is
  prefixed onto the following user text (byte-for-byte what the model saw
  before) or rendered standalone if no user text follows. Consecutive
  interrupts collapse into one token.
* An unanswered tool call is settled by an `Interrupt` entry instead of a
  stored synthetic "Tool call interrupted by user" result. `render_history`
  renders an interrupt right after a tool call as that result, so the model
  sees the same request (when text interrupts an in-flight synchronous
  tool, the result alone now conveys it; no `<interrupt>` prefix follows).
  History thus ends on an interrupt when the thread
  was stopped, and the late real result is still dropped as stale.
* Stored user text is no longer rewritten. Rows written by older versions
  (literal `<interrupt>` prefix, synthetic interrupted results) load and
  render unchanged; replay keeps stripping the legacy prefix and hides
  interrupt entries (as the live stream does).

Note: an older daemon cannot parse histories containing the new
`{"kind":"interrupt"}` entry (forward-incompatible on rollback).
…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`.
…ve rewind)

Adds `ClientMessage::ForkSession { thread_id, user_turn, keeps_session_alive }`:
create a NEW session whose history is the original's conversation just
before its `user_turn`-th user-text turn. The original session is only
read, so "rewinding" is an edit-and-fork that keeps it intact.

* `SessionManager::fork_session` resolves the turn (0-based over the
  session's persisted user-text rows, matching the `UserInputEcho` entries
  in its `Replay`; rows behind a compaction summary still count) and seeds
  a new root thread with existing `ConversationStore` primitives: the rows
  before the cut are copied verbatim and the latest compaction summary at
  or before the cut is carried over. The fork inherits cwd and model and is
  registered last, so a half-seeded fork never lists.
* Like `CreateSession`, the requesting connection is attached to the fork
  and receives `Connected`; clients then send the (edited) turn as
  `UserInput`. Non-sessions and out-of-range turns are reported via
  `DaemonMessage::Error`. `ThreadRef`s are stripped on the
  remote-forwarding path.
…ream-correctness fixes

## Subagent threads
* Subagent output (daemon events whose `thread_id` differs from the session
  root) routes into a dedicated Slack thread, created lazily via an anchor
  message ("🤖 Subagent …") with a permalink cross-link in the root thread.
* Replies/button clicks inside a subagent thread route back to that subagent
  (`SendInput.target_thread`); per-(thread, subagent) stream state.

## Stream correctness
* Streams are finalized before any discrete block (buttons, errors,
  cross-links) is posted into their thread, fixing out-of-order edits above
  interposed blocks.
* Task cards are tracked per stream message (`open_tasks`) and completed on
  every stop/split/recovery path so spinners can never freeze; late
  completions are dropped instead of resurrecting streams.
* `SlackAction` serialization fixes for the embedded binary format
  (no `skip_serializing_if`, string-adapted `serde_json::Value` fields).

Docs updated; new sim/unit tests cover routing, finalization, and task
cards. (Native stop-button support lands with the interrupt wiring in a
later commit, where it works end to end.)
@akainth015
akainth015 force-pushed the pr/slack-subagent-threads branch from 2001008 to 790a43c Compare October 22, 2026 01:18
akainth015 added a commit to hydro-project/infinity-slack that referenced this pull request Oct 29, 2026
…ream-correctness fixes

## Subagent threads
* Subagent output (daemon events whose `thread_id` differs from the session
  root) routes into a dedicated Slack thread, created lazily via an anchor
  message ("🤖 Subagent …") with a permalink cross-link in the root thread.
* Replies/button clicks inside a subagent thread route back to that subagent
  (`SendInput.target_thread`); per-(thread, subagent) stream state.

## Stream correctness
* Streams are finalized before any discrete block (buttons, errors,
  cross-links) is posted into their thread, fixing out-of-order edits above
  interposed blocks.
* Task cards are tracked per stream message (`open_tasks`) and completed on
  every stop/split/recovery path so spinners can never freeze; late
  completions are dropped instead of resurrecting streams.
* `SlackAction` serialization fixes for the embedded binary format
  (no `skip_serializing_if`, string-adapted `serde_json::Value` fields).

Docs updated; new sim/unit tests cover routing, finalization, and task
cards.

Ported from hydro-project/infinity#141.
@akainth015

Copy link
Copy Markdown
Member Author

Moved to hydro-project/infinity-slack@2b5d03bc0ed2. The native stop button / interrupt parts were dropped.

@akainth015 akainth015 closed this Nov 4, 2026
@akainth015
akainth015 deleted the pr/slack-subagent-threads branch November 4, 2026 23:43
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