Skip to content

feat(discord): add /kill and /stop to interrupt a running agent - #253

Open
tbertran wants to merge 5 commits into
moazbuilds:masterfrom
tbertran:feat/discord-kill-stop
Open

feat(discord): add /kill and /stop to interrupt a running agent#253
tbertran wants to merge 5 commits into
moazbuilds:masterfrom
tbertran:feat/discord-kill-stop

Conversation

@tbertran

@tbertran tbertran commented Jul 13, 2026

Copy link
Copy Markdown

Problem

Telegram gained /kill in #184 (feat(telegram): live streaming, /verbose tool display, /fork, /kill), but Discord never got the equivalent.

killActive() and the mainActiveProcs registry already exist in runner.ts and are transport-agnostic — Discord just never imported them. The result is that a Discord user has no way to stop a run in flight. If the agent goes off the rails, the only recourse is killing the daemon process.

Change

Wire the existing killActive() into the Discord message handler:

  • /kill (and /stop as an alias) kills all main-queue claude subprocesses and confirms in-channel.
  • Dispatched immediately after the command is parsed, before skill routing and before runUserMessage() — so it takes effect right away instead of queueing behind the run it is meant to cancel. This works because Discord's gateway message handler is event-driven and runs concurrently with an in-flight run.
  • Fork runs are untouched: mainActiveProcs deliberately excludes them, per the existing contract in runner.ts.

Known limitation: kill is global, not caller-scoped

killActive() terminates every in-flight main run across all Discord channels and threads, not just the caller's. Two consequences worth stating plainly:

This matches Telegram's existing /kill contract (#184), so it is not a regression introduced here, and this PR deliberately does not change it. Thread/channel-scoped kill is tracked separately — the same concern is raised on #237. The limitation is also documented at the call site.

Version

plugin.json and marketplace.json bumped to 1.0.45. The review asked for 1.0.44, but upstream master consumed 1.0.44 via a merged PR in the interim, so 1.0.45 is the next free version.

Also included

chore(discord): update fallback model to claude-sonnet-5 — an unrelated one-line model-string update that was sitting in the same working tree. Kept as its own commit so it can be dropped independently if you would rather it ship separately.

Testing

  • bun build src/index.ts — clean.
  • bun test131 pass, 6 fail. The same 6 fail on a clean master on this machine (pre-existing: plugins.test.ts, and jobs.test.ts asserting toContain("/.claude/") against Windows backslash paths). No change in results.
  • Demonstrated end-to-end on a live Discord daemon. /kill typed in a real channel against an in-flight run returned Killed active agent. and the run stopped. The terminated subprocess then surfaced Error (exit 1): Unknown error in-channel — that is the killed process's abnormal exit travelling the normal error path, and is corroborating evidence that the process really was terminated rather than allowed to finish. Suppressing that message when the exit was a deliberate kill is worth a follow-up.

Branch is synced with upstream master via a merge commit (no force-push).

🤖 Generated with Claude Code

Telegram gained /kill in moazbuilds#184 but Discord never got the equivalent, so a
Discord user has no way to stop a run in flight — the only recourse is
restarting the daemon.

