talon-agent 5.18.2 → 5.19.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 (195) hide show
  1. package/README.md +2 -1
  2. package/package.json +2 -2
  3. package/prompts/system/agent-brief.md +20 -3
  4. package/src/app.ts +13 -0
  5. package/src/backend/claude-sdk/handler.ts +4 -4
  6. package/src/backend/claude-sdk/mcp-ready.ts +16 -2
  7. package/src/backend/claude-sdk/one-shot.ts +32 -2
  8. package/src/backend/claude-sdk/stream.ts +2 -2
  9. package/src/backend/codex/auth.ts +1 -1
  10. package/src/backend/codex/handler/message.ts +11 -11
  11. package/src/backend/codex/init.ts +4 -9
  12. package/src/backend/codex/mcp-config.ts +1 -2
  13. package/src/backend/codex/oauth-incompat.ts +8 -4
  14. package/src/backend/codex/one-shot.ts +1 -1
  15. package/src/backend/openai-agents/builtins.ts +55 -27
  16. package/src/backend/openai-agents/factory.ts +3 -3
  17. package/src/backend/openai-agents/handler/message.ts +3 -5
  18. package/src/backend/openai-agents/mcp-pool.ts +10 -27
  19. package/src/backend/remote-server/chat-turn.ts +6 -6
  20. package/src/backend/remote-server/events.ts +1 -5
  21. package/src/backend/remote-server/index.ts +0 -1
  22. package/src/backend/remote-server/messages.ts +3 -7
  23. package/src/backend/remote-server/one-shot.ts +1 -3
  24. package/src/backend/remote-server/session-helpers.ts +1 -4
  25. package/src/backend/remote-server/sse-stream.ts +8 -9
  26. package/src/backend/runtime/metrics.ts +7 -13
  27. package/src/backend/runtime/sleep.ts +1 -2
  28. package/src/backend/runtime/turn/handle-retry.ts +48 -2
  29. package/src/backend/runtime/turn/handler-to-events.ts +3 -4
  30. package/src/bootstrap.ts +9 -1
  31. package/src/cli/doctor.ts +3 -0
  32. package/src/cli/index.ts +8 -10
  33. package/src/cli/logs.ts +148 -9
  34. package/src/cli/setup.ts +9 -11
  35. package/src/cli/status.ts +29 -0
  36. package/src/core/agent-runtime/README.md +5 -19
  37. package/src/core/agent-runtime/events.ts +3 -39
  38. package/src/core/agent-runtime/model-ref.ts +0 -8
  39. package/src/core/agents/registry.ts +65 -2
  40. package/src/core/auth/expiry-monitor.ts +9 -1
  41. package/src/core/auth/login-flow.ts +9 -1
  42. package/src/core/auth/status.ts +31 -3
  43. package/src/core/background/cron/scheduler.ts +25 -5
  44. package/src/core/background/dream/index.ts +29 -10
  45. package/src/core/background/failure-backoff.ts +30 -0
  46. package/src/core/background/heartbeat/agent.ts +2 -27
  47. package/src/core/background/heartbeat/index.ts +0 -2
  48. package/src/core/background/heartbeat/scheduler.ts +21 -13
  49. package/src/core/background/heartbeat/state.ts +10 -2
  50. package/src/core/background/isolated-agent.ts +6 -2
  51. package/src/core/background/pulse/pulse.ts +9 -0
  52. package/src/core/background/triggers/exit.ts +54 -0
  53. package/src/core/background/triggers/index.ts +1 -3
  54. package/src/core/background/triggers/resume.ts +2 -4
  55. package/src/core/backup/archive/tar.ts +14 -4
  56. package/src/core/backup/plan.ts +1 -0
  57. package/src/core/backup/restore.ts +21 -13
  58. package/src/core/backup/scheduler.ts +28 -9
  59. package/src/core/backup/snapshot.ts +43 -10
  60. package/src/core/backup/store.ts +7 -19
  61. package/src/core/backup/targets.ts +69 -17
  62. package/src/core/config/index.ts +14 -0
  63. package/src/core/daemon/crash-marker.ts +141 -0
  64. package/src/core/daemon/crash.ts +9 -2
  65. package/src/core/daemon/handoff.ts +15 -0
  66. package/src/core/daemon/health-alerts.ts +297 -0
  67. package/src/core/daemon/log-reader.ts +289 -0
  68. package/src/core/doctor/index.ts +18 -2
  69. package/src/core/doctor/logs.ts +124 -0
  70. package/src/core/doctor/types.ts +1 -1
  71. package/src/core/engine/backend-controller/index.ts +1 -13
  72. package/src/core/engine/backend-router/router.ts +1 -1
  73. package/src/core/engine/dispatcher.ts +55 -2
  74. package/src/core/engine/fault-text.ts +40 -0
  75. package/src/core/engine/gateway-actions/agents/index.ts +3 -2
  76. package/src/core/engine/gateway-actions/agents/report.ts +62 -0
  77. package/src/core/engine/gateway-actions/history.ts +2 -4
  78. package/src/core/engine/gateway-actions/mesh.ts +26 -14
  79. package/src/core/engine/gateway-actions/native/exec.ts +13 -16
  80. package/src/core/engine/gateway-actions/native/teleport.ts +1 -1
  81. package/src/core/engine/gateway.ts +60 -1
  82. package/src/core/engine/turn-health.ts +222 -0
  83. package/src/core/errors.ts +2 -2
  84. package/src/core/frontend-runtime/admin-notify.ts +1 -1
  85. package/src/core/frontend-runtime/alerts.ts +130 -0
  86. package/src/core/mcp-hub/children.ts +78 -29
  87. package/src/core/mcp-hub/index.ts +21 -18
  88. package/src/core/mcp-hub/proxy-server.ts +8 -4
  89. package/src/core/mcp-hub/talon-server.ts +5 -12
  90. package/src/core/mesh/credentials/store.ts +16 -1
  91. package/src/core/mesh/devices/registry.ts +1 -17
  92. package/src/core/mesh/devices/service.ts +49 -17
  93. package/src/core/mesh/devices/teleport.ts +14 -2
  94. package/src/core/mesh/links/node-binaries.ts +13 -6
  95. package/src/core/mesh/persist.ts +22 -10
  96. package/src/core/mesh/transfers/device-files.ts +5 -23
  97. package/src/core/mesh/transfers/transfers.ts +16 -3
  98. package/src/core/models/active-model.ts +2 -55
  99. package/src/core/plugin/actions.ts +19 -20
  100. package/src/core/plugin/builtins.ts +80 -90
  101. package/src/core/plugin/index.ts +1 -4
  102. package/src/core/plugin/loader.ts +25 -33
  103. package/src/core/plugin/mcp.ts +3 -5
  104. package/src/core/plugin/registry.ts +19 -35
  105. package/src/core/plugin/types.ts +2 -5
  106. package/src/core/prompt/assemble.ts +15 -3
  107. package/src/core/scripts/lua.ts +6 -2
  108. package/src/core/tasks/table.ts +8 -2
  109. package/src/core/tools/bridge.ts +2 -4
  110. package/src/core/tools/chat/cross-send.ts +1 -1
  111. package/src/core/tools/chat/messaging.ts +1 -1
  112. package/src/core/tools/index.ts +2 -2
  113. package/src/core/tools/mcp-env.ts +2 -59
  114. package/src/core/tools/ops/agents.ts +22 -1
  115. package/src/core/tools/schemas.ts +4 -9
  116. package/src/core/vfs/fusefs.ts +0 -5
  117. package/src/core/vfs/index.ts +9 -2
  118. package/src/core/vfs/mounts/diagnostics.ts +109 -0
  119. package/src/core/vfs/mounts/proc.ts +17 -1
  120. package/src/core/vfs/workspace.ts +7 -3
  121. package/src/core/weaver/shuttle.ts +8 -1
  122. package/src/core/weaver/turn-log.ts +320 -0
  123. package/src/core/weaver/weaver.ts +48 -5
  124. package/src/frontend/discord/actions/index.ts +8 -1
  125. package/src/frontend/discord/diagnostics.ts +82 -6
  126. package/src/frontend/discord/handlers/index.ts +0 -2
  127. package/src/frontend/discord/middleware.ts +7 -13
  128. package/src/frontend/discord/runtime.ts +1 -3
  129. package/src/frontend/health/delivery.ts +115 -0
  130. package/src/frontend/health/outage.ts +116 -0
  131. package/src/frontend/native/bridge/routes/chats.ts +3 -5
  132. package/src/frontend/native/bridge/server.ts +106 -21
  133. package/src/frontend/native/index.ts +1 -1
  134. package/src/frontend/native/media/media.ts +5 -1
  135. package/src/frontend/native/runtime.ts +12 -7
  136. package/src/frontend/native/surface/handlers.ts +1 -1
  137. package/src/frontend/native/surface/memory.ts +1 -1
  138. package/src/frontend/native/surface/models.ts +3 -3
  139. package/src/frontend/native/surface/settings.ts +20 -8
  140. package/src/frontend/native/turn/context.ts +6 -8
  141. package/src/frontend/native/turn/turn-meta.ts +2 -5
  142. package/src/frontend/native/turn/turn.ts +8 -10
  143. package/src/frontend/presentation/format.ts +2 -4
  144. package/src/frontend/presentation/session-status.ts +2 -6
  145. package/src/frontend/teams/actions.ts +8 -1
  146. package/src/frontend/teams/graph.ts +0 -1
  147. package/src/frontend/teams/index.ts +1 -4
  148. package/src/frontend/teams/poll.ts +40 -2
  149. package/src/frontend/teams/runtime.ts +14 -5
  150. package/src/frontend/telegram/actions/index.ts +4 -1
  151. package/src/frontend/telegram/actions/send.ts +8 -0
  152. package/src/frontend/telegram/handlers/context.ts +13 -2
  153. package/src/frontend/telegram/handlers/delivery.ts +12 -9
  154. package/src/frontend/telegram/handlers/index.ts +0 -2
  155. package/src/frontend/telegram/index.ts +35 -9
  156. package/src/frontend/telegram/polling/poll-health.ts +110 -0
  157. package/src/frontend/telegram/userbot.ts +100 -36
  158. package/src/frontend/terminal/builtins/session.ts +2 -2
  159. package/src/frontend/terminal/index.ts +1 -3
  160. package/src/frontend/terminal/renderer.ts +2 -18
  161. package/src/frontend/whatsapp/actions/index.ts +12 -1
  162. package/src/frontend/whatsapp/actions/messaging.ts +7 -2
  163. package/src/frontend/whatsapp/connection/connection.ts +29 -9
  164. package/src/frontend/whatsapp/connection/health.ts +89 -0
  165. package/src/frontend/whatsapp/connection/identity.ts +4 -4
  166. package/src/frontend/whatsapp/runtime.ts +11 -5
  167. package/src/native/blake3.ts +28 -2
  168. package/src/native/fusefs.ts +23 -5
  169. package/src/native/registry.ts +1 -1
  170. package/src/native/warden.ts +33 -5
  171. package/src/plugins/github/index.ts +0 -1
  172. package/src/plugins/mempalace/index.ts +9 -3
  173. package/src/plugins/playwright/index.ts +2 -4
  174. package/src/plugins/playwright/provision.ts +12 -4
  175. package/src/storage/chat-settings.ts +6 -1
  176. package/src/storage/cron.ts +29 -4
  177. package/src/storage/daily-log.ts +43 -47
  178. package/src/storage/db.ts +61 -33
  179. package/src/storage/history.ts +6 -1
  180. package/src/storage/journal.ts +9 -2
  181. package/src/storage/kv.ts +19 -6
  182. package/src/storage/media-index.ts +28 -6
  183. package/src/storage/repositories/chat-settings-repo.ts +8 -2
  184. package/src/storage/repositories/sessions-repo.ts +10 -3
  185. package/src/storage/scripts.ts +24 -13
  186. package/src/storage/sessions.ts +11 -2
  187. package/src/storage/skills.ts +21 -2
  188. package/src/storage/stickers.ts +17 -3
  189. package/src/storage/triggers.ts +8 -3
  190. package/src/storage/turn-meta.ts +25 -7
  191. package/src/util/log.ts +189 -6
  192. package/src/util/logging/turn-scope.ts +85 -0
  193. package/src/util/time.ts +3 -3
  194. package/src/util/watchdog.ts +30 -0
  195. package/src/core/engine/backend-controller/legacy.ts +0 -111
