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
@@ -12,11 +12,13 @@ import type {
12
12
  SDKPartialAssistantMessage,
13
13
  SDKAssistantMessage,
14
14
  SDKResultMessage,
15
+ SDKUserMessage,
15
16
  ModelUsage,
16
17
  } from "@anthropic-ai/claude-agent-sdk";
17
18
  import type { BetaRawContentBlockDeltaEvent } from "@anthropic-ai/sdk/resources/beta/messages/messages.mjs";
18
19
  import { STREAM_INTERVAL } from "./constants.js";
19
20
  import { log } from "../../util/log.js";
21
+ import { checkModelDrift } from "./model-drift.js";
20
22
 
21
23
  // ── Stream state accumulator ────────────────────────────────────────────────
22
24
 
@@ -61,6 +63,29 @@ export type StreamState = {
61
63
  * may have appeared in the same assistant message before the terminator.
62
64
  */
63
65
  turnTerminated: boolean;
66
+ /**
67
+ * Per-token text chunks accumulated since the last throttled flush.
68
+ * Drained into a `text_delta` event when `processStreamDelta` decides
69
+ * the throttle interval has elapsed.
70
+ */
71
+ unflushedTextDelta: string;
72
+ /**
73
+ * Same as `unflushedTextDelta` but for thinking-phase tokens — drained
74
+ * into a `reasoning` event. Tracked separately so a thinking burst
75
+ * doesn't poison the visible-text delta buffer.
76
+ */
77
+ unflushedThinkingDelta: string;
78
+ /**
79
+ * Set when the SDK's final result message reported a failed turn —
80
+ * either an error subtype (`error_during_execution`, `error_max_turns`,
81
+ * …) or `is_error: true` on a `success` result. The latter is how API
82
+ * errors surface: the SDK converts them (usage limits, 429s, auth
83
+ * failures) into a synthetic assistant message and finishes the turn
84
+ * "successfully" with the error text in `result` — it never throws.
85
+ * Carries the best error text available so the handler can fail the
86
+ * turn with the REAL message instead of treating it as a normal reply.
87
+ */
88
+ resultErrorText: string | undefined;
64
89
  };
65
90
 
66
91
  export function createStreamState(): StreamState {
@@ -80,6 +105,9 @@ export function createStreamState(): StreamState {
80
105
  lastTrailingText: "",
81
106
  deliveredTextNorms: [],
82
107
  turnTerminated: false,
108
+ unflushedTextDelta: "",
109
+ unflushedThinkingDelta: "",
110
+ resultErrorText: undefined,
83
111
  };
84
112
  }
85
113
 
@@ -99,47 +127,75 @@ export function isAssistant(msg: SDKMessage): msg is SDKAssistantMessage {
99
127
  return msg.type === "assistant";
100
128
  }
101
129
 
130
+ export function isUserMessage(msg: SDKMessage): msg is SDKUserMessage {
131
+ return msg.type === "user" && "message" in msg;
132
+ }
133
+
102
134
  export function isResult(msg: SDKMessage): msg is SDKResultMessage {
103
135
  return msg.type === "result";
104
136
  }
105
137
 
106
138
  // ── Message processors ──────────────────────────────────────────────────────
107
139
 
140
+ /** Output of `processStreamDelta` when the throttle interval has elapsed. */
141
+ export type StreamDeltaEmit =
142
+ { phase: "text"; text: string } | { phase: "thinking"; text: string };
143
+
108
144
  /**
109
- * Process a streaming delta event — accumulates text and fires throttled
110
- * callbacks for thinking and text phases.
145
+ * Process a streaming delta event — accumulates per-token chunks
146
+ * into `state.currentBlockText` (text) / unflushed buffers, and
147
+ * returns the chunk to emit when the throttle interval has elapsed.
148
+ *
149
+ * Returns `null` when nothing should be emitted yet (either the delta
150
+ * wasn't a content-block-delta, or the throttle window is still open).
151
+ * Callers yield a `text_delta` / `reasoning` event with the returned
152
+ * `text`. The throttle keeps the event volume bounded — Telegram /
153
+ * terminal renderers re-accumulate, but at human-readable cadence.
111
154
  */
