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
@@ -1,9 +1,14 @@
1
1
  /**
2
- * Main message handler — executes a user query through the Claude Agent SDK.
2
+ * Claude SDK chat-turn handler — natively emits `AgentEvent`s.
3
3
  *
4
- * Orchestrates the full lifecycle: prompt formatting, SDK query, stream
5
- * processing, error recovery (session expired / context overflow / model
6
- * fallback), token accounting, and session persistence.
4
+ * Drives the full lifecycle: prompt formatting, SDK query, native
5
+ * event emission per stream message, error recovery (session expired
6
+ * / context overflow / model fallback via `applyRetryDecisionStream`),
7
+ * token accounting, session persistence, and the flow-violation
8
+ * re-prompt loop.
9
+ *
10
+ * The exported async generator `runChatTurn` is what the factory wires
11
+ * onto `ChatBackend.runChatTurn` — no wrapper, no callback shim.
7
12
  */
8
13
 
9
14
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -11,27 +16,35 @@ import {
11
16
  getSession,
12
17
  incrementTurns,
13
18
  recordUsage,
14
- resetSession,
15
19
  setSessionId,
16
20
  setSessionName,
21
+ updateLiveTurn,
17
22
  } from "../../storage/sessions.js";
18
- import { getChatSettings, setChatModel } from "../../storage/chat-settings.js";
19
- import { classify } from "../../core/errors.js";
20
23
  import { log, logError, logWarn } from "../../util/log.js";
21
24
  import { traceMessage } from "../../util/trace.js";
22
- import { incrementCounter, recordHistogram } from "../../util/metrics.js";
23
- import { isTurnTerminator, stripMcpPrefix } from "../../core/tools/index.js";
25
+ import { incrementCounter } from "../../util/metrics.js";
26
+ import { isTurnTerminator } from "../../core/tools/index.js";
24
27
 
25
28
  import type { Query } from "@anthropic-ai/claude-agent-sdk";
26
- import type { QueryParams, QueryResult } from "../../core/types.js";
29
+ import {
30
+ type AgentEvent,
31
+ classifiedToAgentError,
32
+ } from "../../core/agent-runtime/events.js";
33
+ import type { ChatRunParams } from "../../core/agent-runtime/capabilities.js";
34
+ import { makeBareModelRef } from "../../core/agent-runtime/model-ref.js";
35
+ import { applyRetryDecisionStream } from "../shared/handle-retry.js";
27
36
  import { getConfig } from "./state.js";
