talon-agent 3.1.1 → 3.2.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.1.1",
3
+ "version": "3.2.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,7 +103,7 @@
103
103
  "@kilocode/sdk": "^7.2.22",
104
104
  "@modelcontextprotocol/sdk": "^1.29.0",
105
105
  "@openai/agents": "^0.13.0",
106
- "@openai/codex-sdk": "^0.144.0",
106
+ "@openai/codex-sdk": "^0.145.0",
107
107
  "@opencode-ai/sdk": "^1.17.4",
108
108
  "@playwright/mcp": "^0.0.78",
109
109
  "@types/cross-spawn": "^6.0.6",
@@ -57,4 +57,12 @@ export const meshHandlers: SharedActionHandlers = {
57
57
  body.apk_path,
58
58
  body.remote_path,
59
59
  ),
60
+ // Remote self-update for a headless talon-node: push a new binary and have
61
+ // the node verify, swap, and restart into it.
62
+ update_node: (body) =>
63
+ getMeshService().updateNodeBinary(
64
+ body.device,
65
+ body.binary_path,
66
+ body.remote_path,
67
+ ),
60
68
  };
@@ -873,6 +873,87 @@ export class MeshService {
873
873
  };
874
874
  }
875
875
 
876
+ /**
877
+ * `update_node`: remote self-update for a headless talon-node. Streams a
878
+ * replacement binary to the node, then sends `update_node` so the node
879
+ * verifies the digest, atomically swaps its own binary, and restarts into
880
+ * it (an in-place execve under systemd/launchd, so the mesh connection
881
+ * returns on its own within seconds — the same UX as the Android path).
882
+ *
883
+ * The binary is hashed here and the digest travels with the command; the
884
+ * node re-hashes the pushed file and refuses to swap on a mismatch, so a
885
+ * truncated transfer can never be installed. Build the replacement for the
886
+ * node's platform/arch (talon-node-<os>-<arch>) before calling.
887
+ */
888
+ async updateNodeBinary(
889
+ query: unknown,
890
+ localBinaryPath: unknown,
891
+ remotePath?: unknown,
892
+ ): Promise<MeshToolResult> {
893
+ const local =
894
+ typeof localBinaryPath === "string" && localBinaryPath.trim()
895
+ ? resolve(dirs.workspace, localBinaryPath.trim())
896
+ : "";
897
+ if (!local) return { ok: false, text: "A local binary path is required." };
898
+ await this.load();
899
+ const resolved = this.resolveDevice(query);
900
+ if ("error" in resolved) return { ok: false, text: resolved.error };
901
+ const target = resolved.target;
902
+ if (target.capabilities && !target.capabilities.includes("update_node")) {
903
+ return {
904
+ ok: false,
905
+ text: `${target.name} can't self-update — it needs the update_node capability (a talon-node headless device).`,
906
+ };
907
+ }
908
+
909
+ let sha256: string;
910
+ let size: number;
911
+ try {
912
+ ({ sha256, size } = await hashFile(local));
913
+ } catch (err) {
914
+ return {
915
+ ok: false,
916
+ text: `Cannot read binary ${local}: ${(err as Error).message}`,
917
+ };
918
+ }
919
+ if (size === 0) return { ok: false, text: `Binary ${local} is empty.` };
920
+
921
+ // Default staging path is /tmp on unix nodes (the node re-stages next to
922
+ // its own executable before the atomic swap, so this is only transient).
923
+ const remote =
924
+ typeof remotePath === "string" && remotePath.trim()
925
+ ? remotePath.trim()
926
+ : "/tmp/talon-node.update";
927
+
928
+ // 1. Stream the new binary to the node.
929
+ const push = await this.pushFileToDevice(target.id, local, remote);
930
+ if (!push.ok) {
931
+ return { ok: false, text: `Update aborted — push failed: ${push.text}` };
932
+ }
933
+
934
+ // 2. Trigger the swap + restart (node verifies the digest first).
935
+ const dispatched = await this.dispatchCommand(
936
+ target.id,
937
+ "update_node",
938
+ { path: remote, sha256 },
939
+ this.commandTimeoutMs,
940
+ );
941
+ if ("error" in dispatched) return { ok: false, text: dispatched.error };
942
+ if (!dispatched.result.ok) {
943
+ return {
944
+ ok: false,
945
+ text: dispatched.result.message ?? `${target.name} refused the update.`,
946
+ };
947
+ }
948
+ return {
949
+ ok: true,
950
+ text:
951
+ `Pushed ${formatBytes(size)} and staged the update on ${target.name}. ` +
952
+ `${dispatched.result.message ?? "Restarting now."} ` +
953
+ `Confirm with get_device_status once it reconnects (appVersion should change).`,
954
+ };
955
+ }
956
+
876
957
  /**
877
958
  * Chunked read of a remote file into a Buffer. Loops `read_file` with
878
959
  * increasing offsets until the device reports EOF.
@@ -185,4 +185,25 @@ export const meshTools: ToolDefinition[] = [
185
185
  execute: (params, bridge) => bridge("update_device", params),
186
186
  tag: "mesh",
187
187
  },
188
+ {
189
+ name: "update_node",
190
+ description:
191
+ "Remotely update a headless talon-node (Linux/macOS/Windows server on the mesh): stream a new node binary from the daemon and have the node verify its hash, atomically swap its own binary, and restart into it. On Linux/macOS this is an in-place execve so the mesh connection returns within seconds; the node re-hashes the pushed file and refuses a truncated or mismatched binary. Build the replacement for the node's platform/arch first (talon-node-<os>-<arch>). Confirm success with get_device_status afterwards (appVersion should change). This is the node counterpart of update_device — use update_device for Android companions.",
192
+ schema: {
193
+ device: deviceParam,
194
+ binary_path: z
195
+ .string()
196
+ .describe(
197
+ "Path to the new talon-node binary, relative to the workspace (or absolute), built for the node's OS/arch.",
198
+ ),
199
+ remote_path: z
200
+ .string()
201
+ .optional()
202
+ .describe(
203
+ "Where to stage the binary on the node (default /tmp/talon-node.update; the node re-stages next to its own executable before the atomic swap).",
204
+ ),
205
+ },
206
+ execute: (params, bridge) => bridge("update_node", params),
207
+ tag: "mesh",
208
+ },
188
209
  ];