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,212 @@
1
+ /**
2
+ * `native_glob` / `native_search` — find files by pattern, find text inside
3
+ * them.
4
+ *
5
+ * ripgrep is the fast path on both hosts; when it isn't installed the local
6
+ * runs fall back to a pure-JS walker rather than lying "no matches", and a
7
+ * teleported run falls back to the device's find/grep.
8
+ */
9
+
10
+ import { glob as fsGlob, readFile, stat as fsStat } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { getTeleport } from "../../../mesh/teleport.js";
13
+ import { bashTeleported } from "./exec-remote.js";
14
+ import { resolvePathParam, str } from "./params.js";
15
+ import type { Result } from "./results.js";
16
+ import { runLocal, shellQuote } from "./shell.js";
17
+ import type { SharedActionHandlers } from "../types.js";
18
+
19
+ /**
20
+ * ripgrep binary, resolved from PATH (env-overridable for tests). NOT a
21
+ * hardcoded absolute path: /usr/bin/rg only exists on some Linux installs,
22
+ * and a missing binary must fall back loudly (or to the pure-JS walker),
23
+ * never masquerade as "no matches".
24
+ */
25
+ function rgBin(): string {
26
+ return process.env.TALON_NATIVE_RG ?? "rg";
27
+ }
28
+ /** Caps for the pure-JS glob/search fallbacks (rg unavailable). */
29
+ const MAX_JS_RESULTS = 2_000;
30
+ const MAX_JS_FILE_BYTES = 2 * 1024 * 1024;
31
+ const SKIP_DIRS_RE = /(^|[\\/])(node_modules|\.git)([\\/]|$)/;
32
+
33
+ export const searchHandlers: SharedActionHandlers = {
34
+ native_glob: (body, chatId) => glob(chatId, body.pattern, body.path),
35
+ native_search: (body, chatId) =>
36
+ search(chatId, body.pattern, body.path, body.glob, body.case_insensitive),
37
+ };
38
+
39
+ async function glob(
40
+ chatId: number,
41
+ pattern: unknown,
42
+ path: unknown,
43
+ ): Promise<Result> {
44
+ const pat = str(pattern);
45
+ if (!pat) return { ok: false, text: "A glob pattern is required." };
46
+ const active = await getTeleport(chatId);
47
+ const root = resolvePathParam(str(path) ?? ".", active?.deviceName);
48
+ if (active) {
49
+ // Prefer rg on the device; fall back to find (basename patterns via
50
+ // -name, path patterns via -path). `command -v` gates the choice so a
51
+ // no-match rg exit (1) isn't misread as "rg missing, run find too".
52
+ const findExpr = pat.includes("/")
53
+ ? `-type f -path ${shellQuote(`*${pat}`)}`
54
+ : `-type f -name ${shellQuote(pat)}`;
55
+ const cmd =
56
+ `if command -v rg >/dev/null 2>&1; ` +
57
+ `then rg --files -g ${shellQuote(pat)} ${shellQuote(root)}; ` +
58
+ `else find ${shellQuote(root)} ${findExpr} 2>/dev/null; fi`;
59
+ return bashTeleported(chatId, active.deviceId, cmd, 30_000);
60
+ }
61
+ const res = await runLocal(rgBin(), ["--files", "-g", pat, root]);
62
+ let files: string[];
63
+ if (res.code === 127) {
64
+ // rg not installed — pure-JS fallback rather than lying "no matches".
65
+ files = await globJs(pat, root);
66
+ } else if (res.code > 1 && !res.stdout.trim()) {
67
+ // exit 2 with output = partial results (e.g. permission-denied subdirs);
68
+ // exit 2 with none = a real error worth surfacing.
69
+ return {
70
+ ok: false,
71
+ text: `glob failed: ${res.stderr.trim() || `ripgrep exit ${res.code}`}`,
72
+ };
73
+ } else {
74
+ files = res.stdout.trim().split("\n").filter(Boolean);
75
+ }
76
+ // rg's parallel directory walk (and the JS fallback's) emit in
77
+ // nondeterministic order — sort so identical calls render identically.
78
+ files.sort();
79
+ return {
80
+ ok: true,
81
+ text: files.length
82
+ ? `${files.length} match(es):\n${files.slice(0, 200).join("\n")}${files.length > 200 ? `\n… (${files.length - 200} more)` : ""}`
83
+ : `No files match ${pat} under ${root}.`,
84
+ };
85
+ }
86
+
87
+ async function search(
88
+ chatId: number,
89
+ pattern: unknown,
90
+ path: unknown,
91
+ globPat: unknown,
92
+ caseInsensitive: unknown,
93
+ ): Promise<Result> {
94
+ const pat = str(pattern);
95
+ if (!pat) return { ok: false, text: "A search pattern is required." };
96
+ const active = await getTeleport(chatId);
97
+ const root = resolvePathParam(str(path) ?? ".", active?.deviceName);
98
+ const g = str(globPat);
99
+ const ci = caseInsensitive === true;
100
+ // `-e` keeps a pattern that starts with "-" from being parsed as a flag
101
+ // (same idiom for rg and grep).
102
+ const flags = [
103
+ "-n",
104
+ "--color=never",
105
+ ...(ci ? ["-i"] : []),
106
+ ...(g ? ["-g", g] : []),
107
+ ];
108
+ if (active) {
109
+ // Prefer rg on the device, fall back to grep (Android toybox has grep
110
+ // but rarely rg). --include is grep's closest analogue of -g.
111
+ const grepFlags = [
112
+ "-rn",
113
+ ...(ci ? ["-i"] : []),
114
+ ...(g ? [`--include=${g}`] : []),
115
+ ];
116
+ const cmd =
117
+ `if command -v rg >/dev/null 2>&1; ` +
118
+ `then rg ${flags.map(shellQuote).join(" ")} -e ${shellQuote(pat)} ${shellQuote(root)}; ` +
119
+ `else grep ${grepFlags.map(shellQuote).join(" ")} -e ${shellQuote(pat)} ${shellQuote(root)} 2>/dev/null; fi`;
120
+ return bashTeleported(chatId, active.deviceId, cmd, 30_000);
121
+ }
122
+ const res = await runLocal(rgBin(), [...flags, "-e", pat, root]);
123
+ let lines: string[];
124
+ if (res.code === 127) {
125
+ // rg not installed — pure-JS fallback rather than lying "no matches".
126
+ try {
127
+ lines = await searchJs(pat, root, g, ci);
128
+ } catch (err) {
129
+ return { ok: false, text: `search failed: ${(err as Error).message}` };
130
+ }
131
+ } else if (res.code > 1 && !res.stdout.trim()) {
132
+ // exit 2 with output = partial results; exit 2 with none = real error.
133
+ return {
134
+ ok: false,
135
+ text: `search failed: ${res.stderr.trim() || `ripgrep exit ${res.code}`}`,
136
+ };
137
+ } else {
138
+ const out = res.stdout.trim();
139
+ lines = out ? out.split("\n") : [];
140
+ }
141
+ return {
142
+ ok: true,
143
+ text: lines.length
144
+ ? `${lines.length} match line(s):\n${lines.slice(0, 200).join("\n")}${lines.length > 200 ? `\n… (${lines.length - 200} more)` : ""}`
145
+ : `No matches for ${pat} under ${root}.`,
146
+ };
147
+ }
148
+
149
+ // ── pure-JS glob/search fallbacks (no ripgrep on the host) ──────────────────
150
+
151
+ /**
152
+ * Glob without ripgrep, via node:fs `glob`. Mirrors rg's -g semantics for
153
+ * bare names (a pattern without "/" matches at any depth) and skips
154
+ * node_modules/.git, which rg would exclude via gitignore.
155
+ */
156
+ async function globJs(pat: string, root: string): Promise<string[]> {
157
+ const pattern = pat.includes("/") ? pat : `**/${pat}`;
158
+ const out: string[] = [];
159
+ try {
160
+ for await (const entry of fsGlob(pattern, {
161
+ cwd: root,
162
+ exclude: (e: unknown) => {
163
+ const name =
164
+ typeof e === "string" ? e : ((e as { name?: string }).name ?? "");
165
+ return (
166
+ SKIP_DIRS_RE.test(name) || name === "node_modules" || name === ".git"
167
+ );
168
+ },
169
+ })) {
170
+ out.push(join(root, String(entry)));
171
+ if (out.length >= MAX_JS_RESULTS) break;
172
+ }
173
+ } catch {
174
+ // unreadable root etc. — empty result, caller reports "no matches"
175
+ }
176
+ return out;
177
+ }
178
+
179
+ /** Content search without ripgrep: walk text files and regex-match lines. */
180
+ async function searchJs(
181
+ pat: string,
182
+ root: string,
183
+ globPat: string | undefined,
184
+ caseInsensitive: boolean,
185
+ ): Promise<string[]> {
186
+ const re = new RegExp(pat, caseInsensitive ? "i" : "");
187
+ let files: string[];
188
+ try {
189
+ const st = await fsStat(root);
190
+ files = st.isFile() ? [root] : await globJs(globPat ?? "**/*", root);
191
+ } catch {
192
+ return [];
193
+ }
194
+ const lines: string[] = [];
195
+ for (const f of files) {
196
+ let content: string;
197
+ try {
198
+ const st = await fsStat(f);
199
+ if (!st.isFile() || st.size > MAX_JS_FILE_BYTES) continue;
200
+ content = await readFile(f, "utf8");
201
+ } catch {
202
+ continue;
203
+ }
204
+ if (content.includes("\0")) continue; // binary
205
+ const fileLines = content.split("\n");
206
+ for (let i = 0; i < fileLines.length; i++) {
207
+ if (re.test(fileLines[i])) lines.push(`${f}:${i + 1}:${fileLines[i]}`);
208
+ if (lines.length >= MAX_JS_RESULTS) return lines;
209
+ }
210
+ }
211
+ return lines;
212
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Talking to a shell: POSIX single-quoting for a command Talon builds, and a
3
+ * plain local spawn that collects exit code + stdout + stderr (used for the
4
+ * ripgrep invocations behind glob/search).
5
+ */
6
+
7
+ import { spawn } from "node:child_process";
8
+ import { createOutputCapture } from "../../../../util/exec-output.js";
9
+
10
+ export function shellQuote(s: string): string {
11
+ return `'${s.replace(/'/g, `'\\''`)}'`;
12
+ }
13
+
14
+ export function runLocal(
15
+ bin: string,
16
+ args: string[],
17
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
18
+ return new Promise((resolvePromise) => {
19
+ const child = spawn(bin, args, { env: process.env });
20
+ const stdout = createOutputCapture();
21
+ const stderr = createOutputCapture();
22
+ child.stdout.on("data", stdout.push);
23
+ child.stderr.on("data", stderr.push);
24
+ child.on("error", () =>
25
+ resolvePromise({
26
+ code: 127,
27
+ stdout: stdout.value(),
28
+ stderr: stderr.value(),
29
+ }),
30
+ );
31
+ child.on("close", (code) =>
32
+ resolvePromise({
33
+ code: code ?? 0,
34
+ stdout: stdout.value(),
35
+ stderr: stderr.value(),
36
+ }),
37
+ );
38
+ });
39
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Teleport control — which host the native tools in this directory target.
3
+ *
4
+ * teleport(device) → native tools run ON that companion device
5
+ * teleport_back() → native tools run on the daemon host again
6
+ */
7
+
8
+ import { getMeshService } from "../../../mesh/index.js";
9
+ import { clearTeleport, setTeleport } from "../../../mesh/teleport.js";
10
+ import type { Result } from "./results.js";
11
+ import type { SharedActionHandlers } from "../types.js";
12
+
13
+ export const teleportHandlers: SharedActionHandlers = {
14
+ teleport: (body, chatId) => teleport(chatId, body.device),
15
+ teleport_back: (_body, chatId) => teleportBack(chatId),
16
+ };
17
+
18
+ async function teleport(chatId: number, query: unknown): Promise<Result> {
19
+ const svc = getMeshService();
20
+ await svc.list();
21
+ const resolved = svc.resolveDevice(query);
22
+ if ("error" in resolved) return { ok: false, text: resolved.error };
23
+ const target = resolved.target;
24
+ if (!target.online) {
25
+ return {
26
+ ok: false,
27
+ text: `${target.name} appears offline — cannot teleport onto a device that isn't connected.`,
28
+ };
29
+ }
30
+ if (target.capabilities && !target.capabilities.includes("exec")) {
31
+ return {
32
+ ok: false,
33
+ text: `${target.name} does not advertise the "exec" capability, so teleport can't run commands on it.`,
34
+ };
35
+ }
36
+ await setTeleport(chatId, target.id, target.name);
37
+ return {
38
+ ok: true,
39
+ text: [
40
+ `🛰️ Teleported onto ${target.name}. Native bash/read/write/edit/glob/search now run ON that device.`,
41
+ `Working dir starts at the device default; \`cd\` in bash persists across calls.`,
42
+ `Call teleport_back to return to the daemon host.`,
43
+ ].join(" "),
44
+ };
45
+ }
46
+
47
+ async function teleportBack(chatId: number): Promise<Result> {
48
+ const prior = await clearTeleport(chatId);
49
+ return {
50
+ ok: true,
51
+ text: prior
52
+ ? `↩️ Teleported back from ${prior.deviceName}. Native tools run on the daemon host again.`
53
+ : "Not teleported — native tools already run on the daemon host.",
54
+ };
55
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * `native_write` / `native_edit` — the file-mutating half of the native
3
+ * tools, local or teleported.
4
+ *
5
+ * `edit` splices literally (never String.replace, whose `$` patterns would
6
+ * corrupt shell snippets and regexes), refuses binary files, and points at
7
+ * the closest-looking line when old_string doesn't match byte-for-byte.
8
+ */
9
+
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import { dirname } from "node:path";
12
+ import { getMeshService } from "../../../mesh/index.js";
13
+ import { getTeleport } from "../../../mesh/teleport.js";
14
+ import { resolvePathParam, str } from "./params.js";
15
+ import type { Result } from "./results.js";
16
+ import type { SharedActionHandlers } from "../types.js";
17
+
18
+ export const writeHandlers: SharedActionHandlers = {
19
+ native_write: (body, chatId) => write(chatId, body.path, body.content),
20
+ native_edit: (body, chatId) =>
21
+ edit(chatId, body.path, body.old_string, body.new_string, body.replace_all),
22
+ };
23
+
24
+ async function write(
25
+ chatId: number,
26
+ path: unknown,
27
+ content: unknown,
28
+ ): Promise<Result> {
29
+ const address = str(path);
30
+ if (!address) return { ok: false, text: "A file path is required." };
31
+ const body = typeof content === "string" ? content : "";
32
+ const active = await getTeleport(chatId);
33
+ const p = resolvePathParam(address, active?.deviceName);
34
+ const shown = p === address ? p : `${address} → ${p}`;
35
+ if (active) {
36
+ return getMeshService().writeFileToDevice(active.deviceId, p, body);
37
+ }
38
+ try {
39
+ await mkdir(dirname(p), { recursive: true });
40
+ await writeFile(p, body);
41
+ } catch (err) {
42
+ return {
43
+ ok: false,
44
+ text: `Cannot write ${shown}: ${(err as Error).message}`,
45
+ };
46
+ }
47
+ return {
48
+ ok: true,
49
+ text: `Wrote ${Buffer.byteLength(body, "utf8")} bytes to ${shown} [local].`,
50
+ };
51
+ }
52
+
53
+ async function edit(
54
+ chatId: number,
55
+ path: unknown,
56
+ oldString: unknown,
57
+ newString: unknown,
58
+ replaceAll: unknown,
59
+ ): Promise<Result> {
60
+ const address = str(path);
61
+ if (!address) return { ok: false, text: "A file path is required." };
62
+ const from = typeof oldString === "string" ? oldString : "";
63
+ const to = typeof newString === "string" ? newString : "";
64
+ if (from === to)
65
+ return { ok: false, text: "old_string and new_string are identical." };
66
+ const active = await getTeleport(chatId);
67
+ const p = resolvePathParam(address, active?.deviceName);
68
+ const shown = p === address ? p : `${address} → ${p}`;
69
+ const svc = getMeshService();
70
+
71
+ let content: string;
72
+ if (active) {
73
+ const res = await svc.readFileBytes(active.deviceId, p);
74
+ if ("error" in res) return { ok: false, text: res.error };
75
+ content = res.data.toString("utf8");
76
+ } else {
77
+ try {
78
+ content = await readFile(p, "utf8");
79
+ } catch (err) {
80
+ return {
81
+ ok: false,
82
+ text: `Cannot read ${shown}: ${(err as Error).message}`,
83
+ };
84
+ }
85
+ }
86
+
87
+ // Binary guard: decoding binary as UTF-8 is lossy (invalid sequences
88
+ // become U+FFFD), so a read→replace→write round-trip would corrupt every
89
+ // non-text byte — even when old_string matches a clean region.
90
+ if (content.includes("\0")) {
91
+ return {
92
+ ok: false,
93
+ text: `${shown} looks binary — refusing a text edit that would corrupt it. Use bash for byte-level changes.`,
94
+ };
95
+ }
96
+
97
+ const count = from ? content.split(from).length - 1 : 0;
98
+ if (count === 0) {
99
+ return {
100
+ ok: false,
101
+ text: `old_string not found in ${shown}.${nearMissHint(content, from)}`,
102
+ };
103
+ }
104
+ if (count > 1 && replaceAll !== true) {
105
+ return {
106
+ ok: false,
107
+ text: `old_string appears ${count}× in ${shown}; pass replace_all or make it unique.`,
108
+ };
109
+ }
110
+ // Splice, not String.replace: replace() treats dollar-sign substitution
111
+ // patterns ($&, $', $BACKTICK, $$) in the replacement as directives, silently
112
+ // corrupting any new_string that contains them (shell snippets, regexes,
113
+ // Makefiles...). split/join and index-splice are both literal.
114
+ const updated =
115
+ replaceAll === true
116
+ ? content.split(from).join(to)
117
+ : spliceOnce(content, from, to);
118
+ const firstLine = content.slice(0, content.indexOf(from)).split("\n").length;
119
+
120
+ if (active) {
121
+ const res = await svc.writeFileToDevice(active.deviceId, p, updated);
122
+ return res.ok
123
+ ? {
124
+ ok: true,
125
+ text: `Edited ${shown} [${active.deviceName}] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
126
+ }
127
+ : res;
128
+ }
129
+ try {
130
+ await writeFile(p, updated);
131
+ } catch (err) {
132
+ return {
133
+ ok: false,
134
+ text: `Cannot write ${shown}: ${(err as Error).message}`,
135
+ };
136
+ }
137
+ return {
138
+ ok: true,
139
+ text: `Edited ${shown} [local] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * When an edit's old_string doesn't match verbatim, the cause is almost
145
+ * always invisible: indentation, tabs vs spaces, or a trailing space. Point
146
+ * at the closest-looking line so the model re-reads that region instead of
147
+ * blindly retrying the same string.
148
+ */
149
+ function nearMissHint(content: string, from: string): string {
150
+ const probe =
151
+ from
152
+ .split("\n")
153
+ .map((l) => l.trim())
154
+ .find((l) => l.length >= 8) ?? from.trim();
155
+ if (!probe) return "";
156
+ const lines = content.split("\n");
157
+ const idx = lines.findIndex((l) => l.trim().includes(probe));
158
+ if (idx === -1) return "";
159
+ return (
160
+ `\nLine ${idx + 1} looks close — whitespace/indentation must match the file byte-for-byte. ` +
161
+ `Re-read that region before retrying:\n${String(idx + 1).padStart(6)}\t${lines[idx]}`
162
+ );
163
+ }
164
+
165
+ /** Replace the first occurrence of `from` with `to`, both taken literally. */
166
+ function spliceOnce(content: string, from: string, to: string): string {
167
+ const idx = content.indexOf(from);
168
+ if (idx === -1) return content;
169
+ return content.slice(0, idx) + to + content.slice(idx + from.length);
170
+ }
@@ -30,7 +30,6 @@ import {
30
30
  } from "./gateway-routes.js";
31
31
  import { handlePluginAction } from "../plugin/index.js";
32
32
  import type { FrontendActionHandler } from "../types.js";
33
- import { BOT_MESSAGE_ACTIONS, noteBotMessage } from "../soul/taps.js";
34
33
  import type { Backend } from "../agent-runtime/capabilities.js";
35
34
  import { resolveOwnerFrontendId } from "../frontend-runtime/routing.js";
36
35
 
@@ -320,11 +319,6 @@ export class Gateway {
320
319
  if (frontendHandler) {
321
320
  const result = await frontendHandler(body, chatId);
322
321
  if (result) {
323
- // Soul tap: remember our own outgoing message ids so a later
324
- // reaction update can be attributed to one of Talon's messages.
325
- // No-op unless the soul is enabled.
326
- if (BOT_MESSAGE_ACTIONS.has(action) && result.ok && result.message_id)
327
- noteBotMessage(chatId, Number(result.message_id));
328
322
  logDebug("gateway", `${action} chat=${chatId} ${Date.now() - t0}ms`);
329
323
  return result;
330
324
  }
@@ -7,7 +7,7 @@
7
7
  * imports `src/frontend/factories.ts`.
8
8
  *
9
9
  * Chat-id shapes are the long-standing conventions from
10
- * `util/chat-id.ts`. Priorities encode the historical resolution order
10
+ * `./chat-id.ts`. Priorities encode the historical resolution order
11
11
  * (terminal, native, teams, discord, telegram) — load-bearing in two
12
12
  * places: telegram's numeric matcher is a near-catch-all so it must run
13
13
  * last, and the terminal claims the legacy chat id "1" that telegram's
@@ -25,7 +25,7 @@ import {
25
25
  isTelegramChatId,
26
26
  isTerminalChatId,
27
27
  isWhatsAppChatId,
28
- } from "../../util/chat-id.js";
28
+ } from "./chat-id.js";
29
29
 
30
30
  function registerBuiltinFrontends(): void {
31
31
  registerFrontend({
@@ -67,3 +67,33 @@ export function isWhatsAppChatId(chatId: string): boolean {
67
67
  export function isDiscordChatId(chatId: string): boolean {
68
68
  return chatId.startsWith("discord_");
69
69
  }
70
+
71
+ /**
72
+ * Classify a canonical chat ID as a one-to-one chat, a multi-party one,
73
+ * or something this grammar cannot tell apart.
74
+ *
75
+ * The ID is the only signal engine-side code has: a frontend's own
76
+ * `isGroup` flag rides on the inbound message, while stores, jobs and
77
+ * gateway actions are keyed by the string ID alone. Per frontend:
78
+ *
79
+ * - `discord_guild_…` / `discord_dm_…` and `wa_group_…` / `wa_dm_…`
80
+ * say which they are outright;
81
+ * - a Telegram ID is bare digits — negative for a group, supergroup or
82
+ * channel, positive for the user's own ID (a DM);
83
+ * - terminal (`t_…`, `"1"`) and native (`d_…`) chats are local,
84
+ * single-operator surfaces, so they are DMs;
85
+ * - `teams_chat_…` covers 1:1 *and* group Teams chats alike, so it is
86
+ * `"unknown"` — as is anything a future frontend invents.
87
+ *
88
+ * Callers decide what `"unknown"` means for them; anything trust- or
89
+ * privacy-sensitive should treat it as a group and fail closed.
90
+ */
91
+ export function chatScope(chatId: string): "dm" | "group" | "unknown" {
92
+ if (chatId.startsWith("discord_guild_")) return "group";
93
+ if (chatId.startsWith("discord_dm_")) return "dm";
94
+ if (chatId.startsWith("wa_group_")) return "group";
95
+ if (chatId.startsWith("wa_dm_")) return "dm";
96
+ if (isTelegramChatId(chatId)) return chatId.startsWith("-") ? "group" : "dm";
97
+ if (isTerminalChatId(chatId) || isNativeChatId(chatId)) return "dm";
98
+ return "unknown";
99
+ }
@@ -38,7 +38,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
38
38
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
39
39
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
40
40
  import { getPluginMcpServers } from "../plugin/index.js";
41
- import { wrapMcpServer } from "../../util/mcp-launcher.js";
41
+ import { wrapMcpServer } from "./launcher.js";
42
42
  import { log, logError } from "../../util/log.js";
43
43
  import { buildTalonToolServer, VALID_TOOL_FRONTENDS } from "./talon-server.js";
44
44
  import { buildProxyServer } from "./proxy-server.js";
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  import { composeTools } from "../tools/index.js";
19
- import { createBridge, textResult } from "../tools/bridge.js";
19
+ import { createBridge, textResult } from "../tools/ops/bridge.js";
20
20
  import type { ToolFrontend, ToolTag } from "../tools/types.js";
21
21
 
22
22
  export const VALID_TOOL_FRONTENDS: ReadonlySet<string> = new Set([
@@ -352,8 +352,9 @@ export function importMemoryFile(path: string = files.memory): ImportCounts {
352
352
 
353
353
  /**
354
354
  * Import the daily notes: one `episode` per `YYYY-MM-DD.md`, subject the
355
- * date. `diary-*.md` is the soul's first-person writing — a reflection,
356
- * never a fact source (plan §3.1) — so the filename pattern excludes it.
355
+ * date. `diary-*.md` is first-person writing — a reflection, never a fact
356
+ * source (plan §3.1) — so the filename pattern excludes it. The soul kernel
357
+ * that wrote those files is gone, but its output is still on disk.
357
358
  */
358
359
  export function importDailyNotes(dir: string = dirs.dailyMemory): ImportCounts {
359
360
  if (!existsSync(dir)) return emptyCounts();