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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor's "what went wrong lately" probes: the last hour of talon.log
|
|
3
|
+
* summarised by component, and the operator alerts still open — so
|
|
4
|
+
* `talon doctor` points at the failing subsystem instead of only at the
|
|
5
|
+
* environment.
|
|
6
|
+
*
|
|
7
|
+
* Alerts come from the daemon's in-memory table when doctor runs inside
|
|
8
|
+
* the daemon (the chat /doctor command). `talon doctor` is its own
|
|
9
|
+
* process and sees an empty table, so it replays the alert lines in the
|
|
10
|
+
* log instead; those are reported as warnings without counting toward
|
|
11
|
+
* the issue total — the log can only say what was last announced.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
readLogRecords,
|
|
16
|
+
replayAlerts,
|
|
17
|
+
type LogRecord,
|
|
18
|
+
} from "../daemon/log-reader.js";
|
|
19
|
+
import { activeAlerts } from "../frontend-runtime/alerts.js";
|
|
20
|
+
import { files } from "../../util/paths.js";
|
|
21
|
+
import type { DoctorCheck } from "./types.js";
|
|
22
|
+
|
|
23
|
+
const HOUR_MS = 3_600_000;
|
|
24
|
+
/** How far back the alert replay looks for a raise still unresolved. */
|
|
25
|
+
const ALERT_WINDOW_MS = 24 * HOUR_MS;
|
|
26
|
+
const READ = { limit: Number.MAX_SAFE_INTEGER, maxBytesPerFile: 16 << 20 };
|
|
27
|
+
const DETAIL_CHARS = 160;
|
|
28
|
+
|
|
29
|
+
export type RecentLogOptions = {
|
|
30
|
+
logPath?: string;
|
|
31
|
+
now?: number;
|
|
32
|
+
/** The live alert table; defaults to this process's `activeAlerts()`. */
|
|
33
|
+
liveAlerts?: ReturnType<typeof activeAlerts>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function clip(text: string): string {
|
|
37
|
+
return text.length > DETAIL_CHARS ? `${text.slice(0, DETAIL_CHARS)}…` : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function byComponent(records: readonly LogRecord[]): string {
|
|
41
|
+
const counts = new Map<string, number>();
|
|
42
|
+
for (const r of records) {
|
|
43
|
+
const c = r.component ?? "?";
|
|
44
|
+
counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
return [...counts]
|
|
47
|
+
.sort((a, b) => b[1] - a[1])
|
|
48
|
+
.map(([c, n]) => `${c} ${n}`)
|
|
49
|
+
.join(", ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The last hour's warn/error lines, as one doctor check. */
|
|
53
|
+
export function checkRecentErrors(opts: RecentLogOptions = {}): DoctorCheck {
|
|
54
|
+
const logPath = opts.logPath ?? files.log;
|
|
55
|
+
const now = opts.now ?? Date.now();
|
|
56
|
+
const recent = readLogRecords(logPath, {
|
|
57
|
+
...READ,
|
|
58
|
+
filter: { since: now - HOUR_MS, minLevel: "warn" },
|
|
59
|
+
});
|
|
60
|
+
const errors = recent.filter(
|
|
61
|
+
(r) => r.level === "error" || r.level === "fatal",
|
|
62
|
+
);
|
|
63
|
+
const warnings = recent.length - errors.length;
|
|
64
|
+
if (errors.length === 0) {
|
|
65
|
+
return warnings === 0
|
|
66
|
+
? { label: "Log: no errors or warnings in the last hour", status: "ok" }
|
|
67
|
+
: {
|
|
68
|
+
label: `Log: ${warnings} warning(s) in the last hour`,
|
|
69
|
+
status: "info",
|
|
70
|
+
detail: byComponent(recent),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const last = errors.at(-1)!;
|
|
74
|
+
const at = new Date(last.ts).toTimeString().slice(0, 5);
|
|
75
|
+
const what = clip(`${last.msg}${last.err ? ` (${last.err})` : ""}`);
|
|
76
|
+
return {
|
|
77
|
+
label: `Log: ${errors.length} error(s) in the last hour — ${byComponent(errors)}`,
|
|
78
|
+
status: "warn",
|
|
79
|
+
detail:
|
|
80
|
+
`latest ${at} ${last.component ?? "?"}: ${what} — ` +
|
|
81
|
+
`talon logs --errors --since 1h, or ~/.talon/ns/proc/errors`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Open alerts, one check each (or one "none" line). */
|
|
86
|
+
export function checkOpenAlerts(opts: RecentLogOptions = {}): DoctorCheck[] {
|
|
87
|
+
const now = opts.now ?? Date.now();
|
|
88
|
+
const live = opts.liveAlerts ?? activeAlerts();
|
|
89
|
+
const fromLog = live.length === 0;
|
|
90
|
+
const alerts = fromLog ? loggedAlerts(opts.logPath ?? files.log, now) : live;
|
|
91
|
+
if (alerts.length === 0)
|
|
92
|
+
return [{ label: "Alerts: none open", status: "ok" }];
|
|
93
|
+
return alerts.map((a) => {
|
|
94
|
+
const mins = Math.max(0, Math.round((now - a.since) / 60_000));
|
|
95
|
+
const label = `Alert ${a.key} (${a.severity}, ${mins} min)`;
|
|
96
|
+
if (fromLog) {
|
|
97
|
+
return {
|
|
98
|
+
label,
|
|
99
|
+
status: "warn",
|
|
100
|
+
detail: `last logged: ${clip(a.message)}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
label,
|
|
105
|
+
status: a.severity === "warn" ? "warn" : "fail",
|
|
106
|
+
detail: clip(a.message),
|
|
107
|
+
issue: true,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Replay the last day's alert lines (plus boot lines, which reset them). */
|
|
113
|
+
function loggedAlerts(logPath: string, now: number) {
|
|
114
|
+
const since = now - ALERT_WINDOW_MS;
|
|
115
|
+
const alertLines = readLogRecords(logPath, {
|
|
116
|
+
...READ,
|
|
117
|
+
filter: { since, component: "alert" },
|
|
118
|
+
});
|
|
119
|
+
const boots = readLogRecords(logPath, {
|
|
120
|
+
...READ,
|
|
121
|
+
filter: { since, component: "bot", grep: "Starting Talon" },
|
|
122
|
+
});
|
|
123
|
+
return replayAlerts([...alertLines, ...boots].sort((a, b) => a.ts - b.ts));
|
|
124
|
+
}
|
package/src/core/doctor/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The doctor's data shapes, on their own so a backend factory can declare
|
|
3
3
|
* its checks (`BackendFactory.doctor`) without importing the collector —
|
|
4
|
-
* `core/doctor.ts` reads the registry, so the types living there would
|
|
4
|
+
* `core/doctor/index.ts` reads the registry, so the types living there would
|
|
5
5
|
* close a cycle. Checks are pure data (label / status / detail); renderers
|
|
6
6
|
* decide presentation.
|
|
7
7
|
*/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Different roles, and different chats, have different cost/latency/
|
|
11
11
|
* quality needs. Typical post-Anthropic-metering setup: chat default
|
|
12
12
|
* on free-tier OpenAI Agents, heartbeats on Claude Sonnet, dream
|
|
13
|
-
* shared with chat — plus
|
|
13
|
+
* shared with chat — plus one group chat stays on Claude while DMs use the
|
|
14
14
|
* cheap default. A single-active model can't express that. The pool
|
|
15
15
|
* lets each holder bind independently while deduplicating instances
|
|
16
16
|
* when ids overlap.
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
* - `pool` — init/teardown, role accessors, availability, snapshot,
|
|
25
25
|
* listeners.
|
|
26
26
|
* - `rebind` — rebind/release holders + per-chat accessors.
|
|
27
|
-
* - `legacy` — single-active aliases routed to the chat role.
|
|
28
27
|
*/
|
|
29
28
|
|
|
30
29
|
export * from "./types.js";
|
|
@@ -57,14 +56,3 @@ export {
|
|
|
57
56
|
hasChatBackendOverride,
|
|
58
57
|
resolveChatBackend,
|
|
59
58
|
} from "./rebind.js";
|
|
60
|
-
export {
|
|
61
|
-
initBackendController,
|
|
62
|
-
getActiveBackend,
|
|
63
|
-
hasActiveBackend,
|
|
64
|
-
getActiveBackendOrNull,
|
|
65
|
-
getActiveBackendId,
|
|
66
|
-
getActiveBackendLabel,
|
|
67
|
-
switchBackend,
|
|
68
|
-
cleanupBackendController,
|
|
69
|
-
resetBackendControllerForTest,
|
|
70
|
-
} from "./legacy.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Plan-aware backend routing for background work.
|
|
3
3
|
*
|
|
4
4
|
* Background work — `spawn_agent` sub-agents, cron `query` jobs, the
|
|
5
|
-
* heartbeat —
|
|
5
|
+
* heartbeat — would otherwise inherit whichever backend the chat is on.
|
|
6
6
|
* On a multi-subscription install that is a good way to burn one plan to its
|
|
7
7
|
* ceiling while another sits idle. When nothing is pinned, this picks the
|
|
8
8
|
* backend with the most headroom instead.
|
|
@@ -12,8 +12,15 @@ import type { ExecuteParams, ExecuteResult } from "../types.js";
|
|
|
12
12
|
import { formatRelayBlock, takePendingRelay } from "./cross-chat-relay.js";
|
|
13
13
|
import { recordMessageSignal } from "../memory/taps.js";
|
|
14
14
|
import { log } from "../../util/log.js";
|
|
15
|
+
import { setStuckLoopListener } from "../../util/watchdog.js";
|
|
15
16
|
import { taskTable, type KillOutcome } from "../tasks/index.js";
|
|
16
17
|
import { initWeaver, type Weaver, type WeaverDeps } from "../weaver/index.js";
|
|
18
|
+
import {
|
|
19
|
+
noteTurnFailed,
|
|
20
|
+
noteTurnSucceeded,
|
|
21
|
+
stuckLoopAlert,
|
|
22
|
+
type TurnBinding,
|
|
23
|
+
} from "./turn-health.js";
|
|
17
24
|
|
|
18
25
|
// ── Dependencies (injected at startup) ──────────────────────────────────────
|
|
19
26
|
|
|
@@ -21,7 +28,7 @@ import { initWeaver, type Weaver, type WeaverDeps } from "../weaver/index.js";
|
|
|
21
28
|
* `getBackend` takes the string chat id so it can route per-chat —
|
|
22
29
|
* a chat with a backend override returns its override backend, others
|
|
23
30
|
* fall through to the global chat-role backend. Tests can pass a
|
|
24
|
-
* stub that ignores the chat id. See `core/backend-controller
|
|
31
|
+
* stub that ignores the chat id. See `core/engine/backend-controller/`.
|
|
25
32
|
*
|
|
26
33
|
* `resolveActiveModel` walks the 5-step active-model resolution
|
|
27
34
|
* chain for the chat and returns both the resolved `ModelRef` and
|
|
@@ -33,9 +40,18 @@ import { initWeaver, type Weaver, type WeaverDeps } from "../weaver/index.js";
|
|
|
33
40
|
type DispatcherDeps = WeaverDeps;
|
|
34
41
|
|
|
35
42
|
let weaver: Weaver | null = null;
|
|
43
|
+
let backendIdFor: (chatId: string) => string = () => "unknown";
|
|
36
44
|
|
|
37
45
|
export function initDispatcher(d: DispatcherDeps): void {
|
|
38
46
|
weaver = initWeaver(d);
|
|
47
|
+
backendIdFor = (chatId) => {
|
|
48
|
+
try {
|
|
49
|
+
return d.getBackend(chatId).id;
|
|
50
|
+
} catch {
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
setStuckLoopListener(stuckLoopAlert);
|
|
39
55
|
log("dispatcher", "Initialized (per-chat serial, cross-chat parallel)");
|
|
40
56
|
}
|
|
41
57
|
|
|
@@ -94,5 +110,42 @@ export async function execute(params: ExecuteParams): Promise<ExecuteResult> {
|
|
|
94
110
|
const prompt = relayed.length
|
|
95
111
|
? formatRelayBlock(relayed) + params.prompt
|
|
96
112
|
: params.prompt;
|
|
97
|
-
|
|
113
|
+
const startedAt = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
const result = await weaver.runTurn(
|
|
116
|
+
relayed.length ? { ...params, prompt } : params,
|
|
117
|
+
);
|
|
118
|
+
// Only a turn that reached a backend vouches for it — a refusal or a
|
|
119
|
+
// turn killed in the queue binds no warp this turn.
|
|
120
|
+
const bound = turnBinding(weaver, params, startedAt, false);
|
|
121
|
+
if (bound) noteTurnSucceeded(bound);
|
|
122
|
+
return result;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const bound = turnBinding(weaver, params, startedAt, true);
|
|
125
|
+
if (bound) noteTurnFailed(bound, err);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The backend/model this turn ran on, for turn-health alerts: the warp
|
|
132
|
+
* the Weaver bound for it, or — when the turn failed before binding one
|
|
133
|
+
* — the backend currently serving the chat.
|
|
134
|
+
*/
|
|
135
|
+
function turnBinding(
|
|
136
|
+
w: Weaver,
|
|
137
|
+
params: ExecuteParams,
|
|
138
|
+
startedAt: number,
|
|
139
|
+
failed: boolean,
|
|
140
|
+
): TurnBinding | null {
|
|
141
|
+
const common = {
|
|
142
|
+
chatId: params.chatId,
|
|
143
|
+
source: params.source,
|
|
144
|
+
durationMs: Date.now() - startedAt,
|
|
145
|
+
};
|
|
146
|
+
const warp = w.loom.get(params.chatId)?.warp;
|
|
147
|
+
if (warp && warp.boundAt >= startedAt) {
|
|
148
|
+
return { ...common, backendId: warp.backendId, model: warp.model };
|
|
149
|
+
}
|
|
150
|
+
return failed ? { ...common, backendId: backendIdFor(params.chatId) } : null;
|
|
98
151
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error text an operator alert or a key=value log line may carry.
|
|
3
|
+
*
|
|
4
|
+
* Alerts land on a phone and log lines get pasted into issues, so the
|
|
5
|
+
* raw message is flattened to one line, clipped, and stripped of the
|
|
6
|
+
* credential shapes a provider error can echo back (bearer tokens, API
|
|
7
|
+
* keys, `token=` query parameters).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SECRET_PATTERNS: readonly RegExp[] = [
|
|
11
|
+
/\bBearer\s+[\w.~+/=-]+/gi,
|
|
12
|
+
/\b(?:sk|pk|rk)-[\w-]{8,}/g,
|
|
13
|
+
/\b(?:gh[pousr]|github_pat)_\w{8,}/g,
|
|
14
|
+
/\bxox[abprs]-[\w-]{8,}/g,
|
|
15
|
+
/\b(access_token|refresh_token|api_key|apikey|token|key|secret|password)=[^\s&"']+/gi,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** One-line, clipped, secret-free text for `err`. */
|
|
19
|
+
export function faultText(err: unknown, max = 200): string {
|
|
20
|
+
let raw: string;
|
|
21
|
+
if (err instanceof Error) raw = err.message || err.name;
|
|
22
|
+
else if (typeof err === "string") raw = err;
|
|
23
|
+
else {
|
|
24
|
+
try {
|
|
25
|
+
raw = String(err);
|
|
26
|
+
} catch {
|
|
27
|
+
raw = "[unprintable error]";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let text = raw.replace(/\s+/g, " ").trim();
|
|
31
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
32
|
+
text = text.replace(pattern, (match, name?: string) =>
|
|
33
|
+
typeof name === "string" && match.includes("=")
|
|
34
|
+
? `${name}=[redacted]`
|
|
35
|
+
: "[redacted]",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!text) return "unknown error";
|
|
39
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
40
|
+
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - `control` — what a parent calls: spawn / list / status / wait / send /
|
|
5
5
|
* kill. Works from a chat and from inside another agent.
|
|
6
|
-
* - `report` — what a sub-agent calls about itself
|
|
7
|
-
* message_parent / check_inbox
|
|
6
|
+
* - `report` — what a sub-agent calls about itself and its siblings:
|
|
7
|
+
* report_result / message_parent / check_inbox / list_peers /
|
|
8
|
+
* message_peer.
|
|
8
9
|
*
|
|
9
10
|
* Both sets are reachable from an `agent:<id>` context: the gateway routes
|
|
10
11
|
* those chat keys straight here (see `Gateway.handleAction`), because a
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
agentIdFromContextLabel,
|
|
17
17
|
agentRegistry,
|
|
18
18
|
deliverMessage,
|
|
19
|
+
deliverToAgent,
|
|
19
20
|
describeParent,
|
|
20
21
|
} from "../../../agents/index.js";
|
|
21
22
|
import { logError } from "../../../../util/log.js";
|
|
@@ -86,6 +87,67 @@ export const agentReportHandlers: SharedActionHandlers = {
|
|
|
86
87
|
};
|
|
87
88
|
},
|
|
88
89
|
|
|
90
|
+
list_peers: (_body, _chatId, _backend, chatKey) => {
|
|
91
|
+
const record = callingAgent(chatKey);
|
|
92
|
+
if (!record) return notAnAgent("list_peers");
|
|
93
|
+
const peers = agentRegistry.peersOf(record.id);
|
|
94
|
+
if (peers.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
text:
|
|
98
|
+
"No peers — you are the only agent your parent has running. " +
|
|
99
|
+
"Report to your parent as usual.",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const rendered = peers
|
|
103
|
+
.map(
|
|
104
|
+
(peer) =>
|
|
105
|
+
`- ${peer.label} [${peer.state}]\n ID: ${peer.id}\n Working on: ${peer.brief.slice(0, 160).replace(/\s+/g, " ")}…`,
|
|
106
|
+
)
|
|
107
|
+
.join("\n");
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
text: `${peers.length} peer(s) running alongside you:\n\n${rendered}`,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
message_peer: (body, _chatId, _backend, chatKey) => {
|
|
115
|
+
const record = callingAgent(chatKey);
|
|
116
|
+
if (!record) return notAnAgent("message_peer");
|
|
117
|
+
const id = String(body.agent_id ?? "").trim();
|
|
118
|
+
if (!id) return { ok: false, error: "Missing agent_id" };
|
|
119
|
+
const text = String(body.text ?? "").trim();
|
|
120
|
+
if (!text) return { ok: false, error: "Missing text" };
|
|
121
|
+
// Peers only. An agent may address a sibling, never an arbitrary id:
|
|
122
|
+
// resolving through peersOf is what stops one swarm reaching into
|
|
123
|
+
// another, and it means a wrong id fails closed rather than delivering.
|
|
124
|
+
const peer = agentRegistry
|
|
125
|
+
.peersOf(record.id)
|
|
126
|
+
.find((candidate) => candidate.id === id);
|
|
127
|
+
if (!peer) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error:
|
|
131
|
+
`No peer "${id}". You can only message agents spawned by the same ` +
|
|
132
|
+
`parent as you — call list_peers to see them.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!deliverToAgent(record.id, id, text)) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error:
|
|
139
|
+
`Could not deliver to "${peer.label}" (${id}): it has already ` +
|
|
140
|
+
`settled, or its inbox is full.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
text:
|
|
146
|
+
`Queued for peer "${peer.label}" (${id}). It reads its inbox at its ` +
|
|
147
|
+
`own milestones, so this is not an interrupt.`,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
|
|
89
151
|
check_inbox: (_body, _chatId, _backend, chatKey) => {
|
|
90
152
|
const record = callingAgent(chatKey);
|
|
91
153
|
if (!record) return notAnAgent("check_inbox");
|
|
@@ -19,10 +19,8 @@ export const historyHandlers: SharedActionHandlers = {
|
|
|
19
19
|
const limit = Math.min(100, Number(body.limit ?? 30));
|
|
20
20
|
const cid = chatKey;
|
|
21
21
|
// The tool schema advertises `offset_id` and `before` for paging back;
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// silent no-op on frontends without a platform history API (WhatsApp,
|
|
25
|
-
// native).
|
|
22
|
+
// ignoring them here would make "go further back" a silent no-op on
|
|
23
|
+
// frontends without a platform history API (WhatsApp, native).
|
|
26
24
|
const offsetId = Number(body.offset_id);
|
|
27
25
|
if (Number.isFinite(offsetId) && offsetId > 0) {
|
|
28
26
|
return { ok: true, text: getFormattedBefore(cid, offsetId, limit) };
|
|
@@ -61,23 +61,20 @@ async function bash(
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return bashBackground(cmd, dir);
|
|
64
|
+
if (background === true) {
|
|
65
|
+
if (active) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
text:
|
|
69
|
+
"background:true runs on the daemon host only. On a teleported device, " +
|
|
70
|
+
"background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll " +
|
|
71
|
+
"the log with read.",
|
|
72
|
+
};
|
|
76
73
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
74
|
+
return bashBackground(cmd, dir);
|
|
75
|
+
}
|
|
76
|
+
if (active) return bashTeleported(chatId, active.deviceId, cmd, timeoutMs);
|
|
77
|
+
return bashLocal(cmd, dir, timeoutMs);
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
function bashLocal(
|
|
@@ -14,8 +14,10 @@ import { classify } from "../errors.js";
|
|
|
14
14
|
import { getActiveCount } from "./dispatcher.js";
|
|
15
15
|
import { Loom, getActiveLoom, type ContextRegistry } from "../weaver/index.js";
|
|
16
16
|
import { getHealthStatus } from "../../util/watchdog.js";
|
|
17
|
+
import { activeAlerts } from "../frontend-runtime/alerts.js";
|
|
17
18
|
import { getActiveSessionCount } from "../../storage/sessions.js";
|
|
18
19
|
import { log, logError, logDebug } from "../../util/log.js";
|
|
20
|
+
import { runInChatTurnScope } from "../../util/logging/turn-scope.js";
|
|
19
21
|
import {
|
|
20
22
|
handleSharedAction,
|
|
21
23
|
handleChatFreeAction,
|
|
@@ -36,6 +38,38 @@ import type { FrontendActionHandler } from "../types.js";
|
|
|
36
38
|
import type { Backend } from "../agent-runtime/capabilities.js";
|
|
37
39
|
import { resolveOwnerFrontendId } from "../frontend-runtime/routing.js";
|
|
38
40
|
|
|
41
|
+
/** Serialized size of an action result; -1 when it can't be serialized. */
|
|
42
|
+
function actionResultBytes(result: unknown): number {
|
|
43
|
+
try {
|
|
44
|
+
return Buffer.byteLength(JSON.stringify(result) ?? "");
|
|
45
|
+
} catch {
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `tool.action name=… chat=… ms=… ok=… bytes=… [err=…]` for one bridge action. */
|
|
51
|
+
function logActionOutcome(
|
|
52
|
+
body: Record<string, unknown>,
|
|
53
|
+
chat: string,
|
|
54
|
+
ms: number,
|
|
55
|
+
result: unknown,
|
|
56
|
+
): void {
|
|
57
|
+
const action = typeof body.action === "string" ? body.action : "?";
|
|
58
|
+
const r = result as { ok?: unknown; error?: unknown } | null | undefined;
|
|
59
|
+
const failed = !!r && typeof r === "object" && r.ok === false;
|
|
60
|
+
const line =
|
|
61
|
+
`tool.action name=${action} chat=${chat || "-"} ms=${ms} ` +
|
|
62
|
+
`ok=${!failed} bytes=${actionResultBytes(result)}`;
|
|
63
|
+
if (!failed) {
|
|
64
|
+
logDebug("gateway", line);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const err = String(r.error ?? "")
|
|
68
|
+
.replace(/\s+/g, " ")
|
|
69
|
+
.slice(0, 200);
|
|
70
|
+
log("gateway", `${line} err=${err}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
// ── Retry helper (stateless — standalone export) ─────────────────────────────
|
|
40
74
|
|
|
41
75
|
/**
|
|
@@ -290,7 +324,31 @@ export class Gateway {
|
|
|
290
324
|
return null;
|
|
291
325
|
}
|
|
292
326
|
|
|
293
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Run one bridge action inside the log scope of the turn that issued it
|
|
329
|
+
* (the MCP subprocess reaches us over HTTP, a fresh async root), so the
|
|
330
|
+
* action's own lines carry `turn=<id>`, and close it with one
|
|
331
|
+
* `tool.action` summary line — debug on success, info with the error
|
|
332
|
+
* text when the action answered `ok:false`.
|
|
333
|
+
*/
|
|
334
|
+
private handleAction(body: Record<string, unknown>): Promise<unknown> {
|
|
335
|
+
const rawChatId = body._chatId ? String(body._chatId) : "";
|
|
336
|
+
const numericId = Number(rawChatId);
|
|
337
|
+
const chatKeys = [
|
|
338
|
+
rawChatId,
|
|
339
|
+
rawChatId !== "" && !isNaN(numericId)
|
|
340
|
+
? this.loom.stringIdForNumeric(numericId)
|
|
341
|
+
: null,
|
|
342
|
+
];
|
|
343
|
+
return runInChatTurnScope(chatKeys, async () => {
|
|
344
|
+
const t0 = Date.now();
|
|
345
|
+
const result = await this.routeAction(body);
|
|
346
|
+
logActionOutcome(body, rawChatId, Date.now() - t0, result);
|
|
347
|
+
return result;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async routeAction(body: Record<string, unknown>): Promise<unknown> {
|
|
294
352
|
// Route by _chatId from the MCP subprocess request.
|
|
295
353
|
// _chatId may be a string (Teams: "teams_chat_19:...") or numeric string
|
|
296
354
|
// (Telegram: "123456"). The context map is keyed by numeric chatId, so
|
|
@@ -492,6 +550,7 @@ export class Gateway {
|
|
|
492
550
|
w.msSinceLastMessage < 60000
|
|
493
551
|
? "just now"
|
|
494
552
|
: `${Math.round(w.msSinceLastMessage / 60000)}m ago`,
|
|
553
|
+
alerts: activeAlerts(),
|
|
495
554
|
};
|
|
496
555
|
}
|
|
497
556
|
|