Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ All notable changes to `@inceptionstack/roundhouse` are documented here.

### Added
- Gateway with Telegram adapter, per-thread agent sessions
- `/new`, `/restart`, `/status`, `/compact`, `/verbose`, `/stop`, `/doctor` commands
- `/new`, `/restart`, `/status`, `/compact`, `/verbose`, `/cancel`, `/doctor` commands
- Auto-register bot commands with Telegram on startup
- Draining/drain_complete notification system
- Context token usage with progress bar
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ Roundhouse automatically registers these commands with Telegram on startup:
| `/compact` | Compact session context to free up tokens |
| `/verbose` | Toggle tool status messages on/off for this chat |
| `/status` | Show gateway status: version, agent, model, context usage, uptime, etc. |
| `/stop` | Stop the current agent run (abort tools, LLM calls, compaction) |
| `/cancel` | Stop the current agent run (abort tools, LLM calls, compaction) |
| `/restart` | Restart the gateway service (requires `allowedUsers` to be configured) |
| `/doctor` | Run health checks and show system status |
| `/crons` | Manage scheduled jobs (list, trigger, pause, resume) |
Expand All @@ -242,7 +242,7 @@ Manually triggers context compaction for the current chat's session. Shows befor

Toggles verbose mode for the current chat. When ON, shows tool call status messages (e.g. "⚡ Running `bash`…"). When OFF (default), tool calls execute silently — you only see the agent's text responses. State shown in `/status`.

### `/stop`
### `/cancel`

Aborts the current agent run for this chat — stops any in-progress tool calls, LLM generation, and compaction. The session is preserved; send another message to continue the conversation.

