Skip to content

feat(channel): add /stop command to cancel in-flight tasks#3891

Merged
theonlyhennygod merged 1 commit intozeroclaw-labs:masterfrom
theredspoon:feat/stop-command
Mar 19, 2026
Merged

feat(channel): add /stop command to cancel in-flight tasks#3891
theonlyhennygod merged 1 commit intozeroclaw-labs:masterfrom
theredspoon:feat/stop-command

Conversation

@theredspoon
Copy link
Copy Markdown
Collaborator

@theredspoon theredspoon commented Mar 18, 2026

Summary

  • Base branch target: master
  • Problem: No explicit cancellation mechanism exists for users on non-CLI channels whose agent is mid-task ([Feature]: /reasoning and /stop command #2401 ); the only prior approach (channels: add /reasoning and /stop runtime commands #2855) had a race condition where the /stop handler cancelled itself rather than the in-flight task
  • Why it matters: Clients on Matrix, Telegram, Slack, Discord, etc. cannot stop a runaway or misdirected task without disconnecting
  • What changed: Added /stop slash command intercepted before semaphore acquisition; separated register_in_flight from interrupt_enabled so all non-CLI tasks are reachable
  • What did not change: Auto-cancel-on-new-message behavior (still gated on interrupt_enabled for Telegram/Slack only); CLI behavior; any channel implementation file

Label Snapshot (required)

  • Risk label: risk: medium
  • Size label: size: S
  • Scope labels: channel
  • Module labels: channel: dispatch
  • Contributor tier label: (auto-managed)
  • If any auto-label is incorrect, note requested correction: N/A

Change Metadata

  • Change type: feature
  • Primary scope: channel

Linked Issue

Supersede Attribution (required when Supersedes # is used)

  • Superseded PRs + authors: #2855 by @theredspoon
  • Integrated scope by source PR: Problem statement and /stop command concept; the race condition in channels: add /reasoning and /stop runtime commands #2855 is the root cause this PR resolves with a different implementation approach
  • Co-authored-by trailers added for materially incorporated contributors? No
  • If No, explain why: Same author; no external code carried forward
  • Trailer format check: N/A

Validation Evidence (required)

cargo fmt --all -- --check    # clean
cargo clippy --all-targets -- -D warnings  # clean
cargo test --locked            # 4077 passed, 0 failed
  • Evidence provided: Local test run, pre-push hook passed
  • If any command is intentionally skipped, explain why: N/A

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Privacy and Data Hygiene (required)

  • Data-hygiene status: pass
  • Redaction/anonymization notes: None — no user data handled
  • Neutral wording confirmation: Pass

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

i18n Follow-Through (required when docs or user-facing wording changes)

  • i18n follow-through triggered? No — no user-facing doc changes in this PR

Human Verification (required)

  • Verified scenarios: /stop fires cancellation token for in-flight Slack and Matrix tasks in local testing; "No in-flight task" reply returns correctly when no task is running; CLI channel correctly excluded
  • Edge cases checked: /stop with @botname suffix; case-insensitive input; /stop arriving after task completes (no crash)
  • What was not verified: Live production channel end-to-end (requires deployed instance)

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Async dispatch loop in src/channels/mod.rs — all non-CLI channels
  • Potential unintended effects: None — scope key includes sender, so Bob's /stop in the same room produces a different key than Alice's in-flight task and cannot cancel it
  • Guardrails/monitoring for early detection: Existing observer hooks record task cancellations; ToolLoopCancelled return value is observable in logs

Agent Collaboration Notes (recommended)

  • Agent tools used: Claude Code (claude-sonnet-4-6)
  • Workflow/plan summary: Scoped from gap analysis → implement → TDD → review cycle → push
  • Verification focus: Race condition elimination vs channels: add /reasoning and /stop runtime commands #2855; correct store key construction
  • Confirmation: naming + architecture boundaries followed per AGENTS.md + CONTRIBUTING.md

Rollback Plan (required)

  • Fast rollback command/path: git revert <commit> — single commit
  • Feature flags or config toggles: None — /stop is always available on non-CLI channels; no opt-out config
  • Observable failure symptoms: /stop messages processed as normal agent input rather than cancellation signals

Risks and Mitigations

  • Risk: /stop fast path runs before semaphore acquisition — if in_flight_by_sender lookup has a bug, it silently sends "No in-flight task" rather than crashing
    • Mitigation: The remove operation is atomic under the mutex; the reply is sent via tokio::spawn so it cannot block the dispatch loop

theredspoon added a commit to theredspoon/zeroclaw that referenced this pull request Mar 18, 2026
Wires Matrix into the auto-cancel-on-new-message mechanism that was
already in place for Telegram and Slack.

Changes:
- MatrixConfig: adds interrupt_on_new_message: bool (serde default false,
  backward-compatible — existing configs without the field continue to
  work unchanged)
- InterruptOnNewMessageConfig: adds matrix: bool field; enabled_for_channel
  adds "matrix" => self.matrix arm; runtime init reads the Matrix config
  flag and passes it into the struct at startup
- All test struct literals updated (30 sites) to include matrix: false
- Two new schema tests: matrix_config_deserializes_interrupt_on_new_message_true,
  matrix_config_defaults_interrupt_on_new_message_to_false
- MatrixRecordingChannel helper + integration test
  message_dispatch_interrupts_in_flight_matrix_request_and_preserves_context,
  mirroring the equivalent Telegram and Slack dispatch tests

Depends on zeroclaw-labs#3891

Tests: all new tests pass; pre-existing openai_codex failure is unrelated
and present on the base branch.
Adds an explicit /stop slash command that allows users on any non-CLI
channel (Matrix, Telegram, Discord, Slack, etc.) to cancel an agent
task that is currently running.

Changes:
- is_stop_command(): new helper that detects /stop (case-insensitive,
  optional @botName suffix), not gated on channel type
- /stop fast path in run_message_dispatch_loop: intercepts /stop before
  semaphore acquisition so the target task is never replaced in the store;
  fires CancellationToken on the running task; sends reply via tokio::spawn
  using the established two-step channel lookup pattern
- register_in_flight separated from interrupt_enabled: all non-CLI tasks
  now enter the in_flight_by_sender store, enabling /stop to reach them;
  auto-cancel-on-new-message remains gated on interrupt_enabled (Telegram/
  Slack only) — this is a deliberate broadening, not a side effect

Deferred to follow-up (feat/matrix-interrupt-on-new-message):
- interrupt_on_new_message config field for Matrix
- thread-aware interruption_scope_key (requires per-channel thread_ts
  semantics analysis; Slack always sets thread_ts, Matrix only for replies)

Supersedes zeroclaw-labs#2855

Tests: 7 new unit tests for is_stop_command; all 4075 tests pass.
theredspoon added a commit to theredspoon/zeroclaw that referenced this pull request Mar 18, 2026
Wires Matrix into the auto-cancel-on-new-message mechanism that was
already in place for Telegram and Slack.

Changes:
- MatrixConfig: adds interrupt_on_new_message: bool (serde default false,
  backward-compatible — existing configs without the field continue to
  work unchanged)
- InterruptOnNewMessageConfig: adds matrix: bool field; enabled_for_channel
  adds "matrix" => self.matrix arm; runtime init reads the Matrix config
  flag and passes it into the struct at startup
- All test struct literals updated (30 sites) to include matrix: false
- Two new schema tests: matrix_config_deserializes_interrupt_on_new_message_true,
  matrix_config_defaults_interrupt_on_new_message_to_false
- MatrixRecordingChannel helper + integration test
  message_dispatch_interrupts_in_flight_matrix_request_and_preserves_context,
  mirroring the equivalent Telegram and Slack dispatch tests

Depends on zeroclaw-labs#3891

Tests: all new tests pass; pre-existing openai_codex failure is unrelated
and present on the base branch.
theredspoon added a commit to theredspoon/zeroclaw that referenced this pull request Mar 18, 2026
Wires Matrix into the auto-cancel-on-new-message mechanism that was
already in place for Telegram and Slack.

Changes:
- MatrixConfig: adds interrupt_on_new_message: bool (serde default false,
  backward-compatible — existing configs without the field continue to
  work unchanged)
- InterruptOnNewMessageConfig: adds matrix: bool field; enabled_for_channel
  adds "matrix" => self.matrix arm; runtime init reads the Matrix config
  flag and passes it into the struct at startup
- All test struct literals updated (30 sites) to include matrix: false
- Two new schema tests: matrix_config_deserializes_interrupt_on_new_message_true,
  matrix_config_defaults_interrupt_on_new_message_to_false
- MatrixRecordingChannel helper + integration test
  message_dispatch_interrupts_in_flight_matrix_request_and_preserves_context,
  mirroring the equivalent Telegram and Slack dispatch tests

Depends on zeroclaw-labs#3891

Tests: all new tests pass; pre-existing openai_codex failure is unrelated
and present on the base branch.
theredspoon added a commit to theredspoon/zeroclaw that referenced this pull request Mar 18, 2026
The echo_provider() test helper used a shared temp file created via
OnceLock. While this avoided recreating the file, it introduced race
conditions when parallel tests executed the same script simultaneously:

- ETXTBSY (errno 26): 'Text file busy' when spawning the script
- EPIPE (errno 32): 'Broken pipe' when the script process died early

This commit replaces the shared file with unique per-invocation temp
files using thread ID + nanosecond timestamp. Each test now gets its
own isolated script, eliminating all parallelization races.

Changes:
- Remove OnceLock-based shared script
- Generate unique script path per echo_provider() call
- Add f.sync_all() to ensure file is fully written before chmod
- Use 'exec cat' instead of 'cat' to avoid extra shell process

Testing:
- 5 consecutive runs with --test-threads=20 all pass
- Addresses intermittent CI failures in PRs zeroclaw-labs#3891, zeroclaw-labs#3895

Fixes zeroclaw-labs#3902
theredspoon added a commit to theredspoon/zeroclaw that referenced this pull request Mar 18, 2026
Wires Matrix into the auto-cancel-on-new-message mechanism that was
already in place for Telegram and Slack.

Changes:
- MatrixConfig: adds interrupt_on_new_message: bool (serde default false,
  backward-compatible — existing configs without the field continue to
  work unchanged)
- InterruptOnNewMessageConfig: adds matrix: bool field; enabled_for_channel
  adds "matrix" => self.matrix arm; runtime init reads the Matrix config
  flag and passes it into the struct at startup
- All test struct literals updated (30 sites) to include matrix: false
- Two new schema tests: matrix_config_deserializes_interrupt_on_new_message_true,
  matrix_config_defaults_interrupt_on_new_message_to_false
- MatrixRecordingChannel helper + integration test
  message_dispatch_interrupts_in_flight_matrix_request_and_preserves_context,
  mirroring the equivalent Telegram and Slack dispatch tests

Depends on zeroclaw-labs#3891

Tests: all new tests pass; pre-existing openai_codex failure is unrelated
and present on the base branch.
Copy link
Copy Markdown
Collaborator

@theonlyhennygod theonlyhennygod left a comment

Choose a reason for hiding this comment

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

LGTM — cancellation logic is sound, race condition from #2855 eliminated by fast-path before semaphore. Scope key prevents cross-user cancellation. CI green, no conflicts.

@theonlyhennygod theonlyhennygod merged commit b5668ac into zeroclaw-labs:master Mar 19, 2026
17 checks passed
theonlyhennygod added a commit to theredspoon/zeroclaw that referenced this pull request Mar 19, 2026
Keep both the PR's Mattermost interrupt_on_new_message additions
and master's new fields (pending_new_sessions, prompt_config,
autonomy_level) from the /stop command PR (zeroclaw-labs#3891).
theonlyhennygod added a commit to theredspoon/zeroclaw that referenced this pull request Mar 19, 2026
Keep Discord interrupt additions while incorporating master's
consolidated test structure and new fields (pending_new_sessions,
prompt_config, autonomy_level).
theonlyhennygod added a commit that referenced this pull request Mar 20, 2026
… scoping

Merges PR #3900 from theredspoon with conflict resolution:
- kept Discord/Mattermost interrupt support from master
- kept pending_new_sessions/prompt_config/autonomy_level fields
- resolved all 30 channel construction sites

Depends on #3891
theonlyhennygod added a commit that referenced this pull request Mar 20, 2026
… scoping (#4017)

Add interruption_scope_id to ChannelMessage for thread-aware cancellation. Slack genuine thread replies and Matrix threads get scoped keys, preventing cross-thread cancellation. All other channels preserve existing behavior.

Supersedes #3900. Depends on #3891.
joe2643 added a commit to joe2643/zeroclaw that referenced this pull request Mar 20, 2026
Resolve 28 merge conflicts from master. Main changes from upstream:
- interruption_scope_id for thread-aware cancellation (zeroclaw-labs#4017)
- autonomy_level in channel prompts (zeroclaw-labs#3952)
- /stop command (zeroclaw-labs#3891)
- Various provider and tool fixes

All conflicts resolved by keeping both sides:
- observe_group + interruption_scope_id in ChannelMessage
- All new ChannelRuntimeContext fields in tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@theredspoon theredspoon deleted the feat/stop-command branch March 20, 2026 14:24
webhive pushed a commit to webhive/zeroclaw that referenced this pull request Mar 24, 2026
… scoping (zeroclaw-labs#4017)

Add interruption_scope_id to ChannelMessage for thread-aware cancellation. Slack genuine thread replies and Matrix threads get scoped keys, preventing cross-thread cancellation. All other channels preserve existing behavior.

Supersedes zeroclaw-labs#3900. Depends on zeroclaw-labs#3891.
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