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,161 @@
1
+ /**
2
+ * Delivery-contract text — single source of truth for "how a reply
3
+ * reaches the user" documentation.
4
+ *
5
+ * The response-flow contract is a property of the BACKEND (claude-sdk
6
+ * and openai-agents enforce strict tool-only delivery; codex / kilo /
7
+ * opencode accept plain assistant text), while the delivery TOOL NAMES
8
+ * are a property of the FRONTEND (telegram/discord ship `end_turn` /
9
+ * `send` / `react`; native ships `end_turn` / `send_message` /
10
+ * `react`; teams ships `end_turn` / `send_message`). Before
11
+ * this module, the strict contract was hardcoded into the frontend
12
+ * prompt files (prompts/telegram.md etc.) — buried mid-prompt where
13
+ * models routinely missed it on the first turn of a session, and
14
+ * contradicted by the text-mode backends' own suffixes.
15
+ *
16
+ * Backends now build their suffix from here and append it via
17
+ * `prepareSystemPrompt({ backendSuffix })`, which places the contract
18
+ * at the END of the static prompt — the highest-salience position —
19
+ * without touching the prompt-cache-friendly static/dynamic split.
20
+ *
21
+ * Also here:
22
+ * - `buildFlowViolationReminder` — frontend-aware [FLOW VIOLATION]
23
+ * re-prompt text (the shared constant used to hardcode telegram's
24
+ * tool names, which made the reminder wrong on Teams).
25
+ * - `buildFirstTurnReminder` — a one-line nudge appended to the
26
+ * formatted user prompt on the FIRST turn of a session. First
27
+ * turns are where flow violations cluster: the model hasn't seen
28
+ * the contract in action yet. One short line on turn 0 is far
29
+ * cheaper than the 2x-token violation retry it prevents.
30
+ *
31
+ * The contract bodies live in `prompts/system/contract-*.md`
32
+ * (package-owned templates, see core/prompt/templates.ts). The
33
+ * one-line runtime reminders below stay in code: they are
34
+ * logic-adjacent strings, like log messages.
35
+ */
36
+
37
+ import { loadSystemTemplate } from "../../core/prompt/templates.js";
38
+
39
+ // ── Types ───────────────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * How a backend delivers replies to the user.
43
+ *
44
+ * - `tool-only` — output stream is private scratchpad; replies MUST
45
+ * go through a delivery tool (claude-sdk, openai-agents).
46
+ * - `text-or-tools` — plain assistant text is delivered as the
47
+ * reply; delivery tools add rich content / targeting (codex, kilo).
48
+ * - `text-preferred` — plain text is the normal route; tools only
49
+ * for genuine side effects (opencode).
50
+ */
51
+ export type DeliveryMode = "tool-only" | "text-or-tools" | "text-preferred";
52
+
53
+ /** Frontend-specific delivery tool names. */
54
+ export type DeliveryToolNames = {
55
+ /** Turn-closing final-reply tool. */
56
+ endTurn: string;
57
+ /** Mid-turn / rich-content send tool. */
58
+ send: string;
59
+ /** Emoji-reaction tool, when the frontend supports reactions. */
60
+ react?: string;
61
+ };
62
+
63
+ // ── Frontend tool-name registry ─────────────────────────────────────────────
64
+
65
+ const FRONTEND_TOOLS: Record<string, DeliveryToolNames> = {
66
+ telegram: { endTurn: "end_turn", send: "send", react: "react" },
67
+ discord: { endTurn: "end_turn", send: "send", react: "react" },
68
+ teams: { endTurn: "end_turn", send: "send_message" },
69
+ native: { endTurn: "end_turn", send: "send_message", react: "react" },
70
+ };
71
+
72
+ const DEFAULT_TOOLS: DeliveryToolNames = {
73
+ endTurn: "end_turn",
74
+ send: "send",
75
+ react: "react",
76
+ };
77
+
78
+ /**
79
+ * Delivery tool names for a frontend. Unknown frontends (and
80
+ * `terminal`, which has no outbound messaging surface) fall back to
81
+ * the telegram-shaped defaults — harmless, since backends only enforce
82
+ * the contract when a frontend MCP server is actually wired.
83
+ */
84
+ export function deliveryToolsForFrontend(frontend: string): DeliveryToolNames {
85
+ return FRONTEND_TOOLS[frontend] ?? DEFAULT_TOOLS;
86
+ }
87
+
88
+ /**
89
+ * Register (or override) the delivery tool names for a frontend.
90
+ * Extension seam: a new frontend declares its tool names here at
91
+ * registration time and every backend's contract, flow-violation
92
+ * reminder, and first-turn nudge pick them up — no edits to this
93
+ * module or to any backend.
94
+ */
95
+ export function registerFrontendDeliveryTools(
96
+ frontend: string,
97
+ tools: DeliveryToolNames,
98
+ ): void {
99
+ FRONTEND_TOOLS[frontend] = tools;
100
+ }
101
+
102
+ // ── Contract suffix ─────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Build the response-flow contract section a backend appends to the
106
+ * static system prompt. The prose lives in package-owned templates
107
+ * (`prompts/system/contract-*.md`) — kept deliberately tight: this is
108
+ * read on every session, so every sentence must earn its tokens. The
109
+ * per-tool details (parameter shapes, examples) live in the MCP tool
110
+ * descriptions, which the model also has in context.
111
+ */
112
+ export function buildDeliveryContract(
113
+ mode: DeliveryMode,
114
+ frontend: string,
115
+ ): string {
116
+ const t = deliveryToolsForFrontend(frontend);
117
+ return loadSystemTemplate(`contract-${mode}`, {
118
+ end_turn: t.endTurn,
119
+ send: t.send,
120
+ react: t.react,
121
+ });
122
+ }
123
+
124
+ // ── Flow-violation reminder ─────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Frontend-aware [FLOW VIOLATION] reminder. Same shape as the legacy
128
+ * shared `FLOW_VIOLATION_REMINDER`, but with the frontend's actual
129
+ * tool names (the constant hardcoded \`send\`/\`react\`, which don't
130
+ * exist on Teams).
131
+ */
132
+ export function buildFlowViolationReminder(frontend: string): string {
133
+ const t = deliveryToolsForFrontend(frontend);
134
+ const reactPart = t.react
135
+ ? `, or \`${t.react}(emoji=...)\` for an emoji acknowledgement`
136
+ : "";
137
+ return (
138
+ "[FLOW VIOLATION] Your previous turn ended without a delivery tool, so the " +
139
+ "user saw nothing. Replies must go through a tool call: " +
140
+ `\`${t.endTurn}(text=...)\` for a final reply, ` +
141
+ `\`${t.endTurn}()\` (no args) to close silently, ` +
142
+ `\`${t.send}(...)\` for mid-turn rich content${reactPart}. ` +
143
+ "Tool calls alone do not close the turn. Retry now using the correct tool call."
144
+ );
145
+ }
146
+
147
+ // ── First-turn nudge ────────────────────────────────────────────────────────
148
+
149
+ /**
150
+ * One-line reminder appended to the formatted user prompt on the first
151
+ * turn of a session (strict backends only). Lives in the user message,
152
+ * not the system prompt, so it costs nothing on later turns and never
153
+ * perturbs the cached static prefix.
154
+ */
155
+ export function buildFirstTurnReminder(frontend: string): string {
156
+ const t = deliveryToolsForFrontend(frontend);
157
+ return (
158
+ `[New session — reminder: your prose output is never shown to the user. ` +
159
+ `Deliver your reply via ${t.endTurn}(text=...), or ${t.endTurn}() to stay silent.]`
160
+ );
161
+ }
@@ -35,6 +35,32 @@ import { incrementCounter } from "../../util/metrics.js";
35
35
  /** Route the delivery decision selected. */