Expand Down
2 changes: 1 addition & 1 deletion docs/refactoring-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
| 4.7 | *Run tests* | Green bar |
| 4.8 | *Extract Module: notifications* | Move `notifyStartup`, `postWithFallback`, `registerBotCommands` → `src/gateway/notifications.ts` |
| 4.9 | *Run tests* | Green bar |
| 4.10 | *Extract Module: command-router* | Move Telegram command handlers (`/status`, `/compact`, `/update`, `/crons`, `/stop`, `/verbose`, `/doctor`) → `src/gateway/command-router.ts` as a handler map |
| 4.10 | *Extract Module: command-router* | Move Telegram command handlers (`/status`, `/compact`, `/update`, `/crons`, `/cancel`, `/verbose`, `/doctor`) → `src/gateway/command-router.ts` as a handler map |
| 4.11 | *Run tests* | Green bar |
| 4.12 | *Thin Gateway class* | `Gateway` becomes orchestrator importing from sub-modules (~100 lines) |
| 4.13 | *Move file* | Rename `src/gateway.ts` → `src/gateway/index.ts`, update imports |
Expand Down
2 changes: 1 addition & 1 deletion docs/transport-adapter-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export interface TransportAdapter {
- Agent adapter lifecycle (create, prompt, stream)
- Thread queue / concurrency management
- Memory system (flush, compact, pressure)
- Command routing (/new, /stop, /status, etc.)
- Command routing (/new, /cancel, /status, etc.)
- Config loading
- Allowed user authorization

Expand Down
10 changes: 5 additions & 5 deletions src/gateway/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,25 @@ export async function handleStatus(ctx: CommandContext): Promise<void> {
console.log(`[roundhouse] /status for thread=${thread.id} agentThread=${agentThreadId}`);
}

// ── /stop ────────────────────────────────────────────
// ── /cancel ────────────────────────────────────────────

export interface StopContext {
export interface CancelContext {
thread: any;
agentThreadId: string;
agent: AgentAdapter;
abortControllers: Map<string, AbortController>;
}

export async function handleStop(ctx: StopContext): Promise<void> {
export async function handleCancel(ctx: CancelContext): Promise<void> {
const { thread, agentThreadId, agent, abortControllers } = ctx;
if (agent.abort) {
await agent.abort(agentThreadId);
abortControllers.get(agentThreadId)?.abort();
try { await thread.post("⏹️ Stopped."); } catch {}
try { await thread.post("⏹️ Cancelled."); } catch {}
} else {
try { await thread.post("⚠️ Abort not supported for this agent."); } catch {}
}
console.log(`[roundhouse] /stop for thread=${thread.id} agentThread=${agentThreadId}`);
console.log(`[roundhouse] /cancel for thread=${thread.id} agentThread=${agentThreadId}`);
}

// ── /verbose ─────────────────────────────────────────
Expand Down
16 changes: 8 additions & 8 deletions src/gateway/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { PressureLevel } from "../memory/types";
import { isCommand as _isCmd, isCommandWithArgs as _isCmdArgs, resolveAgentThreadId as _resolveThread, getSystemResources as _getSysRes } from "./helpers";
import { saveAttachments, type AttachmentResult } from "./attachments";
import { handleStreaming as _handleStream, StreamModelOverflowError } from "./streaming";
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleStop, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleCancel, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
import { handleModel, handleModelAction, MODEL_ACTION_ID } from "./model-command";
import { handleLater } from "./later-command";
import { handleTopic, handleTopicAction, TOPIC_ACTION_ID, applyTopicOverride } from "./topic-command";
Expand Down Expand Up @@ -209,10 +209,10 @@ export class Gateway {
// Per-thread verbose toggle (shows tool_start messages)
const verboseThreads = this.verboseThreads;

// Per-thread abort signal for /stop
// Per-thread abort signal for /cancel
const abortControllers = this.abortControllers;

// Per-thread lock to serialize prompts (concurrent mode lets /stop through)
// Per-thread lock to serialize prompts (concurrent mode lets /cancel through)
const threadLocks = this.threadLocks;

// ── Build command descriptors ──────────────────────
Expand Down Expand Up @@ -271,7 +271,7 @@ export class Gateway {
const text = (message.text ?? "").trim();

// Pre-turn commands fire before the main handler (and before the
// session-pressure gate), so /stop etc. still interrupt a mid-run
// session-pressure gate), so /cancel etc. still interrupt a mid-run
// agent. Allowlist is enforced here for all pre-turn handlers.
for (const desc of preTurnCommands) {
if (matchesDescriptor(desc, text, matchers)) {
Expand Down Expand Up @@ -387,7 +387,7 @@ export class Gateway {

const agent = this.router.resolve(agentThreadId);

// Serialize prompts per-thread (concurrent mode allows /stop to bypass)
// Serialize prompts per-thread (concurrent mode allows /cancel to bypass)
const prevLock = threadLocks.get(agentThreadId);
let releaseLock: () => void;
const lockPromise = new Promise<void>((resolve) => { releaseLock = resolve; });
Expand Down Expand Up @@ -750,7 +750,7 @@ export class Gateway {
*
* Stage:
* - "in-turn" (default): runs after allowlist + pairing inside handle()
* - "pre-turn": runs first in handleOrAbort() so commands like /stop
* - "pre-turn": runs first in handleOrAbort() so commands like /cancel
* can interrupt an in-flight agent turn
*
* Per-request state (thread, message, text) comes in via CommandInvocation;
Expand Down Expand Up @@ -813,9 +813,9 @@ export class Gateway {

// ── Pre-turn commands (abort-style; fire even during agent turn) ──
{
triggers: ["/stop"],
triggers: ["/cancel"],
stage: "pre-turn",
invoke: ({ thread, agentThreadId }) => handleStop({
invoke: ({ thread, agentThreadId }) => handleCancel({
thread, agentThreadId,
agent: this.router.resolve(agentThreadId),
abortControllers,
Expand Down
2 changes: 1 addition & 1 deletion src/transports/telegram/bot-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const BOT_COMMANDS: BotCommand[] = [
{ command: "later", description: "Save an idea for later" },
{ command: "topic", description: "Switch conversation topic" },
{ command: "verbose", description: "Toggle verbose tool output" },
{ command: "stop", description: "Stop the current agent run" },
{ command: "cancel", description: "Cancel the current agent run" },
{ command: "restart", description: "Restart agent process" },
{ command: "update", description: "Update roundhouse and restart" },
{ command: "status", description: "Show system status" },
Expand Down
2 changes: 1 addition & 1 deletion test/gateway-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("gateway/helpers", () => {
});

it("rejects non-matching command", () => {
expect(isCommand("/stop", "/start", "mybot")).toBe(false);
expect(isCommand("/cancel", "/start", "mybot")).toBe(false);
});
});

Expand Down
Loading