talon-agent 3.23.1 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "3.23.1",
3
+ "version": "3.24.0",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "Dylan Neve",
6
6
  "license": "MIT",
@@ -18,6 +18,10 @@ type ErrorReason =
18
18
  | "bad_request"
19
19
  | "forbidden"
20
20
  | "telegram_api"
21
+ // The user stopped the turn (/stop, talon kill). Not a fault: the
22
+ // frontends deliver nothing for it — the stop acknowledgement already
23
+ // told the user what happened.
24
+ | "stopped"
21
25
  | "unknown";
22
26
 
23
27
  /**
@@ -342,6 +346,7 @@ const FRIENDLY_MESSAGES: Record<ErrorReason, string> = {
342
346
  bad_request: "Something went wrong. Try /reset if this keeps happening.",
343
347
  forbidden: "Permission denied for this action.",
344
348
  telegram_api: "Telegram API error. Try again.",
349
+ stopped: "⏹ Stopped.",
345
350
  unknown: "Something went wrong. Try again or /reset.",
346
351
  };
347
352
 
@@ -24,6 +24,7 @@ import type { ContextManager, ExecuteParams, ExecuteResult } from "../types.js";
24
24
  import { bus } from "../bus/index.js";
25
25
  import { taskTable, type TaskHandle } from "../tasks/index.js";
26
26
  import { log, logDebug, logWarn } from "../../util/log.js";
27
+ import { TalonError } from "../errors.js";
27
28
  import { Loom } from "./loom.js";
28
29
  import { carryTurnEvents } from "./shuttle.js";
29
30
  import type { Thread, ThreadSnapshot } from "./thread.js";
@@ -141,6 +142,16 @@ export class Weaver {
141
142
  return result;
142
143
  } catch (err) {
143
144
  task.fail(err);
145
+ if (lifecycle.killed) {
146
+ // The backend didn't manage a clean interrupt-completion (some
147
+ // SDK versions surface an interrupted turn as an error result).
148
+ // The user asked for this outcome — don't let it unwind as a
149
+ // fault the frontend then reports to the chat.
150
+ throw new TalonError("Turn stopped by user", {
151
+ reason: "stopped",
152
+ cause: err,
153
+ });
154
+ }
144
155
  throw err;
145
156
  } finally {
146
157
  this.activeCount--;
@@ -108,6 +108,7 @@ export async function handleStatus(
108
108
  `**Workspace** ${formatBytes(s.diskBytes)}`,
109
109
  `**Session** ${s.sessionName ? `"${s.sessionName}" ` : ""}${s.sessionId ? "`" + s.sessionId.slice(0, 8) + "...`" : "_(new)_"} · ${s.sessionAge} old`,
110
110
  `**Uptime** ${s.uptime} · ${s.activeSessionCount} active session${s.activeSessionCount === 1 ? "" : "s"}`,
111
+ `**Runtime** ${s.runtime} · ${formatBytes(s.rssBytes)} RSS`,
111
112
  ];
112
113
  await i.editReply(lines.join("\n"));
113
114
  }
@@ -93,6 +93,13 @@ async function flushQueue(chatId: string): Promise<void> {
93
93
  recordMessageProcessed();
94
94
  } catch (err) {
95
95
  const classified = classify(err);
96
+ // A user-initiated /stop is an outcome, not a fault — the stop command
97
+ // already acknowledged it; don't re-report it as an error.
98
+ if (classified.reason === "stopped") {
99
+ log("bot", `[${chatId}] turn stopped by user`);
100
+ recordMessageSettled();
101
+ return;
102
+ }
96
103
  const promptPreview = combinedPrompt.slice(0, 100).replace(/\n/g, " ");
97
104
  logError(
98
105
  "bot",
@@ -88,6 +88,10 @@ export interface SessionStatusData {
88
88
  sessionId: string | undefined;
89
89
  uptime: string;
90
90
  activeSessionCount: number;
91
+ /** Runtime name + version, e.g. "Bun 1.3.9" or "Node 24.4.0". */
92
+ runtime: string;
93
+ /** Daemon resident set size, in bytes. */
94
+ rssBytes: number;
91
95
  }
92
96
 
93
97
  /**
@@ -202,5 +206,9 @@ export async function collectSessionStatus(
202
206
  sessionId: info.sessionId,
203
207
  uptime: formatDuration(process.uptime() * 1000),
204
208
  activeSessionCount: getActiveSessionCount(),
209
+ runtime: process.versions.bun
210
+ ? `Bun ${process.versions.bun}`
211
+ : `Node ${process.versions.node}`,
212
+ rssBytes: process.memoryUsage().rss,
205
213
  };
206
214
  }
@@ -90,6 +90,7 @@ export function registerSessionCommands(
90
90
  `<b>Workspace</b> ${formatBytes(s.diskBytes)}`,
91
91
  `<b>Session</b> ${s.sessionName ? `"${escapeHtml(s.sessionName)}" ` : ""}${s.sessionId ? "<code>" + escapeHtml(s.sessionId.slice(0, 8)) + "...</code>" : "<i>(new)</i>"} · ${s.sessionAge} old`,
92
92
  `<b>Uptime</b> ${s.uptime} · ${s.activeSessionCount} active session${s.activeSessionCount === 1 ? "" : "s"}`,
93
+ `<b>Runtime</b> ${escapeHtml(s.runtime)} · ${formatBytes(s.rssBytes)} RSS`,
93
94
  ];
94
95
  await ctx.reply(lines.join("\n"), { parse_mode: "HTML" });
95
96
  });
@@ -188,6 +188,15 @@ async function flushQueue(chatId: string): Promise<void> {
188
188
  recordMessageProcessed();
189
189
  } catch (err) {
190
190
  const classified = classify(err);
191
+ // A user-initiated /stop is an outcome, not a fault: the stop command
192
+ // already acknowledged it, so delivering the unwound turn's error here
193
+ // would just contradict that with noise.
194
+ if (classified.reason === "stopped") {
195
+ log("bot", `[${chatId}] turn stopped by user`);
196
+ lastHandledMessageIdByChat.set(chatId, last.messageId);
197
+ recordMessageSettled();
198
+ return;
199
+ }
191
200
  const chatType = last.isGroup ? "group" : "DM";
192
201
  const promptPreview = promptWithContext.slice(0, 100).replace(/\n/g, " ");
193
202
  logError(