36
36
  export type DeliveryRoute = "tool" | "text-part" | "synthetic-error" | "empty";
37
37
 
38
+ export class TextBlockDeliveryError extends Error {
39
+ readonly route: DeliveryRoute;
40
+ readonly chars: number;
41
+
42
+ constructor(route: DeliveryRoute, chars: number, cause: unknown) {
43
+ super(
44
+ `text-block delivery failed (${route}, ${chars} chars): ${errMsg(cause)}`,
45
+ );
46
+ this.name = "TextBlockDeliveryError";
47
+ this.route = route;
48
+ this.chars = chars;
49
+ this.cause = cause;
50
+ }
51
+ }
52
+
53
+ export function buildDeliveryFailureReminder(error: TextBlockDeliveryError) {
54
+ return (
55
+ "[DELIVERY FAILURE] Your previous reply was not shown to the user because " +
56
+ `Talon failed while delivering a ${error.route} response (${error.chars} chars): ${errMsg(error.cause)}. ` +
57
+ "Retry now using delivery tools. If the answer is long, split it across " +
58
+ 'multiple `send(type="text", text=...)` calls and then call `end_turn()`; ' +
59
+ "or shorten the answer and call `end_turn(text=...)`. Do not write the final " +
60
+ "answer as plain assistant text."
61
+ );
62
+ }
63
+
38
64
  /** Outcome of `routeDelivery` — `route` + char count for the log line. */
