talon-agent 3.2.0 → 3.3.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.2.0",
3
+ "version": "3.3.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",
@@ -54,6 +54,17 @@ export type MeshTransport = {
54
54
 
55
55
  export type MeshToolResult = { ok: boolean; text: string };
56
56
 
57
+ /** Outcome of pinging one device (see {@link MeshService.pingAll}). */
58
+ export type MeshPingResult = {
59
+ device: DeviceInfo;
60
+ /** True when the device answered the probe (offline devices are false). */
61
+ reachable: boolean;
62
+ /** Round-trip time of the probe, present only when reachable. */
63
+ latencyMs?: number;
64
+ /** Why the probe didn't land (offline, no transport, timeout). */
65
+ error?: string;
66
+ };
67
+
57
68
  export type MeshServiceOptions = {
58
69
  /** How long a locate waits for a fresh fix before last-known fallback. */
59
70
  freshFixTimeoutMs?: number;
@@ -308,6 +319,36 @@ export class MeshService {
308
319
  };
309
320
  }
310
321
 
322
+ /**
323
+ * Ping every registered device: read the registry, then actively probe
324
+ * each ONLINE device with a lightweight `status` command (concurrently)
325
+ * and measure round-trip latency. Offline devices are reported from
326
+ * presence without a probe (a probe would just burn the timeout). The
327
+ * structured result powers frontend surfaces like Telegram's /mesh — the
328
+ * mesh tools stay text-only for the model, this is for humans.
329
+ */
330
+ async pingAll(timeoutMs = 5_000): Promise<MeshPingResult[]> {
331
+ const { devices } = await this.list();
332
+ const hasTransport = this.transports.size > 0;
333
+ return Promise.all(
334
+ devices.map(async (device): Promise<MeshPingResult> => {
335
+ if (!device.online) return { device, reachable: false };
336
+ if (!hasTransport) {
337
+ return { device, reachable: false, error: "no transport connected" };
338
+ }
339
+ const start = Date.now();
340
+ const result = await this.sendCommand(device, "status", {}, timeoutMs);
341
+ return result.ok
342
+ ? { device, reachable: true, latencyMs: Date.now() - start }
343
+ : {
344
+ device,
345
+ reachable: false,
346
+ error: result.message ?? "no response",
347
+ };
348
+ }),
349
+ );
350
+ }
351
+
311
352
  /**
312
353
  * `get_device_location`: resolve the target (id, name fragment, or the
313
354
  * most recent mobile device), push a locate to connected transports, wait
@@ -22,6 +22,7 @@ export const TELEGRAM_COMMANDS: ReadonlyArray<{
22
22
  },
23
23
  { command: "status", description: "Session info, usage, and stats" },
24
24
  { command: "ping", description: "Health check with latency" },
25
+ { command: "mesh", description: "Ping and list mesh devices" },
25
26
  { command: "model", description: "Show or change model" },
26
27
  { command: "effort", description: "Set thinking effort level" },
27
28
  { command: "pulse", description: "Conversation engagement settings" },
@@ -7,6 +7,8 @@ import { isUserClientReady } from "../userbot.js";
7
7
  import { escapeHtml } from "../formatting.js";
8
8
  import { formatDuration } from "../helpers/index.js";
9
9
  import { getLoadedPlugins } from "../../../core/plugin/index.js";
10
+ import { getMeshService } from "../../../core/mesh/index.js";
11
+ import type { MeshPingResult } from "../../../core/mesh/service.js";
10
12
 
11
13
  export function registerInfoCommands(bot: Bot): void {
12
14
  bot.command("start", (ctx) =>
@@ -42,6 +44,7 @@ export function registerInfoCommands(bot: Bot): void {
42
44
  " /doctor -- environment and native-module health (admin)",
43
45
  " /dream -- force memory consolidation now",
44
46
  " /ping -- health check with latency",
47
+ " /mesh -- ping and list companion mesh devices",
45
48
  " /reset -- clear session and start fresh",
46
49
  " /restart -- restart the bot process",
47
50
  " /plugins -- list loaded plugins",
@@ -100,6 +103,50 @@ export function registerInfoCommands(bot: Bot): void {
100
103
  }
101
104
  });
102
105
 
106
+ bot.command("mesh", async (ctx) => {
107
+ const sent = await ctx.reply("🛰️ Pinging mesh devices…");
108
+ let results: MeshPingResult[];
109
+ try {
110
+ results = await getMeshService().pingAll();
111
+ } catch {
112
+ await editOrReply(
113
+ bot,
114
+ ctx.chat.id,
115
+ sent.message_id,
116
+ "Could not reach the mesh service.",
117
+ );
118
+ return;
119
+ }
120
+ if (results.length === 0) {
121
+ await editOrReply(
122
+ bot,
123
+ ctx.chat.id,
124
+ sent.message_id,
125
+ "<b>🛰️ Mesh</b>\n\nNo devices have registered yet.",
126
+ );
127
+ return;
128
+ }
129
+ // Online (reachable first, by latency) before offline; a stable, useful
130
+ // order for a glance at the fleet.
131
+ const sorted = [...results].sort((a, b) => {
132
+ if (a.device.online !== b.device.online) return a.device.online ? -1 : 1;
133
+ return (a.latencyMs ?? Infinity) - (b.latencyMs ?? Infinity);
134
+ });
135
+ const online = results.filter((r) => r.device.online).length;
136
+ const reachable = results.filter((r) => r.reachable).length;
137
+ const lines = sorted.map((r) => meshLine(r));
138
+ await editOrReply(
139
+ bot,
140
+ ctx.chat.id,
141
+ sent.message_id,
142
+ [
143
+ `<b>🛰️ Mesh</b> — ${results.length} device(s), ${online} online, ${reachable} responding`,
144
+ "",
145
+ ...lines,
146
+ ].join("\n"),
147
+ );
148
+ });
149
+
103
150
  bot.command("plugins", async (ctx) => {
104
151
  const plugins = getLoadedPlugins();
105
152
  if (plugins.length === 0) {
@@ -123,3 +170,38 @@ export function registerInfoCommands(bot: Bot): void {
123
170
  );
124
171
  });
125
172
  }
173
+
174
+ /** One `/mesh` line: presence + reachability + latency + platform. */
175
+ function meshLine(r: MeshPingResult): string {
176
+ const d = r.device;
177
+ const dot = r.reachable ? "🟢" : d.online ? "🟡" : "⚪";
178
+ const name = `<b>${escapeHtml(d.name)}</b>`;
179
+ const bits: string[] = [`${d.platform}`];
180
+ if (r.reachable && typeof r.latencyMs === "number") {
181
+ bits.push(`${r.latencyMs}ms`);
182
+ } else if (d.online && r.error) {
183
+ bits.push(escapeHtml(r.error));
184
+ } else if (!d.online) {
185
+ bits.push(`offline, last seen ${formatDuration(Date.now() - d.lastSeen)}`);
186
+ }
187
+ if (typeof d.battery === "number") {
188
+ bits.push(`${d.battery}%${d.charging ? "⚡" : ""}`);
189
+ }
190
+ return `${dot} ${name} — ${bits.join(" · ")}`;
191
+ }
192
+
193
+ /** Edit the placeholder in place, falling back to a fresh reply. */
194
+ async function editOrReply(
195
+ bot: Bot,
196
+ chatId: number,
197
+ messageId: number,
198
+ text: string,
199
+ ): Promise<void> {
200
+ try {
201
+ await bot.api.editMessageText(chatId, messageId, text, {
202
+ parse_mode: "HTML",
203
+ });
204
+ } catch {
205
+ await bot.api.sendMessage(chatId, text, { parse_mode: "HTML" });
206
+ }
207
+ }