Skip to content

feat(infinity-slack-bot): create a Slack bot - #43

Open
akainth015 wants to merge 11 commits into
mainfrom
push-ssyrmkszpxwn
Open

feat(infinity-slack-bot): create a Slack bot#43
akainth015 wants to merge 11 commits into
mainfrom
push-ssyrmkszpxwn

Conversation

@akainth015

Copy link
Copy Markdown
Member

This is the Slack bot for Infinity agents. I've been using it for about a month, and love it.

❤️ Made with Hydro

@akainth015
akainth015 requested a review from shadaj June 18, 2026 21:10
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploying infinity with  Cloudflare Pages  Cloudflare Pages

Latest commit: 658b599
Status: ✅  Deploy successful!
Preview URL: https://6b7895dd.infinity-dc7.pages.dev
Branch Preview URL: https://push-ssyrmkszpxwn.infinity-dc7.pages.dev

View logs

@akainth015
akainth015 force-pushed the push-ssyrmkszpxwn branch 2 times, most recently from a0573a9 to 6885d51 Compare June 18, 2026 21:25
Comment thread crates/infinity-slack-bot/src/flow.rs
infinity --daemon

# Start the Slack bot
infinity-slack-bot

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 wonder if similar to the providers plugin system, we should have a channels plugin system so that the daemon auto-spawns the Slack bridge and provides it the appropriate socket path?