39
65
  export interface DeliveryDecision {
40
66
  route: DeliveryRoute;
@@ -58,6 +84,11 @@ export interface RouteDeliveryInputs {
58
84
  * `kilo.synthetic_error`). Defaults to a lower-cased backendLabel.
59
85
  */
60
86
  metricNamespace?: string;
87
+ /**
88
+ * When true, failed `onTextBlock` delivery is thrown back to the backend so
89
+ * it can re-prompt the model. Default preserves older non-fatal behaviour.
90
+ */
91
+ propagateDeliveryFailure?: boolean;
61
92
  }
62
93
 
63
94
  /**
@@ -78,6 +109,7 @@ export async function routeDelivery(
78
109
  responseText,
79
110
  onTextBlock,
80
111
  metricNamespace = backendLabel.toLowerCase(),
112
+ propagateDeliveryFailure = false,
81
113
  } = inputs;
82
114
 
83
115
  // Route 1 — bridge delivery already happened.
@@ -116,6 +148,13 @@ export async function routeDelivery(
116
148
  "agent",
117
149
  `[${chatId}] onTextBlock (synthetic-error) failed: ${errMsg(err)}`,
118
150
  );
151
+ if (propagateDeliveryFailure) {
152
+ throw new TextBlockDeliveryError(
153
+ "synthetic-error",
154
+ state.syntheticError.length,
155
+ err,
156
+ );
157
+ }
119
158
  }
120
159
  }
121
160
  return {
@@ -131,6 +170,13 @@ export async function routeDelivery(
131
170
  await onTextBlock(responseText);
132
171
  } catch (err) {
133
172
  logWarn("agent", `[${chatId}] onTextBlock failed: ${errMsg(err)}`);
173
+ if (propagateDeliveryFailure) {
174
+ throw new TextBlockDeliveryError(
175
+ "text-part",
176
+ responseText.length,
177
+ err,
178
+ );
179
+ }
134
180
  }
135
181
  }
136
182
  return { route: "text-part", chars: responseText.length };
@@ -155,6 +201,9 @@ export async function routeDelivery(
155
201
  "agent",
156
202
  `[${chatId}] onTextBlock (empty-turn error) failed: ${errMsg(err)}`,
157
203
  );
204
+ if (propagateDeliveryFailure) {
205
+ throw new TextBlockDeliveryError("empty", 0, err);
206
+ }
158
207
  }
159
208
  }
160
209
  return { route: "empty", chars: 0 };
@@ -29,7 +29,9 @@ export const FLOW_VIOLATION_REMINDER =
29
29
  "`end_turn()` (no args) to close silently, " +
30
30
  "`send(...)` for mid-turn rich content (photos, polls, etc.), or " +
31
31
  "`react(emoji=...)` for an emoji acknowledgement. " +
32
- "Retry now using the correct tool call.";
32
+ "Tool calls alone do not close the turn. Retry now using the correct tool call.";
33
+
34
+ export const FLOW_VIOLATION_MAX_RETRIES = 3;
33
35
 
34
36
  // ── Public API ──────────────────────────────────────────────────────────────
35
37
 
@@ -41,8 +43,21 @@ export type FlowViolationInputs = {
41
43
  turnTerminated: boolean;
42
44
  /** Already-delivered text norms used for dedup. */
