Skip to content

feat(infinity-slack-bot): resume streaming threads across restarts - #150

Draft
akainth015 wants to merge 1 commit into
pr/slack-rewind-shortcutfrom
pr/slack-resume
Draft

akainth015 wants to merge 1 commit into
pr/slack-rewind-shortcutfrom
pr/slack-resume

Conversation

@akainth015

Copy link
Copy Markdown
Member

Clean shutdown (SIGTERM/SIGINT) now: gracefully closes the Socket Mode WebSocket (close handshake; unread envelopes stay un-ACKed so Slack redelivers them — a user message racing shutdown is delayed, never lost), drains buffers and finalizes all active streams (no dangling task cards or streaming messages), and persists resume state (~/.infinity/slack_resume.json, atomic write): per-(thread, subagent) delivery cursors, channel mappings, subagent anchor routes, halt latches, and the choice-button registry.

On startup the bot reconnects to each previously-streaming session (per-thread ConnectSession.target_thread), trims the daemon's Replay with the persisted cursors (UTF-8-safe slicing, clamped if history shrank via rewind/compaction), and posts exactly the missed output — resumed subagent output continues in its existing dedicated thread instead of forking a duplicate, pre-restart choice buttons keep working, and user-stop latches survive. A 30s dirty-checked snapshot bounds loss on non-clean crashes (documented).

63 tests green (10 new resume-store/delta unit tests, 2 new sim tests).

@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: 5cf0a19
Status: ✅  Deploy successful!
Preview URL: https://d4eb6281.infinity-dc7.pages.dev
Branch Preview URL: https://pr-slack-resume.infinity-dc7.pages.dev

View logs

@akainth015
akainth015 force-pushed the pr/slack-rewind-shortcut branch from 9b86ea8 to 4324e18 Compare October 16, 2026 17:43
@akainth015
akainth015 added this pull request to stack #149 October 19, 2026 19:24
@akainth015
akainth015 force-pushed the pr/slack-rewind-shortcut branch from 4324e18 to a3da634 Compare October 19, 2026 20:31
@akainth015
akainth015 requested a review from a team October 19, 2026 20:31
@akainth015
akainth015 removed this pull request from stack #149 October 19, 2026 22:08
@akainth015
akainth015 force-pushed the pr/slack-rewind-shortcut branch from a3da634 to 50e6d6d Compare October 19, 2026 23:51
@akainth015
akainth015 force-pushed the pr/slack-rewind-shortcut branch from 50e6d6d to dffe1eb 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-rewind-shortcut branch from dffe1eb to 70474b2 Compare October 20, 2026 17:42
Restarting the bot used to orphan in-flight responses: the daemon kept
generating, nothing reconnected until the user's next message, and the
output produced meanwhile never reached Slack (and dying mid-stream left
Slack messages dangling in streaming state).

## Clean shutdown (SIGTERM / ctrl-c)

* The inbound task closes the Socket Mode WebSocket with a proper close
  handshake (Slack sees the app offline immediately; envelopes already read
  were ACKed and forwarded, unread ones stay unACKed and are redelivered
  after the restart — a racing user message is delayed, never lost).
* The outbound task drains: flushes buffered text, completes open task
  cards, finalizes every active stream with the disclaimer footer, and
  clears thread statuses. Queued-but-unprocessed actions are deliberately
  dropped — they were never counted as delivered, so the restart replays
  them. The whole drain is bounded by a 10s timeout.
* The resume state is persisted to `~/.infinity/slack_resume.json`
  (atomic temp-file rename): per-stream delivery cursors, channel mappings,
  subagent thread routes, user-stop latches, and posted choice buttons. A
  periodic 30s snapshot bounds the damage of a crash (documented: a crash
  may re-post or drop a few seconds of output).

## Delivery cursors

* `runtime.stream_progress` tracks, per (root thread, subagent), the
  cumulative text bytes and tool call/result cards *delivered to Slack*,
  counted by the I/O layer at delivery time (text when accepted into the
  flush buffer — flushed before shutdown completes — cards when posted).
  Suppressed (user-stopped) output is not counted and stays suppressed
  across restarts via the persisted latch.
* `StreamStop` gains an `end_of_response` flag (set only by the
  ResponseDone-driven stop): a response-final stop drops the stream's
  cursor — cursor presence is exactly "this stream needs resuming".

## Startup resume

* Each persisted stream gets its own daemon connection
  (`ConnectSession.target_thread`; replay — and an already-running
  subagent driver's live broadcast — are per thread, and a new root
  subscriber is not seeded into pre-existing child drivers).
