talon-agent 3.33.4 → 3.34.1

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 (98) hide show
  1. package/package.json +5 -2
  2. package/src/app.ts +16 -8
  3. package/src/backend/claude-sdk/handler.ts +428 -415
  4. package/src/backend/claude-sdk/stream.ts +76 -53
  5. package/src/backend/codex/handler/message.ts +275 -366
  6. package/src/backend/codex/handler/rollout-accounting.ts +137 -0
  7. package/src/backend/openai-agents/handler/message.ts +282 -356
  8. package/src/backend/remote-server/chat-turn.ts +66 -122
  9. package/src/backend/remote-server/index.ts +4 -0
  10. package/src/backend/remote-server/mcp.ts +73 -9
  11. package/src/backend/remote-server/model-catalog/presentation.ts +269 -228
  12. package/src/backend/remote-server/sessions.ts +2 -2
  13. package/src/backend/remote-server/turn.ts +58 -55
  14. package/src/backend/shared/cache-telemetry.ts +17 -1
  15. package/src/backend/shared/handler-to-events.ts +11 -16
  16. package/src/backend/shared/index.ts +14 -15
  17. package/src/backend/shared/result-events.ts +30 -0
  18. package/src/backend/shared/turn-phases.ts +277 -0
  19. package/src/bootstrap.ts +120 -79
  20. package/src/cli/setup.ts +375 -349
  21. package/src/core/background/cron-spec.ts +273 -0
  22. package/src/core/background/heartbeat/agent.ts +167 -104
  23. package/src/core/background/triggers/exit.ts +19 -2
  24. package/src/core/background/triggers/resume.ts +14 -7
  25. package/src/core/background/triggers/state.ts +9 -0
  26. package/src/core/engine/backend-controller/pool.ts +2 -0
  27. package/src/core/engine/backend-controller/state.ts +25 -12
  28. package/src/core/engine/gateway-actions/cron.ts +28 -292
  29. package/src/core/engine/gateway-routes.ts +239 -0
  30. package/src/core/engine/gateway.ts +66 -238
  31. package/src/core/mcp-hub/child-transport.ts +215 -0
  32. package/src/core/mcp-hub/children.ts +72 -13
  33. package/src/core/mcp-hub/index.ts +32 -13
  34. package/src/core/models/active-model.ts +29 -6
  35. package/src/core/vfs/mounts/files.ts +128 -115
  36. package/src/core/weaver/shuttle.ts +33 -2
  37. package/src/core/weaver/weaver.ts +37 -6
  38. package/src/frontend/discord/callbacks/components/agent-buttons.ts +82 -0
  39. package/src/frontend/discord/callbacks/components/backend-select.ts +149 -0
  40. package/src/frontend/discord/callbacks/components/effort.ts +72 -0
  41. package/src/frontend/discord/callbacks/components/index.ts +120 -0
  42. package/src/frontend/discord/callbacks/components/metrics.ts +24 -0
  43. package/src/frontend/discord/callbacks/components/model-nav.ts +93 -0
  44. package/src/frontend/discord/callbacks/components/model-select.ts +118 -0
  45. package/src/frontend/discord/callbacks/components/model.ts +33 -0
  46. package/src/frontend/discord/callbacks/components/pulse.ts +93 -0
  47. package/src/frontend/discord/callbacks/components/settings.ts +243 -0
  48. package/src/frontend/discord/callbacks/components/types.ts +34 -0
  49. package/src/frontend/discord/callbacks/index.ts +4 -4
  50. package/src/frontend/discord/connection.ts +36 -0
  51. package/src/frontend/discord/diagnostics.ts +62 -0
  52. package/src/frontend/discord/guild-policy.ts +89 -0
  53. package/src/frontend/discord/index.ts +44 -305
  54. package/src/frontend/discord/outbound.ts +61 -0
  55. package/src/frontend/discord/ready.ts +73 -0
  56. package/src/frontend/discord/runtime.ts +55 -0
  57. package/src/frontend/native/chat-lifecycle.ts +39 -0
  58. package/src/frontend/native/chat-wire.ts +69 -0
  59. package/src/frontend/native/context.ts +106 -0
  60. package/src/frontend/native/control.ts +78 -0
  61. package/src/frontend/native/emit.ts +167 -0
  62. package/src/frontend/native/empty-chat-sweep.ts +50 -0
  63. package/src/frontend/native/handlers.ts +121 -0
  64. package/src/frontend/native/history.ts +101 -0
  65. package/src/frontend/native/index.ts +90 -1293
  66. package/src/frontend/native/media.ts +43 -0
  67. package/src/frontend/native/models.ts +221 -0
  68. package/src/frontend/native/queue.ts +47 -0
  69. package/src/frontend/native/reset.ts +50 -0
  70. package/src/frontend/native/routes/chats.ts +115 -0
  71. package/src/frontend/native/routes/daemon.ts +70 -0
  72. package/src/frontend/native/routes/host.ts +151 -0
  73. package/src/frontend/native/routes/index.ts +22 -0
  74. package/src/frontend/native/routes/mesh.ts +72 -0
  75. package/src/frontend/native/routes/models.ts +54 -0
  76. package/src/frontend/native/routes/params.ts +29 -0
  77. package/src/frontend/native/routes/pre-auth.ts +94 -0
  78. package/src/frontend/native/routes/table.ts +92 -0
  79. package/src/frontend/native/runtime.ts +109 -0
  80. package/src/frontend/native/server.ts +30 -555
  81. package/src/frontend/native/status.ts +26 -0
  82. package/src/frontend/native/tool-result.ts +48 -0
  83. package/src/frontend/native/turn.ts +341 -0
  84. package/src/frontend/telegram/admin.ts +75 -52
  85. package/src/frontend/whatsapp/access.ts +67 -0
  86. package/src/frontend/whatsapp/connection.ts +280 -0
  87. package/src/frontend/whatsapp/inbound.ts +327 -0
  88. package/src/frontend/whatsapp/index.ts +28 -599
  89. package/src/frontend/whatsapp/runtime.ts +74 -0
  90. package/src/storage/metrics.ts +18 -0
  91. package/src/storage/session-record.ts +29 -0
  92. package/src/storage/sessions.ts +24 -0
  93. package/src/storage/trigger-store.ts +8 -0
  94. package/src/util/boot-timer.ts +31 -0
  95. package/src/util/concurrency.ts +28 -0
  96. package/src/util/watchdog.ts +32 -7
  97. package/src/frontend/discord/callbacks/components.ts +0 -793
  98. package/src/frontend/discord/callbacks/shared.ts +0 -22
