talon-agent 3.12.0 → 3.12.1

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.12.0",
3
+ "version": "3.12.1",
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",
@@ -31,7 +31,7 @@ import { scriptHandlers } from "./scripts.js";
31
31
  import { skillHandlers } from "./skills.js";
32
32
  import { pluginHandlers } from "./plugins.js";
33
33
  import { modelHandlers } from "./models.js";
34
- import { meshHandlers } from "./mesh.js";
34
+ import { meshHandlers, chatFreeActions } from "./mesh.js";
35
35
  import { nativeHandlers } from "./native.js";
36
36
 
37
37
  // Null-prototype so a request `action` of "toString" / "constructor" / etc.
@@ -62,4 +62,29 @@ export async function handleSharedAction(
62
62
  return handler(body, chatId, backend);
63
63
  }
64
64
 
65
+ /**
66
+ * True when the action needs no chat context at all (see `chatFreeActions`).
67
+ * The gateway checks this before resolving a chat so mesh tools stay reachable
68
+ * from heartbeat/background runs.
69
+ */
70
+ export function isChatFreeAction(action: string): boolean {
71
+ return chatFreeActions.has(action);
72
+ }
73
+
74
+ /**
75
+ * Dispatch a chat-free action. The handler signature still takes a chatId
76
+ * (they all share one type); chat-free handlers ignore it, and `0` is passed
77
+ * as an explicit "no chat" sentinel rather than a real id.
78
+ */
79
+ export async function handleChatFreeAction(
80
+ body: Record<string, unknown>,
81
+ ): Promise<ActionResult | null> {
82
+ const action = body.action as string;
83
+ if (!isChatFreeAction(action)) return null;
84
+ const handler = handlers[action];
85
+ if (!handler) return null;
86
+ return handler(body, 0);
87
+ }
88
+
89
+ export { chatFreeActions };
65
90
  export type { SharedActionHandler, SharedActionHandlers } from "./types.js";
@@ -78,3 +78,36 @@ export const meshHandlers: SharedActionHandlers = {
78
78
  body.bridge_url,
79
79
  ),
80
80
  };
81
+
82
+ /**
83
+ * Mesh actions are chat-free: every handler above reads `body` and the
84
+ * daemon-wide mesh registry, and not one of them takes the `chatId` the
85
+ * gateway threads through. Requiring an active chat context to reach them is
86
+ * therefore incidental to the transport, not a property of the action — and
87
+ * it makes the whole mesh unreachable from heartbeat/background runs, which
88
+ * have no ambient chat and no `chat_id` parameter on these tools to promote.
89
+ *
90
+ * The gateway consults this set before chat resolution so `list_devices`,
91
+ * `device_exec` and friends work from any run mode. Kept as an explicit
92
+ * export (rather than `Object.keys(meshHandlers)`) so adding a handler that
93
+ * *does* need a chat is a deliberate opt-out rather than a silent inclusion.
94
+ */
95
+ export const chatFreeActions: ReadonlySet<string> = new Set([
96
+ "list_devices",
97
+ "get_device_location",
98
+ "get_device_history",
99
+ "ring_device",
100
+ "remove_device",
101
+ "get_device_status",
102
+ "device_exec",
103
+ "device_list_dir",
104
+ "device_stat",
105
+ "device_read_file",
106
+ "device_write_file",
107
+ "device_pull_file",
108
+ "device_push_file",
109
+ "update_device",
110
+ "update_node",
111
+ "get_node_binary",
112
+ "make_node_install_link",
113
+ ]);
@@ -20,7 +20,11 @@ import { Loom, getActiveLoom, type ContextRegistry } from "../weaver/index.js";
20
20
  import { getHealthStatus } from "../../util/watchdog.js";
21
21
  import { getActiveSessionCount } from "../../storage/sessions.js";
22
22
  import { log, logError, logDebug } from "../../util/log.js";
23
- import { handleSharedAction } from "./gateway-actions/index.js";
23
+ import {
24
+ handleSharedAction,
25
+ handleChatFreeAction,
26
+ isChatFreeAction,
27
+ } from "./gateway-actions/index.js";
24
28
  import {
25
29
  handleHubRequest,
26
30
  getHubSessionCount,
@@ -249,6 +253,31 @@ export class Gateway {
249
253
  // active-context-required check — the action handler will reach the
250
254
  // chat directly via the Telegram Bot API. The legacy context-required
251
255
  // path remains for chat-mode calls where `chat_id` is absent.
256
+ // Chat-free actions (the device mesh) short-circuit routing entirely:
257
+ // they read daemon-wide state, ignore chatId, and are the only command
258
+ // channel a heartbeat run has to a remote box. Gating them behind an
259
+ // active chat made the whole mesh unreachable from background runs —
260
+ // and unlike send/react they carry no `chat_id` param to promote.
261
+ const requestedAction =
262
+ typeof body.action === "string" ? body.action : undefined;
263
+ if (requestedAction && isChatFreeAction(requestedAction)) {
264
+ const t0 = Date.now();
265
+ try {
266
+ const result = await handleChatFreeAction(body);
267
+ if (result) {
268
+ logDebug(
269
+ "gateway",
270
+ `${requestedAction} chat=none ${Date.now() - t0}ms (chat-free)`,
271
+ );
272
+ return result;
273
+ }
274
+ } catch (err) {
275
+ const msg = err instanceof Error ? err.message : String(err);
276
+ logError("gateway", `${requestedAction} (chat-free) failed: ${msg}`);
277
+ return { ok: false, error: `${requestedAction}: ${msg}` };
278
+ }
279
+ }
280
+
252
281
  const rawChatId = body._chatId ? String(body._chatId) : "";
253
282
  const numericId = Number(rawChatId);
254
283
  const explicitChatIdProvided = typeof body.chat_id !== "undefined";