* The daemon's `Replay` is trimmed by the pure `resume::replay_delta`:
  skip the cursor-covered prefix (slicing inside replayed whole-message
  text chunks on char boundaries; clamped if history shrank via rewind or
  compaction), emit the remainder through the existing rendering arms, and
  append the synthetic `ResponseDone`s replay history never carries —
  counted to restore the exact end-state the live sequence would have
  produced (tool-executing: none; completion in flight: one to consume the
  round-continues flag; idle: one to close, plus one if the delta re-armed
  the flag). Replays for non-resumed streams are dropped (their content is
  already on screen).
* Still-pending choices are re-posted only if their buttons weren't already
  on screen (restored `choice_messages`); pre-restart buttons keep
  working. Subagent output resumes into its existing dedicated thread
  (anchor map seeded from the persisted routes), and replies inside
  subagent threads keep routing to their subagent.

## Tests & docs

* 10 unit tests for the resume store and `replay_delta` (trimming,
  mid-chunk slicing, multibyte boundaries, independent call/result
  cursors, synthetic-done counting, clamping, snapshot/restore round-trip).
* 2 new sim tests: the ResponseDone stop is marked end-of-response, and the
  two-dones-close-a-replayed-tool-round contract `replay_delta` relies on.
* "Restarts and Continuity" section in the Slack bot docs.
@akainth015
akainth015 force-pushed the pr/slack-rewind-shortcut branch from 70474b2 to bc873ca Compare October 22, 2026 01:18
akainth015 added a commit to hydro-project/infinity-slack that referenced this pull request Oct 29, 2026
Restarting the bot used to orphan in-flight responses: the daemon kept
generating, nothing reconnected until the user's next message, and the
output produced meanwhile never reached Slack (and dying mid-stream left
Slack messages dangling in streaming state).

## Clean shutdown (SIGTERM / ctrl-c)

* The inbound task closes the Socket Mode WebSocket with a proper close
  handshake (Slack sees the app offline immediately; envelopes already read
  were ACKed and forwarded, unread ones stay unACKed and are redelivered
  after the restart — a racing user message is delayed, never lost).
* The outbound task drains: flushes buffered text, completes open task
  cards, finalizes every active stream with the disclaimer footer, and
  clears thread statuses. Queued-but-unprocessed actions are deliberately
  dropped — they were never counted as delivered, so the restart replays
  them. The whole drain is bounded by a 10s timeout.
* The resume state is persisted to `~/.infinity/slack_resume.json`
  (atomic temp-file rename): per-stream delivery cursors, channel mappings,
  subagent thread routes, and posted choice buttons. A periodic 30s
  snapshot bounds the damage of a crash (documented: a crash may re-post
  or drop a few seconds of output).

## Delivery cursors

* `runtime.stream_progress` tracks, per (root thread, subagent), the
  cumulative text bytes and tool call/result cards *delivered to Slack*,
  counted by the I/O layer at delivery time (text when accepted into the
  flush buffer — flushed before shutdown completes — cards when posted).
* `StreamStop` gains an `end_of_response` flag (set only by the
  ResponseDone-driven stop): a response-final stop drops the stream's
  cursor — cursor presence is exactly "this stream needs resuming".

## Startup resume

* Each persisted stream gets its own daemon connection
  (`ConnectSession.target_thread`; replay — and an already-running
  subagent driver's live broadcast — are per thread, and a new root
  subscriber is not seeded into pre-existing child drivers).
* The daemon's `Replay` is trimmed by the pure `resume::replay_delta`:
  skip the cursor-covered prefix (slicing inside replayed whole-message
  text chunks on char boundaries; clamped if history shrank, e.g. via
  compaction), emit the remainder through the existing rendering arms, and
  append the synthetic `ResponseDone`s replay history never carries —
  counted to restore the exact end-state the live sequence would have
  produced (tool-executing: none; completion in flight: one to consume the
  round-continues flag; idle: one to close, plus one if the delta re-armed
  the flag). Replays for non-resumed streams are dropped (their content is
  already on screen).
* Still-pending choices are re-posted only if their buttons weren't already
  on screen (restored `choice_messages`); pre-restart buttons keep
  working. Subagent output resumes into its existing dedicated thread
  (anchor map seeded from the persisted routes), and replies inside
  subagent threads keep routing to their subagent.

## Tests & docs

* 10 unit tests for the resume store and `replay_delta` (trimming,
  mid-chunk slicing, multibyte boundaries, independent call/result
  cursors, synthetic-done counting, clamping, snapshot/restore round-trip).
* 2 new sim tests: the ResponseDone stop is marked end-of-response, and the
  two-dones-close-a-replayed-tool-round contract `replay_delta` relies on.
* "Restarts and Continuity" section in the Slack bot docs.

Ported from hydro-project/infinity#150.
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