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.
- package/README.md +2 -1
- package/package.json +2 -2
- package/prompts/system/agent-brief.md +20 -3
- package/src/app.ts +13 -0
- package/src/backend/claude-sdk/handler.ts +4 -4
- package/src/backend/claude-sdk/mcp-ready.ts +16 -2
- package/src/backend/claude-sdk/one-shot.ts +32 -2
- package/src/backend/claude-sdk/stream.ts +2 -2
- package/src/backend/codex/auth.ts +1 -1
- package/src/backend/codex/handler/message.ts +11 -11
- package/src/backend/codex/init.ts +4 -9
- package/src/backend/codex/mcp-config.ts +1 -2
- package/src/backend/codex/oauth-incompat.ts +8 -4
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/openai-agents/builtins.ts +55 -27
- package/src/backend/openai-agents/factory.ts +3 -3
- package/src/backend/openai-agents/handler/message.ts +3 -5
- package/src/backend/openai-agents/mcp-pool.ts +10 -27
- package/src/backend/remote-server/chat-turn.ts +6 -6
- package/src/backend/remote-server/events.ts +1 -5
- package/src/backend/remote-server/index.ts +0 -1
- package/src/backend/remote-server/messages.ts +3 -7
- package/src/backend/remote-server/one-shot.ts +1 -3
- package/src/backend/remote-server/session-helpers.ts +1 -4
- package/src/backend/remote-server/sse-stream.ts +8 -9
- package/src/backend/runtime/metrics.ts +7 -13
- package/src/backend/runtime/sleep.ts +1 -2
- package/src/backend/runtime/turn/handle-retry.ts +48 -2
- package/src/backend/runtime/turn/handler-to-events.ts +3 -4
- package/src/bootstrap.ts +9 -1
- package/src/cli/doctor.ts +3 -0
- package/src/cli/index.ts +8 -10
- package/src/cli/logs.ts +148 -9
- package/src/cli/setup.ts +9 -11
- package/src/cli/status.ts +29 -0
- package/src/core/agent-runtime/README.md +5 -19
- package/src/core/agent-runtime/events.ts +3 -39
- package/src/core/agent-runtime/model-ref.ts +0 -8
- package/src/core/agents/registry.ts +65 -2
- package/src/core/auth/expiry-monitor.ts +9 -1
- package/src/core/auth/login-flow.ts +9 -1
- package/src/core/auth/status.ts +31 -3
- package/src/core/background/cron/scheduler.ts +25 -5
- package/src/core/background/dream/index.ts +29 -10
- package/src/core/background/failure-backoff.ts +30 -0
- package/src/core/background/heartbeat/agent.ts +2 -27
- package/src/core/background/heartbeat/index.ts +0 -2
- package/src/core/background/heartbeat/scheduler.ts +21 -13
- package/src/core/background/heartbeat/state.ts +10 -2
- package/src/core/background/isolated-agent.ts +6 -2
- package/src/core/background/pulse/pulse.ts +9 -0
- package/src/core/background/triggers/exit.ts +54 -0
- package/src/core/background/triggers/index.ts +1 -3
- package/src/core/background/triggers/resume.ts +2 -4
- package/src/core/backup/archive/tar.ts +14 -4
- package/src/core/backup/plan.ts +1 -0
- package/src/core/backup/restore.ts +21 -13
- package/src/core/backup/scheduler.ts +28 -9
- package/src/core/backup/snapshot.ts +43 -10
- package/src/core/backup/store.ts +7 -19
- package/src/core/backup/targets.ts +69 -17
- package/src/core/config/index.ts +14 -0
- package/src/core/daemon/crash-marker.ts +141 -0
- package/src/core/daemon/crash.ts +9 -2
- package/src/core/daemon/handoff.ts +15 -0
- package/src/core/daemon/health-alerts.ts +297 -0
- package/src/core/daemon/log-reader.ts +289 -0
- package/src/core/doctor/index.ts +18 -2
- package/src/core/doctor/logs.ts +124 -0
- package/src/core/doctor/types.ts +1 -1
- package/src/core/engine/backend-controller/index.ts +1 -13
- package/src/core/engine/backend-router/router.ts +1 -1
- package/src/core/engine/dispatcher.ts +55 -2
- package/src/core/engine/fault-text.ts +40 -0
- package/src/core/engine/gateway-actions/agents/index.ts +3 -2
- package/src/core/engine/gateway-actions/agents/report.ts +62 -0
- package/src/core/engine/gateway-actions/history.ts +2 -4
- package/src/core/engine/gateway-actions/mesh.ts +26 -14
- package/src/core/engine/gateway-actions/native/exec.ts +13 -16
- package/src/core/engine/gateway-actions/native/teleport.ts +1 -1
- package/src/core/engine/gateway.ts +60 -1
- package/src/core/engine/turn-health.ts +222 -0
- package/src/core/errors.ts +2 -2
- package/src/core/frontend-runtime/admin-notify.ts +1 -1
- package/src/core/frontend-runtime/alerts.ts +130 -0
- package/src/core/mcp-hub/children.ts +78 -29
- package/src/core/mcp-hub/index.ts +21 -18
- package/src/core/mcp-hub/proxy-server.ts +8 -4
- package/src/core/mcp-hub/talon-server.ts +5 -12
- package/src/core/mesh/credentials/store.ts +16 -1
- package/src/core/mesh/devices/registry.ts +1 -17
- package/src/core/mesh/devices/service.ts +49 -17
- package/src/core/mesh/devices/teleport.ts +14 -2
- package/src/core/mesh/links/node-binaries.ts +13 -6
- package/src/core/mesh/persist.ts +22 -10
- package/src/core/mesh/transfers/device-files.ts +5 -23
- package/src/core/mesh/transfers/transfers.ts +16 -3
- package/src/core/models/active-model.ts +2 -55
- package/src/core/plugin/actions.ts +19 -20
- package/src/core/plugin/builtins.ts +80 -90
- package/src/core/plugin/index.ts +1 -4
- package/src/core/plugin/loader.ts +25 -33
- package/src/core/plugin/mcp.ts +3 -5
- package/src/core/plugin/registry.ts +19 -35
- package/src/core/plugin/types.ts +2 -5
- package/src/core/prompt/assemble.ts +15 -3
- package/src/core/scripts/lua.ts +6 -2
- package/src/core/tasks/table.ts +8 -2
- package/src/core/tools/bridge.ts +2 -4
- package/src/core/tools/chat/cross-send.ts +1 -1
- package/src/core/tools/chat/messaging.ts +1 -1
- package/src/core/tools/index.ts +2 -2
- package/src/core/tools/mcp-env.ts +2 -59
- package/src/core/tools/ops/agents.ts +22 -1
- package/src/core/tools/schemas.ts +4 -9
- package/src/core/vfs/fusefs.ts +0 -5
- package/src/core/vfs/index.ts +9 -2
- package/src/core/vfs/mounts/diagnostics.ts +109 -0
- package/src/core/vfs/mounts/proc.ts +17 -1
- package/src/core/vfs/workspace.ts +7 -3
- package/src/core/weaver/shuttle.ts +8 -1
- package/src/core/weaver/turn-log.ts +320 -0
- package/src/core/weaver/weaver.ts +48 -5
- package/src/frontend/discord/actions/index.ts +8 -1
- package/src/frontend/discord/diagnostics.ts +82 -6
- package/src/frontend/discord/handlers/index.ts +0 -2
- package/src/frontend/discord/middleware.ts +7 -13
- package/src/frontend/discord/runtime.ts +1 -3
- package/src/frontend/health/delivery.ts +115 -0
- package/src/frontend/health/outage.ts +116 -0
- package/src/frontend/native/bridge/routes/chats.ts +3 -5
- package/src/frontend/native/bridge/server.ts +106 -21
- package/src/frontend/native/index.ts +1 -1
- package/src/frontend/native/media/media.ts +5 -1
- package/src/frontend/native/runtime.ts +12 -7
- package/src/frontend/native/surface/handlers.ts +1 -1
- package/src/frontend/native/surface/memory.ts +1 -1
- package/src/frontend/native/surface/models.ts +3 -3
- package/src/frontend/native/surface/settings.ts +20 -8
- package/src/frontend/native/turn/context.ts +6 -8
- package/src/frontend/native/turn/turn-meta.ts +2 -5
- package/src/frontend/native/turn/turn.ts +8 -10
- package/src/frontend/presentation/format.ts +2 -4
- package/src/frontend/presentation/session-status.ts +2 -6
- package/src/frontend/teams/actions.ts +8 -1
- package/src/frontend/teams/graph.ts +0 -1
- package/src/frontend/teams/index.ts +1 -4
- package/src/frontend/teams/poll.ts +40 -2
- package/src/frontend/teams/runtime.ts +14 -5
- package/src/frontend/telegram/actions/index.ts +4 -1
- package/src/frontend/telegram/actions/send.ts +8 -0
- package/src/frontend/telegram/handlers/context.ts +13 -2
- package/src/frontend/telegram/handlers/delivery.ts +12 -9
- package/src/frontend/telegram/handlers/index.ts +0 -2
- package/src/frontend/telegram/index.ts +35 -9
- package/src/frontend/telegram/polling/poll-health.ts +110 -0
- package/src/frontend/telegram/userbot.ts +100 -36
- package/src/frontend/terminal/builtins/session.ts +2 -2
- package/src/frontend/terminal/index.ts +1 -3
- package/src/frontend/terminal/renderer.ts +2 -18
- package/src/frontend/whatsapp/actions/index.ts +12 -1
- package/src/frontend/whatsapp/actions/messaging.ts +7 -2
- package/src/frontend/whatsapp/connection/connection.ts +29 -9
- package/src/frontend/whatsapp/connection/health.ts +89 -0
- package/src/frontend/whatsapp/connection/identity.ts +4 -4
- package/src/frontend/whatsapp/runtime.ts +11 -5
- package/src/native/blake3.ts +28 -2
- package/src/native/fusefs.ts +23 -5
- package/src/native/registry.ts +1 -1
- package/src/native/warden.ts +33 -5
- package/src/plugins/github/index.ts +0 -1
- package/src/plugins/mempalace/index.ts +9 -3
- package/src/plugins/playwright/index.ts +2 -4
- package/src/plugins/playwright/provision.ts +12 -4
- package/src/storage/chat-settings.ts +6 -1
- package/src/storage/cron.ts +29 -4
- package/src/storage/daily-log.ts +43 -47
- package/src/storage/db.ts +61 -33
- package/src/storage/history.ts +6 -1
- package/src/storage/journal.ts +9 -2
- package/src/storage/kv.ts +19 -6
- package/src/storage/media-index.ts +28 -6
- package/src/storage/repositories/chat-settings-repo.ts +8 -2
- package/src/storage/repositories/sessions-repo.ts +10 -3
- package/src/storage/scripts.ts +24 -13
- package/src/storage/sessions.ts +11 -2
- package/src/storage/skills.ts +21 -2
- package/src/storage/stickers.ts +17 -3
- package/src/storage/triggers.ts +8 -3
- package/src/storage/turn-meta.ts +25 -7
- package/src/util/log.ts +189 -6
- package/src/util/logging/turn-scope.ts +85 -0
- package/src/util/time.ts +3 -3
- package/src/util/watchdog.ts +30 -0
- 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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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) {
|
package/src/core/plugin/mcp.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP server
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
|
129
|
-
//
|
|
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);
|
package/src/core/plugin/types.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
package/src/core/scripts/lua.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
/**
|
package/src/core/tasks/table.ts
CHANGED
|
@@ -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 };
|
package/src/core/tools/bridge.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bridge utilities —
|
|
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="-
|
|
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. -
|
|
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()
|
package/src/core/tools/index.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
6
|
+
* talon.json.
|
|
17
7
|
*/
|
|
18
8
|
|
|
19
|
-
|
|
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
|
|
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 (`
|
|
45
|
-
* - integer strings with optional leading minus (`"-
|
|
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
|
-
*
|
|
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()
|
package/src/core/vfs/fusefs.ts
CHANGED
|
@@ -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();
|
package/src/core/vfs/index.ts
CHANGED
|
@@ -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
|
|
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));
|