talon-agent 3.19.0 → 3.20.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.19.0",
3
+ "version": "3.20.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",
@@ -103,9 +103,9 @@
103
103
  "@kilocode/sdk": "^7.2.22",
104
104
  "@modelcontextprotocol/sdk": "^1.29.0",
105
105
  "@openai/agents": "^0.14.0",
106
- "@openai/codex-sdk": "^0.146.0",
106
+ "@openai/codex-sdk": "^0.147.0",
107
107
  "@opencode-ai/sdk": "^1.17.4",
108
- "@playwright/mcp": "0.0.78",
108
+ "@playwright/mcp": "0.0.79",
109
109
  "@types/cross-spawn": "^6.0.6",
110
110
  "big-integer": "^1.6.52",
111
111
  "cheerio": "^1.2.0",
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { ExecuteParams, ExecuteResult } from "../types.js";
12
12
  import { log } from "../../util/log.js";
13
+ import { taskTable, type KillOutcome } from "../tasks/index.js";
13
14
  import {
14
15
  initWeaver,
15
16
  type Weaver,
@@ -57,6 +58,14 @@ export function snapshot(): ThreadSnapshot[] {
57
58
  return weaver?.snapshot() ?? [];
58
59
  }
59
60
 
61
+ /**
62
+ * Stop only this chat's active turn. A later message already waiting in the
63
+ * per-chat FIFO is intentionally left alone and runs after the stop settles.
64
+ */
65
+ export function stopCurrentTurn(chatId: string): KillOutcome {
66
+ return taskTable.killRunningTurn(chatId);
67
+ }
68
+
60
69
  /**
61
70
  * Execute an AI query with full lifecycle management.
62
71
  * Same-chat queries are serialized (FIFO) to avoid session conflicts.
@@ -122,6 +122,24 @@ export class TaskTable {
122
122
  return { ok: true };
123
123
  }
124
124
 
125
+ /**
126
+ * Request an abort for the turn currently running in one chat. Queued turns
127
+ * are deliberately ignored: `/stop` means "stop what is happening now",
128
+ * never "discard my next message".
129
+ */
130
+ killRunningTurn(chatId: string): KillOutcome {
131
+ for (const [id, task] of this.live) {
132
+ if (
133
+ task.record.kind === "turn" &&
134
+ task.record.chatId === chatId &&
135
+ task.record.state === "running"
136
+ ) {
137
+ return this.kill(id);
138
+ }
139
+ }
140
+ return { ok: false, reason: "not-found" };
141
+ }
142
+
125
143
  /**
126
144
  * Every live task plus the bounded settled history, id-ascending.
127
145
  * Snapshots are copies — callers can never mutate table state.
@@ -82,6 +82,10 @@ export function buildCommandDefinitions(devBuild = false): unknown[] {
82
82
  .setName("reset")
83
83
  .setDescription("Clear session and start fresh")
84
84
  .toJSON(),
85
+ new SlashCommandBuilder()
86
+ .setName("stop")
87
+ .setDescription("Stop the current response")
88
+ .toJSON(),
85
89
  new SlashCommandBuilder()
86
90
  .setName("restart")
87
91
  .setDescription("Restart the bot (admin only)")
@@ -31,7 +31,7 @@ export async function handleStart(
31
31
  "",
32
32
  "Mention me, reply to me, or DM me. Attach files, photos, voice notes — I'll read them.",
33
33
  "",
34
- "/status /reset /help",
34
+ "/status /stop /reset /help",
35
35
  ].join("\n"),
36
36
  );
37
37
  }
@@ -54,6 +54,7 @@ export async function handleHelp(
54
54
  "",
55
55
  "**Session**",
56
56
  " /status — session info, usage, and stats",
57
+ " /stop — stop the current response",
57
58
  " /metrics — aggregate performance metrics (admin)",
58
59
  " /dream — force memory consolidation now (admin)",
59
60
  " /ping — health check with latency",
@@ -30,7 +30,7 @@ import {
30
30
  handleMesh,
31
31
  handleUsage,
32
32
  } from "./info.js";
33
- import { handleReset, handleStatus } from "./session.js";
33
+ import { handleReset, handleStatus, handleStop } from "./session.js";
34
34
  import {
35
35
  handleModel,
36
36
  handleEffort,
@@ -143,6 +143,8 @@ async function routeSlashCommand(
143
143
  return handlePulse(interaction, chatId);
144
144
  case "reset":
145
145
  return handleReset(interaction, gateway, chatId);
146
+ case "stop":
147
+ return handleStop(interaction, chatId);
146
148
  case "restart":
147
149
  return handleRestart(interaction);
148
150
  case "metrics":
@@ -23,6 +23,7 @@ import {
23
23
  performSessionReset,
24
24
  collectSessionStatus,
25
25
  } from "../../shared/session-status.js";
26
+ import { stopCurrentTurn } from "../../../core/engine/dispatcher.js";
26
27
  import { reply } from "./shared.js";
27
28
 
28
29
  export async function handleReset(
@@ -42,6 +43,18 @@ export async function handleReset(
42
43
  await reply(i, "Session cleared.", true);
43
44
  }
44
45
 
46
+ export async function handleStop(
47
+ i: ChatInputCommandInteraction,
48
+ chatId: string,
49
+ ): Promise<void> {
50
+ const outcome = stopCurrentTurn(chatId);
51
+ await reply(
52
+ i,
53
+ outcome.ok ? "⏹ Stop requested." : "No response is currently running.",
54
+ true,
55
+ );
56
+ }
57
+
45
58
  export async function handleStatus(
46
59
  i: ChatInputCommandInteraction,
47
60
  config: TalonConfig,
@@ -26,6 +26,7 @@ export const TELEGRAM_COMMANDS: ReadonlyArray<{
26
26
  { command: "model", description: "Show or change model" },
27
27
  { command: "effort", description: "Set thinking effort level" },
28
28
  { command: "pulse", description: "Conversation engagement settings" },
29
+ { command: "stop", description: "Stop the current response" },
29
30
  { command: "reset", description: "Clear session and start fresh" },
30
31
  { command: "restart", description: "Restart the bot (admin)" },
31
32
  { command: "usage", description: "Plan limits across every backend" },
@@ -21,7 +21,7 @@ export function registerInfoCommands(bot: Bot): void {
21
21
  "Send a message, photo, doc, or voice note.",
22
22
  "In groups, @mention or reply to activate.",
23
23
  "",
24
- "/status /reset /help",
24
+ "/status /stop /reset /help",
25
25
  ].join("\n"),
26
26
  { parse_mode: "HTML" },
27
27
  ),
@@ -40,6 +40,7 @@ export function registerInfoCommands(bot: Bot): void {
40
40
  "",
41
41
  "<b>Session</b>",
42
42
  " /status -- session info, usage, and stats",
43
+ " /stop -- stop the current response",
43
44
  " /metrics -- aggregate performance metrics (admin)",
44
45
  " /doctor -- environment and native-module health (admin)",
45
46
  " /dream -- force memory consolidation now",
@@ -20,6 +20,7 @@ import {
20
20
  performSessionReset,
21
21
  collectSessionStatus,
22
22
  } from "../../shared/session-status.js";
23
+ import { stopCurrentTurn } from "../../../core/engine/dispatcher.js";
23
24
  import type { RegisterDeps } from "./state.js";
24
25
 
25
26
  export function registerSessionCommands(
@@ -34,6 +35,13 @@ export function registerSessionCommands(
34
35
  await ctx.reply("Session cleared.");
35
36
  });
36
37
 
38
+ bot.command("stop", async (ctx) => {
39
+ const outcome = stopCurrentTurn(String(ctx.chat.id));
40
+ await ctx.reply(
41
+ outcome.ok ? "⏹ Stop requested." : "No response is currently running.",
42
+ );
43
+ });
44
+
37
45
  bot.command("status", async (ctx) => {
38
46
  const cid = String(ctx.chat.id);
39
47
  const s = await collectSessionStatus(
@@ -134,10 +134,23 @@ function startConnectionMonitor(): void {
134
134
  if (reconnectTimer) return;
135
135
  reconnectTimer = setInterval(async () => {
136
136
  if (!client) return;
137
- if (client.connected) return;
138
137
  if (reconnecting) return; // prevent overlapping reconnect attempts
139
- reconnecting = true;
140
138
 
139
+ // GramJS keeps `client.connected === true` even after the underlying
140
+ // socket has silently died, so a real round-trip is the only reliable
141
+ // liveness signal. Without it the userbot can go stale for hours/days
142
+ // while the monitor believes it is still connected.
143
+ if (client.connected) {
144
+ try {
145
+ // A full API round-trip; throws if the socket is silently dead.
146
+ await client.getMe();
147
+ return; // genuinely alive
148
+ } catch {
149
+ logWarn("userbot", "Liveness check failed — treating socket as dead.");
150
+ }
151
+ }
152
+
153
+ reconnecting = true;
141
154
  logWarn("userbot", "Connection lost, attempting reconnect...");
142
155
  try {
143
156
  await client.connect();