112
155
  export function processStreamDelta(
113
156
  msg: SDKPartialAssistantMessage,
114
157
  state: StreamState,
115
- onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void,
116
- ): void {
117
- if (!onStreamDelta) return;
118
-
158
+ ): StreamDeltaEmit | null {
119
159
  const event = msg.event;
120
- if (event.type !== "content_block_delta") return;
160
+ if (event.type !== "content_block_delta") return null;
121
161
 
122
162
  const deltaEvent = event as BetaRawContentBlockDeltaEvent;
123
163
  const delta = deltaEvent.delta;
124
164
 
125
165
  if (delta.type === "thinking_delta") {
166
+ // The SDK's typed thinking_delta carries `.thinking`; treat
167
+ // missing/non-string defensively so a schema drift can't crash
168
+ // the loop.
169
+ const chunk =
170
+ typeof (delta as { thinking?: unknown }).thinking === "string"
171
+ ? (delta as { thinking: string }).thinking
172
+ : "";
173
+ if (chunk) state.unflushedThinkingDelta += chunk;
126
174
  const now = Date.now();
127
175
  if (now - state.lastStreamUpdate >= STREAM_INTERVAL) {
128
176
  state.lastStreamUpdate = now;
129
- onStreamDelta(state.currentBlockText, "thinking");
177
+ const out = state.unflushedThinkingDelta;
178
+ state.unflushedThinkingDelta = "";
179
+ if (out.length > 0) return { phase: "thinking", text: out };
130
180
  }
131
181
  } else if (delta.type === "text_delta") {
132
182
  state.currentBlockText += delta.text;
183
+ state.unflushedTextDelta += delta.text;
133
184
  const now = Date.now();
134
185
  if (now - state.lastStreamUpdate >= STREAM_INTERVAL) {
135
186
  state.lastStreamUpdate = now;
136
- onStreamDelta(state.currentBlockText, "text");
187
+ const out = state.unflushedTextDelta;
188
+ state.unflushedTextDelta = "";
189
+ if (out.length > 0) return { phase: "text", text: out };
137
190
  }
138
191
  }
192
+ return null;
139
193
  }
140
194
 
141
195
  /** A tool call extracted from an assistant message. */
142
196
  export type ToolCall = {
197
+ /** The SDK's tool_use block id — matches the later tool_result. */
198
+ id: string;
143
199
  name: string;
144
200
  input: Record<string, unknown>;
145
201
  };