package/src/bootstrap.ts CHANGED
@@ -35,6 +35,8 @@ import {
35
35
  import { initDream, maybeStartDream } from "./core/background/dream.js";
36
36
  import { initHeartbeat } from "./core/background/heartbeat/index.js";
37
37
  import { log, logWarn, logDebug } from "./util/log.js";
38
+ import { bootPhase } from "./util/boot-timer.js";
39
+ import { mapConcurrent } from "./util/concurrency.js";
38
40
  import type { TalonConfig } from "./util/config.js";
39
41
  import { resolveFrontendIdAmong } from "./core/frontend-runtime/routing.js";
40
42
  import type { Frontend } from "./core/frontend-runtime/index.js";
@@ -122,11 +124,11 @@ export async function bootstrap(
122
124
  const frontends =
123
125
  options.frontendNames ??
124
126
  (Array.isArray(config.frontend) ? config.frontend : [config.frontend]);
125
- await loadPlugins(config.plugins, frontends);
127
+ await bootPhase("plugins", () => loadPlugins(config.plugins, frontends));
126
128
  }
127
129
 
128
130
  // Built-in plugins (GitHub, MemPalace, mem0, Playwright) — shared with hot-reload
129
- await loadBuiltinPlugins(config);
131
+ await bootPhase("builtin plugins", () => loadBuiltinPlugins(config));
130
132
 
131
133
  rebuildSystemPrompt(config, getPluginPromptAdditions());
132
134
  }
@@ -143,12 +145,14 @@ export async function bootstrap(
143
145
  });
144
146
 
145
147
  initWorkspace(config.workspace);
146
- loadSessions();
147
- loadChatSettings();
148
- loadCronJobs();
149
- loadTriggers();
150
- loadHistory();
151
- loadMediaIndex();
148
+ await bootPhase("stores", () => {
149
+ loadSessions();
150
+ loadChatSettings();
151
+ loadCronJobs();
152
+ loadTriggers();
153
+ loadHistory();
154
+ loadMediaIndex();
155
+ });
152
156
  cleanupOldLogs();