28
- import { buildSdkOptions } from "./options.js";
37
+ import { buildSdkOptions, getActiveFrontends } from "./options.js";
38
+ import { waitForMcpServersReady } from "./mcp-ready.js";
39
+ import { frontendsForChat } from "../shared/frontends.js";
29
40
  import {
30
41
  createStreamState,
31
42
  isSystemInit,
32
43
  isStreamEvent,
33
44
  isAssistant,
34
45
  isResult,
46
+ isUserMessage,
47
+ extractToolResults,
35
48
  processStreamDelta,
36
49
  processAssistantMessage,
37
50
  processResultMessage,
@@ -41,11 +54,46 @@ import {
41
54
  prepareSystemPrompt,
42
55
  extractSessionName,
43
56
  detectFlowViolation,
57
+ FLOW_VIOLATION_MAX_RETRIES,
44
58
  captureDeliveredText,
45
- classifyRetry,
46
59
  summarizeUsage,
60
+ buildDeliveryContract,
61
+ buildFlowViolationReminder,
62
+ buildFirstTurnReminder,
63
+ recordToolCall,
64
+ recordTurnMetrics,
65
+ recordFailedTurnAccounting,
66
+ recordFlowViolation,
47
67
  } from "../shared/index.js";
48
68
 
69
+ // ── Post-result watchdog ────────────────────────────────────────────────────
70
+ // The SDK's PostToolBatch hook is the canonical loop-terminator — it returns
71
+ // `{ continue: false }` after `end_turn`/`send`, and the SDK is supposed to
72
+ // emit a `result` SDKMessage and close the async iterator immediately after.
73
+ // In practice (observed 2026-05-19 14:52Z, chat 352042062, contextTokens=251464,
74
+ // numApiCalls=50) the SDK can emit `result` and then ghost — the for-await loop
75
+ // stays parked forever, holding the dispatcher context and the typing-indicator
76
+ // pulse for hours until someone manually `/restart`s.
77
+ //
78
+ // Workaround: arm a short timer the moment `result` is processed. If the
79
+ // iterator hasn't closed by the grace deadline, abort the controller and
80
+ // force-close the generator via `qi.return()`. The clean-exit case clears the
81
+ // timer in the same turn and pays nothing.
82
+
83
+ const DEFAULT_SDK_POST_RESULT_GRACE_MS = 5_000;
84
+
85
+ function envMs(name: string, fallback: number): number {
86
+ const raw = process.env[name];
87
+ if (!raw) return fallback;
88
+ const n = Number(raw);
89
+ return Number.isFinite(n) && n > 0 ? n : fallback;
90
+ }
91
+
92
+ const SDK_POST_RESULT_GRACE_MS = envMs(
93
+ "TALON_SDK_POST_RESULT_GRACE_MS",
94
+ DEFAULT_SDK_POST_RESULT_GRACE_MS,
95
+ );
96
+
49
97
  // ── Active query store ──────────────────────────────────────────────────────
50
98
  // Holds the Query reference for each in-flight chat so gateway actions
51
99
  // (e.g. reload_plugins) can call control methods like setMcpServers().
@@ -53,62 +101,179 @@ import {
53
101
  const activeQueries = new Map<string, Query>();
54
102
 
55
103
  /** Get the active Query for a chat, if one is in flight. */
104
+ /**
105
+ * Best-effort graceful interrupt of a chat's in-flight turn. Uses the SDK's
106
+ * native `Query.interrupt()`, which stops the agent loop and closes the stream
107
+ * with a `result` (subtype `interrupt`) — so the turn ends as a normal
108
+ * completion (turn_end + usage), NOT an error, and never trips the
109
+ * model-fallback retry path. No-op (returns false) when no turn is running.
110
+ */
111
+ export async function interruptChatTurn(chatId: string): Promise<boolean> {
112
+ const qi = activeQueries.get(chatId);
113
+ if (!qi) return false;
114
+ try {
115
+ await qi.interrupt();
116
+ log("agent", `[${chatId}] turn interrupted by user`);
117
+ incrementCounter("sdk.turn_interrupted");
118
+ return true;
119
+ } catch (err) {
120
+ logWarn(
121
+ "agent",
122
+ `[${chatId}] interrupt failed: ${err instanceof Error ? err.message : err}`,
123
+ );
124
+ return false;
125
+ }
126
+ }
127
+
56
128
  export function getActiveQuery(chatId: string): Query | undefined {
57
129
  return activeQueries.get(chatId);
58
130
  }
59
131
 
60
- // ── Main handler ─────────────────────────────────────────────────────────────
132
+ // ── Internal state passed across recursive retry calls ──────────────────────
133
+
134
+ type InternalState = { flowRetries?: number; errorRetried?: boolean };
61
135
 
62
- export async function handleMessage(
63
- params: QueryParams,
64
- _retried = false,
65
- ): Promise<QueryResult> {
136
+ // ── Main chat-turn generator ────────────────────────────────────────────────
137
+
138
+ /**
139
+ * Native chat-turn generator. Yields the canonical
140
+ * `run_started → text_delta* → reasoning* → assistant_message* →
141
+ * tool_call* → usage → completed` sequence. On error: emits an
142
+ * `error` event (after running the shared retry decision, which may
143
+ * recurse via `yield*` and produce the retry's event stream
144
+ * transparently). On flow violation: `yield* runChatTurn(retry
145
+ * params)` — the recursive call owns its `incrementTurns`, the
146
+ * caller deliberately doesn't increment.
147
+ */
148
+ export async function* runChatTurn(
149
+ params: ChatRunParams,
150
+ _internal: InternalState = {},
151
+ ): AsyncIterable<AgentEvent> {
66
152
  const config = getConfig();
67
153
 
68
- const {
69
- chatId,
70
- text,
71
- senderName,
72
- isGroup,
73
- onTextBlock,
74
- onStreamDelta,
75
- onToolUse,
76
- } = params;
154
+ const { chatId, text, senderName, isGroup } = params;
77
155
  const session = getSession(chatId);
78
156
  const t0 = Date.now();
79
157
 
80
- // Rebuild system prompt on first turn of a new/reset session so identity,
81
- // memory, and workspace listing are fresh. `prepareSystemPrompt` does
82
- // this in place (mutates config.systemPrompt) — the Claude SDK reads
83
- // from config.systemPrompt later via `buildSdkOptions`, so the rebuild
84
- // has to land before that call.
85
- prepareSystemPrompt({ config, previousTurns: session.turns });
158
+ // The chat's OWNING messaging frontend (falling back to the primary
159
+ // for cross-surface chats). Drives the delivery-contract suffix and
160
+ // the frontend-aware flow-violation text — the tool NAMES differ per
161
+ // frontend (native's send tool is send_message, telegram's is send),
162
+ // and with tool servers scoped per chat the wrong contract would
163
+ // instruct a tool that doesn't exist. Empty in terminal mode, where
164
+ // no delivery tools exist and the strict tool-only contract must not
165
+ // be asserted.
166
+ const frontend: string | undefined = frontendsForChat(
167
+ chatId,
168
+ getActiveFrontends(),
169
+ )[0];
170
+
171
+ // Frozen per-session prompt (keyed by session epoch) — stable across
172
+ // turns so the provider's prompt-cache prefix survives other chats'
173
+ // session resets. See backend/shared/system-prompt.ts. The delivery
174
+ // contract joins as the backend suffix — the tail of the static
175
+ // prompt, the highest-salience spot — so models see the tool-only
176
+ // flow before their first turn instead of discovering it via a
177
+ // [FLOW VIOLATION] retry.
178
+ const preparedPrompt = prepareSystemPrompt({
179
+ config,
180
+ previousTurns: session.turns,
181
+ chatId,
182
+ sessionEpoch: session.createdAt,
183
+ backendSuffix: frontend
184
+ ? buildDeliveryContract("tool-only", frontend)
185
+ : undefined,
186
+ });
86
187
 
87
- const { options, activeModel } = buildSdkOptions(chatId);
188
+ const abortController = new AbortController();
189
+ const { options, activeModel } = buildSdkOptions(
190
+ chatId,
191
+ abortController,
192
+ params.model.id,
193
+ preparedPrompt,
194
+ );
88
195
 
89
- const prompt = formatUserPrompt({
196
+ let prompt = formatUserPrompt({
90
197
  text,
91
198
  senderName: senderName ?? "user",
92
199
  isGroup,
93
200
  messageId: params.messageId,
94
201
  });
202
+ // First turn of a session is where flow violations cluster — the
203
+ // model hasn't seen the contract in action yet. One line appended to
204
+ // the turn-0 user message (never the system prompt, so the cached
205
+ // prefix is untouched) pre-empts the 2x-token violation retry.
206
+ // Skipped on flow retries: those already carry the full reminder.
207
+ if (frontend && session.turns === 0 && !_internal.flowRetries) {
208
+ prompt += `\n\n${buildFirstTurnReminder(frontend)}`;
209
+ }
95
210
  log("agent", `[${chatId}] <- (${text.length} chars)`);
96
211
  traceMessage(chatId, "in", text, { senderName, isGroup });
97
212
 
213
+ yield { type: "run_started" };
214
+
98
215
  const qi = query({ prompt, options });
99
216
  activeQueries.set(chatId, qi);
217
+
218
+ // Cold-start delivery-tool race. The Claude SDK spawns a fresh subprocess
219
+ // per turn, and each reconnects to the hub's `${frontend}-tools` server.
220
+ // `alwaysLoad` is meant to block startup until that server connects, but on
221
+ // the FIRST turn of a freshly-opened chat the hub binding is created cold and
222
+ // can still be `pending` when the model builds its turn-1 tool list — so
223
+ // `end_turn`/`send` are absent and the reply silently fails (or the model
224
+ // burns a `tool_search` round-trip to recover it). After a close+reconnect
225
+ // the hub binding is warm, connects instantly, and the tools are present —
226
+ // which is exactly why the bug "heals" on reconnect. Explicitly wait (bounded,
227
+ // non-throwing) for the delivery server to leave `pending` before consuming
228
+ // the stream. This mirrors the proven `refreshTools` gate and returns
229
+ // immediately on warm turns, so it adds no steady-state latency.
230
+ if (frontend) {
231
+ await waitForMcpServersReady(qi, [`${frontend}-tools`], 5_000, 100);
232
+ }
233
+
100
234
  const state = createStreamState();
235
+ // tool_use id → tool name for calls we've announced this turn; lets
236
+ // the tool_result emission name the tool without re-parsing blocks.
237
+ const pendingTools = new Map<string, string>();
238
+
239
+ // Per-API-call usage accumulator for live mid-turn stats. Each
240
+ // assistant message carries its API call's usage as it lands; the
241
+ // authoritative per-turn totals still come from the final result
242
+ // message (processResultMessage) — this only feeds the live-turn
243
+ // overlay so /status moves while a long agentic turn runs, and the
244
+ // failure path below so an errored turn's burn isn't lost.
245
+ const liveAcc = {
246
+ input: 0,
247
+ output: 0,
248
+ cacheRead: 0,
249
+ cacheWrite: 0,
250
+ calls: 0,
251
+ };
252
+
253
+ let postResultTimer: ReturnType<typeof setTimeout> | null = null;
254
+ let postResultForceClosed = false;
255
+ const armPostResultWatchdog = (): void => {
256
+ if (postResultTimer) return;
257
+ const t = setTimeout(() => {
258
+ postResultForceClosed = true;
259
+ logWarn(
260
+ "agent",
261
+ `[${chatId}] SDK iterator stuck ${SDK_POST_RESULT_GRACE_MS}ms after result — aborting`,
262
+ );
263
+ incrementCounter("sdk.iterator_force_close_after_result");
264
+ try {
265
+ abortController.abort();
266
+ } catch {
267
+ /* abort() can throw if already aborted — ignore */
268
+ }
269
+ qi.return(undefined).catch(() => {
270
+ /* the generator may already be in a terminal state — ignore */
271
+ });
272
+ }, SDK_POST_RESULT_GRACE_MS);
273
+ t.unref();
274
+ postResultTimer = t;
275
+ };
101
276
 
102
- // Capture text args from delivery tools (`end_turn`, `send(type="text")`)
103
- // so the end-of-turn trailing-text fallback can dedupe against content
104
- // already delivered. Without this, a model that writes prose AND calls a
105
- // delivery tool with similar text would surface twice in the chat.
106
- //
107
- // Tool names arrive MCP-prefixed (e.g. `mcp__telegram-tools__end_turn`)
108
- // when routed through MCP — strip the prefix so equality checks match
109
- // the registry's bare names.
110
- // `captureDeliveredText` (from shared/) returns the normalized text
111
- // norm — push it into state for the post-turn dedup check.
112
277
  const captureIntoState = (
113
278
  toolName: string,
114
279
  input: Record<string, unknown>,
@@ -117,137 +282,223 @@ export async function handleMessage(
117
282
  if (norm) state.deliveredTextNorms.push(norm);
118
283
  };
119
284
 
285
+ let propagateError: AgentEvent | null = null;
120
286
  try {
121
287
  for await (const message of qi) {
122
- // Session ID capture
123
288
  if (isSystemInit(message)) {
124
289
  state.newSessionId = message.session_id;
125
290
  continue;
126
291
  }
127
292
 
128
- // Stream text deltas and thinking deltas
129
293
  if (isStreamEvent(message)) {
130
- processStreamDelta(message, state, onStreamDelta);
294
+ const emit = processStreamDelta(message, state);
295
+ if (emit) {
296
+ if (emit.phase === "text") {
297
+ yield { type: "text_delta", text: emit.text };
298
+ } else {
299
+ yield { type: "reasoning", text: emit.text };
300
+ }
301
+ }
131
302
  continue;
132
303
  }
133
304
 
134
- // Complete assistant message — extract text blocks and tool calls
135
305
  if (isAssistant(message)) {
136
306
  const result = processAssistantMessage(message, state);
137
-
138
- // Track the trailing text from this assistant message. Multiple
139
- // assistant messages can fire per turn (one per tool-use round-trip);
140
- // only the LAST one's trailingText is the user-facing final reply.
141
307
  state.lastTrailingText = result.trailingText;
142
308
 
143
- // Notify tool usage + capture delivery-tool text for end-of-turn dedup
309
+ const u = message.message.usage;
310
+ if (u) {
311
+ liveAcc.input += u.input_tokens ?? 0;
312
+ liveAcc.output += u.output_tokens ?? 0;
313
+ liveAcc.cacheRead += u.cache_read_input_tokens ?? 0;
314
+ liveAcc.cacheWrite += u.cache_creation_input_tokens ?? 0;
315
+ liveAcc.calls += 1;
316
+ updateLiveTurn(chatId, {
317
+ inputTokens: liveAcc.input,
318
+ outputTokens: liveAcc.output,
319
+ cacheRead: liveAcc.cacheRead,
320
+ cacheWrite: liveAcc.cacheWrite,
321
+ // This call's full prompt = current context fill.
322
+ contextTokens:
323
+ (u.input_tokens ?? 0) +
324
+ (u.cache_read_input_tokens ?? 0) +
325
+ (u.cache_creation_input_tokens ?? 0),
326
+ contextWindow: state.contextWindow ?? 0,
327
+ numApiCalls: liveAcc.calls,
328
+ });
329
+ }
330
+
331
+ // Emit progress text segments BEFORE the tool calls they
332
+ // precede, so a model that says "let me check…" then calls a
333
+ // tool delivers the explanatory text first.
334
+ for (const progress of result.progressTexts) {
335
+ yield { type: "assistant_message", text: progress };
336
+ }
337
+
144
338
  for (const tool of result.tools) {
145
- incrementCounter(`tool_calls.${stripMcpPrefix(tool.name)}`);
339
+ recordToolCall(tool.name, "claude");
146
340
  captureIntoState(tool.name, tool.input);
147
- // Pass tool.input so the soft-terminator opt-out (e.g. react
148
- // with `end_turn: false`) keeps state.turnTerminated correctly
149
- // false — otherwise the trailing-text dedup path mis-treats a
150
- // mid-turn react as the final delivery.
151
341
  if (isTurnTerminator(tool.name, tool.input)) {
152
342
  state.turnTerminated = true;
153
343
  }
154
- if (onToolUse) {
155
- try {
156
- onToolUse(tool.name, tool.input);
157
- } catch {
158
- /* non-fatal */
159
- }
160
- }
344
+ // Use the SDK's tool_use block id so the later tool_result
345
+ // (same id) correlates — a UI spinner opened on this event
346
+ // can only be closed by an event carrying the same id.
347
+ const toolId =
348
+ tool.id ||
349
+ `${tool.name}-${Date.now()}-${Math.random()
350
+ .toString(36)
351
+ .slice(2, 8)}`;
352
+ pendingTools.set(toolId, tool.name);
353
+ yield {
354
+ type: "tool_call",
355
+ id: toolId,
356
+ name: tool.name,
357
+ input: tool.input,
358
+ };
161
359
  }
360
+ continue;
361
+ }
162
362
 
163
- // Send progress text segments (text before each tool call) in order
164
- if (onTextBlock) {
165
- for (const text of result.progressTexts) {
166
- try {
167
- await onTextBlock(text);
168
- } catch {
169
- /* non-fatal don't abort the stream loop */
170
- }
171
- }
363
+ // Tool executions come back as tool_result blocks on synthetic
364
+ // user messages. Emit the matching tool_result event — the other
365
+ // half of the lifecycle a tool_call opens (frontends show a
366
+ // spinner until it arrives).
367
+ if (isUserMessage(message)) {
368
+ for (const tr of extractToolResults(message)) {
369
+ const name = pendingTools.get(tr.toolUseId);
370
+ if (!name) continue; // not a tool we announced (subagent, replay)
371
+ pendingTools.delete(tr.toolUseId);
372
+ yield {
373
+ type: "tool_result",
374
+ id: tr.toolUseId,
375
+ name,
376
+ ...(tr.error ? { error: tr.error } : {}),
377
+ };
172
378
  }
173
-
174
- // Turn-terminator detection happens here (sets `state.turnTerminated`
175
- // for the flow-violation check below) but the actual SDK loop exit
176
- // is owned by the `PostToolBatch` hook in `options.ts`. The hook
177
- // fires after every tool in the batch has resolved, returns
178
- // `{ continue: false }`, and the SDK exits with TerminalReason
179
- // `'hook_stopped'` — no extra "wrap up after end_turn" round-trip,
180
- // no phantom typing, no token spend on a stop_turn.
181
- //
182
- // Historical note: an earlier implementation called `qi.interrupt()`
183
- // here directly. That raced with in-flight MCP tool dispatches in
184
- // the same assistant message — `end_turn` itself is an MCP tool,
185
- // and the model frequently emits sibling tool_use blocks alongside
186
- // it. `interrupt()` cancelled their AbortController mid-flight,
187
- // surfacing as `MCP error -32001: AbortError` and bubbling up as
188
- // "Something went wrong". The `PostToolBatch` hook avoids the race
189
- // by definition (it fires once the entire batch has resolved).
190
379
  continue;
191
380
  }
192
381
 
193
- // Final result — read token counts and context info
194
382
  if (isResult(message)) {
195
383
  processResultMessage(message, state, options.model ?? activeModel);
384
+ armPostResultWatchdog();
196
385
  }
197
386
  }
198
- } catch (err) {
199
- const classified = classify(err);
200
- incrementCounter(`errors.${classified.reason ?? "unknown"}`);
201
-
202
- const decision = classifyRetry({
203
- error: classified,
204
- activeModel,
205
- retried: _retried,
206
- });
207
387
 
208
- if (decision.kind === "reset_and_retry") {
209
- logWarn(
210
- "agent",
211
- `[${chatId}] ${decision.reason}, resetting session and retrying`,
212
- );
213
- resetSession(chatId);
214
- return handleMessage(params, true);
388
+ // The SDK doesn't throw on API errors — it converts them into a
389
+ // synthetic assistant message and finishes the turn with an error-
390
+ // flagged result (usage limits, 429s, auth failures all land here).
391
+ // Rethrow the captured error text so this turn takes the SAME path
392
+ // as a thrown SDK error: retry decision, failed-turn accounting, and
393
+ // an `error` event carrying the real message — instead of being
394
+ // treated as a normal reply (which, with no delivery tool call,
395
+ // would trip the flow-violation re-prompt loop and burn more turns
396
+ // against an already-exhausted limit).
397
+ if (state.resultErrorText) {
398
+ throw new Error(state.resultErrorText);
215
399
  }
216
-
217
- if (decision.kind === "fallback_model") {
218
- logWarn(
219
- "agent",
220
- `[${chatId}] ${classified.reason}, falling back to ${decision.fallbackModelId}`,
221
- );
222
- resetSession(chatId);
223
- const originalModel = getChatSettings(chatId).model;
224
- setChatModel(chatId, decision.fallbackModelId);
225
- try {
226
- return await handleMessage(params, true);
227
- } finally {
228
- setChatModel(chatId, originalModel);
400
+ } catch (err) {
401
+ if (!postResultForceClosed) {
402
+ const buildRetryStream = (
403
+ fallbackModelId?: string,
404
+ ): AsyncIterable<AgentEvent> =>
405
+ runChatTurn(
406
+ fallbackModelId
407
+ ? {
408
+ ...params,
409
+ model: makeBareModelRef(
410
+ params.model.backend,
411
+ fallbackModelId,
412
+ "fallback",
413
+ ),
414
+ }
415
+ : params,
416
+ { ..._internal, errorRetried: true },
417
+ );
418
+ const { retried, classified } = yield* applyRetryDecisionStream({
419
+ err,
420
+ chatId,
421
+ activeModel,
422
+ retried: _internal.errorRetried ?? false,
423
+ buildRetryStream,
424
+ // No backendLabel — historical claude-sdk log shape was un-prefixed.
425
+ });
426
+ if (retried) {
427
+ // The recursive stream already yielded its own usage + completed.
428
+ return;
229
429
  }
430
+ logError("agent", `[${chatId}] SDK error: ${classified.message}`);
431
+ // Defer the actual yield until after the `finally` cleanup runs so the
432
+ // watchdog timer and activeQueries entry are released first.
433
+ propagateError = {
434
+ type: "error",
435
+ error: classifiedToAgentError(classified),
436
+ };
230
437
  }
231
-
232
- logError("agent", `[${chatId}] SDK error: ${classified.message}`);
233
- throw classified;
234
438
  } finally {
439
+ if (postResultTimer) {
440
+ clearTimeout(postResultTimer);
441
+ postResultTimer = null;
442
+ }
235
443
  if (activeQueries.get(chatId) === qi) {
236
444
  activeQueries.delete(chatId);
237
445
  }
238
446
  }
239
447
 
448
+ if (propagateError) {
449
+ // Terminal failure — account for whatever the turn consumed before
450
+ // dying (failed turns burn real tokens). The result message never
451
+ // arrived, so state.sdk* is usually empty; fall back to the per-call
452
+ // accumulator. Retried turns return earlier and never reach here.
453
+ const sawResultUsage =
454
+ state.sdkInputTokens +
455
+ state.sdkOutputTokens +
456
+ state.sdkCacheRead +
457
+ state.sdkCacheWrite >
458
+ 0;
459
+ recordFailedTurnAccounting({
460
+ backend: "claude",
461
+ chatId,
462
+ durationMs: Date.now() - t0,
463
+ toolCalls: state.toolCalls,
464
+ apiCalls: state.numApiCalls || liveAcc.calls,
465
+ model: activeModel,
466
+ usage: sawResultUsage
467
+ ? {
468
+ inputTokens: state.sdkInputTokens,
469
+ outputTokens: state.sdkOutputTokens,
470
+ cacheRead: state.sdkCacheRead,
471
+ cacheWrite: state.sdkCacheWrite,
472
+ }
473
+ : {
474
+ inputTokens: liveAcc.input,
475
+ outputTokens: liveAcc.output,
476
+ cacheRead: liveAcc.cacheRead,
477
+ cacheWrite: liveAcc.cacheWrite,
478
+ },
479
+ contextTokens: state.contextTokens,
480
+ contextWindow: state.contextWindow,
481
+ });
482
+ yield propagateError;
483
+ return;
484
+ }
485
+
240
486
  // ── Persist session and usage ─────────────────────────────────────────────
241
487
 
242
488
  const durationMs = Date.now() - t0;
243
- recordHistogram("response_latency_ms", durationMs);
244
- incrementCounter("queries_total");
489
+ recordTurnMetrics({
490
+ backend: "claude",
491
+ durationMs,
492
+ toolCalls: state.toolCalls,
493
+ apiCalls: state.numApiCalls,
494
+ usage: {
495
+ inputTokens: state.sdkInputTokens,
496
+ outputTokens: state.sdkOutputTokens,
497
+ cacheRead: state.sdkCacheRead,
498
+ cacheWrite: state.sdkCacheWrite,
499
+ },
500
+ });
245
501
  if (state.newSessionId) setSessionId(chatId, state.newSessionId);
246
- // Token usage is recorded for THIS attempt unconditionally — the running
247
- // session totals are additive, so a flow-violation retry that recurses
248
- // through this same path will record its own tokens on top. The turn
249
- // counter, in contrast, must only increment ONCE per user message (see
250
- // the post-violation block below).
251
502
  recordUsage(chatId, {
252
503
  inputTokens: state.sdkInputTokens,
253
504
  outputTokens: state.sdkOutputTokens,
@@ -260,53 +511,64 @@ export async function handleMessage(
260
511
  numApiCalls: state.numApiCalls,
261
512
  });
262
513
 
263
- // Set a descriptive session name from the first message
264
- if (session.turns === 0 && text) {
514
+ // Set a descriptive session name from the first message.
515
+ // Guard against flow-violation retries, which pass the reminder text as
516
+ // `text` — we only want the original user message, not the synthetic prompt.
517
+ if (session.turns === 0 && text && !_internal.flowRetries) {
265
518
  const name = extractSessionName(text);
266
519
  if (name) setSessionName(chatId, name);
267
520
  }
268
521
 
269
522
  // ── Trailing-prose contract + flow-violation retry ──────────────────────
270
- // The output stream is private scratchpad by design. Final replies must go
271
- // through `end_turn` (canonical) or `send` (mid-turn rich content). The
272
- // shared `detectFlowViolation` decides whether trailing prose constitutes
273
- // a missed delivery (and whether to re-prompt the model once with the
274
- // synthetic reminder).
275
- //
276
- // `incrementTurns` is deferred until AFTER this check so the retry path
277
- // (which recurses through `handleMessage` and hits its own
278
- // `incrementTurns` at the end of that call) doesn't double-count a
279
- // single user message as two turns.
280
- const violation = detectFlowViolation({
281
- trailingText: state.lastTrailingText,
282
- turnTerminated: state.turnTerminated,
283
- deliveredTextNorms: state.deliveredTextNorms,
284
- retried: _retried,
285
- });
523
+
524
+ // Only messaging frontends have a tool-only delivery contract to enforce.
525
+ // In terminal mode (`frontend` undefined) there are no delivery tools and
526
+ // trailing prose IS the reply the terminal renderer surfaces it via
527
+ // `result.text` when nothing came through the bridge (see
528
+ // frontend/terminal/index.ts). Running the check there would flag the
529
+ // model's natural prose as a violation and re-prompt it to call `end_turn`,
530
+ // a tool that mode doesn't even register. Mirrors the `frontend`-gated
531
+ // delivery-contract suffix above.
532
+ const violation = frontend
533
+ ? detectFlowViolation({
534
+ trailingText: state.lastTrailingText,
535
+ turnTerminated: state.turnTerminated,
536
+ deliveredTextNorms: state.deliveredTextNorms,
537
+ toolCalls: state.toolCalls,
538
+ retried: (_internal.flowRetries ?? 0) > 0,
539
+ retryCount: _internal.flowRetries ?? 0,
540
+ maxRetries: FLOW_VIOLATION_MAX_RETRIES,
541
+ reminder: buildFlowViolationReminder(frontend),
542
+ })
543
+ : ({ violated: false } as const);
286
544
 
287
545
  if (violation.violated) {
288
- incrementCounter("scratchpad.trailing_text_dropped");
546
+ recordFlowViolation(violation.shouldRetry ? "retried" : "cap_exhausted");
289
547
  log(
290
548
  "agent",
291
- `[${chatId}] flow violation: trailing prose (${violation.trailing.length} chars) without end_turn/send. ${
549
+ `[${chatId}] flow violation: ${violation.reason}. ${
292
550
  violation.shouldRetry
293
551
  ? "Re-prompting with reminder."
294
- : "Already retried — accepting silent drop."
552
+ : `Retry cap (${FLOW_VIOLATION_MAX_RETRIES}) exhausted — accepting silent drop.`
295
553
  }`,
296
554
  );
297
555
 
298
556
  if (violation.shouldRetry) {
299
- incrementCounter("scratchpad.flow_violation_retried");
300
- // The recursive call owns the `incrementTurns` for this user message.
301
- // We deliberately don't increment here.
302
- return handleMessage({ ...params, text: violation.reminder }, true);
557
+ yield* runChatTurn(
558
+ { ...params, text: violation.reminder },
559
+ {
560
+ ..._internal,
561
+ flowRetries: (_internal.flowRetries ?? 0) + 1,
562
+ },
563
+ );
564
+ return;
303
565
  }
304
566
  }
305
567
 
306
568
  // Reached the non-retry path — this turn counts as one user-visible turn.
307
569
  incrementTurns(chatId);
308
570
 
309
- // ── Build result ──────────────────────────────────────────────────────────
571
+ // ── Build result events ──────────────────────────────────────────────────
310
572
 
311
573
  state.allResponseText += state.currentBlockText;
312
574
 
@@ -332,12 +594,21 @@ export async function handleMessage(
332
594
  model: activeModel,
333
595
  });
334
596
 
335
- return {
336
- text: state.allResponseText.trim(),
337
- durationMs,
597
+ const usage = {
338
598
  inputTokens: state.sdkInputTokens,
339
599
  outputTokens: state.sdkOutputTokens,
340
600
  cacheRead: state.sdkCacheRead,
341
601
  cacheWrite: state.sdkCacheWrite,
602
+ modelId: activeModel,
603
+ };
604
+ yield { type: "usage", usage };
605
+ yield {
606
+ type: "completed",
607
+ result: {
608
+ text: state.allResponseText.trim(),
609
+ durationMs,
610
+ usage,
611
+ modelId: activeModel,
612
+ },
342
613
  };
343
614
  }