@@ -15,8 +15,6 @@
15
15
  */
16
16
 
17
17
  import { randomUUID } from "node:crypto";
18
- import { createHash } from "node:crypto";
19
- import { createReadStream } from "node:fs";
20
18
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
21
19
  import { tmpdir } from "node:os";
22
20
  import { basename, dirname, join, resolve } from "node:path";
@@ -29,6 +27,7 @@ import {
29
27
  type MeshToolResult,
30
28
  } from "../tool-surface.js";
31
29
  import {
30
+ hashFile,
32
31
  normalizeGoarch,
33
32
  platformToGoos,
34
33
  type NodeBinaryResolver,
@@ -70,8 +69,8 @@ const FILE_CHUNK_BYTES = 1024 * 1024;
70
69
  * and never throws, and the command result it would have posted afterwards
71
70
  * is never posted. The dispatch timeout is the only thing that ends that
72
71
  * wait — and since turns serialize per chat, the whole chat's message loop
73
- * queues behind it. A flat hour therefore takes a chat offline for an hour
74
- * over one dropped TCP connection.
72
+ * queues behind it. A flat budget sized for the largest transfer would
73
+ * take a chat offline that long over one dropped TCP connection.
75
74
  *
76
75
  * So the budget is sized to the payload instead: a fixed grace for the
77
76
  * handshake and device-side setup, plus the body at a deliberately
@@ -83,9 +82,8 @@ const STREAM_TRANSFER_FLOOR_BYTES_PER_SEC = 32 * 1024;
83
82
  /**
84
83
  * Ceiling, so a payload large enough to out-scale the formula still can't
85
84
  * block a chat indefinitely. Deliberately well above what a big transfer
86
- * over a slow link needs — the common wedge is a SMALL transfer that used
87
- * to inherit the same budget as a huge one, and that case now resolves in
88
- * about a minute.
85
+ * over a slow link needs — the common wedge is a SMALL transfer, and its
86
+ * budget runs out in about a minute.
89
87
  */
90
88
  const STREAM_TRANSFER_MAX_MS = 30 * 60 * 1000;
91
89
 
@@ -745,19 +743,3 @@ function transferRate(bytes: number, startedAtMs: number): string {
745
743
  const seconds = Math.max((Date.now() - startedAtMs) / 1000, 0.001);
746
744
  return `${formatBytes(bytes / seconds)}/s over ${seconds < 10 ? seconds.toFixed(1) : Math.round(seconds)}s`;
747
745
  }
748
-
749
- /** Stream a file through SHA-256 without loading it into memory (APKs are big
750
- * and Buffer has a hard ceiling). Returns the hex digest and byte size. */
751
- async function hashFile(
752
- path: string,
753
- ): Promise<{ sha256: string; size: number }> {
754
- const { size } = await stat(path);
755
- const hash = createHash("sha256");
756
- await new Promise<void>((resolve, reject) => {
757
- createReadStream(path)
758
- .on("data", (chunk) => hash.update(chunk))
759
- .on("end", () => resolve())
760
- .on("error", reject);
761
- });
762
- return { sha256: hash.digest("hex"), size };
763
- }
@@ -55,6 +55,8 @@ type Transfer = {
55
55
  resolve: (bytes: number) => void;
56
56
  reject: (err: Error) => void;
57
57
  };
58
+ /** pull only — tears down the upload once its HTTP leg is streaming. */
59
+ abort?: AbortController;
58
60
  };
