talon-agent 1.40.2 → 1.41.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": "1.40.2",
3
+ "version": "1.41.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,9 @@ export const meshHandlers: SharedActionHandlers = {
18
18
  get_device_history: (body) =>
19
19
  getMeshService().deviceHistory(body.device, body.hours),
20
20
  ring_device: (body) => getMeshService().ringDevice(body.device, body.message),
21
+ // Registry hygiene: drop a stale/superseded device entry. Requires an
22
+ // explicit target — a destructive action must never default-pick a device.
23
+ remove_device: (body) => getMeshService().removeDevice(body.device),
21
24
  get_device_status: (body) => getMeshService().getDeviceStatus(body.device),
22
25
  // Exec + filesystem surface — the substrate teleport routes through.
23
26
  device_exec: (body) =>
@@ -91,10 +91,48 @@ export class MeshRegistry {
91
91
  now,
92
92
  );
93
93
  this.devices.set(device.id, device);
94
+ // Stale-duplicate eviction. A reinstall (or an old app build) carries its
95
+ // own persisted device id, so the same physical machine re-registers
96
+ // under a fresh identity and the old entry lingers forever as an offline
97
+ // ghost. Same name + platform under a different id, currently OFFLINE →
98
+ // superseded install, drop it. An online doppelganger is kept: two live
99
+ // devices can legitimately share a name.
100
+ let evicted = false;
101
+ for (const [id, d] of this.devices) {
102
+ if (id === device.id) continue;
103
+ if (d.name !== device.name || d.platform !== device.platform) continue;
104
+ if (toDeviceInfo(d, now, PRESENCE_TIMEOUT_MS).online) continue;
105
+ this.devices.delete(id);
106
+ this.locations.delete(id);
107
+ this.history.delete(id);
108
+ evicted = true;
109
+ }
94
110
  await this.persistDevices();
111
+ if (evicted) {
112
+ await this.persistLocations();
113
+ await this.persistHistory();
114
+ }
95
115
  return device;
96
116
  }
97
117
 
118
+ /**
119
+ * Drop a device (and its location + history) from the registry. Returns
120
+ * the removed device, or undefined when the id is unknown. Note a still-
121
+ * connected companion re-registers on its next heartbeat — removal is for
122
+ * stale entries.
123
+ */
124
+ async removeDevice(deviceId: string): Promise<DeviceInfo | undefined> {
125
+ const device = this.devices.get(deviceId);
126
+ if (!device) return undefined;
127
+ this.devices.delete(deviceId);
128
+ const hadLocation = this.locations.delete(deviceId);
129
+ const hadHistory = this.history.delete(deviceId);
130
+ await this.persistDevices();
131
+ if (hadLocation) await this.persistLocations();
132
+ if (hadHistory) await this.persistHistory();
133
+ return toDeviceInfo(device, device.lastSeen);
134
+ }
135
+
98
136
  async storeLocation(
99
137
  body: Record<string, unknown>,
100
138
  now = Date.now(),
@@ -340,6 +340,36 @@ export class MeshService {
340
340
  return { ok: true, text: this.locationSummary(target, loc) };
341
341
  }
342
342
 
343
+ /**
344
+ * `remove_device`: drop a stale device from the mesh registry (with its
345
+ * location + history). Destructive, so it requires an explicit target —
346
+ * no "default to the most recent device" like the read tools.
347
+ */
348
+ async removeDevice(query?: unknown): Promise<MeshToolResult> {
349
+ await this.load();
350
+ if (typeof query !== "string" || !query.trim()) {
351
+ return {
352
+ ok: false,
353
+ text: "remove_device needs an explicit device id or name — there is no default target for a destructive operation. See list_devices.",
354
+ };
355
+ }
356
+ const resolved = this.resolveDevice(query);
357
+ if ("error" in resolved) return { ok: false, text: resolved.error };
358
+ const target = resolved.target;
359
+ const removed = await this.registry.removeDevice(target.id);
360
+ if (!removed) {
361
+ return { ok: false, text: this.noSuchDevice(query).text };
362
+ }
363
+ return {
364
+ ok: true,
365
+ text:
366
+ `Removed ${removed.name} [id: ${removed.id}] (${removed.platform}, last seen ${age(Date.now() - removed.lastSeen)}) from the mesh registry.` +
367
+ (target.online
368
+ ? " Note: it was still online — a connected companion re-registers within ~60s, so quit the app first if it keeps coming back."
369
+ : ""),
370
+ };
371
+ }
372
+
343
373
  /** `ring_device`: make the device sound/vibrate so it can be found. */
344
374
  ringDevice(query?: unknown, message?: unknown): Promise<MeshToolResult> {
345
375
  return this.commandTool(query, "ring", {
@@ -60,6 +60,20 @@ export const meshTools: ToolDefinition[] = [
60
60
  execute: (params, bridge) => bridge("ring_device", params),
61
61
  tag: "mesh",
62
62
  },
63
+ {
64
+ name: "remove_device",
65
+ description:
66
+ "Remove a stale device from the mesh registry (with its stored location and history). Use for superseded installs and old duplicates — e.g. a reinstalled companion that re-registered under a new id, leaving the old entry as an offline ghost. Requires an explicit device id or name (no default target). Removing a still-connected device is pointless: it re-registers on its next heartbeat within ~60s.",
67
+ schema: {
68
+ device: z
69
+ .string()
70
+ .describe(
71
+ "Device id, exact name, or unique name fragment of the entry to remove (see list_devices). Prefer the id when duplicates share a name.",
72
+ ),
73
+ },
74
+ execute: (params, bridge) => bridge("remove_device", params),
75
+ tag: "mesh",
76
+ },
63
77
  {
64
78
  name: "get_device_status",
65
79
  description:
@@ -71,7 +85,7 @@ export const meshTools: ToolDefinition[] = [
71
85
  {
72
86
  name: "device_exec",
73
87
  description:
74
- "Run a shell command ON a Talon companion device (e.g. the phone) and return its stdout/stderr/exit code. The device executes it in its own sandbox (Android: app UID, or elevated via Shizuku if available). Use for on-device tasks like tidying a folder. Prefer `teleport` for a sustained session.",
88
+ "Run a shell command ON a Talon companion device (e.g. the phone) and return its stdout/stderr/exit code. The device executes it in its own sandbox (Android: app UID, or elevated via Shizuku if available). Use for on-device tasks like tidying a folder. Prefer `teleport` for a sustained session. For persistent streams (log tailing, watchers), background with output redirected to a file — `cmd > /tmp/out.log 2>&1 &` returns immediately; poll the file with device_read_file.",
75
89
  schema: {
76
90
  device: deviceParam,
77
91
  cmd: z.string().describe("The shell command to run on the device."),