Wire the existing killActive() into the Discord message handler, dispatched
before skill routing so it takes effect immediately rather than queueing
behind the run it is meant to cancel. /stop is accepted as an alias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Solid, well-scoped change and the mechanics check out — /kill and /stop are dispatched before skill routing and runUserMessage, so they take effect immediately rather than queueing behind the run they cancel; the dispatch sits behind the auth gate (unauthorized users can't kill runs); sendMessage(token, channelId, …) is correct for Discord; and it reuses the existing transport-agnostic killActive() with fork runs correctly excluded. It merges clean against master.

Three things before this can ship:

  1. Please demonstrate /kill end-to-end on a live Discord daemon before merge. You've flagged yourself that it hasn't been exercised yet — and an interrupt control is exactly the kind of thing that has to be shown working, not inferred from parity with the Telegram path. A short confirmation (or a clip) that a /kill in a real channel actually stops an in-flight run is the bar here.

  2. Missing version bump. discord.ts is shipped plugin content, so plugin-version-guard / marketplace-version-guard require a bump — but no .claude-plugin/*.json is touched. Run bun run bump:plugin-version + bun run bump:marketplace-version and target 1.0.44 (master is at 1.0.43).

  3. Please note the current blast radius in the PR (and ideally the code comment): killActive() is global — it kills every in-flight main run across all Discord channels/threads, not just the caller's. On a multi-channel server a /kill in one channel cancels everyone's runs, and combined with the channel-scoped allowlist (#248) a user scoped to one channel can kill runs in channels they aren't authorized for. This matches Telegram's existing /kill contract, so it's not a regression and I'm not asking you to fix it here — but it should be documented, and the thread-scoped fix tracked separately (see the same concern raised on #237).

if (command === "/kill" || command === "/stop") {
const killed = killActive();
await sendMessage(config.token, channelId, killed ? "Killed active agent." : "No active agent running.");
return;
}

tbertran and others added 4 commits July 19, 2026 11:17
…st radius

Addresses review feedback on moazbuilds#253:
- plugin-version-guard / marketplace-version-guard require a bump for
  shipped plugin content; upstream master already consumed 1.0.44, so
  this lands on 1.0.45.
- Note the blast radius of killActive() at the call site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
killActive() terminates every in-flight main run across all channels and
threads. Matches Telegram's existing contract (moazbuilds#184), not a regression;
thread-scoped kill tracked separately (moazbuilds#237).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tbertran

tbertran commented Jul 19, 2026

Copy link
Copy Markdown
Author

Thanks for the review — all three addressed.

1. /kill demonstrated end-to-end on a live Discord daemon. Typed /kill in a real channel against an in-flight run; the daemon replied Killed active agent. and the run stopped. One nuance worth recording: immediately after the confirmation, the terminated subprocess surfaced Error (exit 1): Unknown error in-channel. That is the killed process's abnormal exit travelling the normal error path — so it is actually corroborating evidence that the process was genuinely terminated rather than allowed to finish. It is cosmetic noise on an otherwise successful cancel, and suppressing it when the exit was a deliberate kill is worth a follow-up; I did not fold that into this PR to keep the change scoped.

2. Version bumped — to 1.0.45, not 1.0.44. Between your review and this push, upstream master merged another PR and consumed 1.0.44, so 1.0.44 was no longer free. Ran both bump:plugin-version and bump:marketplace-version on top of a merge with current upstream/master; plugin.json and marketplace.json are both at 1.0.45. Flagging explicitly since it deviates from the number you asked for.

3. Blast radius documented. Added a "Known limitation: kill is global, not caller-scoped" section to the PR description covering the cross-channel kill, the interaction with the channel-scoped allowlist (#248), that it matches Telegram's existing contract (#184) and so is not a regression, and that thread-scoped kill is tracked separately (#237). Also noted at the call site in discord.ts.

Two other things worth knowing:

  • The branch is now synced with upstream/master via a merge commit, deliberately not a rebase, so no force-push was needed on a branch already under review.
  • There is one unrelated commit riding along: chore(discord): update fallback model to claude-sonnet-5, a one-line model-string update that was already in the working tree. I kept it as its own commit so you can ask for it to be dropped independently — say the word and I will split it out.

Test results after the merge: 131 pass, 6 fail. The same 6 fail on a clean master on this machine (pre-existing: plugins.test.ts, and jobs.test.ts asserting toContain("/.claude/") against Windows backslash paths).


CI note: plugin-version-guard and marketplace-version-guard are both sitting in action_required on this head — fork PRs need a maintainer to approve the workflow runs before they execute, so the guards you cited have not actually run yet. A click on "Approve and run workflows" should turn them over now that both versions are at 1.0.45.

Separately, claude-review is failing with an empty ANTHROPIC_API_KEY ("anthropic_api_key": "" in the job log). That is the repository secret not being exposed to fork-triggered workflows rather than anything in this change — the same failure is on #249 and #254 at their current heads.

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