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
package/src/core/mesh/service.ts
CHANGED
|
@@ -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
|
+
}
|