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
@@ -16,6 +16,8 @@ import type {
16
16
  import { isMcpPlugin } from "./types.js";
17
17
  import { registry, _deps } from "./registry.js";
18
18
  import { GATEWAY_TOKEN_ENV } from "../engine/gateway-auth.js";
19
+ import { faultText } from "../engine/fault-text.js";
20
+ import { raiseAlert, resolveAlert } from "../frontend-runtime/alerts.js";
19
21
 
20
22
  /**
21
23
  * Candidate entry point paths, checked in order. Exported for
@@ -48,7 +50,6 @@ export async function loadPlugins(
48
50
  );
49
51
  continue;
50
52
  }
51
- // Standalone MCP servers are registered for getPluginMcpServers, not loaded as modules
52
53
  if (isMcpPlugin(entry)) {
53
54
  if (registry.registerMcpEntry(entry)) {
54
55
  log("plugin", `Registered standalone MCP server: ${entry.name}`);
@@ -108,6 +109,7 @@ export async function initPluginWithTimeout(
108
109
 
109
110
  let timer: ReturnType<typeof setTimeout> | undefined;
110
111
 
112
+ const alertKey = `plugin.${plugin.name}`;
111
113
  try {
112
114
  await Promise.race([
113
115
  Promise.resolve(plugin.init(config)),
@@ -120,11 +122,18 @@ export async function initPluginWithTimeout(
120
122
  timer.unref?.();
121
123
  }),
122
124
  ]);
125
+ resolveAlert(alertKey, `Plugin "${plugin.name}" initialised normally.`);
123
126
  } catch (err) {
124
127
  logError(
125
128
  "plugin",
126
129
  `${errorPrefix}: ${err instanceof Error ? err.message : err}`,
127
130
  );
131
+ // Init runs once per boot or reload: a plugin that failed it stays
132
+ // half-loaded until someone fixes it, so this needs no threshold.
133
+ raiseAlert(
134
+ alertKey,
135
+ `Plugin "${plugin.name}" failed to initialise: ${faultText(err)}. Its tools may not work until the next reload or restart.`,
136
+ );
128
137
  } finally {
129
138
  if (timer) clearTimeout(timer);
130
139
  }
@@ -136,7 +145,6 @@ async function loadSinglePlugin(
136
145
  ): Promise<void> {
137
146
  const pluginDir = resolve(entry.path);
138
147
 
139
- // Resolve entry point
140
148
  const entryPoint = resolveEntryPoint(pluginDir);
141
149
  if (!entryPoint) {
142
150
  logError(
@@ -146,7 +154,6 @@ async function loadSinglePlugin(
146
154
  return;
147
155
  }
148
156
 
149
- // Import and extract plugin module
150
157
  const mod = await _deps.importModule(entryPoint);
151
158
  const plugin = extractPlugin(mod);
152
159
  if (!plugin) {
@@ -157,7 +164,6 @@ async function loadSinglePlugin(
157
164
  return;
158
165
  }
159
166
 
160
- // Check frontend whitelist — skip if plugin specifies frontends and none match
161
167
  if (plugin.frontends && plugin.frontends.length > 0 && activeFrontends) {
162
168
  const match = activeFrontends.some((fe) => plugin.frontends!.includes(fe));
163
169
  if (!match) {
@@ -173,7 +179,6 @@ async function loadSinglePlugin(
173
179
  const loaded = registerPluginInstance(plugin, config, pluginDir);
174
180
  if (!loaded) return;
175
181
 
176
- // Run init hook
177
182
  await initPluginWithTimeout(
178
183
  loaded.plugin,
179
184
  loaded.config,
@@ -182,11 +187,14 @@ async function loadSinglePlugin(
182
187
  `Plugin "${loaded.plugin.name}" init failed`,
183
188
  );
184
189
 
185
- const version = loaded.plugin.version ? ` v${loaded.plugin.version}` : "";
186
- const desc = loaded.plugin.description
187
- ? ` — ${loaded.plugin.description}`
188
- : "";
189
- log("plugin", `Loaded: ${loaded.plugin.name}${version}${desc}`);
190
+ log("plugin", `Loaded: ${describePlugin(loaded.plugin)}`);
191
+ }
192
+
193
+ /** `name v1.0 — description`, for load/register log lines. */
194
+ function describePlugin(plugin: TalonPlugin): string {
195
+ const version = plugin.version ? ` v${plugin.version}` : "";
196
+ const desc = plugin.description ? ` — ${plugin.description}` : "";
197
+ return `${plugin.name}${version}${desc}`;
190
198
  }
191
199
 
192
200
  function resolveEntryPoint(pluginDir: string): string | null {
@@ -198,13 +206,11 @@ function resolveEntryPoint(pluginDir: string): string | null {
198
206
  }
199
207
 
200
208
  function extractPlugin(mod: Record<string, unknown>): TalonPlugin | null {
201
- // Support: export default { ... } or module.exports = { ... }
209
+ // `export default { … }` or `module.exports = { … }`
202
210
  const candidate = mod.default ?? mod;
203
211
  if (!candidate || typeof candidate !== "object") return null;
204
212
  const plugin = candidate as Record<string, unknown>;
205
- // Validate required field types
206
213
  if (typeof plugin.name !== "string" || !plugin.name) return null;
207
- // Validate optional fields are the right types if present
208
214
  if (
209
215
  plugin.handleAction !== undefined &&
210
216
  typeof plugin.handleAction !== "function"
@@ -241,33 +247,27 @@ function extractPlugin(mod: Record<string, unknown>): TalonPlugin | null {
241
247
 
242
248
  // ── Public API ─────────────────────────────────────────────────────────────
243
249
 
244
- /** Get all loaded plugins. */
245
250
  export function getLoadedPlugins(): readonly LoadedPlugin[] {
246
251
  return registry.all;
247
252
  }
248
253
 
249
- /** Get a plugin by name. */
250
254
  export function getPlugin(name: string): LoadedPlugin | undefined {
251
255
  return registry.getByName(name);
252
256
  }
253
257
 
254
- /** Number of loaded plugins. */
255
258
  export function getPluginCount(): number {
256
259
  return registry.count;
257
260
  }
258
261
 
259
- /** Destroy all plugins (called during shutdown). */
262
+ /** Shutdown path. */
260
263
  export async function destroyPlugins(): Promise<void> {
261
264
  await registry.destroyAll();
262
265
  }
263
266
 
264
267
  /**
265
- * Register a built-in plugin directly (bypasses filesystem loader).
266
- * Used for tightly-integrated plugins like mempalace that are configured via
267
- * dedicated config fields rather than the plugins[] array.
268
- *
269
- * NOTE: This only registers the plugin — it does NOT call `init()`. The caller
270
- * is responsible for calling `plugin.init()` separately after registration.
268
+ * Register a built-in plugin (configured by its own config section, not a
269
+ * `plugins[]` entry) without the filesystem loader. Does NOT call `init()`;
270
+ * the caller does.
271
271
  */
272
272
  export function registerPlugin(
273
273
  plugin: TalonPlugin,
@@ -275,19 +275,11 @@ export function registerPlugin(
275
275
  ): LoadedPlugin | null {
276
276
  const loaded = registerPluginInstance(plugin, config, "(built-in)");
277
277
  if (!loaded) return null;
278
-
279
- const version = loaded.plugin.version ? ` v${loaded.plugin.version}` : "";
280
- const desc = loaded.plugin.description
281
- ? ` — ${loaded.plugin.description}`
282
- : "";
283
- log("plugin", `Registered built-in: ${loaded.plugin.name}${version}${desc}`);
278
+ log("plugin", `Registered built-in: ${describePlugin(loaded.plugin)}`);
284
279
  return loaded;
285
280
  }
286
281
 
287
- /**
288
- * Collect system prompt additions from all plugins.
289
- * Called during config/prompt assembly.
290
- */
282
+ /** Every plugin's trimmed system-prompt addition, load order. */
291
283
  export function getPluginPromptAdditions(): string[] {
292
284
  const additions: string[] = [];
293
285
  for (const { plugin, config } of registry.all) {
@@ -1,7 +1,7 @@
1
1
  /**
2
- * MCP server config — build the Claude Agent SDK MCP server map for plugins
3
- * that expose an MCP server (via `mcpServer` command/args or `mcpServerPath`),
4
- * plus the standalone MCP entries from config.
2
+ * MCP server specs — the stdio command for every plugin that exposes an MCP
3
+ * server (via `mcpServer` command/args or `mcpServerPath`), plus the
4
+ * standalone MCP entries from config. The hub spawns its children from these.
5
5
  */
6
6
 
7
7
  import { resolve } from "node:path";
@@ -54,7 +54,6 @@ export function getPluginMcpServers(
54
54
  );
55
55
 
56
56
  for (const { plugin, envVars } of registry.all) {
57
- // Skip plugins not in the allow-list when filtering
58
57
  if (only !== undefined && !only.includes(plugin.name)) continue;
59
58
 
60
59
  // Let the plugin re-materialize on-disk state the child will read. Runs
@@ -95,7 +94,6 @@ export function getPluginMcpServers(
95
94
  }
96
95
  }
97
96
 
98
- // Include standalone MCP server entries from config
99
97
  for (const entry of registry.mcpEntries) {
100
98
  if (only !== undefined && !only.includes(entry.name)) continue;
101
99
  servers[`${entry.name}-tools`] = wrapMcpServer({
@@ -27,42 +27,29 @@ class PluginRegistry {
27
27
  return this.plugins.length;
28
28
  }
29
29
 
30
- private getRegistrationSource(name: string): string | undefined {
31
- const existingPlugin = this.plugins.find(
32
- (entry) => entry.plugin.name === name,
30
+ /** True when `name` is free; warns and returns false on a duplicate. */
31
+ private isNameFree(name: string): boolean {
32
+ const source =
33
+ this.plugins.find((entry) => entry.plugin.name === name)?.path ??
34
+ (this.standaloneMcpServers.some((entry) => entry.name === name)
35
+ ? "standalone MCP entry"
36
+ : undefined);
37
+ if (!source) return true;
38
+ logWarn(
39
+ "plugin",
40
+ `Duplicate plugin/MCP name "${name}" — skipping (already registered from ${source})`,
33
41
  );
34
- if (existingPlugin) return existingPlugin.path;
35
-
36
- const existingMcpEntry = this.standaloneMcpServers.find(
37
- (entry) => entry.name === name,
38
- );
39
- if (existingMcpEntry) return "standalone MCP entry";
40
-
41
- return undefined;
42
+ return false;
42
43
  }
43
44
 
44
45
  register(loaded: LoadedPlugin): boolean {
45
- const existingSource = this.getRegistrationSource(loaded.plugin.name);
46
- if (existingSource) {
47
- logWarn(
48
- "plugin",
49
- `Duplicate plugin/MCP name "${loaded.plugin.name}" — skipping (already registered from ${existingSource})`,
50
- );
51
- return false;
52
- }
46
+ if (!this.isNameFree(loaded.plugin.name)) return false;
53
47
  this.plugins.push(loaded);
54
48
  return true;
55
49
  }
56
50
 
57
51
  registerMcpEntry(entry: PluginMcpEntry): boolean {
58
- const existingSource = this.getRegistrationSource(entry.name);
59
- if (existingSource) {
60
- logWarn(
61
- "plugin",
62
- `Duplicate plugin/MCP name "${entry.name}" — skipping (already registered from ${existingSource})`,
63
- );
64
- return false;
65
- }
52
+ if (!this.isNameFree(entry.name)) return false;
66
53
  this.standaloneMcpServers.push(entry);
67
54
  return true;
68
55
  }
@@ -98,14 +85,12 @@ class PluginRegistry {
98
85
  }
99
86
  }
100
87
 
101
- // Module-level singleton
102
88
  export const registry = new PluginRegistry();
103
89
 
104
90
  /**
105
- * Tracks the last reload timestamp. Injected into every MCP subprocess env as
106
- * TALON_RELOAD_AT so the Claude SDK sees a changed env on each reload and
107
- * spawns a fresh subprocess — picking up source-file changes without a full
108
- * Talon restart. On a holder object so other modules can read/update it.
91
+ * The last plugin reload time: the module cache-bust key (see
92
+ * `_deps.importModule`), also passed to every MCP child as TALON_RELOAD_AT.
93
+ * On a holder object so other modules can read/update it.
109
94
  */
110
95
  export const reloadState: { lastReloadAt: string } = {
111
96
  lastReloadAt: new Date().toISOString(),
@@ -125,9 +110,8 @@ export const _deps = {
125
110
  // URL forever, so without a changing query a hot reload re-imports the
126
111
  // *old* module object and the plugin's own source edits are invisible
127
112
  // until a full restart — silently, since the stale module still loads
128
- // fine (observed 2026-09-16: a rewritten plugin kept reporting its
129
- // previous version through three reloads). The timestamp only moves per
130
- // reload, so every module in one load cycle shares a key.
113
+ // fine. The timestamp only moves per reload, so every module in one
114
+ // load cycle shares a key.
131
115
  const url = pathToFileURL(path);
132
116
  url.searchParams.set("talonReloadAt", reloadState.lastReloadAt);
133
117
  return import(url.href);
@@ -35,10 +35,7 @@ export function isMcpPlugin(entry: PluginEntry): entry is PluginMcpEntry {
35
35
  return "command" in entry && "name" in entry && !("path" in entry);
36
36
  }
37
37
 
38
- /**
39
- * Core plugin interface — only `name` is required.
40
- * All other capabilities are optional (Interface Segregation).
41
- */
38
+ /** Core plugin interface — only `name` is required. */
42
39
  export interface TalonPlugin {
43
40
  /** Unique plugin identifier. Used as MCP server name prefix. */
44
41
  readonly name: string;
@@ -134,7 +131,7 @@ export interface LoadedPlugin {
134
131
  readonly path: string;
135
132
  }
136
133
 
137
- /** MCP server configuration for the Claude Agent SDK. */
134
+ /** Stdio MCP server spec (command, args, env). */
138
135
  export interface McpServerConfig {
139
136
  command: string;
140
137
  args: string[];
@@ -63,7 +63,7 @@ import { existsSync, readFileSync } from "node:fs";
63
63
  import { resolve } from "node:path";
64
64
  import { dirs, files as pathFiles } from "../../util/paths.js";
65
65
  import { todayAndYesterday } from "../../util/time.js";
66
- import { log } from "../../util/log.js";
66
+ import { log, logWarn } from "../../util/log.js";
67
67
  import { loadSystemTemplate } from "./templates.js";
68
68
  import { renderMemoryView } from "./memory-view.js";
69
69
  import { renderWorkspaceListing } from "./workspace-listing.js";
@@ -125,11 +125,23 @@ const MEMORY_CHARS_METRIC = "prompt.memory_chars";
125
125
 
126
126
  // ── Helpers ─────────────────────────────────────────────────────────────────
127
127
 
128
+ /** Unreadable prompt files already reported (path + error code). */
129
+ const reportedUnreadable = new Set<string>();
130
+
128
131
  function readOptionalFile(path: string): string {
129
132
  try {
130
133
  if (existsSync(path)) return readFileSync(path, "utf-8").trim();
131
- } catch {
132
- /* ignore */
134
+ } catch (err) {
135
+ // Absent is fine (existsSync above); present-but-unreadable silently
136
+ // drops a prompt section, so say which — once, not every assembly.
137
+ const key = `${path}\0${(err as NodeJS.ErrnoException).code ?? ""}`;
138
+ if (!reportedUnreadable.has(key)) {
139
+ reportedUnreadable.add(key);
140
+ logWarn(
141
+ "workspace",
142
+ `Prompt file unreadable, section omitted path=${path}: ${err instanceof Error ? err.message : String(err)}`,
143
+ );
144
+ }
133
145
  }
134
146
  return "";
135
147
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Lua trigger runner — executes a trigger script inside a WASM-sandboxed
3
3
  * Lua 5.4 VM (wasmoon), speaking the same stdout protocol bash/python/node
4
- * triggers use (see core/background/triggers.ts).
4
+ * triggers use (see core/background/triggers/).
5
5
  *
6
6
  * Why a separate process at all: the trigger supervisor's whole contract
7
7
  * (kill/respawn, SIGTERM grace, log streaming, TALON_FIRE line scanning)
@@ -62,7 +62,11 @@ import { basename } from "node:path";
62
62
  * Dispatched by src/index.ts and src/cli.ts before the app graph loads. */
63
63
  export const LUA_RUN_SUBCOMMAND = "_lua-run";
64
64
 
65
- /** Must match FIRE_PREFIX in core/background/triggers.ts. */
65
+ /**
66
+ * Must match FIRE_PREFIX in core/background/triggers/state.ts. Copied,
67
+ * not imported: that module pulls in the dispatcher, and the `_lua-run`
68
+ * helper process must stay light.
69
+ */
66
70
  const FIRE_PREFIX = "TALON_FIRE:";
67
71
 
68
72
  /**
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { bus } from "../bus/index.js";
16
+ import { logWarn } from "../../util/log.js";
16
17
  import type { TaskSettledEvent, TaskStartedEvent } from "../bus/events.js";
17
18
  import type {
18
19
  KillOutcome,
@@ -114,9 +115,14 @@ export class TaskTable {
114
115
  task.killRequested = true;
115
116
  try {
116
117
  task.abort();
117
- } catch {
118
+ } catch (err) {
118
119
  // The abort hook contract is "must not throw" — a violation must
119
- // not break the kill path for the caller.
120
+ // not break the kill path for the caller, but the run may now
121
+ // never settle, so leave a trace of why.
122
+ logWarn(
123
+ "tasks",
124
+ `Abort hook threw task=${id} kind=${task.record.kind} label=${JSON.stringify(task.record.label)}: ${err instanceof Error ? err.message : String(err)}`,
125
+ );
120
126
  }
121
127
  }
122
128
  return { ok: true };
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Bridge utilities — shared by the unified MCP server.
3
- *
4
- * Extracted from the old per-backend tools.ts files so there's
5
- * exactly one copy of callBridge / textResult.
2
+ * Bridge utilities — the one copy of callBridge / textResult, shared by
3
+ * the unified MCP server.
6
4
  *
7
5
  * At the tools root, not in `ops/`: the group directories hold tool
8
6
  * DOMAINS (one file per family of tool definitions), and this is the
@@ -25,7 +25,7 @@ Media: give file_path (a workspace file) or url (a public URL, fetched by the pl
25
25
  Examples:
26
26
  send_via(frontend="whatsapp", target="+353871234567", text="On my way")
27
27
  send_via(frontend="whatsapp", target="+353871234567", file_path="media/plot.png", text="here's the chart")
28
- send_via(frontend="telegram", target="-1001426819337", text="Build is green")
28
+ send_via(frontend="telegram", target="-1009876543210", text="Build is green")
29
29
 
30
30
  Fails with a clear error when the named frontend is not enabled or not connected.`,
31
31
  schema: {
@@ -257,7 +257,7 @@ Examples:
257
257
  chat_id: chatIdSchema
258
258
  .optional()
259
259
  .describe(
260
- "Target chat ID. Omit to send to the current chat (chat mode). Required from heartbeat mode where there is no ambient chat — use list_chats or known IDs from memory. Telegram supergroup/channel IDs are negative (e.g. -1001426819337); user DMs are positive.",
260
+ "Target chat ID. Omit to send to the current chat (chat mode). Required from heartbeat mode where there is no ambient chat — use list_chats or known IDs from memory. Telegram supergroup/channel IDs are negative (e.g. -1009876543210); user DMs are positive.",
261
261
  ),
262
262
  silent: z
263
263
  .boolean()
@@ -112,7 +112,7 @@ const ALL_TOOL_NAMES: ReadonlySet<string> = new Set(
112
112
  * e.g. `mcp__telegram-tools__end_turn` → `end_turn`.
113
113
  *
114
114
  * 2. **Kilo / OpenCode:** `<server>_<tool>` (single underscore boundary,
115
- * no `mcp__` prefix). e.g. `talon-tools-352042062_send` → `send`,
115
+ * no `mcp__` prefix). e.g. `talon-tools-424242420_send` → `send`,
116
116
  * `talon-tools-heartbeat_end_turn` → `end_turn`. Bare names that
117
117
  * contain underscores (`end_turn`) make a "split on _" approach
118
118
  * ambiguous, so we resolve by checking whether the trailing segment
@@ -132,7 +132,7 @@ export function stripMcpPrefix(toolName: string): string {
132
132
 
133
133
  // Kilo / OpenCode `<server>_<bare>` form. Walk underscore boundaries
134
134
  // from the right; the longest tail that matches a registered tool name
135
- // is the bare name. Prevents `talon-tools-352042062_end_turn` from
135
+ // is the bare name. Prevents `talon-tools-424242420_end_turn` from
136
136
  // resolving to `turn` (which isn't a tool).
137
137
  const segments = toolName.split("_");
138
138
  for (let i = 1; i < segments.length; i++) {
@@ -1,70 +1,13 @@
1
1
  /**
2
- * Talon MCP server environment — the single place that defines the
3
- * env-var contract between backends (which spawn `mcp-server.ts` as a
4
- * subprocess) and the server itself (which reads these vars at boot).
5
- *
6
- * Before this helper, four backends (claude-sdk, codex, openai-agents,
7
- * kilo/opencode via remote-server) each hand-built the same
8
- * `{TALON_BRIDGE_URL, TALON_CHAT_ID, TALON_FRONTEND}` literal —
9
- * adding a variable meant editing all four. Now they call
10
- * `buildTalonMcpEnv` and new vars propagate everywhere.
11
- *
12
2
  * Tool-surface trimming: every registered MCP tool costs context
13
3
  * tokens in EVERY session (name + description + schema). Deployments
14
4
  * that never use whole tool groups (stickers, polls, triggers, …) can
15
5
  * reclaim that budget via `disabledToolTags` / `disabledTools` in
16
- * talon.json — carried to the subprocess as comma-separated env vars.
6
+ * talon.json.
17
7
  */
18
8
 
19
- // ── Env-var names (shared constants, not stringly-typed call sites) ────────
20
-
21
- const ENV_BRIDGE_URL = "TALON_BRIDGE_URL";
22
- const ENV_CHAT_ID = "TALON_CHAT_ID";
23
- const ENV_FRONTEND = "TALON_FRONTEND";
24
- export const ENV_DISABLED_TOOLS = "TALON_DISABLED_TOOLS";
25
- export const ENV_DISABLED_TOOL_TAGS = "TALON_DISABLED_TOOL_TAGS";
26
-
27
- // ── Builder (backend side) ──────────────────────────────────────────────────
28
-
29
- /** The slice of TalonConfig the MCP env cares about. */
9
+ /** The slice of TalonConfig the tool exclusions care about. */
30
10
  export type ToolExclusionConfig = {
31
11
  disabledTools?: readonly string[];
32
12
  disabledToolTags?: readonly string[];
33
13
  };
34
-
35
- export type TalonMcpEnvInputs = {
36
- bridgeUrl: string;
37
- chatId: string;
38
- frontend: string;
39
- /** Pass the Talon config (or the exclusion slice of it) when available. */
40
- config?: ToolExclusionConfig | null;
41
- };
42
-
43
- /** Build the env map for spawning Talon's unified MCP tool server. */
44
- export function buildTalonMcpEnv(
45
- inputs: TalonMcpEnvInputs,
46
- ): Record<string, string> {
47
- const env: Record<string, string> = {
48
- [ENV_BRIDGE_URL]: inputs.bridgeUrl,
49
- [ENV_CHAT_ID]: inputs.chatId,
50
- [ENV_FRONTEND]: inputs.frontend,
51
- };
52
- const tools = inputs.config?.disabledTools;
53
- if (tools?.length) env[ENV_DISABLED_TOOLS] = tools.join(",");
54
- const tags = inputs.config?.disabledToolTags;
55
- if (tags?.length) env[ENV_DISABLED_TOOL_TAGS] = tags.join(",");
56
- return env;
57
- }
58
-
59
- // ── Spawn spec ──────────────────────────────────────────────────────────────
60
-
61
- // ── Parser (server side) ────────────────────────────────────────────────────
62
-
63
- /** Parse a comma-separated env var into a trimmed, de-blanked list. */
64
- export function parseEnvList(raw: string | undefined): string[] {
65
- if (!raw) return [];
66
- return raw
67
- .split(",")
68
- .map((s) => s.trim())
69
- .filter((s) => s.length > 0);
70
- }
@@ -184,10 +184,31 @@ export const agentTools: ToolDefinition[] = [
184
184
  tag: "agents",
185
185
  },
186
186
 
187
+ {
188
+ name: "list_peers",
189
+ description:
190
+ "Sub-agents only. List the other agents your parent spawned alongside you — id, label and what each is working on. These are the only agents you may message directly with message_peer. Call it before assuming you are working alone.",
191
+ schema: {},
192
+ execute: (params, bridge) => bridge("list_peers", params),
193
+ tag: "agents",
194
+ },
195
+
196
+ {
197
+ name: "message_peer",
198
+ description:
199
+ "Sub-agents only. Send a note straight to a peer — an agent spawned by the same parent as you — without routing it through your parent. Use it when you find something that changes another agent's work: a shared fact, a dead end worth not repeating, a correction to something you sent earlier. The peer sees it at its next check_inbox, so it is not an interrupt. You can only address peers (see list_peers); any other id is refused. This does not end your run and does not replace report_result.",
200
+ schema: {
201
+ agent_id: z.string().describe("Peer agent id, from list_peers"),
202
+ text: z.string().min(1).describe("What the peer needs to know"),
203
+ },
204
+ execute: (params, bridge) => bridge("message_peer", params),
205
+ tag: "agents",
206
+ },
207
+
187
208
  {
188
209
  name: "check_inbox",
189
210
  description:
190
- "Sub-agents only. Drain any instructions your parent has sent you. Messages are delivered no other way, so check at milestones: after a phase of work, before a long operation, and before you report.",
211
+ "Sub-agents only. Drain anything sent to you — instructions from your parent, and notes from peer agents working alongside you. Each message names its sender. Messages are delivered no other way, so check at milestones: after a phase of work, before a long operation, and before you report.",
191
212
  schema: {},
192
213
  execute: (_params, bridge) => bridge("check_inbox", {}),
193
214
  tag: "agents",
@@ -41,8 +41,8 @@ export const idSchema = z.union([
41
41
  * sentinel the gateway already treats as falsy/unrouted.
42
42
  *
43
43
  * Accepts:
44
- * - actual non-zero integer numbers (`352042062`, `-1001426819337`)
45
- * - integer strings with optional leading minus (`"-1001426819337"`)
44
+ * - actual non-zero integer numbers (`424242420`, `-1009876543210`)
45
+ * - integer strings with optional leading minus (`"-1009876543210"`)
46
46
  *
47
47
  * Rejects:
48
48
  * - zero (`0`, `"0"`, `"-0"`)
@@ -51,13 +51,8 @@ export const idSchema = z.union([
51
51
  *
52
52
  * Use this for `chat_id` fields on tool input schemas. The bare
53
53
  * `idSchema` is for message/user/reply IDs (always positive) and
54
- * would reject the negative IDs Telegram uses for groups/channels —
55
- * which was the bug PR #150 shipped with: heartbeat outbound `send`
56
- * to a supergroup got `expected number, received string` (the model
57
- * sees a `number` JSON schema, but zod rejects negatives before the
58
- * gateway sees the request). Gateway-side handling for negative
59
- * chat_ids was already tested and correct — only the tool-input
60
- * schema layer needed the fix.
54
+ * would reject the negative IDs Telegram uses for groups/channels
55
+ * before the gateway ever sees the request.
61
56
  */
62
57
  const nonZeroInt = z
63
58
  .number()
@@ -90,11 +90,6 @@ export function namespaceFsStatus(): FuseStatus {
90
90
  return status;
91
91
  }
92
92
 
93
- /** Test seam — force a status without a real mount. */
94
- function _setNamespaceFsStatusForTesting(next: FuseStatus): void {
95
- status = next;
96
- }
97
-
98
93
  /** Test seam — drive one watchdog tick synchronously (no interval wait). */
99
94
  export async function _checkNamespaceFsHealthForTesting(): Promise<void> {
100
95
  await checkNamespaceFsHealth();
@@ -12,7 +12,8 @@
12
12
  * skills/ SKILL.md bundles (writable)
13
13
  * scripts/ agent script bodies (writable)
14
14
  * logs/ daily logs (read-only — the daemon writes these)
15
- * proc/ task table + event bus ring (synthetic, read-only)
15
+ * proc/ task table, event bus ring, and the daemon's own
16
+ * log/errors/alerts views (synthetic, read-only)
16
17
  * plugins/ plugin registry view (synthetic, read-only)
17
18
  *
18
19
  * The namespace IS the filesystem: it lives at ~/.talon/ns (symlink
@@ -25,10 +26,12 @@
25
26
  * grammar in vfs.ts / fusefs.ts; consumers never see it.)
26
27
  */
27
28
 
28
- import { dirs } from "../../util/paths.js";
29
+ import { dirs, files } from "../../util/paths.js";
29
30
  import { bus } from "../bus/index.js";
31
+ import { activeAlerts } from "../frontend-runtime/alerts.js";
30
32
  import { registry } from "../plugin/registry.js";
31
33
  import { taskTable } from "../tasks/index.js";
34
+ import { createDiagnosticViews } from "./mounts/diagnostics.js";
32
35
  import { createFileMount } from "./mounts/files.js";
33
36
  import { createPluginsMount, type PluginView } from "./mounts/plugins.js";
34
37
  import { createProcMount } from "./mounts/proc.js";
@@ -95,6 +98,10 @@ function buildDefaultNamespace(): Vfs {
95
98
  createProcMount({
96
99
  tasks: () => taskTable.list(),
97
100
  events: () => bus.recent(0),
101
+ views: createDiagnosticViews({
102
+ logPath: files.log,
103
+ alerts: activeAlerts,
104
+ }),
98
105
  }),
99
106
  );
100
107
  vfs.mount("plugins", createPluginsMount(pluginViews));