talon-agent 5.18.2 → 5.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- 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/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 +49 -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/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/native/exec.ts +13 -16
- 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/service.ts +26 -15
- 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
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
156
|
+
await loadBuiltin("GitHub", github, 15_000, async () => {
|
|
95
157
|
const { createGitHubPlugin } =
|
|
96
158
|
await import("../../plugins/github/index.js");
|
|
97
|
-
|
|
159
|
+
return createGitHubPlugin({
|
|
98
160
|
token: github.token,
|
|
99
161
|
imageTag: github.imageTag,
|
|
100
162
|
});
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
+
await loadBuiltin("mem0", mem0, 15_000, async () => {
|
|
157
187
|
const { createMem0Plugin } = await import("../../plugins/mem0/index.js");
|
|
158
|
-
|
|
188
|
+
return createMem0Plugin({
|
|
159
189
|
apiKey: mem0.apiKey,
|
|
160
190
|
host: mem0.host,
|
|
161
191
|
userId: mem0.userId,
|
|
162
192
|
});
|
|
163
|
-
|
|
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
|
-
|
|
198
|
+
await loadBuiltin("Playwright", playwright, 15_000, async () => {
|
|
185
199
|
const { createPlaywrightPlugin } =
|
|
186
200
|
await import("../../plugins/playwright/index.js");
|
|
187
|
-
|
|
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
|
-
|
|
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,
|
|
221
|
-
* conversations continue uninterrupted — new MCP servers spawn
|
|
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
|
-
//
|
|
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
|
|
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
|
|
package/src/core/plugin/index.ts
CHANGED
|
@@ -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
|
|
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";
|
|
@@ -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({
|