talon-agent 5.18.1 → 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/LICENSE +202 -21
- package/LICENSE-MIT +21 -0
- package/NOTICE +16 -0
- package/README.md +8 -3
- package/package.json +4 -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/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/outgoing-log.ts +70 -0
- package/src/frontend/telegram/actions/send.ts +12 -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 +147 -41
- 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
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
* shared prompt vocabulary applies uniformly.
|
|
32
32
|
*/
|
|
33
33
|
import { tool } from "@openai/agents";
|
|
34
|
-
import { spawn } from "node:child_process";
|
|
34
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
35
35
|
import { readFile, writeFile, mkdir, glob } from "node:fs/promises";
|
|
36
36
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
37
|
+
import { createOutputCapture } from "../../util/exec-output.js";
|
|
37
38
|
import { expandFsPath as expandPath } from "../../util/fs-path.js";
|
|
38
39
|
|
|
39
40
|
// ── Read ────────────────────────────────────────────────────────────────────
|
|
@@ -204,6 +205,12 @@ const editTool = tool({
|
|
|
204
205
|
|
|
205
206
|
const BASH_DEFAULT_TIMEOUT_MS = 30_000;
|
|
206
207
|
const BASH_MAX_TIMEOUT_MS = 600_000;
|
|
208
|
+
/**
|
|
209
|
+
* After a timeout kill, how long to wait for `close` before settling with
|
|
210
|
+
* what was captured. `close` waits for the stdio pipes to drain, which a
|
|
211
|
+
* descendant that escaped the kill can hold open indefinitely.
|
|
212
|
+
*/
|
|
213
|
+
const BASH_CLOSE_GRACE_MS = 2_000;
|
|
207
214
|
|
|
208
215
|
interface BashInput {
|
|
209
216
|
command: string;
|
|
@@ -235,43 +242,64 @@ function runShell(
|
|
|
235
242
|
],
|
|
236
243
|
}
|
|
237
244
|
: { cmd: "bash", args: ["-lc", command] };
|
|
245
|
+
// detached → own process group on POSIX, so the timeout kill reaches
|
|
246
|
+
// the shell's children too. A surviving child (the `sleep` in
|
|
247
|
+
// `sleep 30; echo`) holds stdout open, and `close` waits for it.
|
|
248
|
+
const detached = process.platform !== "win32";
|
|
238
249
|
const child = spawn(shell.cmd, shell.args, {
|
|
239
250
|
cwd: process.cwd(),
|
|
240
251
|
env: process.env,
|
|
241
252
|
stdio: ["ignore", "pipe", "pipe"],
|
|
253
|
+
detached,
|
|
242
254
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
child.stderr.on("data",
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
// Bounded: an unbounded stream (`yes`, a verbose build) otherwise grows
|
|
256
|
+
// until V8's string limit throws inside the data listener.
|
|
257
|
+
const out = createOutputCapture();
|
|
258
|
+
const err = createOutputCapture();
|
|
259
|
+
child.stdout.on("data", out.push);
|
|
260
|
+
child.stderr.on("data", err.push);
|
|
261
|
+
let settled = false;
|
|
262
|
+
let timedOut = false;
|
|
263
|
+
let closeGrace: NodeJS.Timeout | undefined;
|
|
264
|
+
const finish = (code: number, stderr = err.value()): void => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
settled = true;
|
|
267
|
+
clearTimeout(killer);
|
|
268
|
+
if (closeGrace) clearTimeout(closeGrace);
|
|
269
|
+
resolveResult({ stdout: out.value(), stderr, code, timedOut });
|
|
270
|
+
};
|
|
251
271
|
const killer = setTimeout(() => {
|
|
252
272
|
timedOut = true;
|
|
253
|
-
child
|
|
273
|
+
killGroup(child, detached);
|
|
274
|
+
// A descendant that left the group can still hold the pipes open;
|
|
275
|
+
// don't let it pin the tool call past the timeout.
|
|
276
|
+
closeGrace = setTimeout(() => finish(-1), BASH_CLOSE_GRACE_MS);
|
|
254
277
|
}, timeoutMs);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
clearTimeout(killer);
|
|
261
|
-
resolveResult({
|
|
262
|
-
stdout,
|
|
263
|
-
stderr: err.message,
|
|
264
|
-
code: -1,
|
|
265
|
-
timedOut: false,
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
child.on("close", (code) => {
|
|
269
|
-
clearTimeout(killer);
|
|
270
|
-
resolveResult({ stdout, stderr, code: code ?? -1, timedOut });
|
|
271
|
-
});
|
|
278
|
+
// spawn failed (e.g. bash not in PATH on Windows). Resolve instead
|
|
279
|
+
// of letting Node emit an uncaught error — the tool returns the
|
|
280
|
+
// diagnostic so callers can surface it rather than crashing.
|
|
281
|
+
child.on("error", (spawnErr) => finish(-1, spawnErr.message));
|
|
282
|
+
child.on("close", (code) => finish(code ?? -1));
|
|
272
283
|
});
|
|
273
284
|
}
|
|
274
285
|
|
|
286
|
+
/** SIGKILL the child's whole process group, falling back to the child. */
|
|
287
|
+
function killGroup(child: ChildProcess, detached: boolean): void {
|
|
288
|
+
if (detached && child.pid) {
|
|
289
|
+
try {
|
|
290
|
+
process.kill(-child.pid, "SIGKILL");
|
|
291
|
+
return;
|
|
292
|
+
} catch {
|
|
293
|
+
// group already gone — fall through to the direct kill
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
child.kill("SIGKILL");
|
|
298
|
+
} catch {
|
|
299
|
+
// already dead
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
275
303
|
const bashTool = tool({
|
|
276
304
|
name: "Bash",
|
|
277
305
|
description:
|
|
@@ -93,9 +93,9 @@ const openAIAgentsFactory: BackendFactory = {
|
|
|
93
93
|
|
|
94
94
|
return {
|
|
95
95
|
backend,
|
|
96
|
-
// Cleanup: close every per-chat MCP bundle in the pool so
|
|
97
|
-
//
|
|
98
|
-
//
|
|
96
|
+
// Cleanup: close every per-chat MCP bundle in the pool so its hub
|
|
97
|
+
// sessions don't outlive the backend itself, then drop the cached
|
|
98
|
+
// state.
|
|
99
99
|
cleanup: async () => {
|
|
100
100
|
await releaseAllBundles();
|
|
101
101
|
resetState();
|
|
@@ -118,8 +118,7 @@ function buildTurnPrompt(
|
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* Acquire the per-chat MCP bundle. Persistent across turns — built on
|
|
121
|
-
* first use, kept
|
|
122
|
-
* ~15-subprocess re-spawn the original per-turn build caused.
|
|
121
|
+
* first use, kept until the backend's cleanup releases the pool.
|
|
123
122
|
*/
|
|
124
123
|
async function acquireMcpBundle(
|
|
125
124
|
chatId: string,
|
|
@@ -366,7 +365,7 @@ export async function handleMessage(
|
|
|
366
365
|
} catch (err) {
|
|
367
366
|
// Swallow the terminator abort — the turn completed via a delivery tool.
|
|
368
367
|
if (!isTerminatorAbort(streamState, err)) {
|
|
369
|
-
// MCP bundle is retained across a retry —
|
|
368
|
+
// MCP bundle is retained across a retry — its servers are
|
|
370
369
|
// stateless wrt the model conversation. See `mcp-pool.ts`.
|
|
371
370
|
const outcome = await applyRetryDecision({
|
|
372
371
|
err,
|
|
@@ -400,8 +399,7 @@ export async function handleMessage(
|
|
|
400
399
|
activeAborts.delete(chatId);
|
|
401
400
|
}
|
|
402
401
|
// MCP bundle is NOT closed here — it persists across turns via the
|
|
403
|
-
// pool in `mcp-pool.ts
|
|
404
|
-
// at backend cleanup.
|
|
402
|
+
// pool in `mcp-pool.ts` and is released at backend cleanup.
|
|
405
403
|
}
|
|
406
404
|
|
|
407
405
|
// ── Post-loop accounting ──────────────────────────────────────────────────
|
|
@@ -4,17 +4,11 @@
|
|
|
4
4
|
* Every server is a lightweight `MCPServerStreamableHttp` client
|
|
5
5
|
* pointing at the daemon's MCP hub (`core/mcp-hub`): Talon's own tools
|
|
6
6
|
* run in-process there, and plugin/brave servers are hub-managed
|
|
7
|
-
* children shared across chats and reaped when idle.
|
|
7
|
+
* children shared across chats and reaped when idle. A bundle is just
|
|
8
|
+
* HTTP client objects; the process count is owned and bounded by the hub.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* memory grew linearly with the number of chats. With the hub, a
|
|
12
|
-
* bundle is just HTTP client objects; the process count is owned and
|
|
13
|
-
* bounded by the hub.
|
|
14
|
-
*
|
|
15
|
-
* The bundle is still cached per chat (and released on reset/rebind)
|
|
16
|
-
* so `cacheToolsList` survives across turns and each turn skips the
|
|
17
|
-
* connect handshake.
|
|
10
|
+
* The bundle is cached per chat so `cacheToolsList` survives across
|
|
11
|
+
* turns and each turn skips the connect handshake.
|
|
18
12
|
*
|
|
19
13
|
* Concurrency: `getOrCreateBundle` serialises the build-or-return
|
|
20
14
|
* decision on a per-chat in-flight Promise so two concurrent turns from
|
|
@@ -36,18 +30,13 @@ import { frontendsForChat } from "../runtime/frontends.js";
|
|
|
36
30
|
import { log, logWarn } from "../../util/log.js";
|
|
37
31
|
|
|
38
32
|
/**
|
|
39
|
-
* One per-chat bundle.
|
|
33
|
+
* One per-chat bundle. Its hub sessions stay open until `close()` is
|
|
40
34
|
* called via `releaseBundle()` / `releaseAllBundles()`.
|
|
41
35
|
*/
|
|
42
36
|
export interface OpenAIAgentsMcpBundle {
|
|
43
|
-
/**
|
|
44
|
-
* Connected MCP servers ready to pass to `new Agent({ mcpServers })`.
|
|
45
|
-
* `connectMcpServers` returns the structural `MCPServer` type — the
|
|
46
|
-
* underlying instances are `MCPServerStdio` but the Agent constructor
|
|
47
|
-
* only needs the interface.
|
|
48
|
-
*/
|
|
37
|
+
/** Connected MCP servers ready to pass to `new Agent({ mcpServers })`. */
|
|
49
38
|
servers: MCPServer[];
|
|
50
|
-
/** Close every
|
|
39
|
+
/** Close every server's hub session. Safe to call multiple times. */
|
|
51
40
|
close: () => Promise<void>;
|
|
52
41
|
/** Servers that failed to connect — exposed for diagnostics. */
|
|
53
42
|
failed: ReadonlyArray<{ name: string; error: string }>;
|
|
@@ -115,14 +104,8 @@ export async function getOrCreateBundle(
|
|
|
115
104
|
* Close the bundle for `chatId` and drop it from the pool. No-op when
|
|
116
105
|
* the chat has no live bundle.
|
|
117
106
|
*
|
|
118
|
-
* Call when:
|
|
119
|
-
* - The chat rebinds to a non-openai-agents backend.
|
|
120
|
-
* - The user runs `/reset`.
|
|
121
|
-
* - The chat is destroyed.
|
|
122
|
-
*
|
|
123
107
|
* If `getOrCreateBundle` is in flight when called, releases the bundle
|
|
124
|
-
* once the in-flight build resolves
|
|
125
|
-
* subprocess set.
|
|
108
|
+
* once the in-flight build resolves so it is not left open.
|
|
126
109
|
*/
|
|
127
110
|
export async function releaseBundle(chatId: string): Promise<void> {
|
|
128
111
|
// If a build is in flight, wait for it then close the result.
|
|
@@ -152,8 +135,8 @@ export async function releaseBundle(chatId: string): Promise<void> {
|
|
|
152
135
|
|
|
153
136
|
/**
|
|
154
137
|
* Close every live bundle. Used by the backend factory's `cleanup`
|
|
155
|
-
* hook so unbinding the openai-agents backend leaves no
|
|
156
|
-
*
|
|
138
|
+
* hook so unbinding the openai-agents backend leaves no hub sessions
|
|
139
|
+
* behind.
|
|
157
140
|
*/
|
|
158
141
|
export async function releaseAllBundles(): Promise<void> {
|
|
159
142
|
const ids = [...bundles.keys()];
|
|
@@ -253,12 +253,6 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
|
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
/**
|
|
257
|
-
* If the SSE loop missed the usage info, fall back to the session summary
|
|
258
|
-
* endpoint (which always reflects the final server state). Best-effort:
|
|
259
|
-
* session summaries can race on cancellation, so a failure leaves the
|
|
260
|
-
* counts at zero.
|
|
261
|
-
*/
|
|
262
256
|
/**
|
|
263
257
|
* The turn's user prompt: the shared framing every backend emits, plus
|
|
264
258
|
* whatever this turn's memory retrieval produced. `formatUserPrompt` is
|
|
@@ -276,6 +270,12 @@ function buildTurnPrompt(params: QueryParams): string {
|
|
|
276
270
|
});
|
|
277
271
|
}
|
|
278
272
|
|
|
273
|
+
/**
|
|
274
|
+
* If the SSE loop missed the usage info, fall back to the session summary
|
|
275
|
+
* endpoint (which always reflects the final server state). Best-effort:
|
|
276
|
+
* session summaries can race on cancellation, so a failure leaves the
|
|
277
|
+
* counts at zero.
|
|
278
|
+
*/
|
|
279
279
|
async function fillUsageFromSummary(
|
|
280
280
|
oc: RemoteSessionClient,
|
|
281
281
|
sessionId: string,
|
|
@@ -39,11 +39,7 @@ import {
|
|
|
39
39
|
type RemoteAssistantInfo,
|
|
40
40
|
} from "./session-helpers.js";
|
|
41
41
|
import { log, logDebug } from "../../util/log.js";
|
|
42
|
-
|
|
43
|
-
/** Format an error for a debug log line. */
|
|
44
|
-
function errMsg(err: unknown): string {
|
|
45
|
-
return err instanceof Error ? err.message : String(err);
|
|
46
|
-
}
|
|
42
|
+
import { errMsg } from "./state.js";
|
|
47
43
|
|
|
48
44
|
// ── Streaming timing ───────────────────────────────────────────────────────
|
|
49
45
|
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
* - Bindings (server-bindings.ts, chat-turn.ts, turn.ts, factory.ts) —
|
|
20
20
|
* the helpers closed over one backend's state, the SSE-driven turn,
|
|
21
21
|
* the chat-turn orchestration, and the registry factory composition.
|
|
22
|
-
* This is where the code that used to be copied per backend lives.
|
|
23
22
|
*
|
|
24
23
|
* - Profiles (`profiles/bind.ts` + `profiles/{kilo,opencode}.ts`) —
|
|
25
24
|
* `bindRemoteProfile` closes all of the above over one driver's
|
|
@@ -3,15 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Both `KiloClient.session.messages` and `OpencodeClient.session.messages`
|
|
5
5
|
* return the same wire format: an array of `{ info: { role, ... }, parts: [...] }`
|
|
6
|
-
* objects.
|
|
7
|
-
* helper — same body, drifted formatting, identical `info as unknown as
|
|
8
|
-
* KiloAssistantInfo` / `OpenCodeAssistantInfo` casts. This module centralises
|
|
9
|
-
* the walker and pushes the `info` typing out as a generic the caller supplies.
|
|
6
|
+
* objects. The `info` typing is a generic the caller supplies.
|
|
10
7
|
*
|
|
11
8
|
* The runtime guard (`info.role === "assistant"`) lives here and runs once;
|
|
12
|
-
* the generic just labels what the caller intends to consume
|
|
13
|
-
*
|
|
14
|
-
* `unknown` themselves.
|
|
9
|
+
* the generic just labels what the caller intends to consume, so callers
|
|
10
|
+
* never cast through `unknown` themselves.
|
|
15
11
|
*/
|
|
16
12
|
|
|
17
13
|
/**
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
* frontend), creating an ephemeral session, running the prompt,
|
|
10
10
|
* rendering the response parts into the run log, and cleaning up.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* that drifted (the abort handler landed in one and not the other);
|
|
14
|
-
* the per-backend differences are exactly the `RemoteOneShotBindings`
|
|
12
|
+
* The per-backend differences are exactly the `RemoteOneShotBindings`
|
|
15
13
|
* fields — server bootstrap, model-selection parsing, and the
|
|
16
14
|
* delivery-contract suffix.
|
|
17
15
|
*
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { logWarn } from "../../util/log.js";
|
|
34
34
|
import type { RemoteAgentClient } from "./client.js";
|
|
35
|
+
import { errMsg } from "./state.js";
|
|
35
36
|
|
|
36
37
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
37
38
|
|
|
@@ -153,10 +154,6 @@ export interface RemoteSessionClient extends RemoteAgentClient {
|
|
|
153
154
|
|
|
154
155
|
// ── Local utility ───────────────────────────────────────────────────────────
|
|
155
156
|
|
|
156
|
-
function errMsg(e: unknown): string {
|
|
157
|
-
return e instanceof Error ? e.message : String(e);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
157
|
function hasAssistantUsage(info: RemoteAssistantInfo | undefined): boolean {
|
|
161
158
|
return Boolean(
|
|
162
159
|
info?.tokens?.input ||
|
|
@@ -5,17 +5,12 @@
|
|
|
5
5
|
* `client.global.event()` which (per the upstream wire format) returns
|
|
6
6
|
* a `ServerSentEventsResult` whose `stream` field is an async iterable
|
|
7
7
|
* of typed events. The SDK's published types under-promise this shape —
|
|
8
|
-
* the call signature returns a wider type than the value it produces
|
|
9
|
-
*
|
|
10
|
-
* Previously, every backend that consumed this called
|
|
11
|
-
* `await oc.global.event() as unknown as { stream?: AsyncIterable<unknown> }`
|
|
12
|
-
* inline. Three copies of the same lie. This helper centralises it
|
|
13
|
-
* behind one narrowing function with a runtime guard, so subsequent
|
|
14
|
-
* backends (or the next SDK revision) can update the typing in one
|
|
15
|
-
* place.
|
|
8
|
+
* the call signature returns a wider type than the value it produces,
|
|
9
|
+
* so the narrowing lives here once, behind a runtime guard.
|
|
16
10
|
*/
|
|
17
11
|
|
|
18
|
-
import {
|
|
12
|
+
import { faultText } from "../../core/engine/fault-text.js";
|
|
13
|
+
import { logDebug, logWarn } from "../../util/log.js";
|
|
19
14
|
|
|
20
15
|
/**
|
|
21
16
|
* Minimal client surface — what both `KiloClient` and `OpencodeClient`
|
|
@@ -54,6 +49,10 @@ export async function subscribeSseStream(
|
|
|
54
49
|
lastError = err;
|
|
55
50
|
}
|
|
56
51
|
if (attempt < 3) {
|
|
52
|
+
logDebug(
|
|
53
|
+
"agent",
|
|
54
|
+
`sse.subscribe.retry chat=${chatId} attempt=${attempt} delay_ms=${150 * attempt} error="${faultText(lastError)}"`,
|
|
55
|
+
);
|
|
57
56
|
await new Promise((resolve) => setTimeout(resolve, 150 * attempt));
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -84,11 +84,7 @@ export type TurnMetricInputs = {
|
|
|
84
84
|
usage?: TokenUsageSnapshot;
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
/**
|
|
88
|
-
* Record the uniform per-turn metric set. Replaces the per-backend
|
|
89
|
-
* `recordHistogram("response_latency_ms")` + `incrementCounter
|
|
90
|
-
* ("queries_total")` pairs (and codex's private `codex.*` family).
|
|
91
|
-
*/
|
|
87
|
+
/** Record the uniform per-turn metric set. */
|
|
92
88
|
export function recordTurnMetrics(inputs: TurnMetricInputs): void {
|
|
93
89
|
recordSessionMetrics(inputs.chatId, inputs);
|
|
94
90
|
}
|
|
@@ -97,11 +93,10 @@ export function recordTurnMetrics(inputs: TurnMetricInputs): void {
|
|
|
97
93
|
* Terminal-failure accounting — call right before re-throwing a turn
|
|
98
94
|
* that exhausted its retries.
|
|
99
95
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* This helper closes all three gaps in one call:
|
|
96
|
+
* A failed turn still burned tokens. Without this, they vanish from
|
|
97
|
+
* /status and /metrics, latency histograms sample only successes, and
|
|
98
|
+
* the live-turn overlay leaks until the next turn. One call covers all
|
|
99
|
+
* three:
|
|
105
100
|
*
|
|
106
101
|
* - per-turn metrics with `failed: true` (also feeds
|
|
107
102
|
* `backend.<id>.turn_failed`)
|
|
@@ -158,9 +153,8 @@ export function recordFailedTurnAccounting(inputs: {
|
|
|
158
153
|
/**
|
|
159
154
|
* Record a flow violation (prose written without a delivery tool).
|
|
160
155
|
* Always counts the dropped text; the outcome picks the second
|
|
161
|
-
* counter. Centralised
|
|
162
|
-
*
|
|
163
|
-
* reminder" unanswerable for that backend.
|
|
156
|
+
* counter. Centralised so every backend counts cap exhaustion — "how
|
|
157
|
+
* often do models ignore the reminder" needs both halves.
|
|
164
158
|
*/
|
|
165
159
|
export function recordFlowViolation(
|
|
166
160
|
chatId: string,
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolves after `ms` milliseconds, or immediately when the optional
|
|
5
5
|
* `AbortSignal` fires. Used by the remote-server backends (kilo,
|
|
6
|
-
* opencode) for throttled retries and stream pacing.
|
|
7
|
-
* duplicated inline in `kilo/handler.ts` and `opencode/handler.ts`.
|
|
6
|
+
* opencode) for throttled retries and stream pacing.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
@@ -24,12 +24,42 @@
|
|
|
24
24
|
|
|
25
25
|
import type { QueryParams, QueryResult } from "./handler-types.js";
|
|
26
26
|
import { classify, type TalonError } from "../../../core/errors.js";
|
|
27
|
-
import {
|
|
27
|
+
import { faultText } from "../../../core/engine/fault-text.js";
|
|
28
|
+
import { log, logWarn } from "../../../util/log.js";
|
|
28
29
|
import { incrementCounter } from "../../../storage/metrics.js";
|
|
29
30
|
import { resetSession } from "../../../storage/sessions.js";
|
|
30
|
-
import { classifyRetry } from "./model-retry.js";
|
|
31
|
+
import { classifyRetry, type RetryDecision } from "./model-retry.js";
|
|
31
32
|
import type { AgentEvent } from "../../../core/agent-runtime/events.js";
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* One greppable line per recovery decision — what failed, how it was
|
|
36
|
+
* classified, and what the ladder does next. A retry is a warning (the
|
|
37
|
+
* turn is about to be re-run); a propagate is the turn's final error
|
|
38
|
+
* leaving the backend, and says so.
|
|
39
|
+
*/
|
|
40
|
+
function logRetryDecision(
|
|
41
|
+
chatId: string,
|
|
42
|
+
backendLabel: string | undefined,
|
|
43
|
+
activeModel: string,
|
|
44
|
+
retried: boolean,
|
|
45
|
+
classified: TalonError,
|
|
46
|
+
decision: RetryDecision,
|
|
47
|
+
): void {
|
|
48
|
+
const action =
|
|
49
|
+
decision.kind === "reset_and_retry"
|
|
50
|
+
? `reset_and_retry(${decision.reason})`
|
|
51
|
+
: decision.kind === "fallback_model"
|
|
52
|
+
? `fallback_model(${decision.fallbackModelId})`
|
|
53
|
+
: "propagate";
|
|
54
|
+
const line =
|
|
55
|
+
`retry.decision chat=${chatId} backend=${(backendLabel || "claude").toLowerCase().replace(/\s+/g, "-")} ` +
|
|
56
|
+
`model=${activeModel} attempt=${retried ? 2 : 1} reason=${classified.reason} ` +
|
|
57
|
+
`retryable=${classified.retryable} status=${classified.status ?? "-"} ` +
|
|
58
|
+
`decision=${action} error="${faultText(classified)}"`;
|
|
59
|
+
if (decision.kind === "propagate") log("agent", line);
|
|
60
|
+
else logWarn("agent", line);
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/** Inputs for `applyRetryDecision`. */
|
|
34
64
|
export interface ApplyRetryDecisionInputs {
|
|
35
65
|
/** The error caught by the backend's handler. */
|
|
@@ -112,6 +142,14 @@ export async function applyRetryDecision(
|
|
|
112
142
|
});
|
|
113
143
|
|
|
114
144
|
const prefix = backendLabel ? `${backendLabel} ` : "";
|
|
145
|
+
logRetryDecision(
|
|
146
|
+
chatId,
|
|
147
|
+
backendLabel,
|
|
148
|
+
activeModel,
|
|
149
|
+
retried,
|
|
150
|
+
classified,
|
|
151
|
+
decision,
|
|
152
|
+
);
|
|
115
153
|
|
|
116
154
|
if (decision.kind === "reset_and_retry") {
|
|
117
155
|
logWarn(
|
|
@@ -218,6 +256,14 @@ export async function* applyRetryDecisionStream(
|
|
|
218
256
|
});
|
|
219
257
|
|
|
220
258
|
const prefix = backendLabel ? `${backendLabel} ` : "";
|
|
259
|
+
logRetryDecision(
|
|
260
|
+
chatId,
|
|
261
|
+
backendLabel,
|
|
262
|
+
activeModel,
|
|
263
|
+
retried,
|
|
264
|
+
classified,
|
|
265
|
+
decision,
|
|
266
|
+
);
|
|
221
267
|
|
|
222
268
|
if (decision.kind === "reset_and_retry") {
|
|
223
269
|
logWarn(
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
* skip this entirely and yield events directly — `runChatTurn` lives
|
|
11
11
|
* in the backend module and owns its own stream surface.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* and the `AgentEvent` contract every consumer reads.
|
|
13
|
+
* This is not a legacy adapter: it is the canonical bridge between an
|
|
14
|
+
* SDK that emits via callbacks and the `AgentEvent` contract every
|
|
15
|
+
* consumer reads.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
import {
|
package/src/bootstrap.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { initPulse, resetPulseTimer } from "./core/background/pulse/pulse.js";
|
|
|
28
28
|
import { initCron } from "./core/background/cron/scheduler.js";
|
|
29
29
|
import { initPlanAlerts } from "./core/background/pulse/plan-alerts.js";
|
|
30
30
|
import { setAdminNotifier } from "./core/frontend-runtime/admin-notify.js";
|
|
31
|
+
import { configureAlerts } from "./core/frontend-runtime/alerts.js";
|
|
31
32
|
import { startAuthExpiryMonitor } from "./core/auth/expiry-monitor.js";
|
|
32
33
|
import {
|
|
33
34
|
initTriggers,
|
|
@@ -629,12 +630,19 @@ function initWakeSubsystems(config: TalonConfig): void {
|
|
|
629
630
|
* Wire the admin notification seam to the admin's frontend, and start the
|
|
630
631
|
* login-expiry monitor that rides it: the CLIs' "N days to log in again"
|
|
631
632
|
* banner, delivered to the admin instead of a terminal nobody is watching
|
|
632
|
-
* (/auth then completes the sign-in from the chat).
|
|
633
|
+
* (/auth then completes the sign-in from the chat). Operator alerts ride
|
|
634
|
+
* the same seam, so their settings are applied here too.
|
|
633
635
|
*/
|
|
634
636
|
function wireAdminNotifier(
|
|
635
637
|
config: TalonConfig,
|
|
636
638
|
frontends: Parameters<typeof resolveFrontendByNumericId>[2],
|
|
637
639
|
): void {
|
|
640
|
+
if (config.alerts) {
|
|
641
|
+
configureAlerts({
|
|
642
|
+
enabled: config.alerts.enabled,
|
|
643
|
+
cooldownMs: config.alerts.cooldownMinutes * 60_000,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
638
646
|
if (!config.adminUserId) return;
|
|
639
647
|
const adminChatId = config.adminUserId;
|
|
640
648
|
setAdminNotifier(async (text: string) =>
|
package/src/cli/doctor.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { findRunningInstance } from "../core/daemon/discovery.js";
|
|
9
|
+
import { formatAlertLines } from "./status.js";
|
|
9
10
|
import { printBanner, loadConfig } from "./config.js";
|
|
10
11
|
import { CONFIG_FILE } from "./context.js";
|
|
11
12
|
|
|
@@ -56,6 +57,8 @@ export async function runDoctor(): Promise<void> {
|
|
|
56
57
|
const instance = await findRunningInstance();
|
|
57
58
|
if (instance) {
|
|
58
59
|
console.log(` ${pc.green("✓")} Bot is running (PID ${instance.pid})`);
|
|
60
|
+
for (const line of formatAlertLines(instance.health ?? {}))
|
|
61
|
+
console.log(line);
|
|
59
62
|
} else {
|
|
60
63
|
console.log(` ${pc.dim("-")} Bot is not running`);
|
|
61
64
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { printBanner, ConfigFileError } from "./config.js";
|
|
|
28
28
|
import { runSetup } from "./setup.js";
|
|
29
29
|
import { showStatus } from "./status.js";
|
|
30
30
|
import { viewConfig } from "./config-view.js";
|
|
31
|
-
import {
|
|
31
|
+
import { runLogsCommand } from "./logs.js";
|
|
32
32
|
import { runDoctor } from "./doctor.js";
|
|
33
33
|
import { startChat } from "./chat.js";
|
|
34
34
|
import { daemonStart, daemonStop, daemonRestart } from "./daemon.js";
|
|
@@ -116,7 +116,9 @@ function printHelp(): void {
|
|
|
116
116
|
` ${pc.cyan("mesh")} Device credentials (list/revoke/rotate/scopes)`,
|
|
117
117
|
);
|
|
118
118
|
console.log(` ${pc.cyan("config")} View/edit configuration`);
|
|
119
|
-
console.log(
|
|
119
|
+
console.log(
|
|
120
|
+
` ${pc.cyan("logs")} Tail log file (--errors, --since 1h, --component, --grep, --turn)`,
|
|
121
|
+
);
|
|
120
122
|
console.log(` ${pc.cyan("doctor")} Validate environment`);
|
|
121
123
|
console.log(` ${pc.cyan("--version")} Print the package version`);
|
|
122
124
|
console.log();
|
|
@@ -129,13 +131,9 @@ export async function runCli(): Promise<void> {
|
|
|
129
131
|
try {
|
|
130
132
|
await dispatch(command);
|
|
131
133
|
} catch (err) {
|
|
132
|
-
// A present-but-invalid config.json:
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// would otherwise surface as a bare unhandled-rejection stack trace —
|
|
136
|
-
// or, worse for the main menu, never happen at all, because the old
|
|
137
|
-
// loader swallowed the error and returned defaults, sending a broken
|
|
138
|
-
// install into the first-run wizard, which then saves over the file.
|
|
134
|
+
// A present-but-invalid config.json: say so and exit non-zero, rather
|
|
135
|
+
// than a stack trace — or, for the main menu, a first-run wizard that
|
|
136
|
+
// would save defaults over the operator's real file.
|
|
139
137
|
if (err instanceof ConfigFileError) {
|
|
140
138
|
console.error(`\n ${pc.red("✖")} ${err.message}\n`);
|
|
141
139
|
process.exitCode = 1;
|
|
@@ -152,7 +150,7 @@ const COMMANDS: Record<string, CommandHandler> = {
|
|
|
152
150
|
setup: () => runSetup(),
|
|
153
151
|
status: () => showStatus(),
|
|
154
152
|
config: () => viewConfig(),
|
|
155
|
-
logs: () =>
|
|
153
|
+
logs: (args) => runLogsCommand(args),
|
|
156
154
|
start: async () => {
|
|
157
155
|
printBanner();
|
|
158
156
|
await daemonStart();
|