talon-agent 1.12.0 β†’ 1.34.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.
Files changed (451) hide show
  1. package/README.md +111 -52
  2. package/package.json +39 -12
  3. package/prompts/README.md +112 -0
  4. package/prompts/base.md +12 -12
  5. package/prompts/discord.md +10 -47
  6. package/prompts/heartbeat.md +23 -8
  7. package/prompts/identity.md +12 -22
  8. package/prompts/native.md +23 -0
  9. package/prompts/system/contract-text-or-tools.md +8 -0
  10. package/prompts/system/contract-text-preferred.md +5 -0
  11. package/prompts/system/contract-tool-only.md +10 -0
  12. package/prompts/system/cron.md +3 -0
  13. package/prompts/system/daily-memory.md +3 -0
  14. package/prompts/system/goals.md +7 -0
  15. package/prompts/system/heartbeat-agent.md +44 -0
  16. package/prompts/system/persistent-memory.md +8 -0
  17. package/prompts/system/skills.md +9 -0
  18. package/prompts/system/triggers.md +5 -0
  19. package/prompts/system/workspace.md +9 -0
  20. package/prompts/teams.md +4 -14
  21. package/prompts/telegram.md +17 -89
  22. package/src/app.ts +278 -0
  23. package/src/backend/claude-sdk/constants.ts +22 -10
  24. package/src/backend/claude-sdk/factory.ts +79 -22
  25. package/src/backend/claude-sdk/handler.ts +431 -160
  26. package/src/backend/claude-sdk/index.ts +6 -2
  27. package/src/backend/claude-sdk/mcp-ready.ts +54 -0
  28. package/src/backend/claude-sdk/model-drift.ts +65 -0
  29. package/src/backend/claude-sdk/model-provider.ts +12 -6
  30. package/src/backend/claude-sdk/models/convert.ts +169 -0
  31. package/src/backend/claude-sdk/models/discovery.ts +203 -0
  32. package/src/backend/claude-sdk/models/index.ts +18 -0
  33. package/src/backend/claude-sdk/models/parsing.ts +264 -0
  34. package/src/backend/claude-sdk/models/static.ts +45 -0
  35. package/src/backend/claude-sdk/one-shot.ts +19 -13
  36. package/src/backend/claude-sdk/options.ts +228 -104
  37. package/src/backend/claude-sdk/state.ts +1 -1
  38. package/src/backend/claude-sdk/stream.ts +153 -45
  39. package/src/backend/claude-sdk/warm.ts +12 -5
  40. package/src/backend/codex/auth.ts +216 -40
  41. package/src/backend/codex/constants.ts +63 -22
  42. package/src/backend/codex/discovery.ts +427 -0
  43. package/src/backend/codex/factory.ts +63 -33
  44. package/src/backend/codex/handler/events.ts +239 -0
  45. package/src/backend/codex/handler/index.ts +15 -0
  46. package/src/backend/codex/handler/message.ts +669 -0
  47. package/src/backend/codex/handler/state.ts +14 -0
  48. package/src/backend/codex/handler/usage.ts +68 -0
  49. package/src/backend/codex/index.ts +2 -2
  50. package/src/backend/codex/init.ts +92 -11
  51. package/src/backend/codex/mcp-config.ts +89 -62
  52. package/src/backend/codex/models.ts +305 -54
  53. package/src/backend/codex/oauth-incompat.ts +302 -0
  54. package/src/backend/codex/one-shot.ts +91 -8
  55. package/src/backend/codex/state.ts +54 -3
  56. package/src/backend/codex/token-usage.ts +434 -0
  57. package/src/backend/kilo/factory.ts +60 -35
  58. package/src/backend/kilo/handler/index.ts +13 -0
  59. package/src/backend/kilo/handler/message.ts +317 -0
  60. package/src/backend/kilo/handler/state.ts +14 -0
  61. package/src/backend/kilo/handler/turn.ts +321 -0
  62. package/src/backend/kilo/index.ts +5 -5
  63. package/src/backend/kilo/model-provider.ts +26 -168
  64. package/src/backend/kilo/models/index.ts +52 -0
  65. package/src/backend/kilo/one-shot.ts +20 -142
  66. package/src/backend/kilo/server.ts +14 -21
  67. package/src/backend/openai-agents/builtins.ts +215 -114
  68. package/src/backend/openai-agents/constants.ts +41 -15
  69. package/src/backend/openai-agents/discovery.ts +332 -0
  70. package/src/backend/openai-agents/factory.ts +66 -28
  71. package/src/backend/openai-agents/handler/events.ts +170 -0
  72. package/src/backend/openai-agents/handler/index.ts +13 -0
  73. package/src/backend/openai-agents/handler/message.ts +527 -0
  74. package/src/backend/openai-agents/handler/state.ts +13 -0
  75. package/src/backend/openai-agents/index.ts +22 -4
  76. package/src/backend/openai-agents/init.ts +51 -88
  77. package/src/backend/openai-agents/mcp-pool.ts +252 -0
  78. package/src/backend/openai-agents/models.ts +118 -11
  79. package/src/backend/openai-agents/session.ts +277 -0
  80. package/src/backend/openai-agents/state.ts +82 -11
  81. package/src/backend/opencode/factory.ts +54 -25
  82. package/src/backend/opencode/handler/index.ts +13 -0
  83. package/src/backend/opencode/handler/message.ts +325 -0
  84. package/src/backend/opencode/handler/state.ts +14 -0
  85. package/src/backend/opencode/handler/turn.ts +304 -0
  86. package/src/backend/opencode/index.ts +2 -2
  87. package/src/backend/opencode/model-provider.ts +26 -168
  88. package/src/backend/opencode/models/index.ts +51 -0
  89. package/src/backend/opencode/one-shot.ts +20 -137
  90. package/src/backend/opencode/server.ts +12 -11
  91. package/src/backend/remote-server/client.ts +13 -6
  92. package/src/backend/remote-server/events.ts +59 -9
  93. package/src/backend/remote-server/lifecycle.ts +3 -1
  94. package/src/backend/remote-server/mcp.ts +52 -19
  95. package/src/backend/remote-server/model-catalog/catalog.ts +270 -0
  96. package/src/backend/remote-server/model-catalog/index.ts +100 -0
  97. package/src/backend/remote-server/model-catalog/presentation.ts +298 -0
  98. package/src/backend/remote-server/model-catalog/provider.ts +193 -0
  99. package/src/backend/remote-server/model-catalog/resolve.ts +225 -0
  100. package/src/backend/remote-server/model-catalog/types.ts +136 -0
  101. package/src/backend/remote-server/one-shot.ts +225 -0
  102. package/src/backend/remote-server/session-helpers.ts +3 -4
  103. package/src/backend/remote-server/state.ts +1 -1
  104. package/src/backend/shared/delivery-contract.ts +161 -0
  105. package/src/backend/shared/delivery.ts +49 -0
  106. package/src/backend/shared/flow-violation.ts +38 -7
  107. package/src/backend/shared/frontends.ts +66 -0
  108. package/src/backend/shared/handle-retry.ts +244 -0
  109. package/src/backend/shared/handler-to-events.ts +186 -0
  110. package/src/backend/shared/handler-types.ts +57 -0
  111. package/src/backend/shared/index.ts +51 -5
  112. package/src/backend/shared/metrics.ts +199 -0
  113. package/src/backend/shared/model-retry.ts +1 -1
  114. package/src/backend/shared/prompt-format.ts +69 -0
  115. package/src/backend/shared/session-name.ts +1 -1
  116. package/src/backend/shared/stream-state.ts +36 -2
  117. package/src/backend/shared/system-prompt.ts +200 -23
  118. package/src/bootstrap.ts +338 -38
  119. package/src/cli/chat.ts +49 -0
  120. package/src/cli/config-view.ts +100 -0
  121. package/src/cli/config.ts +105 -0
  122. package/src/cli/context.ts +23 -0
  123. package/src/cli/daemon.ts +95 -0
  124. package/src/cli/doctor.ts +53 -0
  125. package/src/cli/index.ts +139 -0
  126. package/src/cli/logs.ts +60 -0
  127. package/src/cli/menu.ts +104 -0
  128. package/src/cli/setup.ts +455 -0
  129. package/src/cli/status.ts +75 -0
  130. package/src/cli.ts +20 -1211
  131. package/src/core/agent-runtime/README.md +173 -0
  132. package/src/{backend/registry.ts β†’ core/agent-runtime/backend-registry.ts} +15 -6
  133. package/src/core/agent-runtime/capabilities.ts +330 -0
  134. package/src/core/agent-runtime/contract-tests.ts +378 -0
  135. package/src/core/agent-runtime/events.ts +291 -0
  136. package/src/core/agent-runtime/index.ts +62 -0
  137. package/src/core/agent-runtime/model-ref.ts +130 -0
  138. package/src/core/background/cron.ts +469 -0
  139. package/src/core/{dream.ts β†’ background/dream.ts} +118 -53
  140. package/src/core/background/heartbeat/agent.ts +333 -0
  141. package/src/core/background/heartbeat/index.ts +25 -0
  142. package/src/core/background/heartbeat/scheduler.ts +211 -0
  143. package/src/core/background/heartbeat/state.ts +153 -0
  144. package/src/core/background/index.ts +21 -0
  145. package/src/core/background/isolated-agent.ts +115 -0
  146. package/src/core/background/job-health.ts +140 -0
  147. package/src/core/background/job-oneshot.ts +165 -0
  148. package/src/core/background/job-prompt.ts +54 -0
  149. package/src/core/{pulse.ts β†’ background/pulse.ts} +6 -5
  150. package/src/core/background/triggers/command.ts +60 -0
  151. package/src/core/background/triggers/exit.ts +202 -0
  152. package/src/core/background/triggers/index.ts +38 -0
  153. package/src/core/background/triggers/output.ts +142 -0
  154. package/src/core/background/triggers/resume.ts +127 -0
  155. package/src/core/background/triggers/spawn.ts +282 -0
  156. package/src/core/background/triggers/state.ts +52 -0
  157. package/src/core/constants.ts +57 -28
  158. package/src/core/daemon/control.ts +229 -0
  159. package/src/core/daemon/discovery.ts +169 -0
  160. package/src/core/daemon/pidfile.ts +98 -0
  161. package/src/core/doctor.ts +459 -0
  162. package/src/core/engine/backend-controller/holders.ts +17 -0
  163. package/src/core/engine/backend-controller/index.ts +71 -0
  164. package/src/core/engine/backend-controller/legacy.ts +111 -0
  165. package/src/core/engine/backend-controller/pool.ts +296 -0
  166. package/src/core/engine/backend-controller/rebind.ts +205 -0
  167. package/src/core/engine/backend-controller/state.ts +150 -0
  168. package/src/core/engine/backend-controller/types.ts +51 -0
  169. package/src/core/engine/dispatcher.ts +68 -0
  170. package/src/core/engine/gateway-actions/cron.ts +518 -0
  171. package/src/core/engine/gateway-actions/fetch-url.ts +120 -0
  172. package/src/core/engine/gateway-actions/goals.ts +178 -0
  173. package/src/core/engine/gateway-actions/history.ts +53 -0
  174. package/src/core/engine/gateway-actions/index.ts +60 -0
  175. package/src/core/engine/gateway-actions/models.ts +120 -0
  176. package/src/core/engine/gateway-actions/plugins.ts +83 -0
  177. package/src/core/engine/gateway-actions/scripts.ts +127 -0
  178. package/src/core/engine/gateway-actions/shared.ts +66 -0
  179. package/src/core/engine/gateway-actions/skills.ts +120 -0
  180. package/src/core/engine/gateway-actions/triggers.ts +237 -0
  181. package/src/core/engine/gateway-actions/types.ts +18 -0
  182. package/src/core/{gateway.ts β†’ engine/gateway.ts} +223 -60
  183. package/src/core/engine/index.ts +20 -0
  184. package/src/core/engine/model-audit.ts +121 -0
  185. package/src/core/errors.ts +68 -6
  186. package/src/core/mcp-hub/children.ts +254 -0
  187. package/src/core/mcp-hub/index.ts +335 -0
  188. package/src/core/mcp-hub/proxy-server.ts +45 -0
  189. package/src/core/mcp-hub/talon-server.ts +71 -0
  190. package/src/core/memory/retrieval.ts +90 -0
  191. package/src/core/models/active-model.ts +441 -0
  192. package/src/core/{models.ts β†’ models/catalog.ts} +6 -0
  193. package/src/core/models/index.ts +17 -0
  194. package/src/core/models/reasoning-levels.ts +57 -0
  195. package/src/core/plugin/actions.ts +32 -0
  196. package/src/core/plugin/builtins.ts +162 -0
  197. package/src/core/plugin/index.ts +37 -0
  198. package/src/core/plugin/loader.ts +289 -0
  199. package/src/core/plugin/mcp.ts +86 -0
  200. package/src/core/plugin/registry.ts +124 -0
  201. package/src/core/plugin/types.ts +126 -0
  202. package/src/core/prompt/assemble.ts +273 -0
  203. package/src/core/prompt/disk-prompts.ts +41 -0
  204. package/src/core/prompt/embed.ts +107 -0
  205. package/src/core/prompt/embedded-prompts.ts +85 -0
  206. package/src/core/prompt/index.ts +30 -0
  207. package/src/core/prompt/templates.ts +124 -0
  208. package/src/core/prompt/workspace-listing.ts +91 -0
  209. package/src/core/scripting/lua-runner.ts +210 -0
  210. package/src/core/scripts/runner.ts +125 -0
  211. package/src/core/soul/README.md +110 -0
  212. package/src/core/soul/RESEARCH.md +98 -0
  213. package/src/core/soul/associative.ts +98 -0
  214. package/src/core/soul/centrality.ts +98 -0
  215. package/src/core/soul/cluster.ts +83 -0
  216. package/src/core/soul/compiler.ts +207 -0
  217. package/src/core/soul/consolidate.ts +179 -0
  218. package/src/core/soul/critic.ts +159 -0
  219. package/src/core/soul/dag.ts +265 -0
  220. package/src/core/soul/delta.ts +123 -0
  221. package/src/core/soul/drift.ts +99 -0
  222. package/src/core/soul/embedder.ts +129 -0
  223. package/src/core/soul/emergent-critic.ts +96 -0
  224. package/src/core/soul/forgetting.ts +131 -0
  225. package/src/core/soul/governance.ts +93 -0
  226. package/src/core/soul/hash.ts +97 -0
  227. package/src/core/soul/hdc.ts +154 -0
  228. package/src/core/soul/index.ts +140 -0
  229. package/src/core/soul/kernel.ts +540 -0
  230. package/src/core/soul/lattice.ts +103 -0
  231. package/src/core/soul/lens.ts +110 -0
  232. package/src/core/soul/projector.ts +240 -0
  233. package/src/core/soul/reflect.ts +170 -0
  234. package/src/core/soul/reflex.ts +164 -0
  235. package/src/core/soul/retrieve.ts +146 -0
  236. package/src/core/soul/salience.ts +142 -0
  237. package/src/core/soul/service.ts +204 -0
  238. package/src/core/soul/settings.ts +47 -0
  239. package/src/core/soul/signals.ts +117 -0
  240. package/src/core/soul/talon-embedder.ts +80 -0
  241. package/src/core/soul/taps.ts +199 -0
  242. package/src/core/soul/types.ts +298 -0
  243. package/src/core/soul/valence.ts +83 -0
  244. package/src/core/tools/goals.ts +117 -0
  245. package/src/core/tools/index.ts +10 -0
  246. package/src/core/tools/mcp-env.ts +94 -0
  247. package/src/core/tools/mcp-server.ts +27 -4
  248. package/src/core/tools/messaging.ts +122 -29
  249. package/src/core/tools/models.ts +39 -0
  250. package/src/core/tools/scheduling.ts +133 -14
  251. package/src/core/tools/schemas.ts +16 -4
  252. package/src/core/tools/scripts.ts +78 -0
  253. package/src/core/tools/skills.ts +90 -0
  254. package/src/core/tools/stickers.ts +1 -1
  255. package/src/core/tools/triggers.ts +19 -3
  256. package/src/core/tools/types.ts +6 -6
  257. package/src/core/types.ts +62 -112
  258. package/src/core/update/self-update.ts +269 -0
  259. package/src/core/weaver/index.ts +10 -0
  260. package/src/core/weaver/loom.ts +125 -0
  261. package/src/core/weaver/thread-session.ts +51 -0
  262. package/src/core/weaver/thread.ts +196 -0
  263. package/src/core/weaver/weaver.ts +357 -0
  264. package/src/frontend/discord/actions/chat-info.ts +268 -0
  265. package/src/frontend/discord/actions/index.ts +77 -0
  266. package/src/frontend/discord/actions/media.ts +190 -0
  267. package/src/frontend/discord/actions/messaging.ts +277 -0
  268. package/src/frontend/discord/actions/shared.ts +100 -0
  269. package/src/frontend/discord/actions/types.ts +33 -0
  270. package/src/frontend/discord/admin.ts +13 -23
  271. package/src/frontend/discord/callbacks/autocomplete.ts +55 -0
  272. package/src/frontend/discord/{callbacks.ts β†’ callbacks/components.ts} +150 -190
  273. package/src/frontend/discord/callbacks/index.ts +14 -0
  274. package/src/frontend/discord/callbacks/modals.ts +76 -0
  275. package/src/frontend/discord/callbacks/shared.ts +22 -0
  276. package/src/frontend/discord/commands/admin.ts +88 -0
  277. package/src/frontend/discord/commands/definitions.ts +200 -0
  278. package/src/frontend/discord/commands/index.ts +19 -0
  279. package/src/frontend/discord/commands/info.ts +106 -0
  280. package/src/frontend/discord/commands/router.ts +149 -0
  281. package/src/frontend/discord/commands/session.ts +89 -0
  282. package/src/frontend/discord/commands/settings.ts +405 -0
  283. package/src/frontend/discord/commands/shared.ts +50 -0
  284. package/src/frontend/discord/formatting.ts +8 -34
  285. package/src/frontend/discord/handlers/access.ts +248 -0
  286. package/src/frontend/discord/handlers/context.ts +161 -0
  287. package/src/frontend/discord/handlers/delivery.ts +94 -0
  288. package/src/frontend/discord/handlers/index.ts +34 -0
  289. package/src/frontend/discord/handlers/messages.ts +120 -0
  290. package/src/frontend/discord/handlers/queue.ts +135 -0
  291. package/src/frontend/discord/handlers/registry.ts +29 -0
  292. package/src/frontend/discord/handlers/state.ts +94 -0
  293. package/src/frontend/discord/helpers.ts +46 -57
  294. package/src/frontend/discord/index.ts +10 -7
  295. package/src/frontend/discord/middleware.ts +2 -2
  296. package/src/frontend/native/actions.ts +133 -0
  297. package/src/frontend/native/chats.ts +146 -0
  298. package/src/frontend/native/discovery.ts +50 -0
  299. package/src/frontend/native/index.ts +1227 -0
  300. package/src/frontend/native/logs.ts +95 -0
  301. package/src/frontend/native/protocol.ts +253 -0
  302. package/src/frontend/native/server.ts +612 -0
  303. package/src/frontend/native/settings.ts +205 -0
  304. package/src/frontend/native/turn-meta.ts +43 -0
  305. package/src/frontend/shared/format.ts +51 -0
  306. package/src/frontend/shared/reasoning-levels.ts +46 -0
  307. package/src/frontend/shared/session-status.ts +189 -0
  308. package/src/frontend/shared/status-context.ts +125 -0
  309. package/src/frontend/teams/actions.ts +7 -5
  310. package/src/frontend/teams/formatting.ts +7 -18
  311. package/src/frontend/teams/graph.ts +2 -1
  312. package/src/frontend/teams/index.ts +76 -54
  313. package/src/frontend/telegram/actions/chat-info.ts +323 -0
  314. package/src/frontend/telegram/actions/index.ts +68 -0
  315. package/src/frontend/telegram/actions/media.ts +224 -0
  316. package/src/frontend/telegram/actions/messaging.ts +295 -0
  317. package/src/frontend/telegram/actions/shared.ts +58 -0
  318. package/src/frontend/telegram/actions/types.ts +31 -0
  319. package/src/frontend/telegram/admin.ts +12 -22
  320. package/src/frontend/telegram/callbacks/effort.ts +68 -0
  321. package/src/frontend/telegram/callbacks/index.ts +68 -0
  322. package/src/frontend/telegram/callbacks/model.ts +311 -0
  323. package/src/frontend/telegram/callbacks/pulse.ts +50 -0
  324. package/src/frontend/telegram/callbacks/settings.ts +177 -0
  325. package/src/frontend/telegram/callbacks/shared.ts +71 -0
  326. package/src/frontend/telegram/commands/admin.ts +226 -0
  327. package/src/frontend/telegram/commands/definitions.ts +60 -0
  328. package/src/frontend/telegram/commands/index.ts +39 -0
  329. package/src/frontend/telegram/commands/info.ts +125 -0
  330. package/src/frontend/telegram/commands/session.ts +77 -0
  331. package/src/frontend/telegram/commands/settings.ts +345 -0
  332. package/src/frontend/telegram/commands/state.ts +34 -0
  333. package/src/frontend/telegram/formatting.ts +14 -25
  334. package/src/frontend/telegram/handlers/access.ts +287 -0
  335. package/src/frontend/telegram/handlers/context.ts +210 -0
  336. package/src/frontend/telegram/handlers/delivery.ts +236 -0
  337. package/src/frontend/telegram/handlers/index.ts +39 -0
  338. package/src/frontend/telegram/handlers/messages.ts +477 -0
  339. package/src/frontend/telegram/handlers/queue.ts +241 -0
  340. package/src/frontend/telegram/handlers/state.ts +75 -0
  341. package/src/frontend/telegram/helpers/diagnostics.ts +188 -0
  342. package/src/frontend/telegram/helpers/format.ts +41 -0
  343. package/src/frontend/telegram/helpers/index.ts +13 -0
  344. package/src/frontend/telegram/{helpers.ts β†’ helpers/menu.ts} +209 -210
  345. package/src/frontend/telegram/index.ts +24 -27
  346. package/src/frontend/telegram/middleware.ts +16 -3
  347. package/src/frontend/telegram/model-callbacks.ts +14 -0
  348. package/src/frontend/telegram/model-menu.ts +317 -0
  349. package/src/frontend/telegram/sticker-library.ts +178 -0
  350. package/src/frontend/telegram/userbot.ts +2 -44
  351. package/src/frontend/terminal/commands.ts +47 -41
  352. package/src/frontend/terminal/index.ts +46 -31
  353. package/src/frontend/terminal/input.ts +5 -0
  354. package/src/frontend/terminal/renderer.ts +4 -2
  355. package/src/index.ts +13 -171
  356. package/src/login.ts +3 -2
  357. package/src/native/blake3-wasm-bytes.ts +7 -0
  358. package/src/native/blake3.ts +234 -0
  359. package/src/native/htmlents-wasm-bytes.ts +7 -0
  360. package/src/native/htmlents.ts +56 -0
  361. package/src/native/prelude.d.mts +168 -0
  362. package/src/native/prelude.mjs +1561 -0
  363. package/src/native/registry.ts +142 -0
  364. package/src/native/runtime.ts +164 -0
  365. package/src/native/scheduler-core/gleam.d.mts +2 -0
  366. package/src/native/scheduler-core/gleam.mjs +1 -0
  367. package/src/native/scheduler-core/scheduler_core.d.mts +76 -0
  368. package/src/native/scheduler-core/scheduler_core.mjs +252 -0
  369. package/src/native/scheduler-core.ts +124 -0
  370. package/src/native/sqlguard-wasm-bytes.ts +7 -0
  371. package/src/native/sqlguard.ts +107 -0
  372. package/src/native/strsim-wasm-bytes.ts +7 -0
  373. package/src/native/strsim.ts +98 -0
  374. package/src/native/textops-wasm-bytes.ts +7 -0
  375. package/src/native/textops.ts +83 -0
  376. package/src/native/warden.ts +287 -0
  377. package/src/plugins/github/index.ts +1 -1
  378. package/src/plugins/mempalace/index.ts +1 -1
  379. package/src/plugins/playwright/index.ts +1 -1
  380. package/src/storage/chat-settings.ts +335 -85
  381. package/src/storage/cron-store.ts +266 -109
  382. package/src/storage/daily-log.ts +15 -3
  383. package/src/storage/db.ts +206 -0
  384. package/src/storage/goal-store.ts +186 -0
  385. package/src/storage/history.ts +137 -188
  386. package/src/storage/kv.ts +52 -0
  387. package/src/storage/legacy-import.ts +47 -0
  388. package/src/storage/media-index.ts +118 -61
  389. package/src/storage/repositories/chat-settings-repo.ts +60 -0
  390. package/src/storage/repositories/cron-repo.ts +128 -0
  391. package/src/storage/repositories/goals-repo.ts +122 -0
  392. package/src/storage/repositories/history-repo.ts +207 -0
  393. package/src/storage/repositories/kv-repo.ts +25 -0
  394. package/src/storage/repositories/media-index-repo.ts +147 -0
  395. package/src/storage/repositories/scripts-repo.ts +81 -0
  396. package/src/storage/repositories/sessions-repo.ts +129 -0
  397. package/src/storage/repositories/triggers-repo.ts +158 -0
  398. package/src/storage/repositories/turn-meta-repo.ts +32 -0
  399. package/src/storage/scheduled-store.ts +84 -0
  400. package/src/storage/script-store.ts +198 -0
  401. package/src/storage/sessions.ts +227 -102
  402. package/src/storage/skill-store.ts +325 -0
  403. package/src/storage/sql/README.md +18 -0
  404. package/src/storage/sql/chat-settings.sql +11 -0
  405. package/src/storage/sql/cron.sql +40 -0
  406. package/src/storage/sql/db.sql +11 -0
  407. package/src/storage/sql/embed.ts +118 -0
  408. package/src/storage/sql/goals.sql +41 -0
  409. package/src/storage/sql/history.sql +83 -0
  410. package/src/storage/sql/kv.sql +11 -0
  411. package/src/storage/sql/media-index.sql +45 -0
  412. package/src/storage/sql/schema.sql +233 -0
  413. package/src/storage/sql/scripts.sql +28 -0
  414. package/src/storage/sql/sessions.sql +22 -0
  415. package/src/storage/sql/statements.generated.ts +500 -0
  416. package/src/storage/sql/triggers.sql +69 -0
  417. package/src/storage/sql/turn-meta.sql +24 -0
  418. package/src/storage/sticker-store.ts +141 -0
  419. package/src/storage/trigger-store.ts +104 -126
  420. package/src/storage/turn-meta.ts +118 -0
  421. package/src/util/chat-id.ts +30 -0
  422. package/src/util/config.ts +313 -195
  423. package/src/util/fs-path.ts +32 -0
  424. package/src/util/log.ts +53 -31
  425. package/src/util/mcp-launcher.ts +351 -33
  426. package/src/util/paths.ts +45 -17
  427. package/src/util/tail-file.ts +24 -0
  428. package/src/util/workspace.ts +161 -10
  429. package/tsconfig.json +1 -0
  430. package/src/backend/claude-sdk/models.ts +0 -501
  431. package/src/backend/codex/handler.ts +0 -595
  432. package/src/backend/kilo/handler.ts +0 -707
  433. package/src/backend/kilo/models.ts +0 -762
  434. package/src/backend/openai-agents/handler.ts +0 -539
  435. package/src/backend/openai-agents/mcp.ts +0 -139
  436. package/src/backend/opencode/handler.ts +0 -663
  437. package/src/backend/opencode/models.ts +0 -741
  438. package/src/core/cron.ts +0 -184
  439. package/src/core/dispatcher.ts +0 -144
  440. package/src/core/gateway-actions.ts +0 -603
  441. package/src/core/heartbeat.ts +0 -575
  442. package/src/core/plugin.ts +0 -776
  443. package/src/core/triggers.ts +0 -640
  444. package/src/frontend/discord/actions.ts +0 -729
  445. package/src/frontend/discord/commands.ts +0 -1036
  446. package/src/frontend/discord/handlers.ts +0 -798
  447. package/src/frontend/telegram/actions.ts +0 -731
  448. package/src/frontend/telegram/callbacks.ts +0 -419
  449. package/src/frontend/telegram/commands.ts +0 -642
  450. package/src/frontend/telegram/handlers.ts +0 -1352
  451. package/src/util/mcp-launcher.mjs +0 -140
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Message queue β€” debounces rapid-fire messages per chat (concatenating them
3
+ * into a single agent turn) with one transient retry, then best-effort error
4
+ * notification.
5
+ */
6
+
7
+ import { classify, friendlyMessage } from "../../../core/errors.js";
8
+ import { recordMessageProcessed, recordError } from "../../../util/watchdog.js";
9
+ import { appendDailyLog } from "../../../storage/daily-log.js";
10
+ import { log, logError } from "../../../util/log.js";
11
+ import { processAndReply } from "./delivery.js";
12
+ import { sendChunked } from "./context.js";
13
+ import {
14
+ messageQueues,
15
+ DEBOUNCE_MS,
16
+ MAX_QUEUED_PER_CHAT,
17
+ type QueuedMessage,
18
+ } from "./state.js";
19
+ import type { TalonConfig } from "../../../util/config.js";
20
+
21
+ export function enqueueMessage(
22
+ config: TalonConfig,
23
+ chatId: string,
24
+ numericChatId: number,
25
+ msg: QueuedMessage,
26
+ ): void {
27
+ const existing = messageQueues.get(chatId);
28
+ if (existing) {
29
+ if (existing.messages.length >= MAX_QUEUED_PER_CHAT) return;
30
+ existing.messages.push(msg);
31
+ clearTimeout(existing.timer);
32
+ existing.timer = setTimeout(() => flushQueue(chatId), DEBOUNCE_MS);
33
+ return;
34
+ }
35
+ messageQueues.set(chatId, {
36
+ messages: [msg],
37
+ timer: setTimeout(() => flushQueue(chatId), DEBOUNCE_MS),
38
+ config,
39
+ chatId,
40
+ numericChatId,
41
+ });
42
+ }
43
+
44
+ async function flushQueue(chatId: string): Promise<void> {
45
+ const entry = messageQueues.get(chatId);
46
+ if (!entry) return;
47
+ messageQueues.delete(chatId);
48
+
49
+ const { messages, config, numericChatId } = entry;
50
+ const last = messages[messages.length - 1];
51
+ const combinedPrompt =
52
+ messages.length === 1
53
+ ? messages[0].prompt
54
+ : messages.map((m) => m.prompt).join("\n\n");
55
+
56
+ appendDailyLog(last.senderName, combinedPrompt, {
57
+ chatTitle: last.chatTitle,
58
+ username: last.senderUsername,
59
+ });
60
+
61
+ const runTurn = () =>
62
+ processAndReply({
63
+ config,
64
+ chatId,
65
+ numericChatId,
66
+ replyToId: last.replyToId,
67
+ messageId: last.messageId,
68
+ numericMessageId: last.numericMessageId,
69
+ prompt: combinedPrompt,
70
+ senderName: last.senderName,
71
+ isGroup: last.isGroup,
72
+ senderUsername: last.senderUsername,
73
+ senderId: last.senderId,
74
+ channel: last.channel,
75
+ chatTitle: last.chatTitle,
76
+ });
77
+
78
+ try {
79
+ await runTurn();
80
+ recordMessageProcessed();
81
+ } catch (err) {
82
+ const classified = classify(err);
83
+ const promptPreview = combinedPrompt.slice(0, 100).replace(/\n/g, " ");
84
+ logError(
85
+ "bot",
86
+ `[${chatId}] [${last.isGroup ? "guild" : "DM"}] [${last.senderName}] ${classified.reason}: ${classified.message} | prompt: "${promptPreview}"`,
87
+ );
88
+ recordError(classified.message);
89
+
90
+ if (classified.retryable) {
91
+ const delayMs = classified.retryAfterMs ?? 2000;
92
+ log(
93
+ "bot",
94
+ `[${chatId}] Retrying after ${classified.reason} (${delayMs}ms)...`,
95
+ );
96
+ try {
97
+ await new Promise((r) => setTimeout(r, delayMs));
98
+ await runTurn();
99
+ recordMessageProcessed();
100
+ return;
101
+ } catch (retryErr) {
102
+ const retryClassified = classify(retryErr);
103
+ logError("bot", `[${chatId}] Retry failed: ${retryClassified.message}`);
104
+ // Error-recovery send must never throw β€” if the network is fully down
105
+ // the queue handler would otherwise propagate up and stall future
106
+ // messages. Best-effort: notify if we can, log + move on otherwise.
107
+ try {
108
+ await sendChunked(
109
+ last.channel,
110
+ friendlyMessage(retryClassified),
111
+ last.replyToId,
112
+ );
113
+ } catch (notifyErr) {
114
+ logError(
115
+ "bot",
116
+ `[${chatId}] Could not notify user about retry failure: ${notifyErr instanceof Error ? notifyErr.message : notifyErr}`,
117
+ );
118
+ }
119
+ return;
120
+ }
121
+ }
122
+ try {
123
+ await sendChunked(
124
+ last.channel,
125
+ friendlyMessage(classified),
126
+ last.replyToId,
127
+ );
128
+ } catch (notifyErr) {
129
+ logError(
130
+ "bot",
131
+ `[${chatId}] Could not notify user about error: ${notifyErr instanceof Error ? notifyErr.message : notifyErr}`,
132
+ );
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Chat-registry accessors β€” map between numeric/string chat ids and the
3
+ * Discord channel info the action handler needs to post back.
4
+ */
5
+
6
+ import {
7
+ chatRegistry,
8
+ chatRegistryByString,
9
+ type DiscordChatInfo,
10
+ } from "./state.js";
11
+
12
+ export type { DiscordChatInfo } from "./state.js";
13
+
14
+ export function registerDiscordChat(info: DiscordChatInfo): void {
15
+ chatRegistry.set(info.numericChatId, info);
16
+ chatRegistryByString.set(info.chatId, info);
17
+ }
18
+
19
+ export function lookupDiscordChat(
20
+ numericChatId: number,
21
+ ): DiscordChatInfo | undefined {
22
+ return chatRegistry.get(numericChatId);
23
+ }
24
+
25
+ export function lookupDiscordChatByString(
26
+ chatId: string,
27
+ ): DiscordChatInfo | undefined {
28
+ return chatRegistryByString.get(chatId);
29
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shared module-level state for the Discord message-handling pipeline:
3
+ * the chat registry, access config, DM-user tracking, unauthorized cooldown,
4
+ * per-user rate-limit windows, and the debounce queue. Created once here and
5
+ * imported by the handler submodules so all state stays coherent across files.
6
+ */
7
+
8
+ import type { TextBasedChannel } from "discord.js";
9
+ import type { TalonConfig } from "../../../util/config.js";
10
+
11
+ // ── Chat registry: numericChatId β†’ Discord channel info ─────────────────────
12
+ // The gateway/dispatcher uses numeric chatIds. Action handlers need to map
13
+ // back to the Discord channel they should post to.
14
+
15
+ export type DiscordChatInfo = {
16
+ channelId: string;
17
+ guildId: string | null;
18
+ userId: string | null; // for DM contexts
19
+ numericChatId: number;
20
+ chatId: string;
21
+ };
22
+
23
+ export const chatRegistry = new Map<number, DiscordChatInfo>();
24
+ export const chatRegistryByString = new Map<string, DiscordChatInfo>();
25
+
26
+ // ── Access control state ─────────────────────────────────────────────────────
27
+
28
+ export type AccessConfig = {
29
+ allowedUsers: Set<string>;
30
+ allowedGuilds: Set<string>;
31
+ allowedChannels: Set<string>; // empty = all channels in allowedGuilds
32
+ adminUserIds: Set<string>;
33
+ respondMode: "mention" | "channel";
34
+ };
35
+
36
+ /**
37
+ * Reassignable access config on a holder object so `setAccessControl` can
38
+ * swap it and every reader sees the update through the import.
39
+ */
40
+ export const accessState: { access: AccessConfig } = {
41
+ access: {
42
+ allowedUsers: new Set(),
43
+ allowedGuilds: new Set(),
44
+ allowedChannels: new Set(),
45
+ adminUserIds: new Set(),
46
+ respondMode: "mention",
47
+ },
48
+ };
49
+
50
+ // ── First-time DM user tracking ──────────────────────────────────────────────
51
+
52
+ export const knownDmUsers = new Set<string>();
53
+ export const KNOWN_DM_USERS_CAP = 10_000;
54
+
55
+ // ── Unauthorized notification cooldown (10 min) ──────────────────────────────
56
+
57
+ export const unauthorizedCooldown = new Map<string, number>();
58
+ export const UNAUTHORIZED_COOLDOWN_MS = 10 * 60 * 1000;
59
+ export const MAX_UNAUTHORIZED_COOLDOWNS = 5000;
60
+
61
+ // ── Per-user rate limiting ───────────────────────────────────────────────────
62
+
63
+ export const userMessageTimestamps = new Map<string, number[]>();
64
+ export const RATE_LIMIT_WINDOW_MS = 60_000;
65
+ export const RATE_LIMIT_MAX_MESSAGES = 15;
66
+
67
+ // ── Message queue (debounce rapid-fire messages per chat) ────────────────────
68
+
69
+ export type QueuedMessage = {
70
+ prompt: string;
71
+ replyToId: string;
72
+ messageId: string;
73
+ numericMessageId: number;
74
+ senderName: string;
75
+ senderUsername?: string;
76
+ senderId: string;
77
+ isGroup: boolean;
78
+ channel: TextBasedChannel;
79
+ chatTitle?: string;
80
+ };
81
+
82
+ export const messageQueues = new Map<
83
+ string,
84
+ {
85
+ messages: QueuedMessage[];
86
+ timer: ReturnType<typeof setTimeout>;
87
+ config: TalonConfig;
88
+ chatId: string;
89
+ numericChatId: number;
90
+ }
91
+ >();
92
+
93
+ export const DEBOUNCE_MS = 500;
94
+ export const MAX_QUEUED_PER_CHAT = 20;
@@ -7,21 +7,28 @@
7
7
  * - chat IDs are Discord snowflakes (strings), not numbers.
8
8
  */
9
9
 
10
- import { resolveModel } from "../../core/models.js";
10
+ import { REASONING_LEVEL_DESCRIPTIONS } from "../../core/models/reasoning-levels.js";
11
11
  import { DISCORD_MAX_TEXT, DISCORD_SAFE_RESERVE } from "./formatting.js";
12
+ import {
13
+ DEFAULT_PULSE_INTERVAL_MS,
14
+ formatDuration,
15
+ formatModelLabel,
16
+ } from "../shared/format.js";
17
+
18
+ export {
19
+ parseInterval,
20
+ formatDuration,
21
+ formatTokenCount,
22
+ formatBytes,
23
+ formatModelLabel,
24
+ } from "../shared/format.js";
12
25
 
13
- const DEFAULT_PULSE_INTERVAL_MS = 5 * 60 * 1000;
14
26
  /** Per-message length budget for metrics output. */
15
27
  const DEFAULT_METRICS_MESSAGE_MAX = DISCORD_MAX_TEXT - DISCORD_SAFE_RESERVE;
16
28
 
17
29
  /** Effort level descriptions shown next to each option in select menus. */
18
30
  export const EFFORT_DESCRIPTIONS: Record<string, string> = {
19
- off: "no extra thinking β€” fastest",
20
- low: "short reasoning pass",
21
- medium: "balanced reasoning",
22
- high: "deeper reasoning, slower",
23
- max: "most thorough β€” slowest",
24
- adaptive: "model decides when to think",
31
+ ...REASONING_LEVEL_DESCRIPTIONS,
25
32
  };
26
33
 
27
34
  type MetricsSnapshot = {
@@ -32,45 +39,6 @@ type MetricsSnapshot = {
32
39
  >;
33
40
  };
34
41
 
35
- /** Parse a duration string like "30m", "2h", "1d", "1h30m", "1d6h" into ms. */
36
- export function parseInterval(input: string): number | null {
37
- const match = input.match(/^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
38
- if (!match || (!match[1] && !match[2] && !match[3])) return null;
39
- const days = parseInt(match[1] || "0", 10);
40
- const hours = parseInt(match[2] || "0", 10);
41
- const minutes = parseInt(match[3] || "0", 10);
42
- const ms = (days * 24 * 60 + hours * 60 + minutes) * 60 * 1000;
43
- return ms > 0 ? ms : null;
44
- }
45
-
46
- export function formatDuration(ms: number): string {
47
- const safeMs = Math.max(0, Math.round(ms));
48
- if (safeMs < 1000) return `${safeMs}ms`;
49
- const s = Math.floor(safeMs / 1000);
50
- if (s < 60) return `${s}s`;
51
- const m = Math.floor(s / 60);
52
- if (m < 60) return `${m}m ${s % 60}s`;
53
- const h = Math.floor(m / 60);
54
- return `${h}h ${m % 60}m`;
55
- }
56
-
57
- export function formatTokenCount(n: number): string {
58
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
59
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
60
- return String(n);
61
- }
62
-
63
- export function formatBytes(bytes: number): string {
64
- if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
65
- if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;
66
- if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KB`;
67
- return `${bytes} B`;
68
- }
69
-
70
- export function formatModelLabel(modelId: string): string {
71
- return resolveModel(modelId)?.displayName ?? modelId;
72
- }
73
-
74
42
  function truncateMetricLabel(label: string, max = 60): string {
75
43
  return label.length <= max ? label : `${label.slice(0, max - 3)}...`;
76
44
  }
@@ -87,18 +55,30 @@ export function renderMetricsMessages(
87
55
  const continuationHeader = "**πŸ“Š Metrics (cont.)**";
88
56
  const sections: string[][] = [];
89
57
 
58
+ // Histograms come in two flavours: durations (keys ending in `_ms`,
59
+ // rendered as human times) and plain counts like `tool_calls_per_turn`
60
+ // (rendered as bare numbers β€” "p50=1ms" for a count is nonsense).
90
61
  const histKeys = Object.keys(metrics.histograms).sort();
91
- if (histKeys.length > 0) {
62
+ const durationKeys = histKeys.filter((key) => key.endsWith("_ms"));
63
+ const countKeys = histKeys.filter((key) => !key.endsWith("_ms"));
64
+ const histLine = (key: string, fmt: (v: number) => string): string => {
65
+ const h = metrics.histograms[key];
66
+ return (
67
+ ` \`${truncateMetricLabel(key)}\` n=${h.count} ` +
68
+ `p50=${fmt(h.p50)} p95=${fmt(h.p95)} ` +
69
+ `p99=${fmt(h.p99)} avg=${fmt(h.avg)}`
70
+ );
71
+ };
72
+ if (durationKeys.length > 0) {
92
73
  sections.push([
93
74
  "**Latency**",
94
- ...histKeys.map((key) => {
95
- const h = metrics.histograms[key];
96
- return (
97
- ` \`${truncateMetricLabel(key)}\` n=${h.count} ` +
98
- `p50=${formatDuration(h.p50)} p95=${formatDuration(h.p95)} ` +
99
- `p99=${formatDuration(h.p99)} avg=${formatDuration(h.avg)}`
100
- );
101
- }),
75
+ ...durationKeys.map((key) => histLine(key, formatDuration)),
76
+ ]);
77
+ }
78
+ if (countKeys.length > 0) {
79
+ sections.push([
80
+ "**Distributions**",
81
+ ...countKeys.map((key) => histLine(key, String)),
102
82
  ]);
103
83
  }
104
84
 
@@ -111,7 +91,16 @@ export function renderMetricsMessages(
111
91
  groups.get(prefix)!.push(key);
112
92
  }
113
93
  for (const prefix of [...groups.keys()].sort()) {
114
- const keys = groups.get(prefix)!;
94
+ // tool_calls reads best as a leaderboard β€” busiest tools first.
95
+ // Other groups keep alphabetical order (stable lookup by name).
96
+ const keys =
97
+ prefix === "tool_calls"
98
+ ? [...groups.get(prefix)!].sort(
99
+ (a, b) =>
100
+ metrics.counters[b]! - metrics.counters[a]! ||
101
+ a.localeCompare(b),
102
+ )
103
+ : groups.get(prefix)!;
115
104
  sections.push([
116
105
  `**${prefix}**`,
117
106
  ...keys.map((key) => {
@@ -23,20 +23,20 @@ import {
23
23
  import { once } from "node:events";
24
24
  import type { TalonConfig } from "../../util/config.js";
25
25
  import type { ContextManager } from "../../core/types.js";
26
- import type { Gateway } from "../../core/gateway.js";
26
+ import type { Gateway } from "../../core/engine/gateway.js";
27
27
  import { log, logError, logWarn } from "../../util/log.js";
28
- import { createDiscordActionHandler } from "./actions.js";
28
+ import { createDiscordActionHandler } from "./actions/index.js";
29
29
  import {
30
30
  registerCommandsForGuilds,
31
31
  registerInteractionRouter,
32
- } from "./commands.js";
32
+ } from "./commands/index.js";
33
33
  import { registerMiddleware } from "./middleware.js";
34
34
  import {
35
35
  setAccessControl,
36
36
  lookupDiscordChat,
37
37
  registerDiscordChat,
38
- } from "./handlers.js";
39
- import { sendChunked } from "./handlers.js";
38
+ } from "./handlers/index.js";
39
+ import { sendChunked } from "./handlers/index.js";
40
40
  import { deriveNumericChatId } from "../../util/chat-id.js";
41
41
 
42
42
  // ── Frontend type ───────────────────────────────────────────────────────────
@@ -89,7 +89,7 @@ export function createDiscordFrontend(
89
89
 
90
90
  const context: ContextManager = {
91
91
  acquire: (chatId: number, stringId?: string) =>
92
- gateway.setContext(chatId, stringId),
92
+ gateway.setContext(chatId, stringId, "discord"),
93
93
  release: (chatId: number) => gateway.clearContext(chatId),
94
94
  getMessageCount: (chatId: number) => gateway.getMessageCount(chatId),
95
95
  };
@@ -147,7 +147,10 @@ export function createDiscordFrontend(
147
147
 
148
148
  async init() {
149
149
  // Register Discord action handler with the gateway
150
- gateway.setFrontendHandler(createDiscordActionHandler(client, gateway));
150
+ gateway.registerFrontendHandler(
151
+ "discord",
152
+ createDiscordActionHandler(client, gateway),
153
+ );
151
154
 
152
155
  const port = await gateway.start(19876);
153
156
  log("discord", `Gateway started on port ${port}`);
@@ -18,9 +18,9 @@ import type { Client, Message } from "discord.js";
18
18
  import { ChannelType } from "discord.js";
19
19
  import type { TalonConfig } from "../../util/config.js";
20
20
  import { pushMessage } from "../../storage/history.js";
21
- import { registerChat } from "../../core/pulse.js";
21
+ import { registerChat } from "../../core/background/pulse.js";
22
22
  import { deriveNumericChatId } from "../../util/chat-id.js";
23
- import { handleMessage, getSenderName } from "./handlers.js";
23
+ import { handleMessage, getSenderName } from "./handlers/index.js";
24
24
 
25
25
  export function registerMiddleware(client: Client, config: TalonConfig): void {
26
26
  client.on("messageCreate", (msg: Message) => {
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Native action handler β€” the gateway-side bridge for the agent's delivery
3
+ * tools. When the model calls `end_turn` / `send_message` / `react` / edit /
4
+ * delete, the MCP tool POSTs an action to the gateway, which routes it here
5
+ * by the active chat's numeric id. We translate each action into a Bridge
6
+ * event the connected clients render, persist assistant messages to history,
7
+ * and count deliveries on the gateway (so the dispatcher's trailing-prose
8
+ * fallback knows the turn already produced output).
9
+ */
10
+
11
+ import type { FrontendActionHandler, ActionResult } from "../../core/types.js";
12
+ import type { Gateway } from "../../core/engine/gateway.js";
13
+ import type { NativeChats, ChatEntry } from "./chats.js";
14
+ import type { BridgeEvent, ClientButton } from "./protocol.js";
15
+
16
+ export type NativeActionDeps = {
17
+ chats: NativeChats;
18
+ gateway: Gateway;
19
+ /** Persist + broadcast an assistant message; returns its numeric id. */
20
+ emitAssistant: (
21
+ entry: ChatEntry,
22
+ text: string,
23
+ buttons?: ClientButton[][],
24
+ ) => number;
25
+ /** Persist + broadcast an assistant photo message; returns its numeric id. */
26
+ emitPhoto: (entry: ChatEntry, filePath: string, caption?: string) => number;
27
+ broadcast: (event: BridgeEvent) => void;
28
+ };
29
+
30
+ /** Coerce gateway button rows (`[[{text,url,callback_data}]]`) to wire buttons. */
31
+ function toButtons(rows: unknown): ClientButton[][] | undefined {
32
+ if (!Array.isArray(rows)) return undefined;
33
+ const mapped: ClientButton[][] = [];
34
+ for (const row of rows) {
35
+ if (!Array.isArray(row)) continue;
36
+ const cells: ClientButton[] = [];
37
+ for (const cell of row) {
38
+ if (cell && typeof cell === "object") {
39
+ const c = cell as Record<string, unknown>;
40
+ const text = typeof c.text === "string" ? c.text : "";
41
+ if (!text) continue;
42
+ cells.push({
43
+ text,
44
+ url: typeof c.url === "string" ? c.url : undefined,
45
+ data:
46
+ typeof c.callback_data === "string" ? c.callback_data : undefined,
47
+ });
48
+ }
49
+ }
50
+ if (cells.length) mapped.push(cells);
51
+ }
52
+ return mapped.length ? mapped : undefined;
53
+ }
54
+
55
+ export function createNativeActionHandler(
56
+ deps: NativeActionDeps,
57
+ ): FrontendActionHandler {
58
+ const { chats, gateway, emitAssistant, emitPhoto, broadcast } = deps;
59
+
60
+ return async (body, chatId): Promise<ActionResult | null> => {
61
+ const action = typeof body.action === "string" ? body.action : "";
62
+ const entry = chats.byNumeric(chatId);
63
+ if (!entry) return { ok: false, error: "No active native chat" };
64
+
65
+ switch (action) {
66
+ case "send_message": {
67
+ const text = String(body.text ?? "");
68
+ const id = emitAssistant(entry, text);
69
+ gateway.incrementMessages(chatId);
70
+ return { ok: true, message_id: id };
71
+ }
72
+
73
+ case "send_message_with_buttons": {
74
+ const text = String(body.text ?? "");
75
+ const id = emitAssistant(entry, text, toButtons(body.rows));
76
+ gateway.incrementMessages(chatId);
77
+ return { ok: true, message_id: id };
78
+ }
79
+
80
+ case "send_photo": {
81
+ const filePath = String(body.file_path ?? "");
82
+ if (!filePath) return { ok: false, error: "file_path required" };
83
+ const caption =
84
+ typeof body.caption === "string" ? body.caption : undefined;
85
+ const id = emitPhoto(entry, filePath, caption);
86
+ gateway.incrementMessages(chatId);
87
+ return { ok: true, message_id: id };
88
+ }
89
+
90
+ case "react": {
91
+ const messageId = String(body.message_id ?? "");
92
+ const emoji = String(body.emoji ?? "πŸ‘");
93
+ broadcast({ kind: "reaction", chatId: entry.id, messageId, emoji });
94
+ gateway.incrementMessages(chatId);
95
+ return { ok: true };
96
+ }
97
+
98
+ case "edit_message": {
99
+ const messageId = String(body.message_id ?? "");
100
+ const text = String(body.text ?? "");
101
+ broadcast({
102
+ kind: "message_edited",
103
+ chatId: entry.id,
104
+ messageId,
105
+ text,
106
+ });
107
+ return { ok: true };
108
+ }
109
+
110
+ case "delete_message": {
111
+ const messageId = String(body.message_id ?? "");
112
+ broadcast({ kind: "message_deleted", chatId: entry.id, messageId });
113
+ return { ok: true };
114
+ }
115
+
116
+ case "get_chat_info":
117
+ return {
118
+ ok: true,
119
+ id: entry.numericId,
120
+ type: "private",
121
+ title: entry.title,
122
+ };
123
+
124
+ // Typing is driven by the dispatcher via sendTyping β†’ no-op ack here.
125
+ case "send_chat_action":
126
+ return { ok: true };
127
+
128
+ default:
129
+ // Let the gateway try plugin + shared actions (history, web, cron…).
130
+ return null;
131
+ }
132
+ };
133
+ }