talon-agent 4.6.1 → 5.0.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.
Files changed (310) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -1
  3. package/prompts/README.md +2 -2
  4. package/prompts/system/memory-recall.md +16 -0
  5. package/src/app.ts +22 -7
  6. package/src/backend/claude-sdk/factory.ts +21 -41
  7. package/src/backend/claude-sdk/handler.ts +28 -7
  8. package/src/backend/claude-sdk/host/in-process.ts +147 -0
  9. package/src/backend/claude-sdk/one-shot.ts +1 -1
  10. package/src/backend/claude-sdk/options.ts +7 -6
  11. package/src/backend/claude-sdk/stream.ts +30 -3
  12. package/src/backend/claude-sdk/warm.ts +1 -1
  13. package/src/backend/codex/constants.ts +1 -1
  14. package/src/backend/codex/factory.ts +2 -2
  15. package/src/backend/codex/handler/events.ts +1 -1
  16. package/src/backend/codex/handler/message.ts +26 -12
  17. package/src/backend/codex/handler/rollout-accounting.ts +1 -1
  18. package/src/backend/codex/init.ts +1 -1
  19. package/src/backend/codex/mcp-config.ts +2 -2
  20. package/src/backend/codex/one-shot.ts +1 -1
  21. package/src/backend/kilo/handler/message.ts +4 -1
  22. package/src/backend/openai-agents/constants.ts +1 -1
  23. package/src/backend/openai-agents/factory.ts +2 -2
  24. package/src/backend/openai-agents/handler/events.ts +1 -1
  25. package/src/backend/openai-agents/handler/message.ts +8 -4
  26. package/src/backend/openai-agents/init.ts +1 -1
  27. package/src/backend/openai-agents/mcp-pool.ts +2 -2
  28. package/src/backend/opencode/handler/message.ts +4 -1
  29. package/src/backend/remote-server/chat-turn.ts +31 -23
  30. package/src/backend/remote-server/events.ts +3 -3
  31. package/src/backend/remote-server/factory.ts +6 -3
  32. package/src/backend/remote-server/index.ts +1 -1
  33. package/src/backend/remote-server/mcp.ts +1 -1
  34. package/src/backend/remote-server/one-shot.ts +1 -1
  35. package/src/backend/remote-server/server-bindings.ts +1 -1
  36. package/src/backend/remote-server/turn.ts +1 -1
  37. package/src/backend/runtime/cache/cache-metrics.ts +126 -0
  38. package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
  39. package/src/backend/{shared → runtime}/index.ts +34 -25
  40. package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
  41. package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
  42. package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
  43. package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
  44. package/src/backend/{shared → runtime/turn}/delivery.ts +3 -3
  45. package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
  46. package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
  47. package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
  48. package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
  49. package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
  50. package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
  51. package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
  52. package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
  53. package/src/bootstrap.ts +6 -21
  54. package/src/cli.ts +6 -3
  55. package/src/core/agent-runtime/README.md +15 -0
  56. package/src/core/agent-runtime/agent-host.ts +387 -0
  57. package/src/core/agent-runtime/capabilities.ts +11 -0
  58. package/src/core/agent-runtime/contract-tests.ts +92 -1
  59. package/src/core/agent-runtime/events.ts +1 -1
  60. package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
  61. package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
  62. package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
  63. package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
  64. package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
  65. package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
  66. package/src/core/background/{dream.ts → dream/index.ts} +10 -15
  67. package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
  68. package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
  69. package/src/core/background/triggers/command.ts +2 -2
  70. package/src/{util → core/config}/harden.ts +1 -1
  71. package/src/core/config/index.ts +16 -12
  72. package/src/{util → core/daemon}/boot-timer.ts +15 -1
  73. package/src/core/daemon/pidfile.ts +1 -1
  74. package/src/core/daemon/resource-sampler.ts +121 -0
  75. package/src/{util → core/daemon}/respawn.ts +1 -1
  76. package/src/core/doctor/index.ts +1 -1
  77. package/src/core/engine/dispatcher.ts +16 -0
  78. package/src/core/engine/gateway-actions/cron.ts +2 -2
  79. package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
  80. package/src/core/engine/gateway-actions/index.ts +4 -1
  81. package/src/core/engine/gateway-actions/memory.ts +335 -0
  82. package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
  83. package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
  84. package/src/core/engine/gateway-actions/native/exec.ts +182 -0
  85. package/src/core/engine/gateway-actions/native/index.ts +46 -0
  86. package/src/core/engine/gateway-actions/native/params.ts +46 -0
  87. package/src/core/engine/gateway-actions/native/read.ts +182 -0
  88. package/src/core/engine/gateway-actions/native/results.ts +34 -0
  89. package/src/core/engine/gateway-actions/native/search.ts +212 -0
  90. package/src/core/engine/gateway-actions/native/shell.ts +39 -0
  91. package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
  92. package/src/core/engine/gateway-actions/native/write.ts +170 -0
  93. package/src/core/engine/gateway.ts +0 -6
  94. package/src/core/frontend-runtime/builtins.ts +2 -2
  95. package/src/{util → core/frontend-runtime}/chat-id.ts +30 -0
  96. package/src/core/mcp-hub/index.ts +1 -1
  97. package/src/core/mcp-hub/talon-server.ts +1 -1
  98. package/src/core/memory/import.ts +3 -2
  99. package/src/core/memory/taps.ts +199 -0
  100. package/src/core/memory/turn-retrieval.ts +222 -0
  101. package/src/core/plugin/mcp.ts +1 -1
  102. package/src/core/prompt/assemble.ts +2 -12
  103. package/src/core/prompt/index.ts +2 -2
  104. package/src/core/prompt/invalidation.ts +1 -1
  105. package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +1 -1
  106. package/src/core/tasks/index.ts +1 -1
  107. package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
  108. package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
  109. package/src/core/tools/{history.ts → chat/history.ts} +2 -2
  110. package/src/core/tools/{media.ts → chat/media.ts} +1 -1
  111. package/src/core/tools/{members.ts → chat/members.ts} +2 -2
  112. package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
  113. package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
  114. package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
  115. package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
  116. package/src/core/tools/content/memory.ts +128 -0
  117. package/src/core/tools/{web.ts → content/web.ts} +1 -1
  118. package/src/core/tools/index.ts +21 -19
  119. package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
  120. package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
  121. package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
  122. package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
  123. package/src/core/tools/{models.ts → ops/models.ts} +1 -1
  124. package/src/core/tools/{native.ts → ops/native.ts} +1 -1
  125. package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
  126. package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
  127. package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
  128. package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
  129. package/src/core/tools/types.ts +1 -0
  130. package/src/{util → core/vfs}/workspace.ts +2 -2
  131. package/src/core/weaver/turn-cpu.ts +32 -0
  132. package/src/core/weaver/weaver.ts +36 -0
  133. package/src/frontend/discord/admin.ts +1 -1
  134. package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
  135. package/src/frontend/discord/callbacks/components/effort.ts +1 -1
  136. package/src/frontend/discord/callbacks/components/index.ts +1 -1
  137. package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
  138. package/src/frontend/discord/callbacks/components/settings.ts +2 -2
  139. package/src/frontend/discord/callbacks/modals.ts +4 -1
  140. package/src/frontend/discord/commands/admin.ts +2 -36
  141. package/src/frontend/discord/commands/definitions.ts +0 -11
  142. package/src/frontend/discord/commands/info.ts +1 -1
  143. package/src/frontend/discord/commands/interaction.ts +1 -1
  144. package/src/frontend/discord/commands/router.ts +0 -3
  145. package/src/frontend/discord/commands/session.ts +8 -2
  146. package/src/frontend/discord/commands/settings.ts +2 -2
  147. package/src/frontend/discord/handlers/messages.ts +1 -1
  148. package/src/frontend/discord/handlers/state.ts +1 -1
  149. package/src/frontend/discord/middleware.ts +2 -26
  150. package/src/frontend/discord/ready.ts +1 -1
  151. package/src/frontend/discord/render.ts +41 -259
  152. package/src/frontend/discord/runtime.ts +1 -2
  153. package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
  154. package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
  155. package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
  156. package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
  157. package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
  158. package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
  159. package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
  160. package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
  161. package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
  162. package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
  163. package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
  164. package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
  165. package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
  166. package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
  167. package/src/frontend/native/index.ts +13 -10
  168. package/src/frontend/native/{media.ts → media/media.ts} +3 -3
  169. package/src/frontend/native/runtime.ts +1 -1
  170. package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
  171. package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
  172. package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
  173. package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
  174. package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
  175. package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
  176. package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
  177. package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
  178. package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
  179. package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
  180. package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
  181. package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
  182. package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
  183. package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
  184. package/src/frontend/{shared → presentation}/format.ts +4 -3
  185. package/src/frontend/{shared → presentation}/model-commands.ts +1 -1
  186. package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
  187. package/src/frontend/presentation/reports.ts +493 -0
  188. package/src/frontend/{shared → presentation}/session-status.ts +36 -9
  189. package/src/frontend/{shared → presentation}/status-context.ts +101 -2
  190. package/src/frontend/teams/commands.ts +2 -2
  191. package/src/frontend/teams/turn.ts +1 -1
  192. package/src/frontend/telegram/admin/background.ts +1 -1
  193. package/src/frontend/telegram/admin/health.ts +1 -1
  194. package/src/frontend/telegram/admin/sessions.ts +1 -1
  195. package/src/frontend/telegram/callbacks/effort.ts +2 -2
  196. package/src/frontend/telegram/callbacks/metrics.ts +1 -1
  197. package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
  198. package/src/frontend/telegram/callbacks/model/views.ts +1 -1
  199. package/src/frontend/telegram/callbacks/pulse.ts +1 -1
  200. package/src/frontend/telegram/callbacks/query.ts +1 -1
  201. package/src/frontend/telegram/callbacks/settings.ts +3 -3
  202. package/src/frontend/telegram/commands/admin.ts +6 -41
  203. package/src/frontend/telegram/commands/index.ts +1 -1
  204. package/src/frontend/telegram/commands/info.ts +2 -2
  205. package/src/frontend/telegram/commands/session.ts +9 -3
  206. package/src/frontend/telegram/commands/settings.ts +5 -3
  207. package/src/frontend/telegram/handlers/messages.ts +0 -10
  208. package/src/frontend/telegram/handlers/state.ts +1 -1
  209. package/src/frontend/telegram/index.ts +1 -5
  210. package/src/frontend/telegram/middleware.ts +1 -15
  211. package/src/frontend/telegram/model-menu.ts +1 -1
  212. package/src/frontend/telegram/render/html.ts +30 -0
  213. package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
  214. package/src/frontend/telegram/render/reports.ts +182 -0
  215. package/src/frontend/terminal/builtins/context.ts +2 -2
  216. package/src/frontend/terminal/builtins/session.ts +1 -1
  217. package/src/frontend/terminal/builtins/status.ts +2 -2
  218. package/src/frontend/terminal/index.ts +2 -2
  219. package/src/frontend/terminal/renderer.ts +1 -1
  220. package/src/frontend/whatsapp/access.ts +2 -2
  221. package/src/frontend/whatsapp/actions/history.ts +2 -2
  222. package/src/frontend/whatsapp/actions/messaging.ts +2 -2
  223. package/src/frontend/whatsapp/actions/moderation.ts +1 -1
  224. package/src/frontend/whatsapp/actions/send.ts +1 -1
  225. package/src/frontend/whatsapp/commands.ts +12 -6
  226. package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
  227. package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
  228. package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
  229. package/src/frontend/whatsapp/index.ts +4 -4
  230. package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
  231. package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
  232. package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
  233. package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
  234. package/src/frontend/whatsapp/registry.ts +2 -2
  235. package/src/frontend/whatsapp/runtime.ts +1 -1
  236. package/src/index.ts +6 -3
  237. package/src/storage/db.ts +6 -1
  238. package/src/storage/memory.ts +59 -8
  239. package/src/storage/metrics.ts +38 -0
  240. package/src/storage/repositories/sessions-repo.ts +6 -0
  241. package/src/storage/session-record.ts +13 -2
  242. package/src/storage/sessions.ts +12 -0
  243. package/src/storage/sql/db.sql +5 -0
  244. package/src/storage/sql/schema.sql +4 -0
  245. package/src/storage/sql/sessions.sql +3 -3
  246. package/src/storage/sql/statements.generated.ts +10 -3
  247. package/src/storage/sql/turn-meta.sql +1 -1
  248. package/src/util/log.ts +1 -1
  249. package/src/util/paths.ts +0 -2
  250. package/src/util/runtime.ts +1 -1
  251. package/src/core/engine/gateway-actions/native.ts +0 -1035
  252. package/src/core/soul/README.md +0 -110
  253. package/src/core/soul/RESEARCH.md +0 -98
  254. package/src/core/soul/associative.ts +0 -98
  255. package/src/core/soul/centrality.ts +0 -98
  256. package/src/core/soul/cluster.ts +0 -83
  257. package/src/core/soul/compiler.ts +0 -207
  258. package/src/core/soul/consolidate.ts +0 -179
  259. package/src/core/soul/critic.ts +0 -162
  260. package/src/core/soul/dag.ts +0 -265
  261. package/src/core/soul/delta.ts +0 -123
  262. package/src/core/soul/drift.ts +0 -99
  263. package/src/core/soul/embedder.ts +0 -129
  264. package/src/core/soul/emergent-critic.ts +0 -96
  265. package/src/core/soul/forgetting.ts +0 -131
  266. package/src/core/soul/governance.ts +0 -93
  267. package/src/core/soul/hash.ts +0 -97
  268. package/src/core/soul/hdc.ts +0 -154
  269. package/src/core/soul/kernel.ts +0 -540
  270. package/src/core/soul/lattice.ts +0 -103
  271. package/src/core/soul/lens.ts +0 -110
  272. package/src/core/soul/projector.ts +0 -240
  273. package/src/core/soul/reflect.ts +0 -170
  274. package/src/core/soul/reflex.ts +0 -164
  275. package/src/core/soul/retrieve.ts +0 -146
  276. package/src/core/soul/salience.ts +0 -146
  277. package/src/core/soul/service.ts +0 -204
  278. package/src/core/soul/settings.ts +0 -47
  279. package/src/core/soul/signals.ts +0 -117
  280. package/src/core/soul/talon-embedder.ts +0 -80
  281. package/src/core/soul/taps.ts +0 -199
  282. package/src/core/soul/types.ts +0 -298
  283. package/src/core/soul/valence.ts +0 -83
  284. package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
  285. package/src/frontend/telegram/helpers/format.ts +0 -42
  286. package/src/frontend/telegram/helpers/index.ts +0 -13
  287. package/src/util/cleanup-registry.ts +0 -36
  288. /package/src/backend/{shared → runtime}/frontends.ts +0 -0
  289. /package/src/backend/{shared → runtime}/metrics.ts +0 -0
  290. /package/src/backend/{shared → runtime}/sleep.ts +0 -0
  291. /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
  292. /package/src/backend/{shared → runtime}/usage.ts +0 -0
  293. /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
  294. /package/src/{util → core/tools/content}/web-content.ts +0 -0
  295. /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
  296. /package/src/{util → core/weaver}/session-name.ts +0 -0
  297. /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
  298. /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
  299. /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
  300. /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
  301. /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
  302. /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
  303. /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
  304. /package/src/frontend/{shared → presentation}/access.ts +0 -0
  305. /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
  306. /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
  307. /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
  308. /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
  309. /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
  310. /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `native_bash` — the entry point and the local foreground run.