@@ -181,7 +237,7 @@ export function processAssistantMessage(
181
237
  typeof block.input === "object" && block.input !== null
182
238
  ? (block.input as Record<string, unknown>)
183
239
  : {};
184
- tools.push({ name: block.name, input });
240
+ tools.push({ id: block.id, name: block.name, input });
185
241
  // Text before this tool call is a progress message
186
242
  if (blockText.trim()) {
187
243
  progressTexts.push(blockText.trim());
@@ -201,6 +257,62 @@ export function processAssistantMessage(
201
257
  return { progressTexts, tools, trailingText };
202
258
  }
203
259
 
260
+ /** One completed tool execution, extracted from an SDK user message. */
261
+ export type ToolResultInfo = {
262
+ toolUseId: string;
263
+ /** Present when the tool failed — first ~500 chars of the error text. */
264
+ error?: string;
265
+ };
266
+
267
+ /**
268
+ * Extract tool results from an SDK user message. Tool executions come
269
+ * back as `tool_result` content blocks (keyed by `tool_use_id`) on
270
+ * synthetic user messages — the missing half of the tool lifecycle:
271
+ * without these, a UI that shows a spinner on `tool_call` never gets
272
+ * the event that stops it.
273
+ */
274
+ export function extractToolResults(msg: SDKUserMessage): ToolResultInfo[] {
275
+ const content = msg.message?.content;
276
+ if (!Array.isArray(content)) return [];
277
+ const results: ToolResultInfo[] = [];
278
+ for (const block of content) {
279
+ if (
280
+ !block ||
281
+ typeof block !== "object" ||
282
+ (block as { type?: string }).type !== "tool_result"
283
+ ) {
284
+ continue;
285
+ }
286
+ const b = block as {
287
+ tool_use_id?: string;
288
+ is_error?: boolean;
289
+ content?: unknown;
290
+ };
291
+ if (typeof b.tool_use_id !== "string") continue;
292
+ let error: string | undefined;
293
+ if (b.is_error) {
294
+ const text =
295
+ typeof b.content === "string"
296
+ ? b.content
297
+ : Array.isArray(b.content)
298
+ ? b.content
299
+ .filter(
300
+ (c): c is { type: "text"; text: string } =>
301
+ !!c &&
302
+ typeof c === "object" &&
303
+ (c as { type?: string }).type === "text" &&
304
+ typeof (c as { text?: unknown }).text === "string",
305
+ )
306
+ .map((c) => c.text)
307
+ .join("\n")
308
+ : "";
309
+ error = (text || "tool failed").slice(0, 500);
310
+ }
311
+ results.push({ toolUseId: b.tool_use_id, error });
312
+ }
313
+ return results;
314
+ }
315
+
204
316
  /**
205
317
  * Process the final result message — extracts token counts, context info,
206
318
  * and API call counts from the typed SDK result.
@@ -227,6 +339,11 @@ export function processResultMessage(
227
339
  // and contains cumulative session totals per model — summing all entries
228
340
  // double-counts when switching models mid-session.
229
341
  const modelUsage: Record<string, ModelUsage> = msg.modelUsage;
342
+ // Drift check: modelUsage is keyed by the model that ACTUALLY served
343
+ // the turn. If none of the keys is (an alias of) the model we asked
344
+ // for, the API substituted — warn instead of burying it in the
345
+ // accounting line.
346
+ checkModelDrift(sdkModel, Object.keys(modelUsage ?? {}));
230
347
  const mu = modelUsage[sdkModel] ?? Object.values(modelUsage).at(-1);
231
348
  if (mu) {
232
349
  state.sdkInputTokens = mu.inputTokens ?? 0;
@@ -253,41 +370,32 @@ export function processResultMessage(
253
370
  ) {
254
371
  state.currentBlockText = msg.result;
255
372
  }
256
- }
257
-
258
- // ── Trailing-text fallback dedup ────────────────────────────────────────────
259
373
 
260
- /**
261
- * Normalize text for fuzzy comparison trim, lowercase, collapse whitespace,
262
- * strip emoji. Used to detect whether trailing prose duplicates content
263
- * already delivered via `end_turn` / `send(type="text")`.
264
- */
265
- export function normalizeForDedupe(text: string): string {
266
- return text
267
- .trim()
268
- .toLowerCase()
269
- .replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, "")
270
- .replace(/\s+/g, " ")
271
- .trim();
374
+ // Failed turn. Two shapes (see StreamState.resultErrorText):
375
+ // - error subtype: no `result` field, but `errors[]` carries diagnostics.
376
+ // - success subtype with is_error: `result` (and the trailing assistant
377
+ // text) IS the API error message, e.g. "You've hit your weekly limit
378
+ // · resets Jul 10, 9am". Prefer that text — it's the user-facing one.
379
+ if (msg.subtype !== "success") {
380
+ const diagnostics = (msg.errors ?? [])
381
+ .filter((e) => typeof e === "string" && !e.startsWith("[ede_diagnostic]"))
382
+ .join("; ")
383
+ .slice(0, 500);
384
+ state.resultErrorText =
385
+ state.lastTrailingText.trim() ||
386
+ diagnostics ||
387
+ `Claude SDK turn failed (${msg.subtype})`;
388
+ } else if (msg.is_error) {
389
+ state.resultErrorText =
390
+ (typeof msg.result === "string" && msg.result.trim()) ||
391
+ state.lastTrailingText.trim() ||
392
+ "Claude SDK turn failed (API error)";
393
+ }
272
394
  }
273
395
 
274
- const MIN_DEDUP_LENGTH = 10;
275
-
276
- /**
277
- * Returns true if `candidate` is substantively the same as any text in
278
- * `deliveredNorms`. "Substantively" = one is a substring of the other after
279
- * normalization; both must be at least MIN_DEDUP_LENGTH chars to avoid
280
- * dropping short legitimate replies.
281
- */
282
- export function isDuplicateOfDelivered(
283
- candidate: string,
284
- deliveredNorms: string[],
285
- ): boolean {
286
- if (deliveredNorms.length === 0) return false;
287
- const norm = normalizeForDedupe(candidate);
288
- if (norm.length < MIN_DEDUP_LENGTH) return false;
289
- return deliveredNorms.some(
290
- (d) =>
291
- d.length >= MIN_DEDUP_LENGTH && (norm.includes(d) || d.includes(norm)),
292
- );
293
- }
396
+ // ── Trailing-text fallback dedup ────────────────────────────────────────────
397
+ // Re-export from shared so legacy imports do not grow a second implementation.
398
+ export {
399
+ normalizeForDedupe,
400
+ isDuplicateOfDelivered,
401
+ } from "../shared/delivered-text.js";
@@ -23,10 +23,18 @@ export async function warmSession(chatId: string): Promise<void> {
23
23
 
24
24
  const abort = new AbortController();
25
25
  try {
26
- // Warm-up always rebuilds the system prompt (it's effectively a
27
- // fresh session). `previousTurns: 0` triggers the rebuild branch.
28
- prepareSystemPrompt({ config: getConfig(), previousTurns: 0 });
29
- const { options } = buildSdkOptions(chatId);
26
+ // Snapshot the prompt under this chat's fresh session epoch. The
27
+ // first real turn finds the same snapshot (same epoch) and sends a
28
+ // byte-identical prompt so the warm-up's cache write is actually
29
+ // reused instead of being invalidated by a re-timestamped rebuild.
30
+ const session = getSession(chatId);
31
+ const prepared = prepareSystemPrompt({
32
+ config: getConfig(),
33
+ previousTurns: session.turns,
34
+ chatId,
35
+ sessionEpoch: session.createdAt,
36
+ });
37
+ const { options } = buildSdkOptions(chatId, undefined, undefined, prepared);
30
38
 
31
39
  // Streaming input mode: pass an async iterable that never yields a user message
32
40
  const neverYield = async function* (): AsyncGenerator<never> {
@@ -69,7 +77,6 @@ export async function warmSession(chatId: string): Promise<void> {
69
77
  } finally {
70
78
  if (timeoutId !== undefined) clearTimeout(timeoutId);
71
79
  }
72
- const session = getSession(chatId);
73
80
  if (ctx.maxTokens > 0) session.usage.contextWindow = ctx.maxTokens;
74
81
  if (ctx.totalTokens > 0) session.usage.contextTokens = ctx.totalTokens;
75
82
  log(
@@ -4,10 +4,11 @@
4
4
  * The Codex CLI supports two distinct authentication modes, with
5
5
  * materially different capabilities:
6
6
  *
7
- * - **API key** (`OPENAI_API_KEY` env var, or `apiKey` passed to the
8
- * SDK's `Codex` constructor, or `OPENAI_API_KEY` stored in
9
- * `~/.codex/auth.json`). Full model catalog access, billed via the
10
- * standard OpenAI API. `gpt-5-codex` is the flagship.
7
+ * - **API key** (`CODEX_API_KEY` / `TALON_CODEX_KEY` env var,
8
+ * `codexApiKey` passed to the SDK's `Codex` constructor, or
9
+ * `OPENAI_API_KEY` stored in `~/.codex/auth.json`). Full model
10
+ * catalog access, billed via the configured OpenAI-compatible API.
11
+ * `gpt-5-codex` is the flagship on OpenAI's native endpoint.
11
12
  *
12
13
  * - **ChatGPT OAuth** (`~/.codex/auth.json` `auth_mode: "chatgpt"`
13
14
  * after running `codex login`). Restricted to a subset of models —
@@ -22,12 +23,21 @@
22
23
  *
23
24
  * Detection order (each step short-circuits if it succeeds):
24
25
  *
25
- * 1. `OPENAI_API_KEY` env var → `"api-key"`.
26
- * 2. `openaiApiKey` in Talon config → `"api-key"`.
27
- * 3. `~/.codex/auth.json` exists + `auth_mode` field is readreturns
26
+ * 1. `CODEX_API_KEY` env var → `"api-key"` (Codex CLI convention).
27
+ * 2. `TALON_CODEX_KEY` env var → `"api-key"` (Talon-scoped alias).
28
+ * 3. `codexApiKey` in Talon config`"api-key"`.
29
+ * Any Codex-specific API key is paired with `openaiBaseUrl` when
30
+ * configured; the SDK maps that to Codex's `openai_base_url`
31
+ * override.
32
+ * 4. `~/.codex/auth.json` exists + `auth_mode` field is read → returns
28
33
  * that value (`"chatgpt"` for OAuth, `"api-key"` if the JSON also
29
34
  * has a non-null `OPENAI_API_KEY`).
30
- * 4. Nothing → `"none"`. First turn will fail with an auth error;
35
+ * 5. `OPENAI_API_KEY` env var → `"api-key"` as a generic fallback.
36
+ * 6. `openaiApiKey` in Talon config → `"api-key"` as a legacy fallback.
37
+ * These shared OpenAI credentials are deliberately after the Codex
38
+ * auth file so other Talon backends can keep their credentials
39
+ * without hijacking a logged-in Codex CLI.
40
+ * 7. Nothing → `"none"`. First turn will fail with an auth error;
31
41
  * caller emits a startup warning pointing to `codex login`.
32
42
  */
33
43
 
@@ -37,55 +47,157 @@ import { join } from "node:path";
37
47
  /** Detected auth mode. */
38
48
  export type CodexAuthMode = "api-key" | "chatgpt" | "none";
39
49
 
50
+ export type CodexApiKeySource =
51
+ | "env:CODEX_API_KEY"
52
+ | "env:TALON_CODEX_KEY"
53
+ | "config:codexApiKey"
54
+ | "env:OPENAI_API_KEY"
55
+ | "config:openaiApiKey";
56
+
40
57
  /** Resolved auth state plus diagnostics for the startup log. */
41
58
  export interface CodexAuthInfo {
42
59
  mode: CodexAuthMode;
43
60
  /** Where the credential was found, for logging. */
44
- source:
45
- | "env:OPENAI_API_KEY"
46
- | "config:openaiApiKey"
47
- | "file:~/.codex/auth.json"
48
- | "missing";
61
+ source: CodexApiKeySource | "file:~/.codex/auth.json" | "missing";
62
+ /** API key to pass into the Codex SDK, when auth is explicit. */
63
+ apiKey?: string;
64
+ /** Base URL to pass into the Codex SDK alongside explicit API-key auth. */
65
+ baseUrl?: string;
49
66
  /** Path to the auth file when present (for diagnostics). */
50
67
  authFilePath?: string;
51
68
  /** Whether the auth file (if present) parsed correctly. */
52
69
  authFileParsed: boolean;
53
70
  /** Raw parse error when the file existed but couldn't be parsed. */
54
71
  parseError?: string;
72
+ /** Diagnostics about credential resolution. */
73
+ diagnostics: string[];
74
+ }
75
+
76
+ export interface DetectCodexAuthInput {
77
+ codexApiKey?: string;
78
+ openaiApiKey?: string;
79
+ openaiBaseUrl?: string;
80
+ env?: NodeJS.ProcessEnv;
81
+ }
82
+
83
+ export function normalizeCodexBaseUrl(
84
+ baseUrl: string | undefined,
85
+ ): string | undefined {
86
+ const trimmed = baseUrl?.trim();
87
+ if (!trimmed) return undefined;
88
+ return trimmed;
89
+ }
90
+
91
+ function normalizeSecret(value: string | undefined): string | undefined {
92
+ const trimmed = value?.trim();
93
+ if (!trimmed) return undefined;
94
+ return trimmed;
95
+ }
96
+
97
+ export function resolveCodexApiKey(input: DetectCodexAuthInput): {
98
+ apiKey?: string;
99
+ baseUrl?: string;
100
+ source?: CodexApiKeySource;
101
+ diagnostics: string[];
102
+ } {
103
+ const env = input.env ?? process.env;
104
+ const baseUrl = normalizeCodexBaseUrl(input.openaiBaseUrl);
105
+ const diagnostics: string[] = [];
106
+
107
+ const codexApiKeyEnv = normalizeSecret(env.CODEX_API_KEY);
108
+ if (codexApiKeyEnv) {
109
+ return {
110
+ apiKey: codexApiKeyEnv,
111
+ baseUrl,
112
+ source: "env:CODEX_API_KEY",
113
+ diagnostics,
114
+ };
115
+ }
116
+
117
+ const talonCodexKeyEnv = normalizeSecret(env.TALON_CODEX_KEY);
118
+ if (talonCodexKeyEnv) {
119
+ return {
120
+ apiKey: talonCodexKeyEnv,
121
+ baseUrl,
122
+ source: "env:TALON_CODEX_KEY",
123
+ diagnostics,
124
+ };
125
+ }
126
+
127
+ const configCodexApiKey = normalizeSecret(input.codexApiKey);
128
+ if (configCodexApiKey) {
129
+ return {
130
+ apiKey: configCodexApiKey,
131
+ baseUrl,
132
+ source: "config:codexApiKey",
133
+ diagnostics,
134
+ };
135
+ }
136
+
137
+ return { diagnostics };
138
+ }
139
+
140
+ function resolveGenericOpenAiApiKey(input: DetectCodexAuthInput): {
141
+ apiKey?: string;
142
+ baseUrl?: string;
143
+ source?: Extract<
144
+ CodexApiKeySource,
145
+ "env:OPENAI_API_KEY" | "config:openaiApiKey"
146
+ >;
147
+ diagnostics: string[];
148
+ } {
149
+ const env = input.env ?? process.env;
150
+ const baseUrl = normalizeCodexBaseUrl(input.openaiBaseUrl);
151
+ const diagnostics: string[] = [];
152
+
153
+ const openAiApiKeyEnv = normalizeSecret(env.OPENAI_API_KEY);
154
+ if (openAiApiKeyEnv) {
155
+ return {
156
+ apiKey: openAiApiKeyEnv,
157
+ baseUrl,
158
+ source: "env:OPENAI_API_KEY",
159
+ diagnostics,
160
+ };
161
+ }
162
+
163
+ const configOpenAiApiKey = normalizeSecret(input.openaiApiKey);
164
+ if (configOpenAiApiKey) {
165
+ return {
166
+ apiKey: configOpenAiApiKey,
167
+ baseUrl,
168
+ source: "config:openaiApiKey",
169
+ diagnostics,
170
+ };
171
+ }
172
+
173
+ return { diagnostics };
55
174
  }
56
175
 
57
176
  /**
58
177
  * Detect the active Codex auth mode.
59
178
  *
60
- * `configKey` is the value of `config.openaiApiKey` from Talon's main
61
- * config passed in by the caller so this module doesn't depend on
62
- * the config module. `envOverride` defaults to `process.env` so tests
63
- * can substitute a frozen env map.
179
+ * All config values are passed in by the caller so this module doesn't
180
+ * depend on the config module.
64
181
  */
65
182
  export function detectCodexAuth(
66
- configKey?: string,
67
- envOverride: NodeJS.ProcessEnv = process.env,
183
+ input: DetectCodexAuthInput = {},
68
184
  ): CodexAuthInfo {
69
- // 1. Explicit API key in the environment.
70
- if (envOverride.OPENAI_API_KEY) {
185
+ const env = input.env ?? process.env;
186
+ const explicitKey = resolveCodexApiKey(input);
187
+ if (explicitKey.apiKey && explicitKey.source) {
71
188
  return {
72
189
  mode: "api-key",
73
- source: "env:OPENAI_API_KEY",
190
+ source: explicitKey.source,
191
+ apiKey: explicitKey.apiKey,
192
+ baseUrl: explicitKey.baseUrl,
74
193
  authFileParsed: false,
194
+ diagnostics: explicitKey.diagnostics,
75
195
  };
76
196
  }
77
197
 
78
- // 2. Explicit API key in Talon's config.
79
- if (configKey) {
80
- return {
81
- mode: "api-key",
82
- source: "config:openaiApiKey",
83
- authFileParsed: false,
84
- };
85
- }
86
-
87
- // 3. Codex CLI auth file (created by `codex login`).
88
- const home = envOverride.HOME ?? envOverride.USERPROFILE;
198
+ let authFileResult: CodexAuthInfo | null = null;
199
+ // Codex CLI auth file (created by `codex login`).
200
+ const home = env.HOME ?? env.USERPROFILE;
89
201
  if (home) {
90
202
  const authFilePath = join(home, ".codex", "auth.json");
91
203
  if (existsSync(authFilePath)) {
@@ -98,15 +210,18 @@ export function detectCodexAuth(
98
210
  // The auth.json schema includes `OPENAI_API_KEY` as a top-level
99
211
  // field even on ChatGPT-mode installs; when it's non-null it
100
212
  // takes precedence (mirrors what the CLI itself does).
101
- if (
102
- parsed.OPENAI_API_KEY &&
213
+ const fileApiKey =
103
214
  typeof parsed.OPENAI_API_KEY === "string"
104
- ) {
215
+ ? normalizeSecret(parsed.OPENAI_API_KEY)
216
+ : undefined;
217
+ if (fileApiKey) {
105
218
  return {
106
219
  mode: "api-key",
107
220
  source: "file:~/.codex/auth.json",
221
+ apiKey: fileApiKey,
108
222
  authFilePath,
109
223
  authFileParsed: true,
224
+ diagnostics: explicitKey.diagnostics,
110
225
  };
111
226
  }
112
227
  if (parsed.auth_mode === "chatgpt") {
@@ -115,33 +230,53 @@ export function detectCodexAuth(
115
230
  source: "file:~/.codex/auth.json",
116
231
  authFilePath,
117
232
  authFileParsed: true,
233
+ diagnostics: explicitKey.diagnostics,
118
234
  };
119
235
  }
120
236
  // File parsed but doesn't carry a recognised mode — treat as
121
- // missing so the caller surfaces a `codex login` reminder.
122
- return {
237
+ // missing for now; generic OpenAI fallback may still be usable.
238
+ authFileResult = {
123
239
  mode: "none",
124
240
  source: "missing",
125
241
  authFilePath,
126
242
  authFileParsed: true,
243
+ diagnostics: explicitKey.diagnostics,
127
244
  };
128
245
  } catch (err) {
129
- return {
246
+ authFileResult = {
130
247
  mode: "none",
131
248
  source: "missing",
132
249
  authFilePath,
133
250
  authFileParsed: false,
134
251
  parseError: err instanceof Error ? err.message : String(err),
252
+ diagnostics: explicitKey.diagnostics,
135
253
  };
136
254
  }
137
255
  }
138
256
  }
139
257
 
140
- // 4. Nothing.
258
+ const genericKey = resolveGenericOpenAiApiKey(input);
259
+ if (genericKey.apiKey && genericKey.source) {
260
+ return {
261
+ mode: "api-key",
262
+ source: genericKey.source,
263
+ apiKey: genericKey.apiKey,
264
+ baseUrl: genericKey.baseUrl,
265
+ authFileParsed: authFileResult?.authFileParsed ?? false,
266
+ authFilePath: authFileResult?.authFilePath,
267
+ parseError: authFileResult?.parseError,
268
+ diagnostics: [...explicitKey.diagnostics, ...genericKey.diagnostics],
269
+ };
270
+ }
271
+
272
+ if (authFileResult) return authFileResult;
273
+
274
+ // Nothing.
141
275
  return {
142
276
  mode: "none",
143
277
  source: "missing",
144
278
  authFileParsed: false,
279
+ diagnostics: explicitKey.diagnostics,
145
280
  };
146
281
  }
147
282
 
@@ -163,3 +298,44 @@ export function isChatGptModelMismatchError(message: string): boolean {
163
298
  message,
164
299
  );
165
300
  }
301
+
302
+ /**
303
+ * Detect the *silent* OAuth-incompat exit shape — the one that hit
304
+ * Pandario on 2026-05-20 at 23:13Z.
305
+ *
306
+ * On a free ChatGPT-OAuth credential the Codex CLI silently rejects
307
+ * most model strings (only `gpt-5.5` is verified working). Crucially,
308
+ * for some rejected models the CLI exits 1 *without* emitting a
309
+ * structured `error` or `turn.failed` event over the JSON stream, AND
310
+ * without writing the canonical
311
+ * `"not supported when using Codex with a ChatGPT account"` text to
312
+ * stderr. The only signal the SDK surfaces is:
313
+ *
314
+ * `Codex Exec exited with code 1: Reading prompt from stdin...\n`
315
+ *
316
+ * (`"Reading prompt from stdin..."` is the CLI's startup banner — it
317
+ * always prints that and then exits before the prompt even reaches
318
+ * the model.)
319
+ *
320
+ * The detector heuristic:
321
+ * - Error text contains `"Codex Exec exited"` (SDK's wrapper);
322
+ * - Error text contains `"Reading prompt from stdin"` (CLI banner);
323
+ * - Error text does NOT contain the explicit mismatch phrase (the
324
+ * other detector handles that case).
325
+ *
326
+ * Callers MUST additionally check `authInfo.mode === "chatgpt"` and
327
+ * that the model isn't already the OAuth default before falling back —
328
+ * a silent exit-1 on api-key auth is a different bug class (network,
329
+ * config error, etc.) and shouldn't be misclassified.
330
+ *
331
+ * Case-insensitive; whitespace-tolerant. Matches both `code 1` and
332
+ * `code 2` (the CLI has been observed using both).
333
+ */
334
+ export function isSilentOAuthExitError(message: string): boolean {
335
+ if (!message) return false;
336
+ if (isChatGptModelMismatchError(message)) return false;
337
+ return (
338
+ /Codex\s+Exec\s+exited\s+with\s+code\s+\d+/i.test(message) &&
339
+ /Reading\s+prompt\s+from\s+stdin/i.test(message)
340
+ );
341
+ }