153
157
 
154
158
  return { config };
@@ -165,6 +169,104 @@ export async function bootstrap(
165
169
  * heartbeat all read through `getActiveBackend()` so a runtime swap
166
170
  * via `switchBackend(id, config)` propagates without any re-init.
167
171
  */
172
+ /** The backend-controller surface `reconcileChatBindings` needs. */
173
+ type ChatBindingDeps = {
174
+ isBackendAvailable: (id: string, config: TalonConfig) => boolean;
175
+ releaseChat: (chatId: string) => Promise<void>;
176
+ rebindChat: (
177
+ chatId: string,
178
+ backendId: string,
179
+ config: TalonConfig,
180
+ ) => Promise<{ ok: boolean; error?: string }>;
181
+ getBackendIdForChat: (chatId: string) => string;
182
+ getBackendForChat: (chatId: string) => Backend;
183
+ isModelValidForBackend: (backend: Backend, model: string) => Promise<boolean>;
184
+ };
185
+
186
+ const CHAT_BINDING_CONCURRENCY = 8;
187
+ const REBIND_RETRY_DELAY_MS = 1_500;
188
+
189
+ /**
190
+ * Re-establish every chat's stored backend/model override against the
191
+ * backends this boot actually has. Chats are independent, so they are
192
+ * reconciled `CHAT_BINDING_CONCURRENCY` at a time; a shared backend that
193
+ * two chats need at once is initialised exactly once by the pool.
194
+ */
195
+ export async function reconcileChatBindings(
196
+ config: TalonConfig,
197
+ deps: ChatBindingDeps,
198
+ ): Promise<void> {
199
+ const { getAllChatSettings } = await import("./storage/chat-settings.js");
200
+ await mapConcurrent(
201
+ Object.entries(getAllChatSettings()),
202
+ CHAT_BINDING_CONCURRENCY,
203
+ ([cid, settings]) => reconcileChatBinding(cid, settings, config, deps),
204
+ );
205
+ }
206
+
207
+ async function reconcileChatBinding(
208
+ cid: string,
209
+ settings: { backend?: string; model?: string },
210
+ config: TalonConfig,
211
+ deps: ChatBindingDeps,
212
+ ): Promise<void> {
213
+ const { getAllChatSettings, setChatBackend, setChatModel } =
214
+ await import("./storage/chat-settings.js");
215
+ let resetVolatileState = false;
216
+ if (settings.backend) {
217
+ if (!deps.isBackendAvailable(settings.backend, config)) {
218
+ log(
219
+ "bot",
220
+ `Per-chat backend ${settings.backend} for ${cid} is no longer available — resetting chat to default backend`,
221
+ );
222
+ await deps.releaseChat(cid);
223
+ setChatBackend(cid, undefined);
224
+ setChatModel(cid, undefined);
225
+ resetVolatileState = true;
226
+ } else {
227
+ let result = await deps.rebindChat(cid, settings.backend, config);
228
+ if (!result.ok) {
229
+ await new Promise((r) => setTimeout(r, REBIND_RETRY_DELAY_MS));
230
+ result = await deps.rebindChat(cid, settings.backend, config);
231
+ }
232
+ if (!result.ok) {
233
+ log(
234
+ "bot",
235
+ `Per-chat backend rebind failed for ${cid} → ${settings.backend}: ${result.error} — keeping the setting; will serve on the default backend until re-selected`,
236
+ );
237
+ }
238
+ }
239
+ }
240
+ const bindingMatchesSetting =
241
+ !settings.backend || deps.getBackendIdForChat(cid) === settings.backend;
242
+ const currentModel = getAllChatSettings()[cid]?.model;
243
+ if (currentModel && bindingMatchesSetting) {
244
+ const be = deps.getBackendForChat(cid);
245
+ try {
246
+ const valid = await deps.isModelValidForBackend(be, currentModel);
247
+ if (!valid) {
248
+ log(
249
+ "bot",
250
+ `Per-chat model ${currentModel} for ${cid} is not valid for its backend — resetting model to default`,
251
+ );
252
+ setChatModel(cid, undefined);
253
+ resetVolatileState = true;
254
+ }
255
+ } catch (err) {
256
+ log(
257
+ "bot",
258
+ `Per-chat model validation failed for ${cid} (${currentModel}): ${
259
+ err instanceof Error ? err.message : String(err)
260
+ } — keeping stored model`,
261
+ );
262
+ }
263
+ }
264
+ if (resetVolatileState) {
265
+ resetSession(cid);
266
+ clearHistory(cid);
267
+ }
268
+ }
269
+
168
270
  export async function initBackendAndDispatcher(
169
271
  config: TalonConfig,
170
272
  frontend: FrontendSelection,
@@ -258,77 +360,16 @@ export async function initBackendAndDispatcher(
258
360
  // for the backend that would serve it, clear the override and reset
259
361
  // volatile chat state so the next user message starts a fresh default
260
362
  // session instead of crashing on an orphaned model id.
261
- const { getAllChatSettings, setChatBackend, setChatModel } =
262
- await import("./storage/chat-settings.js");
263
- for (const [cid, settings] of Object.entries(getAllChatSettings())) {
264
- let resetVolatileState = false;
265
-
266
- if (settings.backend) {
267
- if (!isBackendAvailable(settings.backend, config)) {
268
- log(
269
- "bot",
270
- `Per-chat backend ${settings.backend} for ${cid} is no longer available — resetting chat to default backend`,
271
- );
272
- await releaseChat(cid);
273
- setChatBackend(cid, undefined);
274
- setChatModel(cid, undefined);
275
- resetVolatileState = true;
276
- } else {
277
- // A boot-time rebind failure is usually TRANSIENT (backend slow to
278
- // spawn, auth endpoint briefly unreachable) — it must not destroy
279
- // the user's persisted choice, session, and history. Retry once,
280
- // then keep the setting and move on: the chat runs on the default
281
- // backend until a later switch succeeds, and clients keep showing
282
- // the user's chosen backend from settings (the source of truth).
283
- let result = await rebindChat(cid, settings.backend, config);
284
- if (!result.ok) {
285
- await new Promise((r) => setTimeout(r, 1500));
286
- result = await rebindChat(cid, settings.backend, config);
287
- }
288
- if (!result.ok) {
289
- log(
290
- "bot",
291
- `Per-chat backend rebind failed for ${cid} → ${settings.backend}: ${result.error} — keeping the setting; will serve on the default backend until re-selected`,
292
- );
293
- }
294
- }
295
- }
296
-
297
- // Only validate the stored model against the backend that will really
298
- // serve this chat. After a kept-but-unbound override (transient rebind
299
- // failure above) the chat temporarily runs on the default backend — the
300
- // stored model belongs to the chosen backend, so validating it against
301
- // the default would wrongly clear it.
302
- const bindingMatchesSetting =
303
- !settings.backend || getBackendIdForChat(cid) === settings.backend;
304
- const currentModel = getAllChatSettings()[cid]?.model;
305
- if (currentModel && bindingMatchesSetting) {
306
- const be = getBackendForChat(cid);
307
- try {
308
- const valid = await isModelValidForBackend(be, currentModel);
309
- if (!valid) {
310
- log(
311
- "bot",
312
- `Per-chat model ${currentModel} for ${cid} is not valid for its backend — resetting model to default`,
313
- );
314
- setChatModel(cid, undefined);
315
- resetVolatileState = true;
316
- }
317
- } catch (err) {
318
- log(
319
- "bot",
320
- `Per-chat model validation failed for ${cid} (${currentModel}): ${
321
- err instanceof Error ? err.message : String(err)
322
- } — keeping stored model`,
323
- );
324
- }
325
- }
326
-
327
- if (resetVolatileState) {
328
- resetSession(cid);
329
- clearHistory(cid);
330
- }
331
- }
363
+ await bootPhase("chat bindings", () =>
364
+ reconcileChatBindings(config, {
365
+ isBackendAvailable,
366
+ releaseChat,
367
+ rebindChat,
368
+ getBackendIdForChat,
369
+ getBackendForChat,
370
+ isModelValidForBackend,
371
+ }),
372
+ );
332
373
 
333
374
  initDispatcher({
334
375
  // Dispatcher reads the backend per query so per-chat overrides