3
+ *
4
+ * The handler picks one of three paths: a detached background launch
5
+ * (exec-background.ts), a teleported on-device run (exec-remote.ts), or the
6
+ * local spawn below, which owns the timeout escalation (SIGTERM the process
7
+ * group, SIGKILL the survivors) so a runaway pipeline can still flush what
8
+ * it produced.
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { stat as fsStat } from "node:fs/promises";
13
+ import { getTeleport } from "../../../mesh/teleport.js";
14
+ import { createOutputCapture } from "../../../../util/exec-output.js";
15
+ import { bashBackground } from "./exec-background.js";
16
+ import { bashTeleported } from "./exec-remote.js";
17
+ import { resolvePathParam } from "./params.js";
18
+ import { renderExec, type Result } from "./results.js";
19
+ import type { SharedActionHandlers } from "../types.js";
20
+
21
+ const DEFAULT_EXEC_TIMEOUT_MS = 60_000;
22
+ const MAX_EXEC_TIMEOUT_MS = 300_000;
23
+ /** Grace between SIGTERM and SIGKILL on timeout, so pipelines can flush. */
24
+ const KILL_GRACE_MS = 2_000;
25
+ /** Appended to a timed-out bash result so the model self-corrects. */
26
+ const TIMEOUT_HINT =
27
+ "(Streaming/never-ending commands — adb logcat, tail -f, dev servers, watchers — " +
28
+ "will always hit this wall. Re-run with background:true to launch it detached with " +
29
+ "output captured to a log file, or bound the command itself: `adb logcat -d`, " +
30
+ "`timeout 30 …`, `head -n 200`.)";
31
+
32
+ export const execHandlers: SharedActionHandlers = {
33
+ native_bash: (body, chatId) =>
34
+ bash(chatId, body.command, body.cwd, body.timeout_sec, body.background),
35
+ };
36
+
37
+ async function bash(
38
+ chatId: number,
39
+ command: unknown,
40
+ cwd: unknown,
41
+ timeoutSec: unknown,
42
+ background?: unknown,
43
+ ): Promise<Result> {
44
+ const cmd = typeof command === "string" ? command : "";
45
+ if (!cmd.trim()) return { ok: false, text: "No command given." };
46
+ const timeoutMs = clampTimeout(timeoutSec);
47
+ const active = await getTeleport(chatId);
48
+
49
+ let dir = typeof cwd === "string" && cwd.trim() ? cwd.trim() : undefined;
50
+ if (dir !== undefined && !active) {
51
+ dir = resolvePathParam(dir, undefined);
52
+ // Validate here: a bad cwd makes spawn fail with "spawn bash ENOENT",
53
+ // which reads as "bash is missing" and sends the model down the wrong
54
+ // path. Name the actual problem instead.
55
+ try {
56
+ const st = await fsStat(dir);
57
+ if (!st.isDirectory())
58
+ return { ok: false, text: `cwd is not a directory: ${dir}` };
59
+ } catch {
60
+ return { ok: false, text: `Working directory does not exist: ${dir}` };
61
+ }
62
+ }
63
+
64
+ const result = await (async () => {
65
+ if (background === true) {
66
+ if (active) {
67
+ return {
68
+ ok: false,
69
+ text:
70
+ "background:true runs on the daemon host only. On a teleported device, " +
71
+ "background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll " +
72
+ "the log with read.",
73
+ };
74
+ }
75
+ return bashBackground(cmd, dir);
76
+ }
77
+ if (active) return bashTeleported(chatId, active.deviceId, cmd, timeoutMs);
78
+ return bashLocal(cmd, dir, timeoutMs);
79
+ })();
80
+ return result;
81
+ }
82
+
83
+ function bashLocal(
84
+ cmd: string,
85
+ cwd: string | undefined,
86
+ timeoutMs: number,
87
+ ): Promise<Result> {
88
+ return new Promise((resolvePromise) => {
89
+ // detached → own process group on POSIX, so a timeout can kill the whole
90
+ // tree (bash's children included), not just the shell itself.
91
+ const detached = process.platform !== "win32";
92
+ const child = spawn("bash", ["-c", cmd], {
93
+ ...(cwd ? { cwd } : {}),
94
+ env: process.env,
95
+ detached,
96
+ });
97
+ const stdout = createOutputCapture();
98
+ const stderr = createOutputCapture();
99
+ let killed = false;
100
+ // Timeout escalation: SIGTERM the whole process group first (lets
101
+ // pipelines flush + children clean up), SIGKILL any survivors after a
102
+ // short grace. Straight-to-SIGKILL used to eat buffered output.
103
+ const killTree = (signal: NodeJS.Signals) => {
104
+ if (detached && child.pid) {
105
+ try {
106
+ process.kill(-child.pid, signal);
107
+ return;
108
+ } catch {
109
+ // group already gone — fall through to the direct kill
110
+ }
111
+ }
112
+ try {
113
+ child.kill(signal);
114
+ } catch {
115
+ // already dead
116
+ }
117
+ };
118
+ let settled = false;
119
+ let killTimer: NodeJS.Timeout | undefined;
120
+ let forceTimer: NodeJS.Timeout | undefined;
121
+ const finish = (r: Result) => {
122
+ if (settled) return;
123
+ settled = true;
124
+ clearTimeout(timer);
125
+ if (killTimer) clearTimeout(killTimer);
126
+ if (forceTimer) clearTimeout(forceTimer);
127
+ resolvePromise(r);
128
+ };
129
+ const timedOutResult = () => {
130
+ const body = renderExec(
131
+ "local",
132
+ `⏱️ timed out after ${timeoutMs / 1000}s — killed; partial output kept below`,
133
+ stdout.value(),
134
+ stderr.value(),
135
+ );
136
+ return { ok: false, text: `${body}\n${TIMEOUT_HINT}` };
137
+ };
138
+ const timer = setTimeout(() => {
139
+ killed = true;
140
+ killTree("SIGTERM");
141
+ killTimer = setTimeout(() => killTree("SIGKILL"), KILL_GRACE_MS);
142
+ // `close` waits for the stdio pipes to drain — a surviving grandchild
143
+ // that inherited stdout (Windows has no process groups; a detached
144
+ // POSIX grandchild can escape the group kill) would otherwise hold
145
+ // this promise open long past the timeout. Force-resolve with the
146
+ // partial output once the escalation window has passed.
147
+ forceTimer = setTimeout(
148
+ () => finish(timedOutResult()),
149
+ KILL_GRACE_MS + 1_000,
150
+ );
151
+ }, timeoutMs);
152
+ child.stdout.on("data", stdout.push);
153
+ child.stderr.on("data", stderr.push);
154
+ child.on("error", (err) => {
155
+ finish({ ok: false, text: `Failed to run: ${err.message}` });
156
+ });
157
+ child.on("close", (code) => {
158
+ if (killed) {
159
+ finish(timedOutResult());
160
+ return;
161
+ }
162
+ finish({
163
+ ok: (code ?? 0) === 0,
164
+ text: renderExec(
165
+ "local",
166
+ `exit ${code ?? 0}`,
167
+ stdout.value(),
168
+ stderr.value(),
169
+ ),
170
+ });
171
+ });
172
+ });
173
+ }
174
+
175
+ function clampTimeout(value: unknown): number {
176
+ const sec = typeof value === "number" ? value : Number(value);
177
+ if (!Number.isFinite(sec) || sec <= 0) return DEFAULT_EXEC_TIMEOUT_MS;
178
+ return Math.min(
179
+ MAX_EXEC_TIMEOUT_MS,
180
+ Math.max(1_000, Math.round(sec * 1_000)),
181
+ );
182
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Native tools — Talon's own shell/filesystem tools, replacing the SDK's
3
+ * built-in Bash/Read/Write/Edit/Glob/Grep when `config.nativeTools` is on.
4
+ *
5
+ * Their defining feature: every one checks the current chat's active
6
+ * `teleport` target. With no teleport for that chat, they run on the daemon
7
+ * host (local spawn / local fs / local ripgrep). With a teleport engaged, they
8
+ * run ON the companion device via the mesh exec/fs channel — so
9
+ * `bash`/`read`/`write`/… transparently operate on the phone for that chat.
10
+ *
11
+ * teleport(device) → native tools target that device
12
+ * teleport_back() → native tools run locally again
13
+ *
14
+ * The teleported path reuses the exec/fs command surface on MeshService; the
15
+ * local path is a thin, well-scoped reimplementation of the built-ins.
16
+ *
17
+ * One module per concern; this file is only the registry:
18
+ *
19
+ * - `teleport` — teleport / teleport_back
20
+ * - `exec` — native_bash (dispatch + local foreground run)
21
+ * - `exec-background` — the detached background launch
22
+ * - `exec-remote` — the on-device run over the mesh exec channel
23
+ * - `read` — native_read
24
+ * - `write` — native_write / native_edit
25
+ * - `search` — native_glob / native_search
26
+ *
27
+ * The spread order below reproduces the key order of the single object
28
+ * literal these came from. Action names are the model's tool vocabulary and
29
+ * sit in the prompt-cache prefix via the tool descriptions, so neither the
30
+ * names nor their order may change (pinned by native-tools.test.ts).
31
+ */
32
+
33
+ import { teleportHandlers } from "./teleport.js";
34
+ import { execHandlers } from "./exec.js";
35
+ import { readHandlers } from "./read.js";
36
+ import { writeHandlers } from "./write.js";
37
+ import { searchHandlers } from "./search.js";
38
+ import type { SharedActionHandlers } from "../types.js";
39
+
40
+ export const nativeHandlers: SharedActionHandlers = {
41
+ ...teleportHandlers,
42
+ ...execHandlers,
43
+ ...readHandlers,
44
+ ...writeHandlers,
45
+ ...searchHandlers,
46
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tool-argument resolution: the one rule for every path parameter, plus the
3
+ * string/number coercions that turn an untyped request body field into
4
+ * something the handlers can use.
5
+ */
6
+
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ // — path parameter resolution ----------------------------------------------
11
+
12
+ /** Expand a leading `~` — local runs only; a device's home is not ours. */
13
+ function expandHome(path: string): string {
14
+ if (path === "~") return homedir();
15
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
16
+ return path;
17
+ }
18
+
19
+ /**
20
+ * One rule for every path parameter: `~` expands (local runs only),
21
+ * everything else passes through untouched.
22
+ *
23
+ * The namespace has no tool-facing address scheme — its nodes are reached
24
+ * by their real paths (`~/.talon/ns/…`, kept real by the symlink farm in
25
+ * nsdir.ts and the FUSE layer in fusefs.ts). Real paths need no
26
+ * translation and behave identically here, in a bare shell, in another
27
+ * backend's built-in shell (Codex), and in any spawned child process.
28
+ * Teleported, paths belong to the device and pass through verbatim.
29
+ */
30
+ export function resolvePathParam(
31
+ path: string,
32
+ teleportedTo: string | undefined,
33
+ ): string {
34
+ return teleportedTo !== undefined ? path : expandHome(path);
35
+ }
36
+
37
+ // — scalar coercion --------------------------------------------------------
38
+
39
+ export function str(value: unknown): string | undefined {
40
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
41
+ }
42
+
43
+ export function num(value: unknown): number | undefined {
44
+ const n = typeof value === "number" ? value : Number(value);
45
+ return Number.isFinite(n) ? n : undefined;
46
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `native_read` — read a file from the daemon host or, when teleported, from
3
+ * the device.
4
+ *
5
+ * Two things it refuses rather than fakes: a file too big to slurp into
6
+ * memory (bash can slice it) and binary content decoded as UTF-8. Images are
7
+ * the exception — they come back as a viewable image block instead of bytes.
8
+ */
9
+
10
+ import { readFile, stat as fsStat } from "node:fs/promises";
11
+ import { extname } from "node:path";
12
+ import { getMeshService } from "../../../mesh/index.js";
13
+ import { getTeleport } from "../../../mesh/teleport.js";
14
+ import { num, resolvePathParam, str } from "./params.js";
15
+ import type { Result } from "./results.js";
16
+ import type { SharedActionHandlers } from "../types.js";
17
+
18
+ const MAX_READ_LINES = 2_000;
19
+ /**
20
+ * Refuse to slurp huge files into memory: `read` loads the whole file to
21
+ * slice lines, so an unbounded readFile of a multi-GB log balloons RSS.
22
+ * Past this, bash is the right tool (`sed -n`, `tail`, `head`).
23
+ */
24
+ const MAX_READ_FILE_BYTES = 32 * 1024 * 1024;
25
+ /**
26
+ * Image extensions the model can actually view, mapped to their MIME type.
27
+ * Reading one returns an image content block instead of the file's raw bytes
28
+ * decoded as (garbage) UTF-8 — so `read`ing a photo/screenshot/design shows
29
+ * the picture, not mojibake.
30
+ */
31
+ const IMAGE_MIME: Record<string, string> = {
32
+ ".png": "image/png",
33
+ ".jpg": "image/jpeg",
34
+ ".jpeg": "image/jpeg",
35
+ ".gif": "image/gif",
36
+ ".webp": "image/webp",
37
+ };
38
+ /** Anthropic caps a single image around 5MB; refuse larger with a downscale hint. */
39
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
40
+
41
+ export const readHandlers: SharedActionHandlers = {
42
+ native_read: (body, chatId) =>
43
+ read(chatId, body.path, body.offset, body.limit),
44
+ };
45
+
46
+ async function read(
47
+ chatId: number,
48
+ path: unknown,
49
+ offset: unknown,
50
+ limit: unknown,
51
+ ): Promise<Result> {
52
+ const address = str(path);
53
+ if (!address) return { ok: false, text: "A file path is required." };
54
+ const active = await getTeleport(chatId);
55
+ const where = active ? active.deviceName : "local";
56
+
57
+ const p = resolvePathParam(address, active?.deviceName);
58
+ const shown = p === address ? p : `${address} → ${p}`;
59
+
60
+ // Image files: return a viewable image block, not the raw bytes decoded as
61
+ // UTF-8. Without this, `read`ing a photo/screenshot/design hands the model
62
+ // mojibake and it can't see the picture at all.
63
+ const mime = IMAGE_MIME[extname(p).toLowerCase()];
64
+ if (mime) {
65
+ let bytes: Buffer;
66
+ if (active) {
67
+ const res = await getMeshService().readFileBytes(active.deviceId, p);
68
+ if ("error" in res) return { ok: false, text: res.error };
69
+ bytes = res.data;
70
+ } else {
71
+ try {
72
+ // stat first — no point slurping a 40MB photo just to refuse it.
73
+ const st = await fsStat(p);
74
+ if (st.size > MAX_IMAGE_BYTES)
75
+ return oversizeImageResult(shown, where, st.size, p);
76
+ bytes = await readFile(p);
77
+ } catch (err) {
78
+ return {
79
+ ok: false,
80
+ text: `Cannot read ${shown}: ${(err as Error).message}`,
81
+ };
82
+ }
83
+ }
84
+ if (bytes.length > MAX_IMAGE_BYTES) {
85
+ return oversizeImageResult(shown, where, bytes.length, p);
86
+ }
87
+ return {
88
+ ok: true,
89
+ text: `${shown} [${where}] — image (${mime}, ${bytes.length} bytes)`,
90
+ image: { data: bytes.toString("base64"), mimeType: mime },
91
+ };
92
+ }
93
+
94
+ // Clamp: a negative offset would silently flip slice() into
95
+ // count-from-the-end (with line numbers that lie), and a zero/negative
96
+ // limit would return an empty read that looks like an empty file.
97
+ const start = Math.max(0, Math.trunc(num(offset) ?? 0));
98
+ const max = Math.min(
99
+ Math.max(1, Math.trunc(num(limit) ?? MAX_READ_LINES)),
100
+ MAX_READ_LINES,
101
+ );
102
+ let content: string;
103
+ if (active) {
104
+ const res = await getMeshService().readFileBytes(active.deviceId, p);
105
+ if ("error" in res) return { ok: false, text: res.error };
106
+ if (res.data.length > MAX_READ_FILE_BYTES)
107
+ return oversizeReadResult(shown, where, res.data.length, p);
108
+ content = res.data.toString("utf8");
109
+ } else {
110
+ try {
111
+ const st = await fsStat(p);
112
+ if (st.isFile() && st.size > MAX_READ_FILE_BYTES)
113
+ return oversizeReadResult(shown, where, st.size, p);
114
+ content = await readFile(p, "utf8");
115
+ } catch (err) {
116
+ return {
117
+ ok: false,
118
+ text: `Cannot read ${shown}: ${(err as Error).message}`,
119
+ };
120
+ }
121
+ }
122
+ // Binary files decoded as UTF-8 are mojibake the model can't use — same
123
+ // rationale as the image branch, but with no viewable representation to
124
+ // return. Point at the shell tools that can actually inspect the bytes.
125
+ if (content.includes("\0")) {
126
+ return {
127
+ ok: false,
128
+ text:
129
+ `${shown} [${where}] looks binary — refusing to render it as text. ` +
130
+ `Inspect it with bash instead: \`file '${p}'\`, \`xxd '${p}' | head\`, \`strings '${p}'\`.`,
131
+ };
132
+ }
133
+ const lines = content.split("\n");
134
+ if (start >= lines.length) {
135
+ return {
136
+ ok: false,
137
+ text: `${shown} [${where}] has ${lines.length} lines — offset ${start} is past the end.`,
138
+ };
139
+ }
140
+ const slice = lines.slice(start, start + max);
141
+ const numbered = slice
142
+ .map((line, i) => `${String(start + i + 1).padStart(6)}\t${line}`)
143
+ .join("\n");
144
+ const more =
145
+ lines.length > start + max
146
+ ? `\n… (${lines.length - start - max} more lines; raise limit/offset)`
147
+ : "";
148
+ return {
149
+ ok: true,
150
+ text: `${shown} [${where}] — ${lines.length} lines\n${numbered}${more}`,
151
+ };
152
+ }
153
+
154
+ function oversizeReadResult(
155
+ shown: string,
156
+ where: string,
157
+ size: number,
158
+ p: string,
159
+ ): Result {
160
+ return {
161
+ ok: false,
162
+ text:
163
+ `${shown} [${where}] is ${(size / 1_048_576).toFixed(1)}MB — over the ` +
164
+ `${MAX_READ_FILE_BYTES / 1_048_576}MB read limit. Slice it with bash instead: ` +
165
+ `\`sed -n '1,200p' '${p}'\`, \`tail -n 200 '${p}'\`, or \`grep\` for what you need.`,
166
+ };
167
+ }
168
+
169
+ function oversizeImageResult(
170
+ shown: string,
171
+ where: string,
172
+ size: number,
173
+ p: string,
174
+ ): Result {
175
+ return {
176
+ ok: false,
177
+ text:
178
+ `${shown} [${where}] is ${(size / 1_048_576).toFixed(1)}MB — over the ` +
179
+ `${MAX_IMAGE_BYTES / 1_048_576}MB image limit. Downscale it first ` +
180
+ `(e.g. \`convert '${p}' -resize 1568x /tmp/small.jpg\`) and read that.`,
181
+ };
182
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What a native tool hands back: the `Result` shape every handler in this
3
+ * directory returns, and the renderer that turns an exec run (local or
4
+ * teleported) into the `[where] status / stdout / stderr` block the model
5
+ * reads.
6
+ */
7
+
8
+ import { clampExecOutput } from "../../../../util/exec-output.js";
9
+
10
+ export type Result = {
11
+ ok: boolean;
12
+ text: string;
13
+ /** Set for image files so the tool result carries a viewable image block. */
14
+ image?: { data: string; mimeType: string };
15
+ };
16
+
17
+ export function renderExec(
18
+ where: string,
19
+ status: string,
20
+ stdout: string,
21
+ stderr: string,
22
+ ): string {
23
+ const parts = [`[${where}] ${status}`];
24
+ if (stdout.trim())
25
+ parts.push(
26
+ `--- stdout ---\n${clampExecOutput(stdout.replace(/\s+$/, ""))}`,
27
+ );
28
+ if (stderr.trim())
29
+ parts.push(
30
+ `--- stderr ---\n${clampExecOutput(stderr.replace(/\s+$/, ""))}`,
31
+ );
32
+ if (!stdout.trim() && !stderr.trim()) parts.push("(no output)");
33
+ return parts.join("\n");
34
+ }