59
61
 
60
62
  export class TransferStore {
@@ -104,12 +106,19 @@ export class TransferStore {
104
106
  return { token };
105
107
  }
106
108
 
107
- /** Drop a token (transfer arrangement failed before the HTTP leg). */
109
+ /**
110
+ * Drop a token — the arrangement failed, or the daemon gave up waiting.
111
+ * An upload already streaming is aborted too: otherwise a peer that
112
+ * stalls mid-body holds the request (and its temp file) open until the
113
+ * bridge's whole-request deadline, and one that finishes late renames a
114
+ * file into place after the caller was told the pull failed.
115
+ */
108
116
  cancel(token: string): void {
109
117
  const t = this.transfers.get(token);
110
118
  if (t?.uploadDone && !t.consumed) {
111
119
  t.uploadDone.reject(new Error("transfer cancelled"));
112
120
  }
121
+ t?.abort?.abort(new Error("transfer cancelled"));
113
122
  this.transfers.delete(token);
114
123
  }
115
124
 
@@ -129,11 +138,16 @@ export class TransferStore {
129
138
  if (!t)
130
139
  return { ok: false, error: "Unknown or already-used transfer token." };
131
140
  const tmp = `${t.localPath}.part-${randomBytes(4).toString("hex")}`;
141
+ const abort = new AbortController();
142
+ t.abort = abort;
132
143
  try {
133
144
  await mkdir(dirname(t.localPath), { recursive: true });
134
145
  let bytes = 0;
135
146
  body.on("data", (d: Buffer) => (bytes += d.length));
136
- await pipeline(body, createWriteStream(tmp, { mode: 0o600 }));
147
+ await pipeline(body, createWriteStream(tmp, { mode: 0o600 }), {
148
+ signal: abort.signal,
149
+ });
150
+ abort.signal.throwIfAborted();
137
151
  await rename(tmp, t.localPath);
138
152
  this.transfers.delete(token);
139
153
  t.uploadDone?.resolve(bytes);
@@ -162,7 +176,6 @@ export class TransferStore {
162
176
  if (!s.isFile()) throw new Error("not a file");
163
177
  return { path: t.localPath, size: s.size };
164
178
  } catch {
165
- this.transfers.delete(token);
166
179
  return null;
167
180
  } finally {
168
181
  // Single-use either way; the device retries by re-arranging.
@@ -60,10 +60,7 @@
60
60
  * have their stored override returned verbatim — no way to validate.
61
61
  */
62
62
 
63
- import {
64
- getChatModelForBackend,
65
- getChatSettings,
66
- } from "../../storage/chat-settings.js";
63
+ import { getChatModelForBackend } from "../../storage/chat-settings.js";
67
64
  import type { Backend } from "../agent-runtime/capabilities.js";
68
65
  import {
69
66
  isBackendId,
@@ -82,7 +79,7 @@ import { logWarn } from "../../util/log.js";
82
79
  * (default)" vs "Model: X (override)" vs "No model selected" without
83
80
  * second-guessing the chain.
84
81
  */
85
- export type ActiveModelSource =
82
+ type ActiveModelSource =
86
83
  | "override-valid"
87
84
  | "override-invalid-fallback"
88
85
  | "backend-canonical"
@@ -311,25 +308,6 @@ async function safeBackendDefault(backend: Backend): Promise<string | null> {
311
308
  }
312
309
  }
313
310
 
314
- /**
315
- * Convenience: same as `resolveActiveModelForChat` but returns just
316
- * the model id (or `null`). Use when the source tag isn't needed.
317
- */
318
- export async function getActiveModelForChat(
319
- chatId: string,
320
- backend: Backend | null,
321
- backendId: string | null,
322
- config: TalonConfig,
323
- ): Promise<string | null> {
324
- const { model } = await resolveActiveModelForChat(
325
- chatId,
326
- backend,
327
- backendId,
328
- config,
329
- );
330
- return model;
331
- }
332
-
333
311
  // ── ref enrichment ──────────────────────────────────────────────────────────
334
312
 
335
313
  /**
@@ -412,34 +390,3 @@ function mapCacheSupport(backend: Backend | null): CacheSupport {
412
390
  return "none";
413
391
  }
414
392
  }
415
-
416
- /**
417
- * Human-readable label for the source — used in toast wording so the
418
- * user sees *why* the resolved model is what it is. Keeps the wording
419
- * centralised so the UI stays consistent across frontends.
420
- */
421
- export function describeActiveModelSource(source: ActiveModelSource): string {
422
- switch (source) {
423
- case "override-valid":
424
- return "your pick";
425
- case "override-invalid-fallback":
426
- return "previous pick invalid — falling back to default";
427
- case "backend-canonical":
428
- return "backend default";
429
- case "config-backend-defaults":
430
- return "configured default";
431
- case "config-legacy-global":
432
- return "global default";
433
- case "none":
434
- return "no model selected";
435
- }
436
- }
437
-
438
- /** Read-only helper to surface the `modelByBackend` map for a chat
439
- * (e.g. for /status rendering). Returns a shallow copy. */
440
- export function getModelByBackendSnapshot(
441
- chatId: string,
442
- ): Record<string, string> {
443
- const settings = getChatSettings(chatId);
444
- return { ...settings.modelByBackend };
445
- }
@@ -14,6 +14,22 @@
14
14
  import { logError } from "../../util/log.js";
15
15
  import type { ActionResult } from "../types.js";
16
16
  import { registry } from "./registry.js";
17
+ import type { TalonPlugin } from "./types.js";
18
+
19
+ /** Run a plugin that has `handleAction`; a throw becomes an error result. */
20
+ async function runAction(
21
+ plugin: TalonPlugin,
22
+ body: Record<string, unknown>,
23
+ chatId: string,
24
+ ): Promise<ActionResult | null> {
25
+ try {
26
+ return await plugin.handleAction!(body, chatId);
27
+ } catch (err) {
28
+ const detail = err instanceof Error ? err.message : String(err);
29
+ logError("plugin", `${plugin.name} action error: ${detail}`);
30
+ return { ok: false, error: `Plugin ${plugin.name}: ${detail}` };
31
+ }
32
+ }
17
33
 
18
34
  export async function handlePluginAction(
19
35
  body: Record<string, unknown>,
@@ -21,19 +37,8 @@ export async function handlePluginAction(
21
37
  ): Promise<ActionResult | null> {
22
38
  for (const { plugin } of registry.all) {
23
39
  if (!plugin.handleAction) continue;
24
- try {
25
- const result = await plugin.handleAction(body, chatId);
26
- if (result) return result;
27
- } catch (err) {
28
- logError(
29
- "plugin",
30
- `${plugin.name} action error: ${err instanceof Error ? err.message : err}`,
31
- );
32
- return {
33
- ok: false,
34
- error: `Plugin ${plugin.name}: ${err instanceof Error ? err.message : err}`,
35
- };
36
- }
40
+ const result = await runAction(plugin, body, chatId);
41
+ if (result) return result;
37
42
  }
38
43
  return null;
39
44
  }
@@ -56,11 +61,5 @@ export async function handlePluginActionIn(
56
61
  ): Promise<ActionResult | null> {
57
62
  const entry = registry.all.find(({ plugin }) => plugin.name === name);
58
63
  if (!entry?.plugin.handleAction) return null;
59
- try {
60
- return await entry.plugin.handleAction(body, chatId);
61
- } catch (err) {
62
- const detail = err instanceof Error ? err.message : String(err);
63
- logError("plugin", `${name} action error: ${detail}`);
64
- return { ok: false, error: `Plugin ${name}: ${detail}` };
65
- }
64
+ return runAction(entry.plugin, body, chatId);
66
65
  }
@@ -4,7 +4,10 @@
4
4
  */
5
5
 
6
6
  import { log, logError, logWarn } from "../../util/log.js";
7
+ import { raiseAlert, resolveAlert } from "../frontend-runtime/alerts.js";
8
+ import { faultText } from "../engine/fault-text.js";
7
9
  import type { TalonConfig } from "../config/index.js";
10
+ import type { TalonPlugin } from "./types.js";
8
11
  import { registry, reloadState } from "./registry.js";
9
12
  import type { ProvisionOutcome } from "./provision.js";
10
13
  import { NATIVE_RUNTIMES, type NativePluginId } from "./native-runtimes.js";
@@ -37,6 +40,7 @@ function reportProvision(
37
40
  if (outcome.status === "failed" && outcome.error) {
38
41
  logError(pluginName, `provision failed: ${outcome.error}`);
39
42
  }
43
+ alertOnProvision(pluginName, outcome.status, outcome.error);
40
44
  const background = outcome.background;
41
45
  if (background) {
42
46
  // Tracked so the post-update report waits for it to settle (its
@@ -56,6 +60,27 @@ function reportProvision(
56
60
  }
57
61
  }
58
62
 
63
+ /**
64
+ * A native runtime that could not be provisioned leaves its plugin down
65
+ * until a human looks (disk, network, a broken toolchain) — alert on the
66
+ * failed pass, clear once a later pass leaves it usable.
67
+ */
68
+ function alertOnProvision(
69
+ pluginName: NativePluginId,
70
+ status: ProvisionOutcome["status"] | "threw",
71
+ error: unknown,
72
+ ): void {
73
+ const key = `provision.${pluginName}`;
74
+ if (status === "failed" || status === "threw") {
75
+ raiseAlert(
76
+ key,
77
+ `Installing the ${pluginName} runtime failed: ${faultText(error ?? "no detail")}. The ${pluginName} plugin will not work until it is fixed (see \`talon doctor\`).`,
78
+ );
79
+ } else if (status === "ready" || status === "degraded") {
80
+ resolveAlert(key, `The ${pluginName} runtime is installed and usable.`);
81
+ }
82
+ }
83
+
59
84
  /**
60
85
  * Provision every enabled native runtime (see native-runtimes.ts) and
61
86
  * return the outcomes so plugin construction can read what it got
@@ -77,136 +102,109 @@ async function provisionNativeRuntimes(
77
102
  runtime.id,
78
103
  `provision: ${err instanceof Error ? err.message : err}`,
79
104
  );
105
+ alertOnProvision(runtime.id, "threw", err);
80
106
  }
81
107
  }
82
108
  return outcomes;
83
109
  }
84
110
 
111
+ /**
112
+ * Register one built-in plugin from its config section and run its init.
113
+ * A plugin that fails to build or init is logged, never fatal.
114
+ */
115
+ async function loadBuiltin(
116
+ label: string,
117
+ section: object,
118
+ initTimeoutMs: number,
119
+ build: () => Promise<TalonPlugin>,
120
+ ): Promise<void> {
121
+ try {
122
+ const loaded = registerPlugin(
123
+ await build(),
124
+ section as Record<string, unknown>,
125
+ );
126
+ if (loaded) {
127
+ await initPluginWithTimeout(
128
+ loaded.plugin,
129
+ loaded.config,
130
+ initTimeoutMs,
131
+ `${label} init`,
132
+ `${label} init`,
133
+ );
134
+ }
135
+ } catch (err) {
136
+ logError(
137
+ "plugin",
138
+ `${label} init: ${err instanceof Error ? err.message : err}`,
139
+ );
140
+ raiseAlert(
141
+ `plugin.${label.toLowerCase()}`,
142
+ `Built-in plugin ${label} failed to load: ${faultText(err)}. Its tools are unavailable until the next reload or restart.`,
143
+ );
144
+ }
145
+ }
146
+
85
147
  /**
86
148
  * Load built-in plugins (GitHub, MemPalace, mem0, Playwright) based on config flags.
87
- * Shared by both bootstrap and hot-reload to avoid duplication.
149
+ * Shared by both bootstrap and hot-reload.
88
150
  */
89
151
  export async function loadBuiltinPlugins(config: TalonConfig): Promise<void> {
90
152
  const provisioned = await provisionNativeRuntimes(config);
91
153
 
92
154
  const github = config.github;
93
155
  if (github?.enabled) {
94
- try {
156
+ await loadBuiltin("GitHub", github, 15_000, async () => {
95
157
  const { createGitHubPlugin } =
96
158
  await import("../../plugins/github/index.js");
97
- const gh = createGitHubPlugin({
159
+ return createGitHubPlugin({
98
160
  token: github.token,
99
161
  imageTag: github.imageTag,
100
162
  });
101
- const ghConfig = github as unknown as Record<string, unknown>;
102
- const loaded = registerPlugin(gh, ghConfig);
103
- if (loaded) {
104
- await initPluginWithTimeout(
105
- loaded.plugin,
106
- loaded.config,
107
- 15_000,
108
- "GitHub init",
109
- "GitHub init",
110
- );
111
- }
112
- } catch (err) {
113
- logError(
114
- "plugin",
115
- `GitHub init: ${err instanceof Error ? err.message : err}`,
116
- );
117
- }
163
+ });
118
164
  }
119
165
 
120
166
  const mempalace = config.mempalace;
121
167
  if (mempalace?.enabled) {
122
- try {
168
+ await loadBuiltin("MemPalace", mempalace, 30_000, async () => {
123
169
  const { createMempalacePlugin } =
124
170
  await import("../../plugins/mempalace/index.js");
125
171
  const { resolveMempalacePaths } =
126
172
  await import("../../plugins/mempalace/provision.js");
127
173
  const { pythonPath, palacePath } = resolveMempalacePaths(mempalace);
128
- const mp = createMempalacePlugin({
174
+ return createMempalacePlugin({
129
175
  pythonPath,
130
176
  palacePath,
131
177
  entityLanguages: mempalace.entityLanguages,
132
178
  verbose: mempalace.verbose,
133
179
  installedVersion: provisioned.get("mempalace")?.version,
134
180
  });
135
- const mpConfig = mempalace as unknown as Record<string, unknown>;
136
- const loaded = registerPlugin(mp, mpConfig);
137
- if (loaded) {
138
- await initPluginWithTimeout(
139
- loaded.plugin,
140
- loaded.config,
141
- 30_000,
142
- "MemPalace init",
143
- "MemPalace init",
144
- );
145
- }
146
- } catch (err) {
147
- logError(
148
- "plugin",
149
- `MemPalace init: ${err instanceof Error ? err.message : err}`,
150
- );
151
- }
181
+ });
152
182
  }
153
183
 
154
184
  const mem0 = config.mem0;
155
185
  if (mem0?.enabled) {
156
- try {
186
+ await loadBuiltin("mem0", mem0, 15_000, async () => {
157
187
  const { createMem0Plugin } = await import("../../plugins/mem0/index.js");
158
- const m0 = createMem0Plugin({
188
+ return createMem0Plugin({
159
189
  apiKey: mem0.apiKey,
160
190
  host: mem0.host,
161
191
  userId: mem0.userId,
162
192
  });
163
- const m0Config = mem0 as unknown as Record<string, unknown>;
164
- const loaded = registerPlugin(m0, m0Config);
165
- if (loaded) {
166
- await initPluginWithTimeout(
167
- loaded.plugin,
168
- loaded.config,
169
- 15_000,
170
- "mem0 init",
171
- "mem0 init",
172
- );
173
- }
174
- } catch (err) {
175
- logError(
176
- "plugin",
177
- `mem0 init: ${err instanceof Error ? err.message : err}`,
178
- );
179
- }
193
+ });
180
194
  }
181
195
 
182
196
  const playwright = config.playwright;
183
197
  if (playwright?.enabled) {
184
- try {
198
+ await loadBuiltin("Playwright", playwright, 15_000, async () => {
185
199
  const { createPlaywrightPlugin } =
186
200
  await import("../../plugins/playwright/index.js");
187
- const pwConfig = playwright as unknown as Record<string, unknown>;
188
- const pw = createPlaywrightPlugin({
201
+ return createPlaywrightPlugin({
189
202
  browser: playwright.browser,
190
203
  headless: playwright.headless,
191
204
  endpoint: playwright.endpoint,
192
205
  endpointFile: playwright.endpointFile,
193
206
  });
194
- const loaded = registerPlugin(pw, pwConfig);
195
- if (loaded) {
196
- await initPluginWithTimeout(
197
- loaded.plugin,
198
- loaded.config,
199
- 15_000,
200
- "Playwright init",
201
- "Playwright init",
202
- );
203
- }
204
- } catch (err) {
205
- logError(
206
- "plugin",
207
- `Playwright init: ${err instanceof Error ? err.message : err}`,
208
- );
209
- }
207
+ });
210
208
  }
211
209
  }
212
210
 
@@ -217,9 +215,9 @@ export async function loadBuiltinPlugins(config: TalonConfig): Promise<void> {
217
215
  *
218
216
  * Throws on config parse/validation failure so the gateway can report an error.
219
217
  *
220
- * Does NOT restart the main process, Claude session, or bot connection. Active
221
- * conversations continue uninterrupted — new MCP servers spawn automatically
222
- * on the next tool call.
218
+ * Does NOT restart the main process, backend session, or bot connection.
219
+ * Active conversations continue uninterrupted — new MCP servers spawn
220
+ * automatically on the next tool call.
223
221
  */
224
222
  export async function reloadPlugins(
225
223
  activeFrontends?: string[],
@@ -229,30 +227,22 @@ export async function reloadPlugins(
229
227
  const { loadConfig, getFrontends } = await import("../config/index.js");
230
228
  const config = loadConfig();
231
229
 
232
- // Derive frontends from config if not explicitly provided
233
230
  const frontends = activeFrontends ?? getFrontends(config);
234
231
 
235
- // Bump reload timestamp so every MCP subprocess env differs from the previous
236
- // load — the Claude SDK will see a changed env and spawn fresh subprocesses,
237
- // picking up any source-file changes without a full Talon restart.
232
+ // New cache-bust key: the re-import below must load edited plugin source.
238
233
  reloadState.lastReloadAt = new Date().toISOString();
239
234
 
240
- // Config is valid — safe to destroy current plugins now
241
235
  log("plugin", "Hot-reload: destroying current plugins...");
242
236
  await registry.destroyAndClear();
243
237
 
244
- // Re-load external plugins
245
238
  if (config.plugins.length > 0) {
246
239
  await loadPlugins(config.plugins, frontends);
247
240
  }
248
-
249
- // Re-load built-in plugins using shared helper
250
241
  await loadBuiltinPlugins(config);
251
242
 
252
243
  // Retire the hub's MCP children: the next tool call (any chat) spawns
253
244
  // fresh processes from the reloaded registry, while in-flight calls
254
- // drain on the old ones (stdio-era behaviour was per-turn respawn via
255
- // the TALON_RELOAD_AT env bump; the hub owns lifecycles directly).
245
+ // drain on the old ones.
256
246
  const { reloadHubChildren } = await import("../mcp-hub/index.js");
257
247
  reloadHubChildren();
258
248
 
@@ -7,10 +7,7 @@
7
7
  * - `loader` — load/validate/register plugins, init hooks, query helpers
8
8
  * - `builtins` — built-in plugin loading + hot-reload
9
9
  * - `actions` — gateway-action routing through plugins
10
- * - `mcp` — MCP server config assembly for the Claude Agent SDK
11
- *
12
- * Re-exports the same public surface the old single-file module exposed,
13
- * including the `_deps` test seam.
10
+ * - `mcp` — MCP server spec assembly for the hub
14
11
  */
15
12
 
16
13
  export { _deps } from "./registry.js";