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,296 @@
1
+ /**
2
+ * Pool lifecycle + role bindings + observability: init/teardown, role
3
+ * accessors, availability queries, the `/model` snapshot, and change
4
+ * listeners.
5
+ */
6
+
7
+ import type { Backend } from "../../agent-runtime/capabilities.js";
8
+ import type { TalonConfig } from "../../../util/config.js";
9
+ import {
10
+ listBackends,
11
+ type BackendInitContext,
12
+ } from "../../agent-runtime/backend-registry.js";
13
+ import { log, logWarn } from "../../../util/log.js";
14
+ import { roleHolder } from "./holders.js";
15
+ import {
16
+ pool,
17
+ bindings,
18
+ listeners,
19
+ ctx,
20
+ ensurePoolEntry,
21
+ releaseHolderFromEntry,
22
+ resolveRoleBackendId,
23
+ hasBackendPool,
24
+ } from "./state.js";
25
+ import {
26
+ ALL_ROLES,
27
+ type BackendChangeListener,
28
+ type BackendHolder,
29
+ type BackendRole,
30
+ type PoolSnapshot,
31
+ } from "./types.js";
32
+
33
+ export { hasBackendPool };
34
+
35
+ /**
36
+ * Initialise the backend pool with bindings for every role.
37
+ *
38
+ * Reads `config.backend` (chat), `config.heartbeatBackend` (heartbeat,
39
+ * falls back to chat), `config.dreamBackend` (dream, falls back to
40
+ * chat). Identical ids share a pool instance — Talon does NOT spin up
41
+ * two Claude SDK runtimes just because chat and heartbeat both want
42
+ * Claude.
43
+ *
44
+ * If any role's init fails, previously-initialised roles are torn
45
+ * down before the throw bubbles up — so a partial-init state never
46
+ * leaks past bootstrap.
47
+ */
48
+ export async function initBackendPool(
49
+ config: TalonConfig,
50
+ initContext: BackendInitContext,
51
+ ): Promise<void> {
52
+ ctx.initCtx = initContext;
53
+ ctx.poolConfig = config;
54
+ const initialisedHolders: BackendHolder[] = [];
55
+ try {
56
+ for (const role of ALL_ROLES) {
57
+ const holder = roleHolder(role);
58
+ const id = resolveRoleBackendId(role, config);
59
+ const entry = await ensurePoolEntry(id, config);
60
+ entry.holders.add(holder);
61
+ bindings.set(holder, id);
62
+ initialisedHolders.push(holder);
63
+ }
64
+ } catch (err) {
65
+ // Roll back partial init — release every holder that succeeded so
66
+ // the pool isn't left holding orphaned instances.
67
+ for (const holder of initialisedHolders) {
68
+ const id = bindings.get(holder);
69
+ bindings.delete(holder);
70
+ if (id) {
71
+ const entry = pool.get(id);
72
+ if (entry) await releaseHolderFromEntry(entry, holder);
73
+ }
74
+ }
75
+ ctx.initCtx = null;
76
+ throw err;
77
+ }
78
+ const summary = ALL_ROLES.map(
79
+ (r) => `${r}=${bindings.get(roleHolder(r))}`,
80
+ ).join(" ");
81
+ log("backend-controller", `Pool initialised — ${summary}`);
82
+ }
83
+
84
+ /** Backend instance for a role. Throws if the role isn't bound. */
85
+ export function getBackendForRole(role: BackendRole): Backend {
86
+ const holder = roleHolder(role);
87
+ const id = bindings.get(holder);
88
+ if (!id) {
89
+ throw new Error(
90
+ `Backend role "${role}" not bound — initialise the pool or rebind first`,
91
+ );
92
+ }
93
+ const entry = pool.get(id);
94
+ if (!entry) {
95
+ throw new Error(
96
+ `Backend role "${role}" bound to "${id}" but pool entry missing`,
97
+ );
98
+ }
99
+ return entry.backend;
100
+ }
101
+
102
+ /** Backend id bound to a role. Throws if the role isn't bound. */
103
+ export function getBackendIdForRole(role: BackendRole): string {
104
+ const id = bindings.get(roleHolder(role));
105
+ if (!id) {
106
+ throw new Error(`Backend role "${role}" not bound`);
107
+ }
108
+ return id;
109
+ }
110
+
111
+ /** Display label for the role's bound backend. */
112
+ export function getBackendLabelForRole(role: BackendRole): string {
113
+ const id = getBackendIdForRole(role);
114
+ const entry = pool.get(id);
115
+ if (!entry) {
116
+ throw new Error(`Pool entry for "${id}" missing`);
117
+ }
118
+ return entry.label;
119
+ }
120
+
121
+ /**
122
+ * All registered backends, optionally filtered by `config.enabledBackends`.
123
+ *
124
+ * When `config.enabledBackends` is set, only ids on the whitelist are
125
+ * returned (in the whitelist's order). When unset, every registered
126
+ * backend is returned, sorted by id for deterministic output.
127
+ */
128
+ export function listAvailableBackends(
129
+ config?: TalonConfig,
130
+ ): { id: string; label: string }[] {
131
+ const all = listBackends().map((b) => ({ id: b.id, label: b.label }));
132
+ const enabled = config?.enabledBackends;
133
+ if (!enabled || enabled.length === 0) return all;
134
+ const byId = new Map(all.map((b) => [b.id, b]));
135
+ return enabled
136
+ .map((id) => byId.get(id))
137
+ .filter((b): b is { id: string; label: string } => Boolean(b));
138
+ }
139
+
140
+ /** Whether a backend id is registered and currently exposed by config. */
141
+ export function isBackendAvailable(id: string, config?: TalonConfig): boolean {
142
+ return listAvailableBackends(config).some((b) => b.id === id);
143
+ }
144
+
145
+ /**
146
+ * Available backends using the config captured at init — convenience for
147
+ * runtime readers (e.g. the `list_backends` tool) that don't have config
148
+ * plumbed through. Falls back to all registered backends pre-init.
149
+ */
150
+ export function getAvailableBackends(): { id: string; label: string }[] {
151
+ return listAvailableBackends(ctx.poolConfig ?? undefined);
152
+ }
153
+
154
+ /**
155
+ * The live pooled `Backend` instance for an id, or `null` if it isn't currently
156
+ * pooled. Unlike `getBackendForRole`/`getBackendForChat` this never initialises
157
+ * on demand — it only returns backends already spun up (role backends + chat
158
+ * overrides).
159
+ */
160
+ export function getPooledBackend(id: string): Backend | null {
161
+ return pool.get(id)?.backend ?? null;
162
+ }
163
+
164
+ /** Monotonic counter for synthetic transient holders. */
165
+ let transientHolderSeq = 0;
166
+
167
+ /**
168
+ * Acquire a backend instance transiently — boots it on demand if it isn't
169
+ * already pooled, pinned by a synthetic `read:N` holder so a concurrent rebind
170
+ * can't tear it down mid-use. The caller MUST call the returned `release()`,
171
+ * which drops the holder and cleans the instance up iff nothing else holds it
172
+ * (so an already-active backend is left running).
173
+ *
174
+ * Used by read-only callers that need a non-current backend's catalog (e.g.
175
+ * `list_models backend=<other>`) without permanently switching the chat to it.
176
+ */
177
+ export async function acquireBackendInstance(
178
+ id: string,
179
+ ): Promise<{ backend: Backend; release: () => Promise<void> }> {
180
+ if (!ctx.poolConfig) {
181
+ throw new Error(
182
+ "Backend pool not initialised — call initBackendPool first",
183
+ );
184
+ }
185
+ const entry = await ensurePoolEntry(id, ctx.poolConfig);
186
+ const holder = `read:${++transientHolderSeq}` as BackendHolder;
187
+ entry.holders.add(holder);
188
+ let released = false;
189
+ return {
190
+ backend: entry.backend,
191
+ release: async () => {
192
+ if (released) return;
193
+ released = true;
194
+ const e = pool.get(id);
195
+ if (e) await releaseHolderFromEntry(e, holder);
196
+ },
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Validate a persisted model id against the backend that will serve
202
+ * it. Resolves through the catalog's `resolveModelInfo` — exact
203
+ * matches with `selectable !== false` are kept; everything else
204
+ * falls back. Backends without a catalog return `true` (we can't
205
+ * prove the value is stale, so we trust the chat-settings store).
206
+ */
207
+ export async function isModelValidForBackend(
208
+ backend: Backend,
209
+ model: string,
210
+ ): Promise<boolean> {
211
+ const catalog = backend.models;
212
+ if (!catalog) return true;
213
+ const resolution = await catalog.resolveModelInfo(model);
214
+ return resolution.kind === "exact" && resolution.model.selectable !== false;
215
+ }
216
+
217
+ /** Snapshot of the live pool + bindings for `/model` rendering. */
218
+ export function getPoolSnapshot(): PoolSnapshot {
219
+ const instances = [...pool.values()]
220
+ .sort((a, b) => a.id.localeCompare(b.id))
221
+ .map((entry) => {
222
+ const holdersList = [...entry.holders].sort();
223
+ const roles = holdersList
224
+ .filter((h) => h.startsWith("role:"))
225
+ .map((h) => h.slice(5) as BackendRole)
226
+ .sort();
227
+ const chats = holdersList
228
+ .filter((h) => h.startsWith("chat:"))
229
+ .map((h) => h.slice(5))
230
+ .sort();
231
+ return {
232
+ id: entry.id,
233
+ label: entry.label,
234
+ holders: holdersList,
235
+ roles,
236
+ chats,
237
+ };
238
+ });
239
+ const bindingsObj: Partial<Record<BackendRole, string>> = {};
240
+ for (const role of ALL_ROLES) {
241
+ const id = bindings.get(roleHolder(role));
242
+ if (id) bindingsObj[role] = id;
243
+ }
244
+ return {
245
+ instances,
246
+ bindings: bindingsObj as Record<BackendRole, string>,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Register a backend-change listener.
252
+ *
253
+ * Listener receives the holder that changed (`role:chat`, `chat:1234`,
254
+ * etc.), the new backend, and id/label. Fires AFTER the rebind is
255
+ * committed and the new entry is in the pool, but BEFORE the previous
256
+ * entry's cleanup runs (when applicable).
257
+ */
258
+ export function onBackendChange(listener: BackendChangeListener): () => void {
259
+ listeners.add(listener);
260
+ return () => listeners.delete(listener);
261
+ }
262
+
263
+ /** Tear down the entire pool. Idempotent. */
264
+ export async function cleanupBackendPool(): Promise<void> {
265
+ const entries = [...pool.values()];
266
+ pool.clear();
267
+ bindings.clear();
268
+ for (const entry of entries) {
269
+ if (entry.cleanup) {
270
+ try {
271
+ await entry.cleanup();
272
+ } catch (err) {
273
+ logWarn(
274
+ "backend-controller",
275
+ `Pool shutdown cleanup of ${entry.label} failed: ${
276
+ err instanceof Error ? err.message : String(err)
277
+ }`,
278
+ );
279
+ }
280
+ }
281
+ }
282
+ ctx.initCtx = null;
283
+ }
284
+
285
+ /** Test-only state reset. */
286
+ export function resetBackendPoolForTest(): void {
287
+ pool.clear();
288
+ bindings.clear();
289
+ ctx.initCtx = null;
290
+ ctx.poolConfig = null;
291
+ }
292
+
293
+ /** Test-only: drop all listeners. */
294
+ export function clearBackendChangeListenersForTest(): void {
295
+ listeners.clear();
296
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Rebind / release holders, plus the per-chat accessors that resolve a chat
3
+ * to its override (or the chat-role default).
4
+ */
5
+
6
+ import type { Backend } from "../../agent-runtime/capabilities.js";
7
+ import type { TalonConfig } from "../../../util/config.js";
8
+ import {
9
+ getBackend,
10
+ listBackends,
11
+ } from "../../agent-runtime/backend-registry.js";
12
+ import { log, logWarn } from "../../../util/log.js";
13
+ import { chatHolder, roleHolder } from "./holders.js";
14
+ import {
15
+ pool,
16
+ bindings,
17
+ ctx,
18
+ ensurePoolEntry,
19
+ releaseHolderFromEntry,
20
+ notifyListeners,
21
+ hasBackendPool,
22
+ type PoolEntry,
23
+ } from "./state.js";
24
+ import { getBackendForRole, getBackendIdForRole } from "./pool.js";
25
+ import type { BackendHolder, BackendRole, RebindResult } from "./types.js";
26
+
27
+ /**
28
+ * Rebind any holder to a different backend id.
29
+ *
30
+ * Pool semantics:
31
+ * - If the target id is already pooled (another holder uses it),
32
+ * the existing instance is reused — no double init.
33
+ * - The previous binding's instance loses a refcount; cleanup fires
34
+ * only if no other holder still references it.
35
+ * - Failed init leaves the previous binding in place.
36
+ *
37
+ * Use `rebindRole` / `rebindChat` for typed wrappers.
38
+ */
39
+ export async function rebindHolder(
40
+ holder: BackendHolder,
41
+ newId: string,
42
+ config: TalonConfig,
43
+ ): Promise<RebindResult> {
44
+ if (!ctx.initCtx) {
45
+ return { ok: false, error: "Backend pool not initialised" };
46
+ }
47
+ const currentId = bindings.get(holder);
48
+ if (currentId === newId) {
49
+ return {
50
+ ok: false,
51
+ error: `Holder "${holder}" already bound to "${newId}"`,
52
+ };
53
+ }
54
+
55
+ const factory = getBackend(newId);
56
+ if (!factory) {
57
+ const known = listBackends()
58
+ .map((b) => `"${b.id}"`)
59
+ .join(", ");
60
+ return {
61
+ ok: false,
62
+ error: `Unknown backend "${newId}" — known: ${known}`,
63
+ };
64
+ }
65
+
66
+ const newReused = pool.has(newId);
67
+ let newEntry: PoolEntry;
68
+ try {
69
+ newEntry = await ensurePoolEntry(newId, config);
70
+ } catch (err) {
71
+ const msg = err instanceof Error ? err.message : String(err);
72
+ logWarn(
73
+ "backend-controller",
74
+ `rebindHolder(${holder}, ${newId}) init failed: ${msg} — binding unchanged`,
75
+ );
76
+ return { ok: false, error: `Failed to init ${factory.label}: ${msg}` };
77
+ }
78
+ newEntry.holders.add(holder);
79
+ bindings.set(holder, newId);
80
+
81
+ let previousReused = false;
82
+ if (currentId) {
83
+ const oldEntry = pool.get(currentId);
84
+ if (oldEntry) {
85
+ previousReused = oldEntry.holders.size > 1;
86
+ await releaseHolderFromEntry(oldEntry, holder);
87
+ }
88
+ }
89
+
90
+ log(
91
+ "backend-controller",
92
+ `Rebound ${holder}: ${currentId ?? "(unbound)"} → ${newId}` +
93
+ (newReused ? " (reused pool instance)" : ""),
94
+ );
95
+ notifyListeners(holder, newEntry.backend, {
96
+ id: newId,
97
+ label: newEntry.label,
98
+ });
99
+
100
+ return {
101
+ ok: true,
102
+ from: currentId,
103
+ to: newId,
104
+ previousReused,
105
+ newReused,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Release a holder. If the entry it was holding has no other holders,
111
+ * the entry is cleaned up.
112
+ *
113
+ * No-op when the holder isn't bound — useful for "clear my override"
114
+ * flows that don't want to care whether the override was ever set.
115
+ */
116
+ export async function releaseHolder(holder: BackendHolder): Promise<void> {
117
+ const id = bindings.get(holder);
118
+ if (!id) return;
119
+ bindings.delete(holder);
120
+ const entry = pool.get(id);
121
+ if (entry) await releaseHolderFromEntry(entry, holder);
122
+ }
123
+
124
+ /** Rebind a role. Convenience wrapper over `rebindHolder`. */
125
+ export function rebindRole(
126
+ role: BackendRole,
127
+ newId: string,
128
+ config: TalonConfig,
129
+ ): Promise<RebindResult> {
130
+ return rebindHolder(roleHolder(role), newId, config);
131
+ }
132
+
133
+ /**
134
+ * Rebind a per-chat override.
135
+ *
136
+ * Pool refcounts the new backend in addition to whatever the role
137
+ * holders already pin. Releasing the chat (`releaseChat`) reverts the
138
+ * chat to whichever backend the chat-role is bound to.
139
+ */
140
+ export function rebindChat(
141
+ chatId: string,
142
+ newId: string,
143
+ config: TalonConfig,
144
+ ): Promise<RebindResult> {
145
+ return rebindHolder(chatHolder(chatId), newId, config);
146
+ }
147
+
148
+ /** Release a per-chat override — the chat reverts to the global default. */
149
+ export function releaseChat(chatId: string): Promise<void> {
150
+ return releaseHolder(chatHolder(chatId));
151
+ }
152
+
153
+ /**
154
+ * Resolve the backend a chat should use right now.
155
+ *
156
+ * If the chat has an override pooled, return that backend; otherwise
157
+ * fall back to the global chat-role backend.
158
+ */
159
+ export function getBackendForChat(chatId: string): Backend {
160
+ const overrideId = bindings.get(chatHolder(chatId));
161
+ if (overrideId) {
162
+ const entry = pool.get(overrideId);
163
+ if (entry) return entry.backend;
164
+ }
165
+ return getBackendForRole("chat");
166
+ }
167
+
168
+ /** Backend id the chat would resolve to (override → role default). */
169
+ export function getBackendIdForChat(chatId: string): string {
170
+ return bindings.get(chatHolder(chatId)) ?? getBackendIdForRole("chat");
171
+ }
172
+
173
+ /** Whether this chat has an override pinning a non-default backend. */
174
+ export function hasChatBackendOverride(chatId: string): boolean {
175
+ return bindings.has(chatHolder(chatId));
176
+ }
177
+
178
+ /**
179
+ * Safe variant of `getBackendForChat` that never throws — returns
180
+ * the per-chat / role:chat backend when the pool is initialised, or
181
+ * `fallback` (typically `gateway?.backend`) when it isn't.
182
+ *
183
+ * The pool is always initialised in production at bootstrap; this
184
+ * helper exists so unit tests and legacy code paths that wire the
185
+ * frontend before the pool can degrade gracefully instead of
186
+ * throwing on every `/model` command. Frontends should call this
187
+ * everywhere they previously read `gateway?.backend` for /model,
188
+ * /status, and warmSession — otherwise per-chat backend overrides
189
+ * silently lose effect.
190
+ */
191
+ export function resolveChatBackend(
192
+ chatId: string,
193
+ fallback?: Backend | null,
194
+ ): Backend | null {
195
+ if (hasBackendPool()) {
196
+ try {
197
+ return getBackendForChat(chatId);
198
+ } catch {
199
+ // Pool initialised but the chat-role binding is somehow missing —
200
+ // shouldn't happen in practice, but degrade to fallback rather
201
+ // than crash a /model render.
202
+ }
203
+ }
204
+ return fallback ?? null;
205
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Backend-pool shared state + internal helpers.
3
+ *
4
+ * All pool modules import the SAME Map/Set instances and the SAME `ctx`
5
+ * holder from here, so refcounting and rebinds stay coherent across files.
6
+ * `initCtx`/`poolConfig` are reassignable, so they live on the mutable `ctx`
7
+ * object rather than as module-level `let`s (which can't be reassigned from
8
+ * another module).
9
+ */
10
+
11
+ import type { Backend } from "../../agent-runtime/capabilities.js";
12
+ import type { TalonConfig } from "../../../util/config.js";
13
+ import {
14
+ getBackend,
15
+ listBackends,
16
+ type BackendInitContext,
17
+ } from "../../agent-runtime/backend-registry.js";
18
+ import { log, logWarn } from "../../../util/log.js";
19
+ import { roleHolder } from "./holders.js";
20
+ import type { BackendChangeListener, BackendHolder } from "./types.js";
21
+
22
+ export interface PoolEntry {
23
+ id: string;
24
+ label: string;
25
+ backend: Backend;
26
+ cleanup?: () => Promise<void> | void;
27
+ holders: Set<BackendHolder>;
28
+ }
29
+
30
+ /** id → entry. */
31
+ export const pool = new Map<string, PoolEntry>();
32
+ /** holder → backend id (only present for holders currently bound). */
33
+ export const bindings = new Map<BackendHolder, string>();
34
+
35
+ export const listeners = new Set<BackendChangeListener>();
36
+
37
+ /**
38
+ * Reassignable init context, captured at first init so subsequent calls don't
39
+ * need ctx/config plumbed through. On a holder object so other modules can
40
+ * reassign it.
41
+ */
42
+ export const ctx: {
43
+ initCtx: BackendInitContext | null;
44
+ poolConfig: TalonConfig | null;
45
+ } = {
46
+ initCtx: null,
47
+ poolConfig: null,
48
+ };
49
+
50
+ // ── Internal helpers ────────────────────────────────────────────────────────
51
+
52
+ /** Look up or initialise a pool entry for a backend id. */
53
+ export async function ensurePoolEntry(
54
+ id: string,
55
+ config: TalonConfig,
56
+ ): Promise<PoolEntry> {
57
+ const existing = pool.get(id);
58
+ if (existing) return existing;
59
+
60
+ const factory = getBackend(id);
61
+ if (!factory) {
62
+ const known = listBackends()
63
+ .map((b) => `"${b.id}"`)
64
+ .join(", ");
65
+ throw new Error(`Unknown backend "${id}" — known: ${known}`);
66
+ }
67
+ if (!ctx.initCtx) {
68
+ throw new Error(
69
+ "Backend pool not initialised — call initBackendPool first",
70
+ );
71
+ }
72
+ const instance = await factory.init(config, ctx.initCtx);
73
+ const entry: PoolEntry = {
74
+ id: factory.id,
75
+ label: factory.label,
76
+ backend: instance.backend,
77
+ cleanup: instance.cleanup,
78
+ holders: new Set(),
79
+ };
80
+ pool.set(factory.id, entry);
81
+ log("backend-controller", `Pool init: ${factory.label} (${factory.id})`);
82
+ return entry;
83
+ }
84
+
85
+ /** Drop a holder from an entry; clean up the entry if its refcount hits zero. */
86
+ export async function releaseHolderFromEntry(
87
+ entry: PoolEntry,
88
+ holder: BackendHolder,
89
+ ): Promise<void> {
90
+ entry.holders.delete(holder);
91
+ if (entry.holders.size > 0) {
92
+ log(
93
+ "backend-controller",
94
+ `Released ${holder} from ${entry.label} (still held by: ${[...entry.holders].join(", ")})`,
95
+ );
96
+ return;
97
+ }
98
+ pool.delete(entry.id);
99
+ if (entry.cleanup) {
100
+ try {
101
+ await entry.cleanup();
102
+ log("backend-controller", `Pool cleanup: ${entry.label} (${entry.id})`);
103
+ } catch (err) {
104
+ logWarn(
105
+ "backend-controller",
106
+ `Cleanup of ${entry.label} failed: ${
107
+ err instanceof Error ? err.message : String(err)
108
+ }`,
109
+ );
110
+ }
111
+ } else {
112
+ log("backend-controller", `Pool cleanup: ${entry.label} (no cleanup hook)`);
113
+ }
114
+ }
115
+
116
+ export function notifyListeners(
117
+ holder: BackendHolder,
118
+ backend: Backend,
119
+ ctxInfo: { id: string; label: string },
120
+ ): void {
121
+ for (const listener of listeners) {
122
+ try {
123
+ listener(holder, backend, ctxInfo);
124
+ } catch (err) {
125
+ logWarn(
126
+ "backend-controller",
127
+ `Backend-change listener threw: ${err instanceof Error ? err.message : String(err)}`,
128
+ );
129
+ }
130
+ }
131
+ }
132
+
133
+ export function resolveRoleBackendId(
134
+ role: "chat" | "heartbeat" | "dream",
135
+ config: TalonConfig,
136
+ ): string {
137
+ switch (role) {
138
+ case "chat":
139
+ return config.backend;
140
+ case "heartbeat":
141
+ return config.heartbeatBackend ?? config.backend;
142
+ case "dream":
143
+ return config.dreamBackend ?? config.backend;
144
+ }
145
+ }
146
+
147
+ /** Has the pool been initialised? */
148
+ export function hasBackendPool(): boolean {
149
+ return bindings.has(roleHolder("chat"));
150
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Public types for the backend controller / pool.
3
+ */
4
+
5
+ import type { Backend } from "../../agent-runtime/capabilities.js";
6
+
7
+ /** Roles that map to global holders. */
8
+ export type BackendRole = "chat" | "heartbeat" | "dream";
9
+
10
+ export const ALL_ROLES: readonly BackendRole[] = ["chat", "heartbeat", "dream"];
11
+
12
+ /** Holder identifier — opaque string. Use `roleHolder()` / `chatHolder()`. */
13
+ export type BackendHolder = string;
14
+
15
+ export interface RebindResult {
16
+ ok: boolean;
17
+ /** Backend id the holder was bound to before the rebind (omitted when none). */
18
+ from?: string;
19
+ /** Backend id the holder is now bound to. */
20
+ to?: string;
21
+ /** Whether the previous instance stays alive (still bound elsewhere). */
22
+ previousReused?: boolean;
23
+ /** Whether the new binding reuses an existing pool instance. */
24
+ newReused?: boolean;
25
+ /** Human-readable error on failure. */
26
+ error?: string;
27
+ }
28
+
29
+ /** Snapshot of the pool for observability / `/model` rendering. */
30
+ export interface PoolSnapshot {
31
+ /** Active pool entries, sorted by id, with the holders pinning them. */
32
+ instances: Array<{
33
+ id: string;
34
+ label: string;
35
+ /** Free-form holder strings — useful for diagnostics. */
36
+ holders: BackendHolder[];
37
+ /** Role-only subset, kept sorted, for the common case. */
38
+ roles: BackendRole[];
39
+ /** Per-chat overrides (chat ids only). */
40
+ chats: string[];
41
+ }>;
42
+ /** Current role bindings (always all three roles once initialised). */
43
+ bindings: Record<BackendRole, string>;
44
+ }
45
+
46
+ /** Notified after a successful rebind. */
47
+ export type BackendChangeListener = (
48
+ holder: BackendHolder,
49
+ backend: Backend,
50
+ ctx: { id: string; label: string },
51
+ ) => void;