talon-agent 5.18.2 → 5.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +2 -2
- package/prompts/system/agent-brief.md +20 -3
- package/src/app.ts +13 -0
- package/src/backend/claude-sdk/handler.ts +4 -4
- package/src/backend/claude-sdk/mcp-ready.ts +16 -2
- package/src/backend/claude-sdk/one-shot.ts +32 -2
- package/src/backend/claude-sdk/stream.ts +2 -2
- package/src/backend/codex/auth.ts +1 -1
- package/src/backend/codex/handler/message.ts +11 -11
- package/src/backend/codex/init.ts +4 -9
- package/src/backend/codex/mcp-config.ts +1 -2
- package/src/backend/codex/oauth-incompat.ts +8 -4
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/openai-agents/builtins.ts +55 -27
- package/src/backend/openai-agents/factory.ts +3 -3
- package/src/backend/openai-agents/handler/message.ts +3 -5
- package/src/backend/openai-agents/mcp-pool.ts +10 -27
- package/src/backend/remote-server/chat-turn.ts +6 -6
- package/src/backend/remote-server/events.ts +1 -5
- package/src/backend/remote-server/index.ts +0 -1
- package/src/backend/remote-server/messages.ts +3 -7
- package/src/backend/remote-server/one-shot.ts +1 -3
- package/src/backend/remote-server/session-helpers.ts +1 -4
- package/src/backend/remote-server/sse-stream.ts +8 -9
- package/src/backend/runtime/metrics.ts +7 -13
- package/src/backend/runtime/sleep.ts +1 -2
- package/src/backend/runtime/turn/handle-retry.ts +48 -2
- package/src/backend/runtime/turn/handler-to-events.ts +3 -4
- package/src/bootstrap.ts +9 -1
- package/src/cli/doctor.ts +3 -0
- package/src/cli/index.ts +8 -10
- package/src/cli/logs.ts +148 -9
- package/src/cli/setup.ts +9 -11
- package/src/cli/status.ts +29 -0
- package/src/core/agent-runtime/README.md +5 -19
- package/src/core/agent-runtime/events.ts +3 -39
- package/src/core/agent-runtime/model-ref.ts +0 -8
- package/src/core/agents/registry.ts +65 -2
- package/src/core/auth/expiry-monitor.ts +9 -1
- package/src/core/auth/login-flow.ts +9 -1
- package/src/core/auth/status.ts +31 -3
- package/src/core/background/cron/scheduler.ts +25 -5
- package/src/core/background/dream/index.ts +29 -10
- package/src/core/background/failure-backoff.ts +30 -0
- package/src/core/background/heartbeat/agent.ts +2 -27
- package/src/core/background/heartbeat/index.ts +0 -2
- package/src/core/background/heartbeat/scheduler.ts +21 -13
- package/src/core/background/heartbeat/state.ts +10 -2
- package/src/core/background/isolated-agent.ts +6 -2
- package/src/core/background/pulse/pulse.ts +9 -0
- package/src/core/background/triggers/exit.ts +54 -0
- package/src/core/background/triggers/index.ts +1 -3
- package/src/core/background/triggers/resume.ts +2 -4
- package/src/core/backup/archive/tar.ts +14 -4
- package/src/core/backup/plan.ts +1 -0
- package/src/core/backup/restore.ts +21 -13
- package/src/core/backup/scheduler.ts +28 -9
- package/src/core/backup/snapshot.ts +43 -10
- package/src/core/backup/store.ts +7 -19
- package/src/core/backup/targets.ts +69 -17
- package/src/core/config/index.ts +14 -0
- package/src/core/daemon/crash-marker.ts +141 -0
- package/src/core/daemon/crash.ts +9 -2
- package/src/core/daemon/handoff.ts +15 -0
- package/src/core/daemon/health-alerts.ts +297 -0
- package/src/core/daemon/log-reader.ts +289 -0
- package/src/core/doctor/index.ts +18 -2
- package/src/core/doctor/logs.ts +124 -0
- package/src/core/doctor/types.ts +1 -1
- package/src/core/engine/backend-controller/index.ts +1 -13
- package/src/core/engine/backend-router/router.ts +1 -1
- package/src/core/engine/dispatcher.ts +55 -2
- package/src/core/engine/fault-text.ts +40 -0
- package/src/core/engine/gateway-actions/agents/index.ts +3 -2
- package/src/core/engine/gateway-actions/agents/report.ts +62 -0
- package/src/core/engine/gateway-actions/history.ts +2 -4
- package/src/core/engine/gateway-actions/mesh.ts +26 -14
- package/src/core/engine/gateway-actions/native/exec.ts +13 -16
- package/src/core/engine/gateway-actions/native/teleport.ts +1 -1
- package/src/core/engine/gateway.ts +60 -1
- package/src/core/engine/turn-health.ts +222 -0
- package/src/core/errors.ts +2 -2
- package/src/core/frontend-runtime/admin-notify.ts +1 -1
- package/src/core/frontend-runtime/alerts.ts +130 -0
- package/src/core/mcp-hub/children.ts +78 -29
- package/src/core/mcp-hub/index.ts +21 -18
- package/src/core/mcp-hub/proxy-server.ts +8 -4
- package/src/core/mcp-hub/talon-server.ts +5 -12
- package/src/core/mesh/credentials/store.ts +16 -1
- package/src/core/mesh/devices/registry.ts +1 -17
- package/src/core/mesh/devices/service.ts +49 -17
- package/src/core/mesh/devices/teleport.ts +14 -2
- package/src/core/mesh/links/node-binaries.ts +13 -6
- package/src/core/mesh/persist.ts +22 -10
- package/src/core/mesh/transfers/device-files.ts +5 -23
- package/src/core/mesh/transfers/transfers.ts +16 -3
- package/src/core/models/active-model.ts +2 -55
- package/src/core/plugin/actions.ts +19 -20
- package/src/core/plugin/builtins.ts +80 -90
- package/src/core/plugin/index.ts +1 -4
- package/src/core/plugin/loader.ts +25 -33
- package/src/core/plugin/mcp.ts +3 -5
- package/src/core/plugin/registry.ts +19 -35
- package/src/core/plugin/types.ts +2 -5
- package/src/core/prompt/assemble.ts +15 -3
- package/src/core/scripts/lua.ts +6 -2
- package/src/core/tasks/table.ts +8 -2
- package/src/core/tools/bridge.ts +2 -4
- package/src/core/tools/chat/cross-send.ts +1 -1
- package/src/core/tools/chat/messaging.ts +1 -1
- package/src/core/tools/index.ts +2 -2
- package/src/core/tools/mcp-env.ts +2 -59
- package/src/core/tools/ops/agents.ts +22 -1
- package/src/core/tools/schemas.ts +4 -9
- package/src/core/vfs/fusefs.ts +0 -5
- package/src/core/vfs/index.ts +9 -2
- package/src/core/vfs/mounts/diagnostics.ts +109 -0
- package/src/core/vfs/mounts/proc.ts +17 -1
- package/src/core/vfs/workspace.ts +7 -3
- package/src/core/weaver/shuttle.ts +8 -1
- package/src/core/weaver/turn-log.ts +320 -0
- package/src/core/weaver/weaver.ts +48 -5
- package/src/frontend/discord/actions/index.ts +8 -1
- package/src/frontend/discord/diagnostics.ts +82 -6
- package/src/frontend/discord/handlers/index.ts +0 -2
- package/src/frontend/discord/middleware.ts +7 -13
- package/src/frontend/discord/runtime.ts +1 -3
- package/src/frontend/health/delivery.ts +115 -0
- package/src/frontend/health/outage.ts +116 -0
- package/src/frontend/native/bridge/routes/chats.ts +3 -5
- package/src/frontend/native/bridge/server.ts +106 -21
- package/src/frontend/native/index.ts +1 -1
- package/src/frontend/native/media/media.ts +5 -1
- package/src/frontend/native/runtime.ts +12 -7
- package/src/frontend/native/surface/handlers.ts +1 -1
- package/src/frontend/native/surface/memory.ts +1 -1
- package/src/frontend/native/surface/models.ts +3 -3
- package/src/frontend/native/surface/settings.ts +20 -8
- package/src/frontend/native/turn/context.ts +6 -8
- package/src/frontend/native/turn/turn-meta.ts +2 -5
- package/src/frontend/native/turn/turn.ts +8 -10
- package/src/frontend/presentation/format.ts +2 -4
- package/src/frontend/presentation/session-status.ts +2 -6
- package/src/frontend/teams/actions.ts +8 -1
- package/src/frontend/teams/graph.ts +0 -1
- package/src/frontend/teams/index.ts +1 -4
- package/src/frontend/teams/poll.ts +40 -2
- package/src/frontend/teams/runtime.ts +14 -5
- package/src/frontend/telegram/actions/index.ts +4 -1
- package/src/frontend/telegram/actions/send.ts +8 -0
- package/src/frontend/telegram/handlers/context.ts +13 -2
- package/src/frontend/telegram/handlers/delivery.ts +12 -9
- package/src/frontend/telegram/handlers/index.ts +0 -2
- package/src/frontend/telegram/index.ts +35 -9
- package/src/frontend/telegram/polling/poll-health.ts +110 -0
- package/src/frontend/telegram/userbot.ts +100 -36
- package/src/frontend/terminal/builtins/session.ts +2 -2
- package/src/frontend/terminal/index.ts +1 -3
- package/src/frontend/terminal/renderer.ts +2 -18
- package/src/frontend/whatsapp/actions/index.ts +12 -1
- package/src/frontend/whatsapp/actions/messaging.ts +7 -2
- package/src/frontend/whatsapp/connection/connection.ts +29 -9
- package/src/frontend/whatsapp/connection/health.ts +89 -0
- package/src/frontend/whatsapp/connection/identity.ts +4 -4
- package/src/frontend/whatsapp/runtime.ts +11 -5
- package/src/native/blake3.ts +28 -2
- package/src/native/fusefs.ts +23 -5
- package/src/native/registry.ts +1 -1
- package/src/native/warden.ts +33 -5
- package/src/plugins/github/index.ts +0 -1
- package/src/plugins/mempalace/index.ts +9 -3
- package/src/plugins/playwright/index.ts +2 -4
- package/src/plugins/playwright/provision.ts +12 -4
- package/src/storage/chat-settings.ts +6 -1
- package/src/storage/cron.ts +29 -4
- package/src/storage/daily-log.ts +43 -47
- package/src/storage/db.ts +61 -33
- package/src/storage/history.ts +6 -1
- package/src/storage/journal.ts +9 -2
- package/src/storage/kv.ts +19 -6
- package/src/storage/media-index.ts +28 -6
- package/src/storage/repositories/chat-settings-repo.ts +8 -2
- package/src/storage/repositories/sessions-repo.ts +10 -3
- package/src/storage/scripts.ts +24 -13
- package/src/storage/sessions.ts +11 -2
- package/src/storage/skills.ts +21 -2
- package/src/storage/stickers.ts +17 -3
- package/src/storage/triggers.ts +8 -3
- package/src/storage/turn-meta.ts +25 -7
- package/src/util/log.ts +189 -6
- package/src/util/logging/turn-scope.ts +85 -0
- package/src/util/time.ts +3 -3
- package/src/util/watchdog.ts +30 -0
- package/src/core/engine/backend-controller/legacy.ts +0 -111
|
@@ -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();
|
package/src/cli/logs.ts
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `talon logs` — pretty-print the last lines of the JSON log file and then
|
|
3
3
|
* tail it live.
|
|
4
|
+
*
|
|
5
|
+
* Filters (`--errors`, `--component`, `--since`, `--grep`, `--turn`)
|
|
6
|
+
* switch to the shared log reader (core/daemon/log-reader.ts — the same
|
|
7
|
+
* parser behind proc/log, proc/errors and doctor): the backlog is read
|
|
8
|
+
* across rotated generations, and the live tail applies the same filter.
|
|
9
|
+
* `--no-follow` prints the backlog and exits, for scripts and agents.
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
import pc from "picocolors";
|
|
7
13
|
import { existsSync, readFileSync, watchFile } from "node:fs";
|
|
14
|
+
import type { LogFilter, LogRecord } from "../core/daemon/log-reader.js";
|
|
8
15
|
import { printBanner } from "./config.js";
|
|
9
16
|
import { LOG_FILE } from "./context.js";
|
|
10
17
|
|
|
18
|
+
export type LogsOptions = {
|
|
19
|
+
filter: LogFilter;
|
|
20
|
+
/** Keep tailing after the backlog (default true). */
|
|
21
|
+
follow: boolean;
|
|
22
|
+
/** Backlog size; undefined = 30, or 1000 under `--since`. */
|
|
23
|
+
lines?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const LOGS_USAGE =
|
|
27
|
+
"talon logs [--errors] [--component <c>] [--since <30m|2h|1d>] " +
|
|
28
|
+
"[--grep <text>] [--turn <id>] [-n <lines>] [--no-follow]";
|
|
29
|
+
|
|
30
|
+
/** Parse `talon logs` argv; a string is a usage error to print. */
|
|
31
|
+
export async function parseLogsArgs(
|
|
32
|
+
args: string[],
|
|
33
|
+
now: number = Date.now(),
|
|
34
|
+
): Promise<LogsOptions | string> {
|
|
35
|
+
const { parseLogDuration } = await import("../core/daemon/log-reader.js");
|
|
36
|
+
const opts: LogsOptions = { filter: {}, follow: true };
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const flag = args[i];
|
|
39
|
+
const value = (): string | undefined => args[++i];
|
|
40
|
+
if (flag === "--errors") opts.filter.minLevel = "warn";
|
|
41
|
+
else if (flag === "--no-follow") opts.follow = false;
|
|
42
|
+
else if (flag === "--component") opts.filter.component = value();
|
|
43
|
+
else if (flag === "--grep") opts.filter.grep = value();
|
|
44
|
+
else if (flag === "--turn") opts.filter.turn = value();
|
|
45
|
+
else if (flag === "--since") {
|
|
46
|
+
const ms = parseLogDuration(value() ?? "");
|
|
47
|
+
if (ms === null) return `--since needs a duration like 30m, 2h, 1d`;
|
|
48
|
+
opts.filter.since = now - ms;
|
|
49
|
+
} else if (flag === "-n" || flag === "--lines") {
|
|
50
|
+
const n = Number(value());
|
|
51
|
+
if (!Number.isInteger(n) || n <= 0) return `${flag} needs a count`;
|
|
52
|
+
opts.lines = n;
|
|
53
|
+
} else return `unknown option: ${flag}\n usage: ${LOGS_USAGE}`;
|
|
54
|
+
if (i >= args.length) return `${flag} needs a value`;
|
|
55
|
+
}
|
|
56
|
+
return opts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `parseLogsArgs` then run; usage errors exit non-zero. */
|
|
60
|
+
export async function runLogsCommand(args: string[]): Promise<void> {
|
|
61
|
+
const opts = await parseLogsArgs(args);
|
|
62
|
+
if (typeof opts === "string") {
|
|
63
|
+
console.error(` ${pc.red("✖")} ${opts}`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await tailLogs(opts);
|
|
68
|
+
}
|
|
69
|
+
|
|
11
70
|
const LEVEL_LABELS: Record<number, string> = {
|
|
12
71
|
10: pc.dim("TRC"),
|
|
13
72
|
20: pc.dim("DBG"),
|
|
@@ -17,6 +76,25 @@ const LEVEL_LABELS: Record<number, string> = {
|
|
|
17
76
|
60: pc.bgRed(pc.white("FTL")),
|
|
18
77
|
};
|
|
19
78
|
|
|
79
|
+
const LEVEL_NAME_LABELS: Record<LogRecord["level"], string> = {
|
|
80
|
+
trace: LEVEL_LABELS[10],
|
|
81
|
+
debug: LEVEL_LABELS[20],
|
|
82
|
+
info: LEVEL_LABELS[30],
|
|
83
|
+
warn: LEVEL_LABELS[40],
|
|
84
|
+
error: LEVEL_LABELS[50],
|
|
85
|
+
fatal: LEVEL_LABELS[60],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** A parsed record in the same shape as {@link formatLogLine}. */
|
|
89
|
+
function formatRecord(rec: LogRecord): string {
|
|
90
|
+
const time = pc.dim(new Date(rec.ts).toTimeString().slice(0, 8));
|
|
91
|
+
const comp = pc.cyan((rec.component ?? "?").padEnd(10));
|
|
92
|
+
const turn =
|
|
93
|
+
rec.turn && !rec.msg.includes("turn=") ? ` turn=${rec.turn}` : "";
|
|
94
|
+
const err = rec.err ? pc.red(` (${rec.err})`) : "";
|
|
95
|
+
return ` ${time} ${LEVEL_NAME_LABELS[rec.level]} ${comp} ${rec.msg}${turn}${err}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
20
98
|
function formatLogLine(line: string): string {
|
|
21
99
|
try {
|
|
22
100
|
const obj = JSON.parse(line);
|
|
@@ -31,7 +109,13 @@ function formatLogLine(line: string): string {
|
|
|
31
109
|
}
|
|
32
110
|
}
|
|
33
111
|
|
|
34
|
-
|
|
112
|
+
function hasFilter(filter: LogFilter): boolean {
|
|
113
|
+
return Object.values(filter).some((v) => v !== undefined);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function tailLogs(
|
|
117
|
+
opts: LogsOptions = { filter: {}, follow: true },
|
|
118
|
+
): Promise<void> {
|
|
35
119
|
printBanner();
|
|
36
120
|
if (!existsSync(LOG_FILE)) {
|
|
37
121
|
console.log(
|
|
@@ -39,18 +123,73 @@ export async function tailLogs(): Promise<void> {
|
|
|
39
123
|
);
|
|
40
124
|
return;
|
|
41
125
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
126
|
+
if (hasFilter(opts.filter)) return tailFiltered(opts);
|
|
127
|
+
if (opts.follow) {
|
|
128
|
+
console.log(
|
|
129
|
+
` ${pc.dim("Tailing")} ${pc.dim(LOG_FILE)}\n ${pc.dim("Press Ctrl+C to stop")}\n`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
45
132
|
const content = readFileSync(LOG_FILE, "utf-8");
|
|
46
133
|
const lines = content.trim().split("\n");
|
|
47
|
-
for (const line of lines.slice(-30))
|
|
48
|
-
|
|
49
|
-
|
|
134
|
+
for (const line of lines.slice(-(opts.lines ?? 30)))
|
|
135
|
+
console.log(formatLogLine(line));
|
|
136
|
+
if (!opts.follow) return;
|
|
137
|
+
await followLog(lines.length, (line) => console.log(formatLogLine(line)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The filtered path: backlog across generations, then a filtered tail. */
|
|
141
|
+
async function tailFiltered(opts: LogsOptions): Promise<void> {
|
|
142
|
+
const { readLogRecords, parseLogLine, matchesLogFilter } =
|
|
143
|
+
await import("../core/daemon/log-reader.js");
|
|
144
|
+
const limit = opts.lines ?? (opts.filter.since !== undefined ? 1000 : 30);
|
|
145
|
+
const backlog = readLogRecords(LOG_FILE, {
|
|
146
|
+
limit,
|
|
147
|
+
filter: opts.filter,
|
|
148
|
+
// The CLI owns its process: read whole generations, not a window.
|
|
149
|
+
maxBytesPerFile: 64 * 1024 * 1024,
|
|
150
|
+
});
|
|
151
|
+
const desc = describeFilter(opts.filter);
|
|
152
|
+
console.log(
|
|
153
|
+
` ${pc.dim(`${backlog.length} matching entr${backlog.length === 1 ? "y" : "ies"} (${desc})`)}` +
|
|
154
|
+
(opts.follow ? `\n ${pc.dim("Following — Ctrl+C to stop")}` : "") +
|
|
155
|
+
"\n",
|
|
156
|
+
);
|
|
157
|
+
for (const rec of backlog) console.log(formatRecord(rec));
|
|
158
|
+
if (!opts.follow) return;
|
|
159
|
+
const start = readFileSync(LOG_FILE, "utf-8").trim().split("\n").length;
|
|
160
|
+
await followLog(start, (line) => {
|
|
161
|
+
const rec = parseLogLine(line);
|
|
162
|
+
if (rec && matchesLogFilter(rec, opts.filter))
|
|
163
|
+
console.log(formatRecord(rec));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function describeFilter(filter: LogFilter): string {
|
|
168
|
+
const parts: string[] = [];
|
|
169
|
+
if (filter.minLevel) parts.push(`>=${filter.minLevel}`);
|
|
170
|
+
if (filter.component) parts.push(`component=${filter.component}`);
|
|
171
|
+
if (filter.since !== undefined)
|
|
172
|
+
parts.push(`since ${new Date(filter.since).toTimeString().slice(0, 8)}`);
|
|
173
|
+
if (filter.grep) parts.push(`grep "${filter.grep}"`);
|
|
174
|
+
if (filter.turn) parts.push(`turn=${filter.turn}`);
|
|
175
|
+
return parts.join(", ");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Print each line appended to the log from line `from` on, forever. */
|
|
179
|
+
async function followLog(
|
|
180
|
+
from: number,
|
|
181
|
+
onLine: (line: string) => void,
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
let lastSize = from;
|
|
184
|
+
watchFile(LOG_FILE, { interval: 500 }, (curr, prev) => {
|
|
185
|
+
// A rotation (daemon start → talon.log.old, or the sink's runtime
|
|
186
|
+
// shift to talon.log.1) leaves a new file under the name, and counting
|
|
187
|
+
// its lines against the old one's would stay silent until it outgrew
|
|
188
|
+
// the file it replaced.
|
|
189
|
+
if (curr.ino !== prev.ino || curr.size < prev.size) lastSize = 0;
|
|
50
190
|
try {
|
|
51
191
|
const nl = readFileSync(LOG_FILE, "utf-8").trim().split("\n");
|
|
52
|
-
for (let i = lastSize; i < nl.length; i++)
|
|
53
|
-
console.log(formatLogLine(nl[i]));
|
|
192
|
+
for (let i = lastSize; i < nl.length; i++) onLine(nl[i]);
|
|
54
193
|
lastSize = nl.length;
|
|
55
194
|
} catch {
|
|
56
195
|
/* ignore */
|
package/src/cli/setup.ts
CHANGED
|
@@ -26,11 +26,9 @@ const trimmedOrUndefined = (raw: string) => raw.trim() || undefined;
|
|
|
26
26
|
/**
|
|
27
27
|
* Await a clack prompt; on Esc/Ctrl-C say so and leave the wizard.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ever resolves a symbol to mean "cancelled", so narrowing on `typeof`
|
|
33
|
-
* here removes both.
|
|
29
|
+
* clack only ever resolves a symbol to mean "cancelled", so narrowing on
|
|
30
|
+
* `typeof` replaces a per-prompt `isCancel` guard and the `as` cast that
|
|
31
|
+
* `@clack/core`'s own unique-symbol narrowing would still need.
|
|
34
32
|
*
|
|
35
33
|
* The answer type is subtracted with `Exclude` rather than inferred from a
|
|
36
34
|
* `Promise<T | symbol>` parameter: clack 1.8.1 retyped `CANCEL_SYMBOL` as a
|
|
@@ -667,12 +665,12 @@ function telegramAccess(
|
|
|
667
665
|
*
|
|
668
666
|
* Extracted from `runSetup` so the merge is testable without driving the
|
|
669
667
|
* prompts — the behaviour that matters here is what it *doesn't* touch.
|
|
670
|
-
* The wizard models roughly half of ~/.talon/config.json
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* `
|
|
668
|
+
* The wizard models roughly half of ~/.talon/config.json. Spreading
|
|
669
|
+
* `existing` first keeps every key it does not model (whatsapp, native,
|
|
670
|
+
* memory, github, heartbeat/dream, allowlists, plugin blocks…) — building
|
|
671
|
+
* from the named fields alone silently deletes them. Fields below still
|
|
672
|
+
* override, and an explicit `undefined` still deletes, because
|
|
673
|
+
* `saveConfig` strips undefined.
|
|
676
674
|
*/
|
|
677
675
|
export function buildSetupConfig(
|
|
678
676
|
existing: Config,
|
package/src/cli/status.ts
CHANGED
|
@@ -15,6 +15,29 @@ function formatUptime(seconds: number): string {
|
|
|
15
15
|
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
type HealthAlert = { key: string; severity: string; message: string };
|
|
19
|
+
|
|
20
|
+
/** The daemon's active alerts from its /health body (older daemons: none). */
|
|
21
|
+
function healthAlerts(health: Record<string, unknown>): HealthAlert[] {
|
|
22
|
+
const raw = health.alerts;
|
|
23
|
+
if (!Array.isArray(raw)) return [];
|
|
24
|
+
return raw.filter(
|
|
25
|
+
(a): a is HealthAlert =>
|
|
26
|
+
typeof a === "object" &&
|
|
27
|
+
a !== null &&
|
|
28
|
+
typeof a.key === "string" &&
|
|
29
|
+
typeof a.message === "string",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One line per active alert, coloured by severity. Shared with doctor. */
|
|
34
|
+
export function formatAlertLines(health: Record<string, unknown>): string[] {
|
|
35
|
+
return healthAlerts(health).map((a) => {
|
|
36
|
+
const dot = a.severity === "warn" ? pc.yellow("●") : pc.red("●");
|
|
37
|
+
return ` ${dot} ${pc.bold(a.key)} ${a.message}`;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
export async function showStatus(): Promise<void> {
|
|
19
42
|
printBanner();
|
|
20
43
|
const instance = await findRunningInstance();
|
|
@@ -38,6 +61,12 @@ export async function showStatus(): Promise<void> {
|
|
|
38
61
|
console.log(` ${pc.dim("Queue")} ${h.queue} pending`);
|
|
39
62
|
console.log(` ${pc.dim("Errors")} ${h.errors}`);
|
|
40
63
|
console.log(` ${pc.dim("Last active")} ${h.lastActivity}\n`);
|
|
64
|
+
const alerts = formatAlertLines(h);
|
|
65
|
+
if (alerts.length > 0) {
|
|
66
|
+
console.log(` ${pc.bold("Active alerts")}\n`);
|
|
67
|
+
for (const line of alerts) console.log(line);
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
41
70
|
return;
|
|
42
71
|
}
|
|
43
72
|
|
|
@@ -41,15 +41,16 @@ error | completed
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Plus `UsageSnapshot`, `AgentError` (with `AgentErrorKind`),
|
|
44
|
-
`AgentResult
|
|
45
|
-
`
|
|
44
|
+
`AgentResult`, and `AgentRunError` — what the dispatcher rethrows an
|
|
45
|
+
`error` terminator as. Helpers: `emptyUsage`, `toolInputToRecord`,
|
|
46
|
+
`classifiedToAgentError`.
|
|
46
47
|
|
|
47
48
|
### `model-ref.ts`
|
|
48
49
|
|
|
49
50
|
Typed model identity. `ModelRef = { backend: BackendId, id,
|
|
50
51
|
displayName, ... }`. Owns the `BACKEND_IDS` literal — single source of
|
|
51
52
|
truth for which backends the typed union can route to. Helpers:
|
|
52
|
-
`isBackendId`, `
|
|
53
|
+
`isBackendId`, `makeBareModelRef`.
|
|
53
54
|
|
|
54
55
|
### `capabilities.ts`
|
|
55
56
|
|
|
@@ -101,16 +102,6 @@ Backend contract assertions any conforming `Backend` must pass:
|
|
|
101
102
|
|
|
102
103
|
Each throws `ContractViolation` with a descriptive message.
|
|
103
104
|
|
|
104
|
-
### `event-bridge.ts`
|
|
105
|
-
|
|
106
|
-
The bridge between the canonical `AgentEvent` stream and the
|
|
107
|
-
callback-shaped consumer contract the dispatcher uses upstream of the
|
|
108
|
-
backend. `pipeEventsToCallbacks(stream, callbacks)` consumes an
|
|
109
|
-
`AgentEvent` stream and invokes the supplied callbacks (`onStreamDelta`
|
|
110
|
-
/ `onTextBlock` / `onToolUse`), returns the final `AgentResult`, and
|
|
111
|
-
throws `BridgedAgentError` carrying the original `AgentError` if the
|
|
112
|
-
stream terminates with an error event.
|
|
113
|
-
|
|
114
105
|
## Migration cookbook
|
|
115
106
|
|
|
116
107
|
### Adding a new backend
|
|
@@ -164,11 +155,6 @@ returns `{ model: string | null, ref: ModelRef | null, source }`:
|
|
|
164
155
|
- `source` carries the chain step that produced the model, useful
|
|
165
156
|
for toast wording and stale-slot cleanup.
|
|
166
157
|
|
|
167
|
-
Convenience wrappers:
|
|
168
|
-
|
|
169
|
-
- `getActiveModelForChat(...)` → `model`
|
|
170
|
-
- `getActiveModelRefForChat(...)` → `ref`
|
|
171
|
-
|
|
172
158
|
### Adding a new store
|
|
173
159
|
|
|
174
160
|
New structured state goes in the SQLite layer — see the layering doc
|
|
@@ -178,7 +164,7 @@ in `src/storage/db.ts` (sql/<store>.sql → repositories/<store>-repo.ts
|
|
|
178
164
|
## Invariants
|
|
179
165
|
|
|
180
166
|
- `BACKEND_IDS` in `model-ref.ts` is the source of truth for the typed
|
|
181
|
-
union. `
|
|
167
|
+
union. `core/config/index.ts` zod enums are wired to the same literal.
|
|
182
168
|
- `AgentEvent.type` is the ONLY discrimination mechanism. No class
|
|
183
169
|
hierarchy, no `instanceof` checks.
|
|
184
170
|
- Every `ChatBackend.runChatTurn` stream terminates with `completed`
|