Skip to content

Show that a Slack turn is still running - #1166

Merged
edwin-zvs merged 1 commit into
mainfrom
slack-progress-affordance
Aug 2, 2026
Merged

Show that a Slack turn is still running#1166
edwin-zvs merged 1 commit into
mainfrom
slack-progress-affordance

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Requested after testing #1162: when the agent takes a while, give the person in Slack some sign it's working.

Today a slow turn leaves the thread completely silent until the answer lands. From the thread that's indistinguishable from a dropped delivery — and the reasonable response, sending it again, starts a second turn in the same thread.

Configuration

Per Slack channel, as requested:

[channels.my-bot]
kind = "slack"
progress = "placeholder"   # off | placeholder | reaction | both
value behavior
placeholder (default) Posts a thread message that later becomes the answer itself
reaction 👀 on the triggering message, then ✅ (⚠️ on failure)
both Both
off Silent until the answer is ready

Nothing appears for a turn that answers promptly — it waits 8s first, so the common case stays exactly as clean as it is now. The affordance is for a wait that has already become long enough to look like a failure.

Three things that make it honest rather than decorative

It stops claiming to work when it isn't. A turn parked on a tool approval will not resume until an operator acts in the TUI — and the person waiting in Slack cannot see that prompt. The placeholder changes to name the tool awaiting sign-off. The ingress publishes the turn's phase on a watch channel and the channel renders it; publishing is advisory, so nothing about a turn depends on anyone reading it.

It resolves, including on failure. Whatever was posted is replaced by the outcome. Turn failures previously went only to the daemon log, leaving the asker with silence forever; now the thread says the turn ended without an answer.

A missing scope is cosmetic, never fatal. reactions:write is a scope an existing app install won't have. Construct logs the refusal and still delivers the answer.

Two smaller things

  • The channel put path can't express progress yet, so it carries the existing value forward — otherwise saving an unrelated field (a rotated token, an allowlist edit) would silently reset the operator's choice back to default.
  • Fixes a latent conflation: a reaction belongs on the message that triggered the turn, which is not the thread root for a reply inside a thread. Left as-is, every reaction in a long thread would stack onto its first message.

Tests

a_slow_turn_announces_itself_then_says_what_it_is_blocked_on drives the real affordance against a stub Slack Web API and asserts the whole lifecycle: reaction on 222.22 (the message) while the placeholder goes to thread 111.11, then chat.update rewriting it to the approval text. Runs in 0.04s.

Plus: silence for a fast turn, off touching nothing, progress-phase transitions and their de-duplication, TOML parsing incl. the pre-existing-definition default, and put-path preservation.

cargo test --workspace: 51 test binaries, 0 failures.

Not covered by tests: the Slack calls themselves are exercised against a stub, not a real workspace — worth a live sanity check in your thread before merging.

Notes

  • Default is placeholder, so existing Slack channels get the affordance on upgrade. Set progress = "off" to keep them silent.
  • Service definitions hot-reload (spec 0173), so editing the TOML applies without a restart.
  • Not yet exposed as a row in the TUI channel dialog — that's a contained follow-up if you want it.

New spec 0178-a-channel-shows-that-a-turn-is-still-running.md; docs/services.md gains a Slack channel section (it had none).

Binary

Touches crates/daemon → the relevant binary is construct.

A service turn that takes a while left its Slack thread completely silent
until the answer arrived. From the thread that is indistinguishable from a
delivery that was dropped, and the reasonable response — send it again —
starts a second turn in the same thread.

Slack channels now show that a turn is in flight, configured per channel:

    [channels.my-bot]
    progress = "placeholder"   # off | placeholder | reaction | both

- placeholder (default) posts a thread message that later becomes the answer
- reaction marks the triggering message 👀, then ✅ (⚠️ on failure)

Nothing appears for a turn that answers promptly. The affordance waits 8s
first, so the common case stays exactly as clean as it is today; it exists for
a wait that has already become long enough to look like a failure.

Three things make it honest rather than decorative:

- A turn stopped at a tool approval says so and names the tool. That turn will
  not resume until an operator acts in the TUI, and the person waiting in
  Slack cannot see that prompt — so "working on it" would be a lie. The
  ingress publishes the turn's phase on a watch channel; the channel renders
  it. Publishing is advisory and nothing waits on a reader.
- Whatever the affordance posted is replaced by the outcome, including when
  the turn fails. Failures previously went only to the daemon log, leaving the
  asker with silence; now the thread says the turn ended without an answer.
- Anything cosmetic that Slack refuses is logged, not fatal. reactions:write
  is a scope an existing app install will not have, so a missing scope must
  not turn a delivered answer into a failed delivery.

The channel put path cannot express `progress` yet, so it carries the existing
value forward — otherwise saving an unrelated field would silently reset an
operator's choice.

Also fixes a latent conflation: reactions target the message that triggered
the turn, which is not the thread root for a reply inside a thread.
@edwin-zvs
edwin-zvs merged commit 2feb04a into main Aug 2, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the slack-progress-affordance branch August 2, 2026 15:04
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