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
|
@@ -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));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic proc views — "what went wrong" without knowing a path:
|
|
3
|
+
*
|
|
4
|
+
* proc/log the last lines of talon.log, one human line each
|
|
5
|
+
* proc/errors recent warn/error/fatal entries, with stack frames
|
|
6
|
+
* proc/alerts the operator alerts currently raised
|
|
7
|
+
*
|
|
8
|
+
* Rendered text, not pino JSON, so `cat` answers the question directly.
|
|
9
|
+
* The log views read a bounded tail of talon.log (plus its newest
|
|
10
|
+
* rotated generation when the live file is young) — a regular file
|
|
11
|
+
* outside ~/.talon/ns, so the FUSE deadlock rule is not in play — and
|
|
12
|
+
* memoize for a second: FUSE stats a node before reading it, and both
|
|
13
|
+
* should see the same text without reading the log twice.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
formatLogRecord,
|
|
18
|
+
readLogRecords,
|
|
19
|
+
type LogRecord,
|
|
20
|
+
} from "../../daemon/log-reader.js";
|
|
21
|
+
|
|
22
|
+
export type AlertView = {
|
|
23
|
+
key: string;
|
|
24
|
+
severity: string;
|
|
25
|
+
message: string;
|
|
26
|
+
since: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface DiagnosticViewDeps {
|
|
30
|
+
/** talon.log. */
|
|
31
|
+
logPath: string;
|
|
32
|
+
/** The live alert table (`activeAlerts`). */
|
|
33
|
+
alerts: () => ReadonlyArray<AlertView>;
|
|
34
|
+
now?: () => number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const LOG_LINES = 200;
|
|
38
|
+
const ERROR_ENTRIES = 100;
|
|
39
|
+
const MEMO_MS = 1_000;
|
|
40
|
+
/** Two generations: the live file and the one it rotated from. */
|
|
41
|
+
const READ_OPTS = { maxFiles: 2, maxBytesPerFile: 1024 * 1024 } as const;
|
|
42
|
+
|
|
43
|
+
function memo(render: () => string, now: () => number): () => string {
|
|
44
|
+
let cached: { at: number; text: string } | null = null;
|
|
45
|
+
return () => {
|
|
46
|
+
const t = now();
|
|
47
|
+
if (cached && t - cached.at < MEMO_MS) return cached.text;
|
|
48
|
+
cached = { at: t, text: render() };
|
|
49
|
+
return cached.text;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function body(records: LogRecord[], stackLines: number, max: number): string {
|
|
54
|
+
return records
|
|
55
|
+
.map((r) => formatLogRecord(r, { stackLines, maxMsgChars: max }))
|
|
56
|
+
.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderLogView(logPath: string): string {
|
|
60
|
+
const records = readLogRecords(logPath, { limit: LOG_LINES, ...READ_OPTS });
|
|
61
|
+
if (records.length === 0) return `# ${logPath} is empty or missing\n`;
|
|
62
|
+
return (
|
|
63
|
+
`# last ${records.length} lines of ${logPath}, newest last ` +
|
|
64
|
+
`(older: ${logPath}.1…, or \`talon logs --since 1h\`)\n` +
|
|
65
|
+
`${body(records, 0, 400)}\n`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderErrorsView(logPath: string): string {
|
|
70
|
+
const records = readLogRecords(logPath, {
|
|
71
|
+
limit: ERROR_ENTRIES,
|
|
72
|
+
filter: { minLevel: "warn" },
|
|
73
|
+
...READ_OPTS,
|
|
74
|
+
});
|
|
75
|
+
if (records.length === 0) {
|
|
76
|
+
return `# no warnings or errors in the recent ${logPath}\n`;
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
`# last ${records.length} warn/error entries from ${logPath}, newest last\n` +
|
|
80
|
+
`${body(records, 5, 500)}\n`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderAlertsView(
|
|
85
|
+
alerts: ReadonlyArray<AlertView>,
|
|
86
|
+
now: number,
|
|
87
|
+
): string {
|
|
88
|
+
if (alerts.length === 0) return "# no active alerts\n";
|
|
89
|
+
const lines = alerts.map((a) => {
|
|
90
|
+
const mins = Math.max(0, Math.round((now - a.since) / 60_000));
|
|
91
|
+
return (
|
|
92
|
+
`[${a.severity}] ${a.key} since ${new Date(a.since).toISOString()} ` +
|
|
93
|
+
`(${mins} min)\n ${a.message}`
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
return `# ${alerts.length} active alert(s)\n${lines.join("\n")}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The proc views, keyed by file name — wired by the vfs index. */
|
|
100
|
+
export function createDiagnosticViews(
|
|
101
|
+
deps: DiagnosticViewDeps,
|
|
102
|
+
): Record<string, () => string> {
|
|
103
|
+
const now = deps.now ?? Date.now;
|
|
104
|
+
return {
|
|
105
|
+
log: memo(() => renderLogView(deps.logPath), now),
|
|
106
|
+
errors: memo(() => renderErrorsView(deps.logPath), now),
|
|
107
|
+
alerts: () => renderAlertsView(deps.alerts(), now()),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* proc/
|
|
6
6
|
* tasks/<id> one task table record, pretty JSON
|
|
7
7
|
* events the event bus ring, JSON Lines (newest last)
|
|
8
|
+
* <view> any extra text views the index wires (log, errors,
|
|
9
|
+
* alerts — see ./diagnostics.ts)
|
|
8
10
|
*
|
|
9
11
|
* Providers are injected so the mount is a pure projection — the index
|
|
10
12
|
* wires the real task table and bus, tests wire fixtures. Sizes and
|
|
@@ -20,6 +22,8 @@ import { vfsError, vfsOk } from "../types.js";
|
|
|
20
22
|
export interface ProcMountDeps {
|
|
21
23
|
tasks: () => readonly TaskRecord[];
|
|
22
24
|
events: () => readonly PublishedEvent[];
|
|
25
|
+
/** Extra read-only text files at the proc root, name → renderer. */
|
|
26
|
+
views?: Readonly<Record<string, () => string>>;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
const DIR = (path: string, name: string): VfsStat => ({
|
|
@@ -54,6 +58,13 @@ function renderEvents(events: readonly PublishedEvent[]): string {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export function createProcMount(deps: ProcMountDeps): VfsMount {
|
|
61
|
+
const views: Readonly<Record<string, () => string>> = Object.assign(
|
|
62
|
+
Object.create(null) as Record<string, () => string>,
|
|
63
|
+
deps.views,
|
|
64
|
+
);
|
|
65
|
+
const viewStats = (): VfsStat[] =>
|
|
66
|
+
Object.keys(views).map((name) => fileStat(name, views[name]()));
|
|
67
|
+
|
|
57
68
|
function taskById(id: string): TaskRecord | undefined {
|
|
58
69
|
if (!/^\d+$/.test(id)) return undefined;
|
|
59
70
|
return deps.tasks().find((task) => task.id === Number(id));
|
|
@@ -66,6 +77,7 @@ export function createProcMount(deps: ProcMountDeps): VfsMount {
|
|
|
66
77
|
const events = deps.events();
|
|
67
78
|
return vfsOk(fileStat("events", renderEvents(events), events.at(-1)?.at));
|
|
68
79
|
}
|
|
80
|
+
if (views[rel]) return vfsOk(fileStat(rel, views[rel]()));
|
|
69
81
|
const taskId = rel.startsWith("tasks/") ? rel.slice("tasks/".length) : null;
|
|
70
82
|
if (taskId !== null && !taskId.includes("/")) {
|
|
71
83
|
const task = taskById(taskId);
|
|
@@ -78,7 +90,9 @@ export function createProcMount(deps: ProcMountDeps): VfsMount {
|
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
return {
|
|
81
|
-
description:
|
|
93
|
+
description: deps.views
|
|
94
|
+
? "Live daemon state: task table, event bus ring, daemon log views"
|
|
95
|
+
: "Live daemon state: task table and event bus ring",
|
|
82
96
|
writable: false,
|
|
83
97
|
|
|
84
98
|
stat: resolveNode,
|
|
@@ -89,6 +103,7 @@ export function createProcMount(deps: ProcMountDeps): VfsMount {
|
|
|
89
103
|
return vfsOk([
|
|
90
104
|
DIR("tasks", "tasks"),
|
|
91
105
|
fileStat("events", renderEvents(events), events.at(-1)?.at),
|
|
106
|
+
...viewStats(),
|
|
92
107
|
]);
|
|
93
108
|
}
|
|
94
109
|
if (rel === "tasks") {
|
|
@@ -112,6 +127,7 @@ export function createProcMount(deps: ProcMountDeps): VfsMount {
|
|
|
112
127
|
|
|
113
128
|
read(rel) {
|
|
114
129
|
if (rel === "events") return vfsOk(renderEvents(deps.events()));
|
|
130
|
+
if (views[rel]) return vfsOk(views[rel]());
|
|
115
131
|
if (rel.startsWith("tasks/")) {
|
|
116
132
|
const task = taskById(rel.slice("tasks/".length));
|
|
117
133
|
if (!task) return vfsError("not-found");
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "node:fs";
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
22
|
import { join, resolve } from "node:path";
|
|
23
|
+
import writeFileAtomic from "write-file-atomic";
|
|
23
24
|
import { log } from "../../util/log.js";
|
|
24
25
|
import { dirs, files as pathFiles } from "../../util/paths.js";
|
|
25
26
|
import { listSeedPrompts, readPromptAsset } from "#prompt-assets";
|
|
@@ -259,8 +260,11 @@ function seedPrompts(): void {
|
|
|
259
260
|
const pkgHash = sha256(pkg);
|
|
260
261
|
|
|
261
262
|
if (!existsSync(dst)) {
|
|
263
|
+
// Atomic: a write cut short (ENOSPC, crash) must leave no file
|
|
264
|
+
// rather than a truncated one, which the next boot could not tell
|
|
265
|
+
// from a user edit and would keep forever.
|
|
262
266
|
try {
|
|
263
|
-
|
|
267
|
+
writeFileAtomic.sync(dst, pkg);
|
|
264
268
|
} catch (err) {
|
|
265
269
|
log(
|
|
266
270
|
"workspace",
|
|
@@ -293,7 +297,7 @@ function seedPrompts(): void {
|
|
|
293
297
|
} else if (seededHash !== undefined && curHash === seededHash) {
|
|
294
298
|
// Pristine seeded copy + package changed → refresh on upgrade.
|
|
295
299
|
try {
|
|
296
|
-
|
|
300
|
+
writeFileAtomic.sync(dst, pkg);
|
|
297
301
|
} catch {
|
|
298
302
|
continue; // manifest keeps the old hash — retried next boot
|
|
299
303
|
}
|
|
@@ -308,7 +312,7 @@ function seedPrompts(): void {
|
|
|
308
312
|
|
|
309
313
|
if (manifestDirty) {
|
|
310
314
|
try {
|
|
311
|
-
|
|
315
|
+
writeFileAtomic.sync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
312
316
|
} catch {
|
|
313
317
|
/* non-fatal — worst case we re-derive next boot */
|
|
314
318
|
}
|