43
45
  deliveredTextNorms: readonly string[];
46
+ /** Number of tool calls observed in this turn. */
47
+ toolCalls?: number;
44
48
  /** Whether this is already a retry (prevents looping). */
45
49
  retried: boolean;
50
+ /** Number of synthetic flow-violation retries already attempted. */
51
+ retryCount?: number;
52
+ /** Maximum synthetic flow-violation retries before accepting the drop. */
53
+ maxRetries?: number;
54
+ /**
55
+ * Reminder text to ship on retry. Defaults to the legacy
56
+ * telegram-shaped `FLOW_VIOLATION_REMINDER`; backends should pass
57
+ * `buildFlowViolationReminder(frontend)` so the tool names match
58
+ * what's actually registered (Teams has `send_message`, not `send`).
59
+ */
60
+ reminder?: string;
46
61
  };
47
62
 
48
63
  /** Outcome of the flow-violation check. */
@@ -52,6 +67,8 @@ export type FlowViolationResult =
52
67
  violated: true;
53
68
  /** Trimmed scratchpad prose that would be dropped. */
54
69
  trailing: string;
70
+ /** Human-readable reason logged by handlers. */
71
+ reason: string;
55
72
  /** Whether the handler should re-prompt with `reminder`. False when retried. */
56
73
  shouldRetry: boolean;
57
74
  /** Reminder string to send as the next user prompt (when retrying). */
@@ -65,9 +82,9 @@ export type FlowViolationResult =
65
82
  * Classify a stream-loop outcome against the delivery contract.
66
83
  *
67
84
  * A turn is in violation when ALL of the following hold:
68
- * - The trailing text (after trim) is non-empty.
69
85
  * - No turn-terminator tool fired (e.g. `end_turn`).
70
- * - The trailing text isn't a duplicate of content already delivered
86
+ * - The model produced either trailing prose or tool calls.
87
+ * - Any trailing text isn't a duplicate of content already delivered
71
88
  * through a tool call in the same turn.
72
89
  *
73
90
  * The third condition handles the legitimate "model wrote text AND called
