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,224 @@
1
+ /**
2
+ * Media actions — sending files/photos/videos/animations/voice/audio,
3
+ * stickers, polls, locations, contacts, and dice.
4
+ */
5
+
6
+ import { existsSync, readFileSync, statSync } from "node:fs";
7
+ import { basename } from "node:path";
8
+ import { expandFsPath } from "../../../util/fs-path.js";
9
+ import { markdownToTelegramHtml } from "../formatting.js";
10
+ import { withRetry } from "../../../core/engine/gateway.js";
11
+ import { resolveStickerByEmoji } from "../sticker-library.js";
12
+ import { replyParams } from "./shared.js";
13
+ import type { TelegramActionHandlers } from "./types.js";
14
+
15
+ const sendMediaFile: TelegramActionHandlers[string] = async (
16
+ body,
17
+ chatId,
18
+ { bot, InputFileClass, gateway },
19
+ ) => {
20
+ // Routed for send_file / send_photo / send_video / send_animation /
21
+ // send_voice / send_audio — branch on the action name in the body.
22
+ const action = String(body.action);
23
+ const caption = body.caption
24
+ ? markdownToTelegramHtml(String(body.caption))
25
+ : undefined;
26
+ const captionParseMode = caption ? ("HTML" as const) : undefined;
27
+ gateway.incrementMessages(chatId);
28
+ // Three media sources, in priority order: a public URL or a Telegram
29
+ // file_id (both passed to the Bot API as a plain string — Telegram
30
+ // fetches/reuses server-side, no local bytes involved), else a
31
+ // workspace file path uploaded as multipart.
32
+ const remote = body.url ?? body.file_id;
33
+ let file: string | import("grammy").InputFile;
34
+ if (remote) {
35
+ file = String(remote);
36
+ } else {
37
+ // Fail with guidance the model can act on, not a raw ENOENT: a
38
+ // mistyped path is the most common media-send error, and naming
39
+ // the alternatives (url / file_id) teaches the recovery path.
40
+ if (!body.file_path)
41
+ return {
42
+ ok: false,
43
+ error: `${action}: provide file_path (workspace file), url (public), or file_id (seen in chat)`,
44
+ };
45
+ const filePath = expandFsPath(String(body.file_path));
46
+ if (!existsSync(filePath))
47
+ return {
48
+ ok: false,
49
+ error: `File not found: ${filePath} — check the workspace path, or send by url/file_id instead`,
50
+ };
51
+ const stat = statSync(filePath);
52
+ if (stat.size > 49 * 1024 * 1024)
53
+ return { ok: false, error: "File too large (max 49MB)" };
54
+ const data = readFileSync(filePath);
55
+ file = new InputFileClass(data, basename(filePath));
56
+ }
57
+ const rp = replyParams(body);
58
+ let sent;
59
+ switch (action) {
60
+ case "send_file":
61
+ sent = await withRetry(() =>
62
+ bot.api.sendDocument(chatId, file, {
63
+ caption,
64
+ parse_mode: captionParseMode,
65
+ reply_parameters: rp,
66
+ }),
67
+ );
68
+ break;
69
+ case "send_photo":
70
+ sent = await withRetry(() =>
71
+ bot.api.sendPhoto(chatId, file, {
72
+ caption,
73
+ parse_mode: captionParseMode,
74
+ reply_parameters: rp,
75
+ }),
76
+ );
77
+ break;
78
+ case "send_video":
79
+ sent = await withRetry(() =>
80
+ bot.api.sendVideo(chatId, file, {
81
+ caption,
82
+ parse_mode: captionParseMode,
83
+ reply_parameters: rp,
84
+ }),
85
+ );
86
+ break;
87
+ case "send_animation":
88
+ sent = await withRetry(() =>
89
+ bot.api.sendAnimation(chatId, file, {
90
+ caption,
91
+ parse_mode: captionParseMode,
92
+ reply_parameters: rp,
93
+ }),
94
+ );
95
+ break;
96
+ case "send_audio":
97
+ sent = await withRetry(() =>
98
+ bot.api.sendAudio(chatId, file, {
99
+ caption,
100
+ parse_mode: captionParseMode,
101
+ reply_parameters: rp,
102
+ title: body.title as string | undefined,
103
+ performer: body.performer as string | undefined,
104
+ }),
105
+ );
106
+ break;
107
+ default:
108
+ sent = await withRetry(() =>
109
+ bot.api.sendVoice(chatId, file, {
110
+ caption,
111
+ parse_mode: captionParseMode,
112
+ reply_parameters: rp,
113
+ }),
114
+ );
115
+ break;
116
+ }
117
+ return { ok: true, message_id: sent.message_id };
118
+ };
119
+
120
+ export const mediaHandlers: TelegramActionHandlers = {
121
+ send_file: sendMediaFile,
122
+ send_photo: sendMediaFile,
123
+ send_video: sendMediaFile,
124
+ send_animation: sendMediaFile,
125
+ send_voice: sendMediaFile,
126
+ send_audio: sendMediaFile,
127
+
128
+ send_sticker: async (body, chatId, { bot, gateway }) => {
129
+ // Three addressing modes: a concrete file_id, a public URL of a
130
+ // .webp (Telegram fetches it server-side, like other media), or an
131
+ // emoji resolved against the saved sticker library (optionally
132
+ // pinned to one pack via set_name) — the low-friction path the
133
+ // prompt teaches.
134
+ let fileId = body.file_id
135
+ ? String(body.file_id)
136
+ : body.url
137
+ ? String(body.url)
138
+ : "";
139
+ if (!fileId && body.emoji) {
140
+ const resolved = await resolveStickerByEmoji(
141
+ bot,
142
+ String(body.emoji),
143
+ body.set_name ? String(body.set_name) : undefined,
144
+ );
145
+ if (!resolved) {
146
+ return {
147
+ ok: false,
148
+ error: `No saved sticker matches ${String(body.emoji)}${body.set_name ? ` in pack "${String(body.set_name)}"` : ""} — check the sticker library, or save a pack with save_sticker_pack.`,
149
+ };
150
+ }
151
+ fileId = resolved.fileId;
152
+ }
153
+ if (!fileId)
154
+ return { ok: false, error: "Required: file_id, url, or emoji" };
155
+ gateway.incrementMessages(chatId);
156
+ const sent = await bot.api.sendSticker(chatId, fileId, {
157
+ reply_parameters: replyParams(body),
158
+ });
159
+ return { ok: true, message_id: sent.message_id };
160
+ },
161
+
162
+ send_poll: async (body, chatId, { bot, gateway }) => {
163
+ gateway.incrementMessages(chatId);
164
+ const sent = await bot.api.sendPoll(
165
+ chatId,
166
+ String(body.question ?? ""),
167
+ ((body.options as string[]) ?? []).map((o) => ({ text: o })),
168
+ {
169
+ is_anonymous: body.is_anonymous as boolean | undefined,
170
+ allows_multiple_answers: body.allows_multiple_answers as
171
+ boolean | undefined,
172
+ type: body.type as "regular" | "quiz" | undefined,
173
+ correct_option_ids:
174
+ body.correct_option_id != null
175
+ ? [body.correct_option_id as number]
176
+ : undefined,
177
+ explanation: body.explanation as string | undefined,
178
+ reply_parameters: replyParams(body),
179
+ },
180
+ );
181
+ return { ok: true, message_id: sent.message_id };
182
+ },
183
+
184
+ send_location: async (body, chatId, { bot, gateway }) => {
185
+ gateway.incrementMessages(chatId);
186
+ const sent = await bot.api.sendLocation(
187
+ chatId,
188
+ Number(body.latitude),
189
+ Number(body.longitude),
190
+ { reply_parameters: replyParams(body) },
191
+ );
192
+ return { ok: true, message_id: sent.message_id };
193
+ },
194
+
195
+ send_contact: async (body, chatId, { bot, gateway }) => {
196
+ gateway.incrementMessages(chatId);
197
+ const sent = await bot.api.sendContact(
198
+ chatId,
199
+ String(body.phone_number),
200
+ String(body.first_name),
201
+ {
202
+ last_name: body.last_name as string | undefined,
203
+ reply_parameters: replyParams(body),
204
+ },
205
+ );
206
+ return { ok: true, message_id: sent.message_id };
207
+ },
208
+
209
+ send_dice: async (body, chatId, { bot, gateway }) => {
210
+ gateway.incrementMessages(chatId);
211
+ const sent = await bot.api.sendDice(
212
+ chatId,
213
+ (body.emoji as string) || "🎲",
214
+ {
215
+ reply_parameters: replyParams(body),
216
+ },
217
+ );
218
+ return {
219
+ ok: true,
220
+ message_id: sent.message_id,
221
+ value: sent.dice?.value,
222
+ };
223
+ },
224
+ };
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Messaging actions — send/reply/edit/delete/pin/forward/copy/react,
3
+ * inline-button messages, chat actions, and scheduled messages.
4
+ */
5
+
6
+ import type { Bot } from "grammy";
7
+ import { markdownToTelegramHtml } from "../formatting.js";
8
+ import { withRetry } from "../../../core/engine/gateway.js";
9
+ import { log, logError, logWarn } from "../../../util/log.js";
10
+ import {
11
+ saveScheduled,
12
+ deleteScheduled,
13
+ listScheduled,
14
+ listScheduledForChat,
15
+ MAX_OVERDUE_MS,
16
+ type ScheduledMessage,
17
+ } from "../../../storage/scheduled-store.js";
18
+ import { sendText, toPositiveId } from "./shared.js";
19
+ import { TELEGRAM_MAX_TEXT, type TelegramActionHandlers } from "./types.js";
20
+
21
+ // ── Scheduled sends (persistent) ─────────────────────────────────────────────
22
+
23
+ /** Longest schedulable delay: 24h. Timers re-arm from the store on boot. */
24
+ const MAX_DELAY_SEC = 24 * 60 * 60;
25
+
26
+ /** Deliver one scheduled entry (shared by the live timer and restore). */
27
+ async function fireScheduled(bot: Bot, entry: ScheduledMessage): Promise<void> {
28
+ const chatId = Number(entry.chatId);
29
+ if (entry.rows) {
30
+ const keyboard = entry.rows.map((row) =>
31
+ row.map((btn) =>
32
+ btn.url
33
+ ? { text: btn.text, url: btn.url }
34
+ : { text: btn.text, callback_data: btn.callback_data ?? btn.text },
35
+ ),
36
+ );
37
+ await bot.api.sendMessage(chatId, markdownToTelegramHtml(entry.text), {
38
+ parse_mode: "HTML",
39
+ reply_markup: { inline_keyboard: keyboard },
40
+ reply_parameters: entry.replyTo
41
+ ? { message_id: entry.replyTo }
42
+ : undefined,
43
+ });
44
+ } else {
45
+ await sendText(bot, chatId, entry.text, entry.replyTo);
46
+ }
47
+ }
48
+
49
+ /** Arm a timer for a stored entry; fires, then cleans up store + map. */
50
+ function armScheduled(
51
+ bot: Bot,
52
+ entry: ScheduledMessage,
53
+ timers: Map<string, ReturnType<typeof setTimeout>>,
54
+ ): void {
55
+ const delayMs = Math.max(0, entry.fireAt - Date.now());
56
+ const timer = setTimeout(async () => {
57
+ try {
58
+ await fireScheduled(bot, entry);
59
+ } catch (err) {
60
+ logError("bot", `Scheduled message failed (chat=${entry.chatId})`, err);
61
+ }
62
+ deleteScheduled(entry.id);
63
+ timers.delete(entry.id);
64
+ }, delayMs);
65
+ timers.set(entry.id, timer);
66
+ }
67
+
68
+ /**
69
+ * Re-arm persisted scheduled sends after a restart. Called once when
70
+ * the action handler is created. Overdue entries fire immediately —
71
+ * late beats never — unless they are stale past MAX_OVERDUE_MS, which
72
+ * drops them (a day-late reminder is noise, not delivery).
73
+ */
74
+ export function restoreScheduledMessages(
75
+ bot: Bot,
76
+ timers: Map<string, ReturnType<typeof setTimeout>>,
77
+ ): void {
78
+ const entries = listScheduled("telegram");
79
+ if (entries.length === 0) return;
80
+ let restored = 0;
81
+ let dropped = 0;
82
+ for (const entry of entries) {
83
+ if (Date.now() - entry.fireAt > MAX_OVERDUE_MS) {
84
+ deleteScheduled(entry.id);
85
+ dropped++;
86
+ continue;
87
+ }
88
+ armScheduled(bot, entry, timers);
89
+ restored++;
90
+ }
91
+ log(
92
+ "bot",
93
+ `Restored ${restored} scheduled message(s) from store${dropped > 0 ? `, dropped ${dropped} stale` : ""}`,
94
+ );
95
+ }
96
+
97
+ export const messagingHandlers: TelegramActionHandlers = {
98
+ send_message: async (body, chatId, { bot, gateway }) => {
99
+ const text = String(body.text ?? "");
100
+ const replyTo = toPositiveId(body.reply_to_message_id);
101
+ gateway.incrementMessages(chatId);
102
+ const msgId = await withRetry(() => sendText(bot, chatId, text, replyTo));
103
+ return { ok: true, message_id: msgId };
104
+ },
105
+
106
+ reply_to: async (body, chatId, { bot, gateway }) => {
107
+ const msgId = Number(body.message_id);
108
+ gateway.incrementMessages(chatId);
109
+ const sentId = await withRetry(() =>
110
+ sendText(bot, chatId, String(body.text ?? ""), msgId),
111
+ );
112
+ return { ok: true, message_id: sentId };
113
+ },
114
+
115
+ react: async (body, chatId, { bot, gateway }) => {
116
+ gateway.incrementMessages(chatId);
117
+ const emoji = String(body.emoji ?? "👍");
118
+ try {
119
+ await withRetry(() =>
120
+ bot.api.setMessageReaction(chatId, Number(body.message_id), [
121
+ { type: "emoji", emoji: emoji as "👍" },
122
+ ]),
123
+ );
124
+ } catch (err) {
125
+ logWarn(
126
+ "bot",
127
+ `Custom emoji reaction failed, falling back to 👍: ${err instanceof Error ? err.message : err}`,
128
+ );
129
+ try {
130
+ await bot.api.setMessageReaction(chatId, Number(body.message_id), [
131
+ { type: "emoji", emoji: "👍" },
132
+ ]);
133
+ } catch (e) {
134
+ return {
135
+ ok: false,
136
+ error: `Reaction failed: ${e instanceof Error ? e.message : e}`,
137
+ };
138
+ }
139
+ }
140
+ return { ok: true };
141
+ },
142
+
143
+ edit_message: async (body, chatId, { bot }) => {
144
+ const text = String(body.text ?? "");
145
+ if (text.length > TELEGRAM_MAX_TEXT)
146
+ return {
147
+ ok: false,
148
+ error: `Text too long (max ${TELEGRAM_MAX_TEXT})`,
149
+ };
150
+ const html = markdownToTelegramHtml(text);
151
+ await withRetry(async () => {
152
+ try {
153
+ await bot.api.editMessageText(chatId, Number(body.message_id), html, {
154
+ parse_mode: "HTML",
155
+ });
156
+ } catch {
157
+ await bot.api.editMessageText(chatId, Number(body.message_id), text);
158
+ }
159
+ });
160
+ return { ok: true };
161
+ },
162
+
163
+ delete_message: async (body, chatId, { bot }) => {
164
+ await bot.api.deleteMessage(chatId, Number(body.message_id));
165
+ return { ok: true };
166
+ },
167
+
168
+ pin_message: async (body, chatId, { bot }) => {
169
+ await bot.api.pinChatMessage(chatId, Number(body.message_id));
170
+ return { ok: true };
171
+ },
172
+
173
+ unpin_message: async (body, chatId, { bot }) => {
174
+ await bot.api.unpinChatMessage(
175
+ chatId,
176
+ body.message_id ? Number(body.message_id) : undefined,
177
+ );
178
+ return { ok: true };
179
+ },
180
+
181
+ forward_message: async (body, chatId, { bot }) => {
182
+ if (body.to_chat_id && Number(body.to_chat_id) !== chatId)
183
+ return { ok: false, error: "Cross-chat forwarding not allowed." };
184
+ const sent = await bot.api.forwardMessage(
185
+ chatId,
186
+ chatId,
187
+ Number(body.message_id),
188
+ );
189
+ return { ok: true, message_id: sent.message_id };
190
+ },
191
+
192
+ copy_message: async (body, chatId, { bot }) => {
193
+ const sent = await bot.api.copyMessage(
194
+ chatId,
195
+ chatId,
196
+ Number(body.message_id),
197
+ );
198
+ return { ok: true, message_id: sent.message_id };
199
+ },
200
+
201
+ send_chat_action: async (body, chatId, { bot }) => {
202
+ await bot.api.sendChatAction(
203
+ chatId,
204
+ String(body.chat_action ?? "typing") as "typing",
205
+ );
206
+ return { ok: true };
207
+ },
208
+
209
+ send_message_with_buttons: async (body, chatId, { bot, gateway }) => {
210
+ const text = String(body.text ?? "");
211
+ if (text.length > TELEGRAM_MAX_TEXT)
212
+ return { ok: false, error: `Text too long` };
213
+ const html = markdownToTelegramHtml(text);
214
+ const rows = body.rows as Array<
215
+ Array<{ text: string; url?: string; callback_data?: string }>
216
+ >;
217
+ gateway.incrementMessages(chatId);
218
+ const keyboard = rows.map((row) =>
219
+ row.map((btn) =>
220
+ btn.url
221
+ ? { text: btn.text, url: btn.url }
222
+ : {
223
+ text: btn.text,
224
+ callback_data: btn.callback_data ?? btn.text,
225
+ },
226
+ ),
227
+ );
228
+ try {
229
+ const sent = await bot.api.sendMessage(chatId, html, {
230
+ parse_mode: "HTML",
231
+ reply_markup: { inline_keyboard: keyboard },
232
+ });
233
+ return { ok: true, message_id: sent.message_id };
234
+ } catch {
235
+ const sent = await bot.api.sendMessage(chatId, text, {
236
+ reply_markup: { inline_keyboard: keyboard },
237
+ });
238
+ return { ok: true, message_id: sent.message_id };
239
+ }
240
+ },
241
+
242
+ schedule_message: (body, chatId, { bot, scheduledMessages }) => {
243
+ const text = String(body.text ?? "");
244
+ const replyTo = toPositiveId(body.reply_to_message_id);
245
+ const rows = body.rows as ScheduledMessage["rows"];
246
+ const delaySec = Math.max(
247
+ 1,
248
+ Math.min(MAX_DELAY_SEC, Number(body.delay_seconds ?? 60)),
249
+ );
250
+ const entry: ScheduledMessage = {
251
+ id: `sched_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
252
+ frontend: "telegram",
253
+ chatId: String(chatId),
254
+ text,
255
+ fireAt: Date.now() + delaySec * 1000,
256
+ createdAt: Date.now(),
257
+ replyTo,
258
+ rows,
259
+ };
260
+ // Persist before arming: if we crash between the two, the restore
261
+ // path delivers it; the reverse order could lose it forever.
262
+ saveScheduled(entry);
263
+ armScheduled(bot, entry, scheduledMessages);
264
+ return { ok: true, schedule_id: entry.id, delay_seconds: delaySec };
265
+ },
266
+
267
+ cancel_scheduled: (body, _chatId, { scheduledMessages }) => {
268
+ const id = String(body.schedule_id ?? "");
269
+ const timer = scheduledMessages.get(id);
270
+ if (timer) {
271
+ clearTimeout(timer);
272
+ scheduledMessages.delete(id);
273
+ }
274
+ // The store is the source of truth — an entry can exist without a
275
+ // live timer (scheduled before a restart, not yet re-armed here).
276
+ const existed = deleteScheduled(id) || Boolean(timer);
277
+ return existed
278
+ ? { ok: true, cancelled: true }
279
+ : { ok: false, error: "Schedule not found" };
280
+ },
281
+
282
+ list_scheduled: (body, chatId) => {
283
+ const pending = listScheduledForChat("telegram", String(chatId)).map(
284
+ (e) => ({
285
+ schedule_id: e.id,
286
+ fires_in_seconds: Math.max(
287
+ 0,
288
+ Math.round((e.fireAt - Date.now()) / 1000),
289
+ ),
290
+ text: e.text.length > 200 ? `${e.text.slice(0, 200)}…` : e.text,
291
+ }),
292
+ );
293
+ return { ok: true, scheduled: pending, count: pending.length };
294
+ },
295
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared helpers for Telegram action handlers: reply-parameter extraction and
3
+ * the HTML-with-plain-fallback text sender.
4
+ */
5
+
6
+ import type { Bot } from "grammy";
7
+ import { markdownToTelegramHtml } from "../formatting.js";
8
+ import { logWarn } from "../../../util/log.js";
9
+ import { TELEGRAM_MAX_TEXT } from "./types.js";
10
+
11
+ export function replyParams(
12
+ body: Record<string, unknown>,
13
+ ): { message_id: number } | undefined {
14
+ const replyTo = toPositiveId(body.reply_to ?? body.reply_to_message_id);
15
+ return replyTo !== undefined ? { message_id: replyTo } : undefined;
16
+ }
17
+
18
+ /**
19
+ * Telegram/GramJS APIs expect numeric IDs. `snowflakeOrIdSchema` normalizes IDs
20
+ * to digit-strings (so 17-19 digit Discord snowflakes survive validation), so a
21
+ * Telegram message/reply/offset ID can arrive here as a string. Coerce it back
22
+ * to a positive integer: Telegram IDs are well within 2^53, so this is lossless.
23
+ * Returns undefined for missing / non-positive / non-integer values.
24
+ */
25
+ export function toPositiveId(v: unknown): number | undefined {
26
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
27
+ return Number.isInteger(n) && n > 0 ? n : undefined;
28
+ }
29
+
30
+ export async function sendText(
31
+ bot: Bot,
32
+ chatId: number,
33
+ text: string,
34
+ replyTo?: number,
35
+ ): Promise<number> {
36
+ if (text.length > TELEGRAM_MAX_TEXT) {
37
+ throw new Error(
38
+ `Message too long (${text.length} chars, max ${TELEGRAM_MAX_TEXT}).`,
39
+ );
40
+ }
41
+ const html = markdownToTelegramHtml(text);
42
+ try {
43
+ const sent = await bot.api.sendMessage(chatId, html, {
44
+ parse_mode: "HTML",
45
+ reply_parameters: replyTo ? { message_id: replyTo } : undefined,
46
+ });
47
+ return sent.message_id;
48
+ } catch (err) {
49
+ logWarn(
50
+ "bot",
51
+ `sendText with parse_mode=HTML failed; retrying without parse_mode (chat=${chatId}): ${err instanceof Error ? err.message : err}`,
52
+ );
53
+ const sent = await bot.api.sendMessage(chatId, text, {
54
+ reply_parameters: replyTo ? { message_id: replyTo } : undefined,
55
+ });
56
+ return sent.message_id;
57
+ }
58
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Telegram action-handler types.
3
+ *
4
+ * Each domain module exports a `TelegramActionHandlers` map keyed by action
5
+ * name. Handlers receive the request body, the numeric chat id, and a shared
6
+ * `TelegramActionContext` carrying the bound bot, the InputFile constructor,
7
+ * the bot token, the gateway, and the per-handler `scheduledMessages` timers.
8
+ */
9
+
10
+ import type { Bot, InputFile as GrammyInputFile } from "grammy";
11
+ import type { Gateway } from "../../../core/engine/gateway.js";
12
+ import type { ActionResult } from "../../../core/types.js";
13
+
14
+ export interface TelegramActionContext {
15
+ bot: Bot;
16
+ InputFileClass: typeof GrammyInputFile;
17
+ botToken: string;
18
+ gateway: Gateway;
19
+ /** Active scheduled-message timers, keyed by schedule id (for cancellation). */
20
+ scheduledMessages: Map<string, ReturnType<typeof setTimeout>>;
21
+ }
22
+
23
+ export type TelegramActionHandler = (
24
+ body: Record<string, unknown>,
25
+ chatId: number,
26
+ ctx: TelegramActionContext,
27
+ ) => Promise<ActionResult | null> | ActionResult | null;
28
+
29
+ export type TelegramActionHandlers = Record<string, TelegramActionHandler>;
30
+
31
+ export const TELEGRAM_MAX_TEXT = 4096;
@@ -6,18 +6,20 @@ import type { Bot, Context } from "grammy";
6
6
  import { readFileSync } from "node:fs";
7
7
  import type { TalonConfig } from "../../util/config.js";
8
8
  import { files, dirs } from "../../util/paths.js";
9
+ import { tailFile } from "../../util/tail-file.js";
9
10
  import { escapeHtml } from "./formatting.js";
10
11
  import { resetSession, getAllSessions } from "../../storage/sessions.js";
11
12
  import { clearHistory } from "../../storage/history.js";
12
13
  import { getChatSettings } from "../../storage/chat-settings.js";
13
14
  import {
14
15
  getAllCronJobs,
15
- validateCronExpression,
16
+ describeSchedule,
17
+ nextRunAt,
16
18
  } from "../../storage/cron-store.js";
17
- import { getActiveCount } from "../../core/dispatcher.js";
18
- import { getPulseStatus } from "../../core/pulse.js";
19
+ import { getActiveCount } from "../../core/engine/dispatcher.js";
20
+ import { getPulseStatus } from "../../core/background/pulse.js";
19
21
  import { getHealthStatus, getRecentErrors } from "../../util/watchdog.js";
20
- import { formatDuration, formatModelLabel } from "./helpers.js";
22
+ import { formatDuration, formatModelLabel } from "./helpers/index.js";
21
23
 
22
24
  export async function handleAdminCommand(
23
25
  ctx: Context,
@@ -91,6 +93,7 @@ export async function handleAdminCommand(
91
93
  try {
92
94
  await bot.api.sendMessage(id, text);
93
95
  sent++;
96
+ await new Promise((r) => setTimeout(r, 40));
94
97
  } catch {
95
98
  failed++;
96
99
  }
@@ -116,20 +119,7 @@ export async function handleAdminCommand(
116
119
  case "logs": {
117
120
  const logPath = files.log;
118
121
  try {
119
- const { statSync, openSync, readSync, closeSync } =
120
- await import("node:fs");
121
- const stat = statSync(logPath);
122
- const size = Math.min(8192, stat.size);
123
- const buf = Buffer.alloc(size);
124
- const fd = openSync(logPath, "r");
125
- readSync(fd, buf, 0, size, Math.max(0, stat.size - size));
126
- closeSync(fd);
127
- const lines = buf
128
- .toString("utf-8")
129
- .trim()
130
- .split("\n")
131
- .slice(-20)
132
- .join("\n");
122
+ const lines = tailFile(logPath);
133
123
  await ctx.reply(`<pre>${escapeHtml(lines.slice(0, 3800))}</pre>`, {
134
124
  parse_mode: "HTML",
135
125
  });
@@ -187,14 +177,14 @@ export async function handleAdminCommand(
187
177
  return;
188
178
  }
189
179
  const lines = jobs.map((j) => {
190
- const v = validateCronExpression(j.schedule, j.timezone);
180
+ const nextMs = nextRunAt(j);
191
181
  const last = j.lastRunAt
192
182
  ? new Date(j.lastRunAt).toISOString().slice(0, 16).replace("T", " ")
193
183
  : "never";
194
- const next = v.next
195
- ? new Date(v.next).toISOString().slice(0, 16).replace("T", " ")
184
+ const next = nextMs
185
+ ? new Date(nextMs).toISOString().slice(0, 16).replace("T", " ")
196
186
  : "?";
197
- return `${j.enabled ? "\u2713" : "\u2717"} <b>${escapeHtml(j.name)}</b>\n <code>${j.schedule}</code> | ${j.type} | runs: ${j.runCount} | last: ${last} | next: ${next}`;
187
+ return `${j.enabled ? "\u2713" : "\u2717"} <b>${escapeHtml(j.name)}</b>\n <code>${escapeHtml(describeSchedule(j))}</code> | ${j.type} | runs: ${j.runCount} | last: ${last} | next: ${next}`;
198
188
  });
199
189
  await ctx.reply(
200
190
  `<b>Cron Jobs (${jobs.length})</b>\n\n` + lines.join("\n\n"),