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,477 @@
1
+ /**
2
+ * Per-message-type handlers — text, photo, document, voice, sticker, video,
3
+ * animation, audio, video-note, and callback queries. Each validates access /
4
+ * rate limits, builds a prompt, and enqueues (or runs the agent directly for
5
+ * callback queries).
6
+ */
7
+
8
+ import type { Bot, Context } from "grammy";
9
+ import type { TalonConfig } from "../../../util/config.js";
10
+ import { escapeHtml } from "../formatting.js";
11
+ import { friendlyMessage } from "../../../core/errors.js";
12
+ import { setMessageFilePath } from "../../../storage/history.js";
13
+ import { addMedia } from "../../../storage/media-index.js";
14
+ import { appendDailyLog } from "../../../storage/daily-log.js";
15
+ import { logError } from "../../../util/log.js";
16
+ import { recordMessageSignal } from "../../../core/soul/taps.js";
17
+ import {
18
+ getSenderName,
19
+ getReplyContext,
20
+ getForwardContext,
21
+ downloadReplyPhoto,
22
+ downloadTelegramFile,
23
+ } from "./context.js";
24
+ import { shouldHandleInGroup, isAccessAllowed } from "./access.js";
25
+ import { enqueueMessage, isUserRateLimited } from "./queue.js";
26
+ import { ensurePackSaved } from "../sticker-library.js";
27
+ import { processAndReply, sendHtml } from "./delivery.js";
28
+
29
+ // ── Shared media handler ──────────────────────────────────────────────────────
30
+
31
+ type MediaDescriptor = {
32
+ /** Human-readable media type for prompt (e.g. "photo", "video", "voice message"). */
33
+ type: string;
34
+ /** File ID to download from Telegram. */
35
+ fileId: string;
36
+ /** File name for saving locally. */
37
+ fileName: string;
38
+ /** Extra prompt lines describing the media. */
39
+ promptLines: string[];
40
+ /** Caption from the message, if any. */
41
+ caption?: string;
42
+ /** Optional file size check (reject if too large). */
43
+ fileSize?: number;
44
+ };
45
+
46
+ /**
47
+ * Shared handler for all downloadable media types (photo, document, voice, video, animation).
48
+ * Extracts forward/reply context, downloads the file, builds a prompt, and enqueues.
49
+ */
50
+ async function handleMediaMessage(
51
+ ctx: Context,
52
+ bot: Bot,
53
+ config: TalonConfig,
54
+ media: MediaDescriptor,
55
+ ): Promise<void> {
56
+ if (!ctx.message || !ctx.chat) return;
57
+ if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
58
+
59
+ const chatId = String(ctx.chat.id);
60
+ const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
61
+ const sender = getSenderName(ctx.from);
62
+ const senderUsername = ctx.from?.username;
63
+
64
+ try {
65
+ // File size check
66
+ if (media.fileSize && media.fileSize > 20 * 1024 * 1024) {
67
+ await sendHtml(
68
+ bot,
69
+ ctx.chat.id,
70
+ "File too large (max 20MB).",
71
+ ctx.message.message_id,
72
+ );
73
+ return;
74
+ }
75
+
76
+ const savedPath = await downloadTelegramFile(
77
+ bot,
78
+ config,
79
+ media.fileId,
80
+ media.fileName,
81
+ );
82
+
83
+ // Store file path in history + media index
84
+ setMessageFilePath(chatId, ctx.message.message_id, savedPath);
85
+ addMedia({
86
+ chatId,
87
+ msgId: ctx.message.message_id,
88
+ senderName: sender,
89
+ type: media.type as
90
+ | "photo"
91
+ | "document"
92
+ | "voice"
93
+ | "video"
94
+ | "animation"
95
+ | "audio"
96
+ | "sticker",
97
+ filePath: savedPath,
98
+ caption: media.caption,
99
+ timestamp: Date.now(),
100
+ });
101
+
102
+ const fwdCtx = getForwardContext(
103
+ ctx.message as Parameters<typeof getForwardContext>[0],
104
+ );
105
+ const replyCtx = getReplyContext(
106
+ ctx.message.reply_to_message as Parameters<typeof getReplyContext>[0],
107
+ ctx.me.id,
108
+ (ctx.message as { quote?: { text?: string; is_manual?: boolean } }).quote,
109
+ );
110
+ const replyPhotoCtx = await downloadReplyPhoto(
111
+ ctx.message.reply_to_message as Parameters<typeof downloadReplyPhoto>[0],
112
+ bot,
113
+ config,
114
+ );
115
+
116
+ const promptParts = [
117
+ fwdCtx,
118
+ replyCtx,
119
+ replyPhotoCtx,
120
+ ...media.promptLines.map((l) => l.replace("${savedPath}", savedPath)),
121
+ media.caption ? `Caption: ${media.caption}` : "",
122
+ ].filter(Boolean);
123
+
124
+ const prompt = promptParts.join("\n");
125
+
126
+ enqueueMessage(bot, config, chatId, ctx.chat.id, {
127
+ prompt,
128
+ replyToId: ctx.message.message_id,
129
+ messageId: ctx.message.message_id,
130
+ senderName: sender,
131
+ senderUsername,
132
+ senderId: ctx.from?.id,
133
+ isGroup,
134
+ chatTitle: isGroup ? (ctx.chat as { title?: string }).title : undefined,
135
+ });
136
+ } catch (err) {
137
+ logError(
138
+ "bot",
139
+ `[${chatId}] ${media.type} error (${sender}): ${err instanceof Error ? err.message : err}`,
140
+ );
141
+ await sendHtml(
142
+ bot,
143
+ ctx.chat.id,
144
+ escapeHtml(friendlyMessage(err)),
145
+ ctx.message.message_id,
146
+ );
147
+ }
148
+ }
149
+
150
+ // ── Text message handler ────────────────────────────────────────────────────
151
+
152
+ export async function handleTextMessage(
153
+ ctx: Context,
154
+ bot: Bot,
155
+ config: TalonConfig,
156
+ ): Promise<void> {
157
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
158
+ if (!(await isAccessAllowed(ctx, bot))) return;
159
+ if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
160
+
161
+ const chatId = String(ctx.chat.id);
162
+ const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
163
+ const sender = getSenderName(ctx.from);
164
+ const senderUsername = ctx.from?.username;
165
+
166
+ const replyCtx = getReplyContext(
167
+ ctx.message.reply_to_message as Parameters<typeof getReplyContext>[0],
168
+ ctx.me.id,
169
+ (ctx.message as { quote?: { text?: string; is_manual?: boolean } }).quote,
170
+ );
171
+ const replyPhotoCtx = await downloadReplyPhoto(
172
+ ctx.message.reply_to_message as Parameters<typeof downloadReplyPhoto>[0],
173
+ bot,
174
+ config,
175
+ );
176
+ const fwdCtx = getForwardContext(
177
+ ctx.message as Parameters<typeof getForwardContext>[0],
178
+ );
179
+ const prompt = fwdCtx + replyCtx + replyPhotoCtx + (ctx.message.text ?? "");
180
+
181
+ // Soul tap: a message reaching here is addressed to Talon (DM, mention, or
182
+ // reply). If it reads as a standing directive or a correction, record it as
183
+ // evidence. No-op unless the soul is enabled.
184
+ recordMessageSignal({
185
+ text: ctx.message.text ?? "",
186
+ actor: sender,
187
+ addressedToBot: true,
188
+ });
189
+
190
+ enqueueMessage(bot, config, chatId, ctx.chat.id, {
191
+ prompt,
192
+ replyToId: ctx.message.message_id,
193
+ messageId: ctx.message.message_id,
194
+ senderName: sender,
195
+ senderUsername,
196
+ senderId: ctx.from?.id,
197
+ isGroup,
198
+ chatTitle: isGroup ? (ctx.chat as { title?: string }).title : undefined,
199
+ });
200
+ }
201
+
202
+ export async function handlePhotoMessage(
203
+ ctx: Context,
204
+ bot: Bot,
205
+ config: TalonConfig,
206
+ ): Promise<void> {
207
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
208
+ if (!(await isAccessAllowed(ctx, bot))) return;
209
+
210
+ const photos = ctx.message.photo;
211
+ if (!photos?.length) return;
212
+ const bestPhoto = photos[photos.length - 1];
213
+ const caption = ctx.message.caption || "";
214
+
215
+ await handleMediaMessage(ctx, bot, config, {
216
+ type: "photo",
217
+ fileId: bestPhoto.file_id,
218
+ fileName: `photo_${bestPhoto.file_unique_id}.jpg`,
219
+ promptLines: [
220
+ "User sent a photo saved to: ${savedPath}",
221
+ "Read this file to view it. If you need to reference this image in future turns, re-read the file — image data does not persist between turns.",
222
+ ],
223
+ caption,
224
+ });
225
+ }
226
+
227
+ export async function handleDocumentMessage(
228
+ ctx: Context,
229
+ bot: Bot,
230
+ config: TalonConfig,
231
+ ): Promise<void> {
232
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
233
+ if (!(await isAccessAllowed(ctx, bot))) return;
234
+
235
+ const doc = ctx.message.document;
236
+ if (!doc) return;
237
+
238
+ const fileName = doc.file_name || `doc_${doc.file_unique_id}`;
239
+ const caption = ctx.message.caption || "";
240
+
241
+ await handleMediaMessage(ctx, bot, config, {
242
+ type: "document",
243
+ fileId: doc.file_id,
244
+ fileName,
245
+ fileSize: doc.file_size,
246
+ promptLines: [
247
+ `User sent a document: "${fileName}" (${doc.mime_type || "unknown"}).`,
248
+ "Saved to: ${savedPath}",
249
+ "Read and process this file.",
250
+ ],
251
+ caption,
252
+ });
253
+ }
254
+
255
+ export async function handleVoiceMessage(
256
+ ctx: Context,
257
+ bot: Bot,
258
+ config: TalonConfig,
259
+ ): Promise<void> {
260
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
261
+ if (!(await isAccessAllowed(ctx, bot))) return;
262
+
263
+ const voice = ctx.message.voice;
264
+ if (!voice) return;
265
+
266
+ await handleMediaMessage(ctx, bot, config, {
267
+ type: "voice",
268
+ fileId: voice.file_id,
269
+ fileName: `voice_${voice.file_unique_id}.ogg`,
270
+ promptLines: [
271
+ `User sent a voice message (${voice.duration}s).`,
272
+ "Audio saved to: ${savedPath}. You cannot transcribe audio — acknowledge it and respond based on context.",
273
+ ],
274
+ });
275
+ }
276
+
277
+ export async function handleStickerMessage(
278
+ ctx: Context,
279
+ bot: Bot,
280
+ config: TalonConfig,
281
+ ): Promise<void> {
282
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
283
+ if (!(await isAccessAllowed(ctx, bot))) return;
284
+
285
+ const chatId = String(ctx.chat.id);
286
+ const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
287
+ const sender = getSenderName(ctx.from);
288
+ const senderUsername = ctx.from?.username;
289
+
290
+ const sticker = ctx.message.sticker;
291
+ if (!sticker) return;
292
+
293
+ const emoji = sticker.emoji || "";
294
+ const setName = sticker.set_name || "";
295
+
296
+ // Grow the sticker library organically: any pack a user sends from
297
+ // gets saved in the background (skipped if already saved), so the
298
+ // model can later send from it by emoji without any browsing.
299
+ if (setName) void ensurePackSaved(bot, setName);
300
+
301
+ const prompt = [
302
+ `User sent a sticker: ${emoji}`,
303
+ `Sticker file_id: ${sticker.file_id}`,
304
+ setName ? `Sticker set: ${setName} (saved to your sticker library)` : "",
305
+ sticker.is_animated
306
+ ? "(animated)"
307
+ : sticker.is_video
308
+ ? "(video sticker)"
309
+ : "",
310
+ 'You can send it back via send(type="sticker", file_id=...) — or send any sticker from your library by emoji.',
311
+ ]
312
+ .filter(Boolean)
313
+ .join("\n");
314
+
315
+ enqueueMessage(bot, config, chatId, ctx.chat.id, {
316
+ prompt,
317
+ replyToId: ctx.message.message_id,
318
+ messageId: ctx.message.message_id,
319
+ senderName: sender,
320
+ senderUsername,
321
+ senderId: ctx.from?.id,
322
+ isGroup,
323
+ chatTitle: isGroup ? (ctx.chat as { title?: string }).title : undefined,
324
+ });
325
+ }
326
+
327
+ export async function handleVideoMessage(
328
+ ctx: Context,
329
+ bot: Bot,
330
+ config: TalonConfig,
331
+ ): Promise<void> {
332
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
333
+ if (!(await isAccessAllowed(ctx, bot))) return;
334
+
335
+ const video = ctx.message.video;
336
+ if (!video) return;
337
+
338
+ const fileName = video.file_name || `video_${video.file_unique_id}.mp4`;
339
+ const caption = ctx.message.caption || "";
340
+
341
+ await handleMediaMessage(ctx, bot, config, {
342
+ type: "video",
343
+ fileId: video.file_id,
344
+ fileName,
345
+ promptLines: [
346
+ `User sent a video: "${fileName}" (${video.duration}s, ${video.width}x${video.height}).`,
347
+ "Saved to: ${savedPath}",
348
+ ],
349
+ caption,
350
+ });
351
+ }
352
+
353
+ export async function handleAnimationMessage(
354
+ ctx: Context,
355
+ bot: Bot,
356
+ config: TalonConfig,
357
+ ): Promise<void> {
358
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
359
+ if (!(await isAccessAllowed(ctx, bot))) return;
360
+
361
+ const anim = ctx.message.animation;
362
+ if (!anim) return;
363
+
364
+ const fileName = anim.file_name || `animation_${anim.file_unique_id}.mp4`;
365
+ const caption = ctx.message.caption || "";
366
+
367
+ await handleMediaMessage(ctx, bot, config, {
368
+ type: "animation",
369
+ fileId: anim.file_id,
370
+ fileName,
371
+ promptLines: [
372
+ `User sent a GIF/animation: "${fileName}" (${anim.duration}s).`,
373
+ "Saved to: ${savedPath}",
374
+ `file_id: ${anim.file_id} — re-send it anytime with send(type="animation", file_id=...).`,
375
+ ],
376
+ caption,
377
+ });
378
+ }
379
+
380
+ export async function handleAudioMessage(
381
+ ctx: Context,
382
+ bot: Bot,
383
+ config: TalonConfig,
384
+ ): Promise<void> {
385
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
386
+ if (!(await isAccessAllowed(ctx, bot))) return;
387
+ if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
388
+
389
+ const audio = ctx.message.audio;
390
+ if (!audio) return;
391
+
392
+ const title = audio.title || audio.file_name || "audio";
393
+ const performer = audio.performer ? ` by ${audio.performer}` : "";
394
+ const fileName = audio.file_name || `audio_${audio.file_unique_id}.mp3`;
395
+ const caption = ctx.message.caption || "";
396
+
397
+ await handleMediaMessage(ctx, bot, config, {
398
+ type: "audio",
399
+ fileId: audio.file_id,
400
+ fileName,
401
+ fileSize: audio.file_size,
402
+ promptLines: [
403
+ `User sent an audio file: "${title}"${performer} (${audio.duration}s).`,
404
+ "Saved to: ${savedPath}",
405
+ ],
406
+ caption,
407
+ });
408
+ }
409
+
410
+ export async function handleVideoNoteMessage(
411
+ ctx: Context,
412
+ bot: Bot,
413
+ config: TalonConfig,
414
+ ): Promise<void> {
415
+ if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
416
+ if (!(await isAccessAllowed(ctx, bot))) return;
417
+ if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
418
+
419
+ const videoNote = ctx.message.video_note;
420
+ if (!videoNote) return;
421
+
422
+ await handleMediaMessage(ctx, bot, config, {
423
+ type: "video note",
424
+ fileId: videoNote.file_id,
425
+ fileName: `videonote_${videoNote.file_unique_id}.mp4`,
426
+ fileSize: videoNote.file_size,
427
+ promptLines: [
428
+ `User sent a round video note (${videoNote.duration}s).`,
429
+ "Saved to: ${savedPath}",
430
+ ],
431
+ });
432
+ }
433
+
434
+ export async function handleCallbackQuery(
435
+ ctx: Context,
436
+ bot: Bot,
437
+ config: TalonConfig,
438
+ ): Promise<void> {
439
+ if (!ctx.callbackQuery || !("data" in ctx.callbackQuery)) return;
440
+
441
+ const chatId = String(ctx.chat?.id ?? ctx.from?.id);
442
+ const numericChatId = ctx.chat?.id ?? ctx.from?.id ?? 0;
443
+ const isGroup = ctx.chat?.type === "group" || ctx.chat?.type === "supergroup";
444
+ const sender = getSenderName(ctx.from);
445
+ const callbackData = ctx.callbackQuery.data;
446
+
447
+ // Acknowledge the callback immediately
448
+ await ctx.answerCallbackQuery().catch(() => {});
449
+
450
+ try {
451
+ const prompt = `[Button pressed] User clicked inline button with callback data: "${callbackData}"`;
452
+ const replyToId = ctx.callbackQuery.message?.message_id ?? 0;
453
+
454
+ const chatTitle = isGroup
455
+ ? (ctx.chat as { title?: string })?.title
456
+ : undefined;
457
+ appendDailyLog(sender, `Button: ${callbackData}`, { chatTitle });
458
+
459
+ await processAndReply({
460
+ bot,
461
+ config,
462
+ chatId,
463
+ numericChatId,
464
+ replyToId,
465
+ messageId: replyToId,
466
+ prompt,
467
+ senderName: sender,
468
+ isGroup,
469
+ chatTitle,
470
+ });
471
+ } catch (err) {
472
+ logError(
473
+ "bot",
474
+ `[${chatId}] Callback error (${sender}): ${err instanceof Error ? err.message : err}`,
475
+ );
476
+ }
477
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Message queue + per-user rate limiting.
3
+ *
4
+ * Debounces rapid-fire messages per chat (concatenating them into a single
5
+ * agent turn), tracks per-user rate limits, and surfaces a "messages were
6
+ * skipped" notice for groups.
7
+ */
8
+
9
+ import type { Bot } from "grammy";
10
+ import type { TalonConfig } from "../../../util/config.js";
11
+ import { escapeHtml } from "../formatting.js";
12
+ import { classify, friendlyMessage } from "../../../core/errors.js";
13
+ import {
14
+ getRecentHistory,
15
+ type HistoryMessage,
16
+ } from "../../../storage/history.js";
17
+ import { recordMessageProcessed, recordError } from "../../../util/watchdog.js";
18
+ import { log, logError, logWarn, logDebug } from "../../../util/log.js";
19
+ import { appendDailyLog } from "../../../storage/daily-log.js";
20
+ import { processAndReply, sendHtml } from "./delivery.js";
21
+ import {
22
+ messageQueues,
23
+ lastHandledMessageIdByChat,
24
+ userMessageTimestamps,
25
+ DEBOUNCE_MS,
26
+ MAX_QUEUED_PER_CHAT,
27
+ RATE_LIMIT_WINDOW_MS,
28
+ RATE_LIMIT_MAX_MESSAGES,
29
+ type QueuedMessage,
30
+ } from "./state.js";
31
+
32
+ export function isUserRateLimited(senderId: number): boolean {
33
+ const now = Date.now();
34
+ let timestamps = userMessageTimestamps.get(senderId);
35
+ if (!timestamps) {
36
+ timestamps = [];
37
+ userMessageTimestamps.set(senderId, timestamps);
38
+ }
39
+
40
+ // Remove old entries outside the window
41
+ while (timestamps.length > 0 && timestamps[0] < now - RATE_LIMIT_WINDOW_MS) {
42
+ timestamps.shift();
43
+ }
44
+
45
+ if (timestamps.length >= RATE_LIMIT_MAX_MESSAGES) {
46
+ logDebug("bot", `Rate-limited user ${senderId}`);
47
+ return true;
48
+ }
49
+
50
+ timestamps.push(now);
51
+
52
+ // Evict stale entries — remove users who haven't messaged in 10+ minutes
53
+ if (userMessageTimestamps.size > 5_000) {
54
+ const cutoff = now - 10 * 60_000;
55
+ for (const [userId, ts] of userMessageTimestamps) {
56
+ if (ts.length === 0 || ts[ts.length - 1] < cutoff) {
57
+ userMessageTimestamps.delete(userId);
58
+ }
59
+ if (userMessageTimestamps.size <= 2_500) break; // evict down to half
60
+ }
61
+ }
62
+
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Enqueue a message for processing. If another message arrives within DEBOUNCE_MS,
68
+ * they are concatenated and sent as a single query to avoid duplicate SDK spawns.
69
+ * Queued messages get a hourglass reaction to indicate they've been seen.
70
+ */
71
+ export function enqueueMessage(
72
+ bot: Bot,
73
+ config: TalonConfig,
74
+ chatId: string,
75
+ numericChatId: number,
76
+ msg: QueuedMessage,
77
+ ): void {
78
+ const existing = messageQueues.get(chatId);
79
+ if (existing) {
80
+ if (existing.messages.length >= MAX_QUEUED_PER_CHAT) return; // drop excess
81
+ existing.messages.push(msg);
82
+ // Show hourglass reaction on the queued message to indicate it's been seen
83
+ bot.api
84
+ .setMessageReaction(numericChatId, msg.messageId, [
85
+ {
86
+ type: "emoji",
87
+ emoji: "⏳" as "👍" /* grammY wants union type */,
88
+ },
89
+ ])
90
+ .catch(() => {});
91
+ existing.queuedReactionMsgIds.push(msg.messageId);
92
+ clearTimeout(existing.timer);
93
+ existing.timer = setTimeout(() => flushQueue(chatId), DEBOUNCE_MS);
94
+ return;
95
+ }
96
+
97
+ const entry = {
98
+ messages: [msg],
99
+ timer: setTimeout(() => flushQueue(chatId), DEBOUNCE_MS),
100
+ bot,
101
+ config,
102
+ numericChatId,
103
+ queuedReactionMsgIds: [] as number[],
104
+ };
105
+ messageQueues.set(chatId, entry);
106
+ }
107
+
108
+ async function flushQueue(chatId: string): Promise<void> {
109
+ const entry = messageQueues.get(chatId);
110
+ if (!entry) return;
111
+ messageQueues.delete(chatId);
112
+
113
+ const { messages, bot, config, numericChatId, queuedReactionMsgIds } = entry;
114
+
115
+ // Clear hourglass reactions on queued messages now that we're processing
116
+ for (const msgId of queuedReactionMsgIds) {
117
+ bot.api.setMessageReaction(numericChatId, msgId, []).catch((err) => {
118
+ logWarn(
119
+ "bot",
120
+ `Failed to clear reaction on msg ${msgId}: ${err instanceof Error ? err.message : err}`,
121
+ );
122
+ });
123
+ }
124
+
125
+ // Use last message's metadata for reply context
126
+ const first = messages[0];
127
+ const last = messages[messages.length - 1];
128
+
129
+ // Concatenate prompts (with newlines between them if multiple)
130
+ const combinedPrompt =
131
+ messages.length === 1
132
+ ? messages[0].prompt
133
+ : messages.map((m) => m.prompt).join("\n\n");
134
+ const groupGapContext = buildGroupGapContextNotice({
135
+ isGroup: last.isGroup,
136
+ chatId,
137
+ firstQueuedMessageId: first.messageId,
138
+ lastHandledMessageId: lastHandledMessageIdByChat.get(chatId),
139
+ });
140
+ const promptWithContext = groupGapContext
141
+ ? `${groupGapContext}\n\n${combinedPrompt}`
142
+ : combinedPrompt;
143
+
144
+ const chatContext = {
145
+ chatTitle: last.chatTitle,
146
+ username: last.senderUsername,
147
+ };
148
+ appendDailyLog(last.senderName, promptWithContext, chatContext);
149
+
150
+ const runTurn = () =>
151
+ processAndReply({
152
+ bot,
153
+ config,
154
+ chatId,
155
+ numericChatId,
156
+ replyToId: last.replyToId,
157
+ messageId: last.messageId,
158
+ prompt: promptWithContext,
159
+ senderName: last.senderName,
160
+ isGroup: last.isGroup,
161
+ senderUsername: last.senderUsername,
162
+ senderId: last.senderId,
163
+ chatTitle: last.chatTitle,
164
+ });
165
+
166
+ try {
167
+ await runTurn();
168
+ lastHandledMessageIdByChat.set(chatId, last.messageId);
169
+ recordMessageProcessed();
170
+ } catch (err) {
171
+ const classified = classify(err);
172
+ const chatType = last.isGroup ? "group" : "DM";
173
+ const promptPreview = promptWithContext.slice(0, 100).replace(/\n/g, " ");
174
+ logError(
175
+ "bot",
176
+ `[${chatId}] [${chatType}] [${last.senderName}] ${classified.reason}: ${classified.message} | prompt: "${promptPreview}"`,
177
+ );
178
+ recordError(classified.message);
179
+
180
+ // Retry once for transient errors (rate_limit, overloaded, network)
181
+ if (classified.retryable) {
182
+ const delayMs = classified.retryAfterMs ?? 2000;
183
+ log(
184
+ "bot",
185
+ `[${chatId}] Retrying after ${classified.reason} (${delayMs}ms)...`,
186
+ );
187
+ try {
188
+ await new Promise((r) => setTimeout(r, delayMs));
189
+ await runTurn();
190
+ lastHandledMessageIdByChat.set(chatId, last.messageId);
191
+ recordMessageProcessed();
192
+ return;
193
+ } catch (retryErr) {
194
+ const retryClassified = classify(retryErr);
195
+ logError(
196
+ "bot",
197
+ `[${chatId}] [${chatType}] Retry failed: ${retryClassified.message}`,
198
+ );
199
+ await sendHtml(
200
+ bot,
201
+ numericChatId,
202
+ escapeHtml(friendlyMessage(retryClassified)),
203
+ last.replyToId,
204
+ );
205
+ return;
206
+ }
207
+ }
208
+
209
+ await sendHtml(
210
+ bot,
211
+ numericChatId,
212
+ escapeHtml(friendlyMessage(classified)),
213
+ last.replyToId,
214
+ );
215
+ }
216
+ }
217
+
218
+ export function buildGroupGapContextNotice(inputs: {
219
+ isGroup: boolean;
220
+ chatId: string;
221
+ firstQueuedMessageId: number;
222
+ lastHandledMessageId?: number;
223
+ history?: HistoryMessage[];
224
+ }): string {
225
+ if (!inputs.isGroup || inputs.lastHandledMessageId === undefined) return "";
226
+
227
+ const history = inputs.history ?? getRecentHistory(inputs.chatId, 100);
228
+ const interveningCount = history.filter(
229
+ (m) =>
230
+ m.msgId > inputs.lastHandledMessageId! &&
231
+ m.msgId < inputs.firstQueuedMessageId,
232
+ ).length;
233
+ if (interveningCount === 0) return "";
234
+
235
+ const noun = interveningCount === 1 ? "message" : "messages";
236
+ return (
237
+ `[Group context notice: ${interveningCount} ${noun} were sent in this ` +
238
+ "chat since your last handled turn. If this prompt is vague, ambiguous, " +
239
+ "or asks what you think, use read_chat_history before answering.]"
240
+ );
241
+ }