@akainth015
akainth015 force-pushed the push-ssyrmkszpxwn branch 8 times, most recently from 463bcdb to 789deee Compare June 23, 2026 22:07
@akainth015
akainth015 marked this pull request as ready for review June 23, 2026 22:41
Comment thread crates/infinity-slack-bot/src/flow.rs Outdated
slack_events: Stream<SlackEvent, Process<'a, P>, Unbounded>,
daemon_events: Stream<DaemonEvent, Process<'a, P>, Unbounded>,
) -> (
Stream<SlackAction, Process<'a, P>, Unbounded, NoOrder>,

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.

This seems quite suspicious to me, since the Slack API will care about the order, and the daemon too. So they should probably be totally ordered outputs but have some internal nondets?

akainth015 and others added 8 commits July 31, 2026 18:14
Co-authored-by: Infinity 🤖 <infinity@hydro.run>
…ondet! guards

* Replaced `merge_unordered` with `merge_ordered` for both output streams,
  marking them as `TotalOrder` (the default) to signal that ordering is
  significant to downstream consumers (Slack API, daemon).
* Added `nondet!` guards documenting the sources of non-determinism:
  - `daemon_commands`: button commands vs message commands may interleave
    non-deterministically due to batching.
  - `slack_actions`: daemon-sourced actions vs status actions originate from
    independent event sources with no causal ordering between them.
* Removed the `NoOrder` import since it's no longer used.
* Return type simplified to `Stream<..., Unbounded>` (defaults to TotalOrder).

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
…se appears in a new message

When a button is clicked, the bot now emits a `StreamStop` action for the thread
before the daemon processes the choice. This closes any active streaming message,
so subsequent `TextChunk` events from the daemon start a fresh stream (new Slack
message) rather than appending to the preceding one.

* Clone `button_clicks` and map it to `SlackAction::StreamStop` actions
* Merge these stop actions into the outbound `slack_actions` stream
* The `StreamStop` handler in the sidecar removes the thread from `active_streams`,
  causing the next `StreamAppend` to call `start_fresh` and create a new message

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
…ter click

When a user clicks a button choice, the original message containing the buttons
is now updated via `chat.update` to show "✅ Selected: *{choice}*" instead of
the interactive buttons. This provides clear feedback on which option was chosen.

Changes:
* Add `message_ts` and `button_text` fields to `SlackEvent` to carry the button
  message's timestamp and the clicked button's label text
* Add `InteractiveText` struct and parse `text` from Slack's interactive action payload
* Add `SlackAction::UpdateMessage` variant for updating existing messages
* Add `UpdateMessageRequest` and `SlackClient::update_message()` calling `chat.update`
* In the dataflow, map button clicks to `UpdateMessage` actions that replace the
  button blocks with a static mrkdwn section showing the selection
* Handle `UpdateMessage` in the sidecar outbound loop
* Update all tests to include the new `SlackEvent` fields

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
…teSession and add tests

The previous change only honored `keeps_session_alive` on
`ClientMessage::Connect`, but the Slack bot's primary flow goes through
`CreateSession` + `UserInput`, both of which attached the client with a
hardcoded keep-alive `true`. Slack-created sessions therefore still kept
sessions warm.

Fixes:

* `infinity-protocol`: Added `keeps_session_alive` (serde default `true`) to
  `ClientMessage::CreateSession` as well.

* `client_handler.rs`: Tracks the connection-level `connection_keeps_alive`
  state (set by `CreateSession`/`Connect`) and threads it through
  `attach_client` and `send_input`, so re-attachment after a session restart
  preserves the client's declared keep-alive behavior.

* `infinity-slack-bot`: `create_session` now sends
  `keeps_session_alive: false`.

* Fixed a lost-input race exposed by non-keep-alive connections: when a
  session idles out, its wrapper task takes time to wind down (RAP server
  cleanup), during which `send_input` saw the agent task as still running
  and dropped inputs into the dead agent loop. `Session` now exposes the
  `worker_shutdown` cancellation token; `send_input` treats a cancelled
  token as needing a restart and re-checks synchronously right before
  sending (with one retry), so inputs arriving mid-wind-down transparently
  restart the session instead of vanishing.

Tests (`crates/infinity-daemon/tests/keep_alive.rs`):

* `non_keep_alive_client_does_not_keep_session_warm`: a connected
  Slack-style client must not prevent idle exit (verified to fail against
  the old hardcoded-true behavior).
* `keep_alive_client_keeps_session_warm`: normal clients still keep
  sessions warm.
* `input_after_idle_exit_restarts_session`: follow-up input on the same
  connection after idle exit restarts the session and gets a response.


Co-authored-by: Infinity 🤖 <infinity@hydro.run>
…am age splitting

Three fixes for the Slack bot's interactive and streaming behavior:

## Button dismissal when choice is resolved without a click

Previously, when a user choice (e.g. tool approval buttons) was resolved
by means other than a Slack button click — such as auto-selection by
another client, timeout, or interruption — the button message in Slack
remained interactive indefinitely. Clicking a stale button would silently
fail.

* Added `choice_messages` map to runtime: `choice_id → (channel, message_ts)`
* Extended `SlackAction::PostBlocks` with optional `choice_id`; sidecar
  stores the resulting message_ts when posting choice buttons
* Added `SlackAction::DismissChoiceButtons` variant; on `UserChoiceComplete`
  from the daemon, the sidecar replaces buttons with "⏭️ Choice resolved
  automatically"
* On button click, the entry is removed from tracking so the subsequent
  `UserChoiceComplete` is a no-op

## Tool call indicator indentation after lists

The tool call indicator (🔧 `name(…)`) was rendered indented when it
followed a numbered or bulleted list, because Slack mrkdwn treated a
single `\n` as a list-item continuation.

* Changed the tool call prefix from `\n` to `\n\n` to insert a paragraph
  break that terminates any active list context

## Stream age splitting

Long-running responses previously accumulated into a single streaming
message until it hit the 20k character limit.

* Added `started_at` to `StreamState` and a `MAX_STREAM_AGE` of 3 minutes.
  Once a stream is older than that, the next clean breaking point (chunk
  containing a newline, not inside a code block) stops the current message
  and starts a new one — same mechanism as the existing size-based split.


Co-authored-by: Infinity 🤖 <infinity@hydro.run>
Follows https://docs.slack.dev/ai/developing-agents for an idiomatic agent UX.

* **Manifest (docs)**: migrate `assistant_view` → `agent_view` (with
  `agent_description`), add explicit `app_home` messages-tab settings, and
  subscribe to `app_home_opened` + `app_context_changed`. Warning callouts
  added: manifests replace (not merge) settings, and the `agent_view` switch
  is irreversible.
* **Tool calls as task cards**: `ToolCall` now streams a `task_update` chunk
  (`in_progress`) instead of an inline `🔧` text line; `ToolResult` completes
  the oldest in-flight task. Pending tasks are drained (marked complete) on
  stream stop so messages never finalize with spinners. Falls back to the
  plain-text indicator if the workspace rejects task chunks.
* **Thread titles**: `assistant.threads.setTitle` is called when the daemon
  reports a session title (on `Connected` and via `SessionsUpdated`), deduped
  through a runtime title cache.
* **Suggested prompts**: `app_home_opened` (tab == "messages") pins starter
  prompts to the top of the Messages tab, once per user per run.
* **Content disclaimer**: streams finalize via `chat.stopStream` with an
  AI-generated-content context block, with fallback to a plain stop.
* Runtime gains `tool_tasks`, `tool_task_seq`, `thread_titles`, and
  `app_home_seen` state; `SlackEvent` gains `is_app_home_opened`.
* Tests updated for task updates; new parsing tests for `app_home_opened`.

Not implemented (follow-up): feedback buttons (`context_actions` /
`feedback_buttons`) — needs an interaction handler that doesn't collide with
choice-button parsing, and a feedback sink to record results.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
Previously `UserChoiceRequired` posted the choice buttons as a new message but
left the active stream open, so output produced after the choice continued
appending to the pre-choice message (which sits above the buttons) instead of
starting a new message.

* `UserChoiceRequired` now emits `StreamStop` followed by `PostBlocks`, so the
  pre-choice message is finalized (and buffered text flushed) before the
  buttons are posted and any subsequent output begins a fresh message.
  `StreamStop` is a no-op when no stream is active.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
@akainth015
akainth015 requested a review from a team July 31, 2026 21:44
akainth015 added a commit that referenced this pull request Jul 31, 2026
The CI "Lint" check on PR #43 (infinity-slack-bot) was failing because
the `THIRD-PARTY` license file was stale. The `cargo about generate`
step detected differences between the committed file and what the
current `Cargo.lock` produces.

Applied the exact diff from the CI run to update `THIRD-PARTY`:
- Removed stale `windows-*` v0.53 and `windows-sys` v0.60.2 entries
  (dependencies that were pruned from the lock file)
- Added new license entries for transitive dependencies brought in by
  `infinity-slack-bot`: `sealed 0.6.0`, `toml_datetime 1.1.1`,
  `toml_edit 0.25.13`, `toml_parser 1.1.3`, and many others
  (tokio-tungstenite's dependency tree, hydro_lang's dependencies, etc.)
- Added new license text sections (Apache-2.0 for sealed, MPL-2.0 for
  new crates, MIT for various new transitive deps)

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
akainth015 and others added 2 commits July 31, 2026 22:13
The previous THIRD-PARTY regeneration was applied from the GitHub
Actions log, which strips carriage returns. bincode 1.3.3's LICENSE.md
has CRLF line endings and `cargo about` embeds it verbatim, so the
committed block differed from the generated one by invisible `\r`
characters only.

Restored `\r\n` endings on the 21-line bincode MIT license block
(lines 11299-11319). Verified byte-for-byte: `git hash-object
THIRD-PARTY` now yields d579781, the exact blob CI's regeneration step
produces.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
Previously, switching the default model via /model (or the modal picker)
only updated the in-memory Runtime::default_model, so the selection was
lost on restart.

- config.rs: Config now records the path it was loaded from
  (#[serde(skip)] field, None for in-memory test configs). New
  Config::save_default_model() patches only the default_model field in
  the JSON file via serde_json::Value, preserving all other fields —
  including ones unknown to the Config struct. No-op when there is no
  backing file.
- flow.rs: handle_model_command() calls save_default_model() after
  updating the in-memory default. On save failure it logs the error and
  appends a warning to the Slack reply that the selection will reset on
  restart.
- runtime.rs: test config literal updated for the new path field.

Added unit tests covering field-preserving patching and the no-path
no-op. Verified with cargo test and clippy on infinity-slack-bot.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
…one change

Cut the daemon race-condition fix out of PR #43, per request. The fix
is superseded by the standalone change `uuokousl` ("fix lost-input race
on non-keep-alive session idle exit"), whose diff is essentially
identical to the daemon portion of `wltlpvwo` in the PR stack.

Created commit `twkklplz` on top of `lptsrxyl` (`l`) that:

* Reverts `crates/infinity-daemon/src/session/mod.rs` (worker_shutdown
  token + send_input retry loop) and
  `crates/infinity-daemon/tests/keep_alive.rs` (the
  `input_after_idle_exit_restarts_session` test) to their
  pre-`wltlpvwo` state — verified byte-identical via
  `jj diff --from wltlpvwo- --to @`.
* Keeps `crates/infinity-slack-bot/src/daemon_client.rs` sending
  `keeps_session_alive: false` (the protocol field exists on main, so
  this stands alone).

Verified: no commit between `wltlpvwo` and `lptsrxyl` touches the
reverted files; `cargo check --all-targets` passes for both
`infinity-daemon` and `infinity-slack-bot`; the two remaining
keep-alive tests pass.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
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