@@ -79,14 +96,28 @@ export function detectFlowViolation(
79
96
  inputs: FlowViolationInputs,
80
97
  ): FlowViolationResult {
81
98
  const trailing = inputs.trailingText.trim();
82
- if (trailing.length === 0) return { violated: false };
83
99
  if (inputs.turnTerminated) return { violated: false };
84
- if (isDuplicateOfDelivered(trailing, inputs.deliveredTextNorms))
100
+ const toolCalls = inputs.toolCalls ?? 0;
101
+ if (trailing.length === 0 && toolCalls === 0) return { violated: false };
102
+ if (
103
+ trailing.length > 0 &&
104
+ isDuplicateOfDelivered(trailing, inputs.deliveredTextNorms)
105
+ ) {
85
106
  return { violated: false };
107
+ }
108
+
109
+ const retryCount = inputs.retryCount ?? (inputs.retried ? 1 : 0);
110
+ const maxRetries = inputs.maxRetries ?? FLOW_VIOLATION_MAX_RETRIES;
111
+ const reason =
112
+ trailing.length > 0
113
+ ? `trailing prose (${trailing.length} chars)`
114
+ : `${toolCalls} tool call${toolCalls === 1 ? "" : "s"} with no terminator`;
115
+
86
116
  return {
87
117
  violated: true,
88
118
  trailing,
89
- shouldRetry: !inputs.retried,
90
- reminder: FLOW_VIOLATION_REMINDER,
119
+ reason,
120
+ shouldRetry: retryCount < maxRetries,
121
+ reminder: inputs.reminder ?? FLOW_VIOLATION_REMINDER,
91
122
  };
92
123
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Frontend-list helpers shared across backends.
3
+ *
4
+ * Every backend needs "which frontends get an MCP tool server?" at
5
+ * spawn time; before this helper, claude-sdk, openai-agents, and codex
6
+ * each carried their own copy of the same normalise-and-filter logic.
7
+ */
8
+
9
+ import {
10
+ isNativeChatId,
11
+ isTeamsChatId,
12
+ isDiscordChatId,
13
+ isTelegramChatId,
14
+ } from "../../util/chat-id.js";
15
+
16
+ /**
17
+ * Normalise a config `frontend` value (string or array, possibly
18
+ * undefined) to the list of messaging frontends — i.e. those that
19
+ * need an MCP tool server spawned. `terminal` is excluded: it has no
20
+ * outbound messaging surface (the agent runs to stdout).
21
+ */
22
+ export function nonTerminalFrontends(
23
+ frontend: string | readonly string[] | undefined,
24
+ ): readonly string[] {
25
+ if (!frontend) return [];
26
+ const all = Array.isArray(frontend) ? frontend : [frontend as string];
27
+ return all.filter((f) => f !== "terminal");
28
+ }
29
+
30
+ /**
31
+ * The messaging frontend that owns a chat, inferred from the chat-id
32
+ * shape (the same convention the gateway uses to route actions):
33
+ * native `d_*`, teams `teams_chat_*`, discord `discord_*`, telegram
34
+ * numeric. Returns null for cross-surface contexts — the heartbeat
35
+ * sentinel, isolated one-shots, terminal sessions.
36
+ */
37
+ export function frontendForChatId(chatId: string): string | null {
38
+ if (isNativeChatId(chatId)) return "native";
39
+ if (isTeamsChatId(chatId)) return "teams";
40
+ if (isDiscordChatId(chatId)) return "discord";
41
+ if (isTelegramChatId(chatId)) return "telegram";
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Which frontends' tool servers a chat's turn should register.
47
+ *
48
+ * A chat owned by one messaging frontend gets exactly that frontend's
49
+ * tools: a native-app conversation must see `native-tools`, not a
50
+ * `telegram-tools` server that happens to share the daemon — the
51
+ * model reads surface identity out of tool names, and stray
52
+ * `mcp__telegram-tools__*` entries in a native chat are both confusing
53
+ * UI and an invitation to deliver the reply to the wrong surface.
54
+ *
55
+ * Chats with no owning frontend (heartbeat sentinel, one-shot cron,
56
+ * terminal sessions) keep every configured messaging frontend: those
57
+ * contexts legitimately reach across surfaces via explicit chat_id.
58
+ */
59
+ export function frontendsForChat(
60
+ chatId: string,
61
+ configured: readonly string[],
62
+ ): readonly string[] {
63
+ const owner = frontendForChatId(chatId);
64
+ if (owner && configured.includes(owner)) return [owner];
65
+ return configured;
66
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Shared error-recovery dispatcher.
3
+ *
4
+ * Every backend's handler ends its `try { ... } catch (err) { ... }`
5
+ * block with the same `classifyRetry` decision tree:
6
+ *
7
+ * 1. `reset_and_retry` (session_expired / context_length) — reset
8
+ * the session and recurse.
9
+ * 2. `fallback_model` (retryable + fallback configured) — recurse
10
+ * with the fallback model threaded through the retry's params.
11
+ * 3. `propagate` — let the caller throw.
12
+ *
13
+ * The four backends each duplicated ~30 lines of this — same logic,
14
+ * different log-line prefix. Centralising it here means a future
15
+ * tweak (e.g. adding a third decision kind, changing the
16
+ * transient-model-flip semantics) lands once instead of four times,
17
+ * and the per-backend handlers reduce to a single function call.
18
+ *
19
+ * Caveat: this is the GENERIC recovery path. Backend-specific
20
+ * fallbacks (e.g. `maybeFallbackForChatGptMismatch` in codex) still
21
+ * live in the backend handler — they're a different decision and run
22
+ * BEFORE this one.
23
+ */
24
+
25
+ import type { QueryParams, QueryResult } from "./handler-types.js";
26
+ import { classify, type TalonError } from "../../core/errors.js";
27
+ import { logWarn } from "../../util/log.js";
28
+ import { incrementCounter } from "../../util/metrics.js";
29
+ import { resetSession } from "../../storage/sessions.js";
30
+ import { classifyRetry } from "./model-retry.js";
31
+ import type { AgentEvent } from "../../core/agent-runtime/events.js";
32
+
33
+ /** Inputs for `applyRetryDecision`. */
34
+ export interface ApplyRetryDecisionInputs {
35
+ /** The error caught by the backend's handler. */
36
+ err: unknown;
37
+ /** Active chat id (for log lines + session reset). */
38
+ chatId: string;
39
+ /** Model that was active when the error fired. */
40
+ activeModel: string;
41
+ /** True when the current call is already a retry (short-circuits). */
42
+ retried: boolean;
43
+ /** Original `handleMessage` params — used for the recursive retry. */
44
+ params: QueryParams;
45
+ /**
46
+ * Backend's recursive entry — passed as a callback so we don't have
47
+ * to depend on each backend module. Each backend calls this with its
48
+ * own `handleMessage`.
49
+ */
50
+ recurseWithRetried: (params: QueryParams) => Promise<QueryResult>;
51
+ /**
52
+ * Backend label for log-line prefixes (`"Kilo"`, `"OpenCode"`,
53
+ * `"Codex"`, `"Claude"`). When omitted or empty the prefix is
54
+ * dropped — matches the claude-sdk historical behaviour.
55
+ */
56
+ backendLabel?: string;
57
+ /**
58
+ * Word used in the reset_and_retry log line. Codex resets a "thread"
59
+ * while the remote-server / claude backends reset a "session"; the
60
+ * helper takes whatever the backend prefers.
61
+ */
62
+ resetNoun?: "session" | "thread";
63
+ }
64
+
65
+ /** Outcome — undefined means the caller should propagate the classified error. */
66
+ export interface ApplyRetryDecisionResult {
67
+ /**
68
+ * Set when the helper retried — caller `return retry`s this value
69
+ * directly from its handler.
70
+ */
71
+ retry?: QueryResult;
72
+ /**
73
+ * Set when the helper decided NOT to retry — caller should
74
+ * `throw classified` to propagate.
75
+ */
76
+ classified: TalonError;
77
+ }
78
+
79
+ /**
80
+ * Run the shared retry-decision logic.
81
+ *
82
+ * Side effects (when a retry fires):
83
+ * - `incrementCounter('errors.<reason>')` exactly once per call.
84
+ * - `resetSession(chatId)` before recursion.
85
+ * - For `fallback_model`: the fallback model id is spread into the
86
+ * recursion's params (`params.model` outranks chat settings).
87
+ *
88
+ * When the decision is `propagate`, returns `{classified}` only — the
89
+ * caller throws.
90
+ */
91
+ export async function applyRetryDecision(
92
+ inputs: ApplyRetryDecisionInputs,
93
+ ): Promise<ApplyRetryDecisionResult> {
94
+ const {
95
+ err,
96
+ chatId,
97
+ activeModel,
98
+ retried,
99
+ params,
100
+ recurseWithRetried,
101
+ backendLabel,
102
+ resetNoun = "session",
103
+ } = inputs;
104
+
105
+ const classified = classify(err);
106
+ incrementCounter(`errors.${classified.reason ?? "unknown"}`);
107
+
108
+ const decision = classifyRetry({
109
+ error: classified,
110
+ activeModel,
111
+ retried,
112
+ });
113
+
114
+ const prefix = backendLabel ? `${backendLabel} ` : "";
115
+
116
+ if (decision.kind === "reset_and_retry") {
117
+ logWarn(
118
+ "agent",
119
+ `[${chatId}] ${prefix}${decision.reason}, resetting ${resetNoun} and retrying`,
120
+ );
121
+ resetSession(chatId);
122
+ return { retry: await recurseWithRetried(params), classified };
123
+ }
124
+
125
+ if (decision.kind === "fallback_model") {
126
+ logWarn(
127
+ "agent",
128
+ `[${chatId}] ${classified.reason}, falling back to ${decision.fallbackModelId}`,
129
+ );
130
+ resetSession(chatId);
131
+ return {
132
+ retry: await recurseWithRetried({
133
+ ...params,
134
+ model: decision.fallbackModelId,
135
+ }),
136
+ classified,
137
+ };
138
+ }
139
+
140
+ // `propagate` — caller throws `classified`.
141
+ return { classified };
142
+ }
143
+
144
+ // ── Generator-shaped variant ────────────────────────────────────────────────
145
+
146
+ /** Inputs for `applyRetryDecisionStream`. */
147
+ export interface StreamRetryInputs {
148
+ /** Error caught inside the caller's generator. */
149
+ err: unknown;
150
+ /** Active chat id (for log lines + session reset). */
151
+ chatId: string;
152
+ /** Model that was active when the error fired. */
153
+ activeModel: string;
154
+ /** True when the current call is already a retry (short-circuits). */
155
+ retried: boolean;
156
+ /**
157
+ * Builds the recursive event stream. Called once when the helper
158
+ * decides to retry — yielded via `yield*` so the caller's generator
159
+ * transparently delegates to it. For `fallback_model` decisions the
160
+ * fallback model id is passed through; the builder must thread it
161
+ * into the retry's params (`params.model` outranks chat settings,
162
+ * so a transient `setChatModel` flip would be a silent no-op).
163
+ */
164
+ buildRetryStream: (fallbackModelId?: string) => AsyncIterable<AgentEvent>;
165
+ /** Backend label for log-line prefixes (`"Kilo"`, `"Codex"` …). */
166
+ backendLabel?: string;
167
+ /**
168
+ * Word used in the reset_and_retry log line. Codex resets a "thread"
169
+ * while remote-server / claude backends reset a "session".
170
+ */
171
+ resetNoun?: "session" | "thread";
172
+ }
173
+
174
+ /** Outcome `applyRetryDecisionStream` returns from its generator. */
175
+ export interface StreamRetryResult {
176
+ /**
177
+ * True when the helper recursed (the recursive stream was already
178
+ * yielded into the caller). Caller may finish its generator.
179
+ */
180
+ retried: boolean;
181
+ /**
182
+ * The classified error. When `retried` is false the caller should
183
+ * yield an `error` event derived from this and return.
184
+ */
185
+ classified: TalonError;
186
+ }
187
+
188
+ /**
189
+ * Generator-shaped equivalent of `applyRetryDecision`. The caller
190
+ * uses `yield* applyRetryDecisionStream({...})` so the recursive
191
+ * retry's events flow into the outer stream transparently. Side
192
+ * effects (counter increment, session reset, fallback model threaded
193
+ * through the retry) match the callback-shaped helper exactly.
194
+ *
195
+ * The terminating value (`AsyncGenerator` 2nd type param) tells the
196
+ * caller whether to emit a final `error` event or just return.
197
+ */
198
+ export async function* applyRetryDecisionStream(
199
+ inputs: StreamRetryInputs,
200
+ ): AsyncGenerator<AgentEvent, StreamRetryResult, void> {
201
+ const {
202
+ err,
203
+ chatId,
204
+ activeModel,
205
+ retried,
206
+ buildRetryStream,
207
+ backendLabel,
208
+ resetNoun = "session",
209
+ } = inputs;
210
+
211
+ const classified = classify(err);
212
+ incrementCounter(`errors.${classified.reason ?? "unknown"}`);
213
+
214
+ const decision = classifyRetry({
215
+ error: classified,
216
+ activeModel,
217
+ retried,
218
+ });
219
+
220
+ const prefix = backendLabel ? `${backendLabel} ` : "";
221
+
222
+ if (decision.kind === "reset_and_retry") {
223
+ logWarn(
224
+ "agent",
225
+ `[${chatId}] ${prefix}${decision.reason}, resetting ${resetNoun} and retrying`,
226
+ );
227
+ resetSession(chatId);
228
+ yield* buildRetryStream();
229
+ return { retried: true, classified };
230
+ }
231
+
232
+ if (decision.kind === "fallback_model") {
233
+ logWarn(
234
+ "agent",
235
+ `[${chatId}] ${classified.reason}, falling back to ${decision.fallbackModelId}`,
236
+ );
237
+ resetSession(chatId);
238
+ yield* buildRetryStream(decision.fallbackModelId);
239
+ return { retried: true, classified };
240
+ }
241
+
242
+ // `propagate` — caller should yield an `error` event and return.
243
+ return { retried: false, classified };
244
+ }