talon-agent 5.18.2 → 5.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/prompts/system/agent-brief.md +20 -3
- package/src/app.ts +13 -0
- package/src/backend/claude-sdk/handler.ts +4 -4
- package/src/backend/claude-sdk/mcp-ready.ts +16 -2
- package/src/backend/claude-sdk/stream.ts +2 -2
- package/src/backend/codex/auth.ts +1 -1
- package/src/backend/codex/handler/message.ts +11 -11
- package/src/backend/codex/init.ts +4 -9
- package/src/backend/codex/mcp-config.ts +1 -2
- package/src/backend/codex/oauth-incompat.ts +8 -4
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/openai-agents/builtins.ts +55 -27
- package/src/backend/openai-agents/factory.ts +3 -3
- package/src/backend/openai-agents/handler/message.ts +3 -5
- package/src/backend/openai-agents/mcp-pool.ts +10 -27
- package/src/backend/remote-server/chat-turn.ts +6 -6
- package/src/backend/remote-server/events.ts +1 -5
- package/src/backend/remote-server/index.ts +0 -1
- package/src/backend/remote-server/messages.ts +3 -7
- package/src/backend/remote-server/one-shot.ts +1 -3
- package/src/backend/remote-server/session-helpers.ts +1 -4
- package/src/backend/remote-server/sse-stream.ts +8 -9
- package/src/backend/runtime/metrics.ts +7 -13
- package/src/backend/runtime/sleep.ts +1 -2
- package/src/backend/runtime/turn/handle-retry.ts +48 -2
- package/src/backend/runtime/turn/handler-to-events.ts +3 -4
- package/src/bootstrap.ts +9 -1
- package/src/cli/doctor.ts +3 -0
- package/src/cli/index.ts +8 -10
- package/src/cli/logs.ts +148 -9
- package/src/cli/setup.ts +9 -11
- package/src/cli/status.ts +29 -0
- package/src/core/agent-runtime/README.md +5 -19
- package/src/core/agent-runtime/events.ts +3 -39
- package/src/core/agent-runtime/model-ref.ts +0 -8
- package/src/core/agents/registry.ts +49 -2
- package/src/core/auth/expiry-monitor.ts +9 -1
- package/src/core/auth/login-flow.ts +9 -1
- package/src/core/auth/status.ts +31 -3
- package/src/core/background/cron/scheduler.ts +25 -5
- package/src/core/background/dream/index.ts +29 -10
- package/src/core/background/failure-backoff.ts +30 -0
- package/src/core/background/heartbeat/agent.ts +2 -27
- package/src/core/background/heartbeat/index.ts +0 -2
- package/src/core/background/heartbeat/scheduler.ts +21 -13
- package/src/core/background/heartbeat/state.ts +10 -2
- package/src/core/background/isolated-agent.ts +6 -2
- package/src/core/background/pulse/pulse.ts +9 -0
- package/src/core/background/triggers/exit.ts +54 -0
- package/src/core/background/triggers/index.ts +1 -3
- package/src/core/background/triggers/resume.ts +2 -4
- package/src/core/backup/archive/tar.ts +14 -4
- package/src/core/backup/restore.ts +21 -13
- package/src/core/backup/scheduler.ts +28 -9
- package/src/core/backup/snapshot.ts +43 -10
- package/src/core/backup/store.ts +7 -19
- package/src/core/backup/targets.ts +69 -17
- package/src/core/config/index.ts +14 -0
- package/src/core/daemon/crash-marker.ts +141 -0
- package/src/core/daemon/crash.ts +9 -2
- package/src/core/daemon/handoff.ts +15 -0
- package/src/core/daemon/health-alerts.ts +297 -0
- package/src/core/daemon/log-reader.ts +289 -0
- package/src/core/doctor/index.ts +18 -2
- package/src/core/doctor/logs.ts +124 -0
- package/src/core/doctor/types.ts +1 -1
- package/src/core/engine/backend-controller/index.ts +1 -13
- package/src/core/engine/backend-router/router.ts +1 -1
- package/src/core/engine/dispatcher.ts +55 -2
- package/src/core/engine/fault-text.ts +40 -0
- package/src/core/engine/gateway-actions/agents/index.ts +3 -2
- package/src/core/engine/gateway-actions/agents/report.ts +62 -0
- package/src/core/engine/gateway-actions/history.ts +2 -4
- package/src/core/engine/gateway-actions/native/exec.ts +13 -16
- package/src/core/engine/gateway.ts +60 -1
- package/src/core/engine/turn-health.ts +222 -0
- package/src/core/errors.ts +2 -2
- package/src/core/frontend-runtime/admin-notify.ts +1 -1
- package/src/core/frontend-runtime/alerts.ts +130 -0
- package/src/core/mcp-hub/children.ts +78 -29
- package/src/core/mcp-hub/index.ts +21 -18
- package/src/core/mcp-hub/proxy-server.ts +8 -4
- package/src/core/mcp-hub/talon-server.ts +5 -12
- package/src/core/mesh/credentials/store.ts +16 -1
- package/src/core/mesh/devices/service.ts +26 -15
- package/src/core/mesh/devices/teleport.ts +14 -2
- package/src/core/mesh/links/node-binaries.ts +13 -6
- package/src/core/mesh/persist.ts +22 -10
- package/src/core/mesh/transfers/device-files.ts +5 -23
- package/src/core/mesh/transfers/transfers.ts +16 -3
- package/src/core/models/active-model.ts +2 -55
- package/src/core/plugin/actions.ts +19 -20
- package/src/core/plugin/builtins.ts +80 -90
- package/src/core/plugin/index.ts +1 -4
- package/src/core/plugin/loader.ts +25 -33
- package/src/core/plugin/mcp.ts +3 -5
- package/src/core/plugin/registry.ts +19 -35
- package/src/core/plugin/types.ts +2 -5
- package/src/core/prompt/assemble.ts +15 -3
- package/src/core/scripts/lua.ts +6 -2
- package/src/core/tasks/table.ts +8 -2
- package/src/core/tools/bridge.ts +2 -4
- package/src/core/tools/chat/cross-send.ts +1 -1
- package/src/core/tools/chat/messaging.ts +1 -1
- package/src/core/tools/index.ts +2 -2
- package/src/core/tools/mcp-env.ts +2 -59
- package/src/core/tools/ops/agents.ts +22 -1
- package/src/core/tools/schemas.ts +4 -9
- package/src/core/vfs/fusefs.ts +0 -5
- package/src/core/vfs/index.ts +9 -2
- package/src/core/vfs/mounts/diagnostics.ts +109 -0
- package/src/core/vfs/mounts/proc.ts +17 -1
- package/src/core/vfs/workspace.ts +7 -3
- package/src/core/weaver/shuttle.ts +8 -1
- package/src/core/weaver/turn-log.ts +320 -0
- package/src/core/weaver/weaver.ts +48 -5
- package/src/frontend/discord/actions/index.ts +8 -1
- package/src/frontend/discord/diagnostics.ts +82 -6
- package/src/frontend/discord/handlers/index.ts +0 -2
- package/src/frontend/discord/middleware.ts +7 -13
- package/src/frontend/discord/runtime.ts +1 -3
- package/src/frontend/health/delivery.ts +115 -0
- package/src/frontend/health/outage.ts +116 -0
- package/src/frontend/native/bridge/routes/chats.ts +3 -5
- package/src/frontend/native/bridge/server.ts +106 -21
- package/src/frontend/native/index.ts +1 -1
- package/src/frontend/native/media/media.ts +5 -1
- package/src/frontend/native/runtime.ts +12 -7
- package/src/frontend/native/surface/handlers.ts +1 -1
- package/src/frontend/native/surface/memory.ts +1 -1
- package/src/frontend/native/surface/models.ts +3 -3
- package/src/frontend/native/surface/settings.ts +20 -8
- package/src/frontend/native/turn/context.ts +6 -8
- package/src/frontend/native/turn/turn-meta.ts +2 -5
- package/src/frontend/native/turn/turn.ts +8 -10
- package/src/frontend/presentation/format.ts +2 -4
- package/src/frontend/presentation/session-status.ts +2 -6
- package/src/frontend/teams/actions.ts +8 -1
- package/src/frontend/teams/graph.ts +0 -1
- package/src/frontend/teams/index.ts +1 -4
- package/src/frontend/teams/poll.ts +40 -2
- package/src/frontend/teams/runtime.ts +14 -5
- package/src/frontend/telegram/actions/index.ts +4 -1
- package/src/frontend/telegram/actions/send.ts +8 -0
- package/src/frontend/telegram/handlers/context.ts +13 -2
- package/src/frontend/telegram/handlers/delivery.ts +12 -9
- package/src/frontend/telegram/handlers/index.ts +0 -2
- package/src/frontend/telegram/index.ts +35 -9
- package/src/frontend/telegram/polling/poll-health.ts +110 -0
- package/src/frontend/telegram/userbot.ts +100 -36
- package/src/frontend/terminal/builtins/session.ts +2 -2
- package/src/frontend/terminal/index.ts +1 -3
- package/src/frontend/terminal/renderer.ts +2 -18
- package/src/frontend/whatsapp/actions/index.ts +12 -1
- package/src/frontend/whatsapp/actions/messaging.ts +7 -2
- package/src/frontend/whatsapp/connection/connection.ts +29 -9
- package/src/frontend/whatsapp/connection/health.ts +89 -0
- package/src/frontend/whatsapp/connection/identity.ts +4 -4
- package/src/frontend/whatsapp/runtime.ts +11 -5
- package/src/native/blake3.ts +28 -2
- package/src/native/fusefs.ts +23 -5
- package/src/native/registry.ts +1 -1
- package/src/native/warden.ts +33 -5
- package/src/plugins/github/index.ts +0 -1
- package/src/plugins/mempalace/index.ts +9 -3
- package/src/plugins/playwright/index.ts +2 -4
- package/src/plugins/playwright/provision.ts +12 -4
- package/src/storage/chat-settings.ts +6 -1
- package/src/storage/cron.ts +29 -4
- package/src/storage/daily-log.ts +43 -47
- package/src/storage/db.ts +61 -33
- package/src/storage/history.ts +6 -1
- package/src/storage/journal.ts +9 -2
- package/src/storage/kv.ts +19 -6
- package/src/storage/media-index.ts +28 -6
- package/src/storage/repositories/chat-settings-repo.ts +8 -2
- package/src/storage/repositories/sessions-repo.ts +10 -3
- package/src/storage/scripts.ts +24 -13
- package/src/storage/sessions.ts +11 -2
- package/src/storage/skills.ts +21 -2
- package/src/storage/stickers.ts +17 -3
- package/src/storage/triggers.ts +8 -3
- package/src/storage/turn-meta.ts +25 -7
- package/src/util/log.ts +189 -6
- package/src/util/logging/turn-scope.ts +85 -0
- package/src/util/time.ts +3 -3
- package/src/util/watchdog.ts +30 -0
- package/src/core/engine/backend-controller/legacy.ts +0 -111
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, unlinkSync } from "node:fs";
|
|
19
19
|
import { blake3HexFile } from "../native/blake3.js";
|
|
20
|
-
import { log, logError } from "../util/log.js";
|
|
20
|
+
import { log, logError, logWarn } from "../util/log.js";
|
|
21
21
|
import { recordError } from "../util/watchdog.js";
|
|
22
22
|
import { files } from "../util/paths.js";
|
|
23
23
|
import { importLegacyJson } from "./legacy-import.js";
|
|
24
24
|
import { setMessageFilePath } from "./history.js";
|
|
25
|
+
import { dbErrorFields } from "./db.js";
|
|
25
26
|
import * as repo from "./repositories/media-index-repo.js";
|
|
26
27
|
|
|
27
28
|
export type { MediaEntry } from "./repositories/media-index-repo.js";
|
|
@@ -75,7 +76,11 @@ export function addMedia(entry: Omit<MediaEntry, "id">): void {
|
|
|
75
76
|
try {
|
|
76
77
|
repo.upsert(entry);
|
|
77
78
|
} catch (err) {
|
|
78
|
-
logError(
|
|
79
|
+
logError(
|
|
80
|
+
"media",
|
|
81
|
+
`Media index save failed chat=${entry.chatId} msg=${entry.msgId} path=${entry.filePath}${dbErrorFields(err)}`,
|
|
82
|
+
err,
|
|
83
|
+
);
|
|
79
84
|
recordError(
|
|
80
85
|
`Media index write failed: ${err instanceof Error ? err.message : err}`,
|
|
81
86
|
);
|
|
@@ -116,8 +121,13 @@ async function hashAndDedupe(entry: Omit<MediaEntry, "id">): Promise<void> {
|
|
|
116
121
|
"media",
|
|
117
122
|
`Deduped ${entry.filePath} -> ${canonical.filePath} (blake3 ${hash.slice(0, 12)}…)`,
|
|
118
123
|
);
|
|
119
|
-
} catch {
|
|
120
|
-
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Dedupe is best-effort — the entry already points at the canonical
|
|
126
|
+
// copy — but the duplicate now sits on disk unreferenced.
|
|
127
|
+
logWarn(
|
|
128
|
+
"media",
|
|
129
|
+
`Dedupe unlink failed path=${entry.filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
);
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
}
|
|
@@ -161,17 +171,29 @@ function purgeExpired(): void {
|
|
|
161
171
|
const removed = repo.deleteOlderThan(cutoff);
|
|
162
172
|
// Rows first, files second: content dedupe means several entries can
|
|
163
173
|
// share one file, so only unlink paths no surviving row references.
|
|
174
|
+
let unlinkFailures = 0;
|
|
175
|
+
let firstFailure = "";
|
|
164
176
|
for (const path of new Set(expired.map((e) => e.filePath))) {
|
|
165
177
|
if (repo.countByFilePath(path) > 0) continue;
|
|
166
178
|
try {
|
|
167
179
|
if (existsSync(path)) unlinkSync(path);
|
|
168
|
-
} catch {
|
|
169
|
-
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Skip it, but count: files left behind are disk that the purge
|
|
182
|
+
// thinks it reclaimed.
|
|
183
|
+
if (unlinkFailures++ === 0) {
|
|
184
|
+
firstFailure = `${path}: ${err instanceof Error ? err.message : String(err)}`;
|
|
185
|
+
}
|
|
170
186
|
}
|
|
171
187
|
}
|
|
172
188
|
if (removed > 0) {
|
|
173
189
|
log("media", `Purged ${removed} expired media entries`);
|
|
174
190
|
}
|
|
191
|
+
if (unlinkFailures > 0) {
|
|
192
|
+
logWarn(
|
|
193
|
+
"media",
|
|
194
|
+
`Media purge left ${unlinkFailures} expired file(s) on disk; first=${firstFailure}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
175
197
|
} catch (err) {
|
|
176
198
|
logError("media", "Media index purge failed", err);
|
|
177
199
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* queried independently in SQL.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { logWarn } from "../../util/log.js";
|
|
14
15
|
import { getDatabase, inTransaction } from "../db.js";
|
|
15
16
|
import { chatSettingsSql } from "../sql/statements.generated.js";
|
|
16
17
|
import type { ReasoningEffortLevel } from "../../types/effort.js";
|
|
@@ -102,8 +103,13 @@ export function all(): Array<{ chatId: string; settings: ChatSettings }> {
|
|
|
102
103
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
103
104
|
result.push({ chatId: row.chat_id, settings: parsed });
|
|
104
105
|
}
|
|
105
|
-
} catch {
|
|
106
|
-
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Skip the unparseable row; the chat silently falls back to
|
|
108
|
+
// defaults otherwise.
|
|
109
|
+
logWarn(
|
|
110
|
+
"settings",
|
|
111
|
+
`Skipped corrupt chat settings chat=${row.chat_id}: ${err instanceof Error ? err.message : String(err)}`,
|
|
112
|
+
);
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
return result;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* column default can hold.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { logWarn } from "../../util/log.js";
|
|
15
16
|
import { getDatabase, inTransaction } from "../db.js";
|
|
16
17
|
import { sessionsSql } from "../sql/statements.generated.js";
|
|
17
18
|
import {
|
|
@@ -46,13 +47,19 @@ type Row = {
|
|
|
46
47
|
metrics: string | null;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
function parseMetrics(raw: string | null): SessionMetrics {
|
|
50
|
+
function parseMetrics(raw: string | null, chatId: string): SessionMetrics {
|
|
50
51
|
if (!raw) return emptyMetrics();
|
|
51
52
|
try {
|
|
52
53
|
// Normalise at the load boundary: backfills partial blobs and restores
|
|
53
54
|
// the Infinity minMs sentinel that JSON round-trips as null.
|
|
54
55
|
return normaliseMetrics(JSON.parse(raw));
|
|
55
|
-
} catch {
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Degrade to empty — and say so, since the next upsert overwrites
|
|
58
|
+
// the corrupt blob and the chat's lifetime counters restart at zero.
|
|
59
|
+
logWarn(
|
|
60
|
+
"sessions",
|
|
61
|
+
`Corrupt session metrics chat=${chatId} bytes=${raw.length} — resetting: ${err instanceof Error ? err.message : String(err)}`,
|
|
62
|
+
);
|
|
56
63
|
return emptyMetrics();
|
|
57
64
|
}
|
|
58
65
|
}
|
|
@@ -78,7 +85,7 @@ function rowToSession(row: Row): SessionState {
|
|
|
78
85
|
// NULL = no timed turn yet — the domain sentinel is Infinity.
|
|
79
86
|
fastestResponseMs: row.fastest_response_ms ?? Infinity,
|
|
80
87
|
},
|
|
81
|
-
metrics: parseMetrics(row.metrics),
|
|
88
|
+
metrics: parseMetrics(row.metrics, row.chat_id),
|
|
82
89
|
lastBotMessageId: row.last_bot_message_id ?? undefined,
|
|
83
90
|
sessionName: row.session_name ?? undefined,
|
|
84
91
|
lastModel: row.last_model ?? undefined,
|
package/src/storage/scripts.ts
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
|
-
import { mkdirSync, rmSync
|
|
20
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
21
21
|
import { dirname, resolve } from "node:path";
|
|
22
|
+
import writeFileAtomic from "write-file-atomic";
|
|
23
|
+
import { logWarn } from "../util/log.js";
|
|
22
24
|
import { dirs } from "../util/paths.js";
|
|
23
25
|
import * as repo from "./repositories/scripts-repo.js";
|
|
24
26
|
|
|
@@ -93,8 +95,9 @@ function writeScriptFile(
|
|
|
93
95
|
): string {
|
|
94
96
|
const path = scriptFilePath(name, lang);
|
|
95
97
|
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
-
// 0o700: only the user running Talon should be able to read/exec scripts
|
|
97
|
-
|
|
98
|
+
// 0o700: only the user running Talon should be able to read/exec scripts.
|
|
99
|
+
// Atomic: a failed replace (ENOSPC) must keep the previous body.
|
|
100
|
+
writeFileAtomic.sync(path, body, { encoding: "utf-8", mode: 0o700 });
|
|
98
101
|
return path;
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -117,11 +120,7 @@ export function saveScript(input: {
|
|
|
117
120
|
}): Script {
|
|
118
121
|
const existing = repo.getByName(input.name);
|
|
119
122
|
if (existing && existing.language !== input.language) {
|
|
120
|
-
|
|
121
|
-
rmSync(existing.scriptPath, { force: true });
|
|
122
|
-
} catch {
|
|
123
|
-
/* best effort */
|
|
124
|
-
}
|
|
123
|
+
removeScriptFile(existing.scriptPath, input.name);
|
|
125
124
|
}
|
|
126
125
|
const scriptPath = writeScriptFile(input.name, input.language, input.script);
|
|
127
126
|
const now = Date.now();
|
|
@@ -153,15 +152,27 @@ export function recordScriptUse(name: string): void {
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
/** Delete a script and best-effort clean up its on-disk file. */
|
|
155
|
+
/**
|
|
156
|
+
* Best-effort unlink. `force` already absorbs a missing file, so any
|
|
157
|
+
* throw here is a real fault (permissions, a directory in the way) that
|
|
158
|
+
* leaves an orphan script on disk.
|
|
159
|
+
*/
|
|
160
|
+
function removeScriptFile(path: string, name: string): void {
|
|
161
|
+
try {
|
|
162
|
+
rmSync(path, { force: true });
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logWarn(
|
|
165
|
+
"scripts",
|
|
166
|
+
`Could not remove script file name=${name} path=${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
156
171
|
export function deleteScript(name: string): boolean {
|
|
157
172
|
const script = repo.getByName(name);
|
|
158
173
|
if (!script) return false;
|
|
159
174
|
repo.removeByName(name);
|
|
160
|
-
|
|
161
|
-
rmSync(script.scriptPath, { force: true });
|
|
162
|
-
} catch {
|
|
163
|
-
/* best effort */
|
|
164
|
-
}
|
|
175
|
+
removeScriptFile(script.scriptPath, name);
|
|
165
176
|
return true;
|
|
166
177
|
}
|
|
167
178
|
|
package/src/storage/sessions.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { log, logError } from "../util/log.js";
|
|
|
22
22
|
import { recordError } from "../util/watchdog.js";
|
|
23
23
|
import { files } from "../util/paths.js";
|
|
24
24
|
import { importLegacyJson } from "./legacy-import.js";
|
|
25
|
+
import { dbErrorFields } from "./db.js";
|
|
25
26
|
import * as repo from "./repositories/sessions-repo.js";
|
|
26
27
|
|
|
27
28
|
export type {
|
|
@@ -204,7 +205,11 @@ function persist(chatId: string, session: SessionState): void {
|
|
|
204
205
|
try {
|
|
205
206
|
repo.upsert(chatId, session);
|
|
206
207
|
} catch (err) {
|
|
207
|
-
logError(
|
|
208
|
+
logError(
|
|
209
|
+
"sessions",
|
|
210
|
+
`Failed to persist sessions chat=${chatId}${dbErrorFields(err)}`,
|
|
211
|
+
err,
|
|
212
|
+
);
|
|
208
213
|
recordError(
|
|
209
214
|
`Session save failed: ${err instanceof Error ? err.message : err}`,
|
|
210
215
|
);
|
|
@@ -518,7 +523,11 @@ function removeSessionRow(chatId: string): void {
|
|
|
518
523
|
try {
|
|
519
524
|
repo.remove(chatId);
|
|
520
525
|
} catch (err) {
|
|
521
|
-
logError(
|
|
526
|
+
logError(
|
|
527
|
+
"sessions",
|
|
528
|
+
`Failed to delete session row chat=${chatId}${dbErrorFields(err)}`,
|
|
529
|
+
err,
|
|
530
|
+
);
|
|
522
531
|
recordError(
|
|
523
532
|
`Session delete failed: ${err instanceof Error ? err.message : err}`,
|
|
524
533
|
);
|
package/src/storage/skills.ts
CHANGED
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
writeFileSync,
|
|
27
27
|
} from "node:fs";
|
|
28
28
|
import { basename, resolve } from "node:path";
|
|
29
|
+
import writeFileAtomic from "write-file-atomic";
|
|
29
30
|
import { parseDocument, stringify } from "yaml";
|
|
31
|
+
import { logWarn } from "../util/log.js";
|
|
30
32
|
import { dirs } from "../util/paths.js";
|
|
31
33
|
|
|
32
34
|
export type Skill = {
|
|
@@ -123,6 +125,13 @@ function serializeSkill(input: {
|
|
|
123
125
|
return ["---", yaml, "---", "", input.body.trimEnd(), ""].join("\n");
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Frontmatter errors already reported, keyed by path + message: skills
|
|
130
|
+
* are re-read for every prompt, so a broken SKILL.md warns once, not
|
|
131
|
+
* once per turn.
|
|
132
|
+
*/
|
|
133
|
+
const reportedFrontmatterErrors = new Set<string>();
|
|
134
|
+
|
|
126
135
|
function parseSkill(
|
|
127
136
|
path: string,
|
|
128
137
|
raw: string,
|
|
@@ -161,7 +170,15 @@ function parseSkill(
|
|
|
161
170
|
if (value && typeof value === "object") {
|
|
162
171
|
parsed = value as Record<string, unknown>;
|
|
163
172
|
}
|
|
164
|
-
} catch {
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// The skill still loads (name falls back to its directory, no
|
|
175
|
+
// description) — which is exactly why the reason must be visible.
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
const key = `${path}\0${msg}`;
|
|
178
|
+
if (!reportedFrontmatterErrors.has(key)) {
|
|
179
|
+
reportedFrontmatterErrors.add(key);
|
|
180
|
+
logWarn("skills", `Unparseable frontmatter in ${path}: ${msg}`);
|
|
181
|
+
}
|
|
165
182
|
parsed = {};
|
|
166
183
|
}
|
|
167
184
|
|
|
@@ -216,7 +233,9 @@ export function saveSkill(input: {
|
|
|
216
233
|
mkdirSync(dir, { recursive: true });
|
|
217
234
|
const path = skillFilePath(input.name);
|
|
218
235
|
const content = serializeSkill(input);
|
|
219
|
-
|
|
236
|
+
// Atomic: a failed replace (ENOSPC) must keep the previous SKILL.md,
|
|
237
|
+
// not leave a truncated one.
|
|
238
|
+
writeFileAtomic.sync(path, content, { encoding: "utf-8", mode: 0o600 });
|
|
220
239
|
return readSkill(input.name)!;
|
|
221
240
|
}
|
|
222
241
|
|
package/src/storage/stickers.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { readFileSync, readdirSync } from "node:fs";
|
|
16
16
|
import { resolve } from "node:path";
|
|
17
|
+
import { logWarn } from "../util/log.js";
|
|
17
18
|
import { dirs } from "../util/paths.js";
|
|
18
19
|
|
|
19
20
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -50,7 +51,15 @@ export function listSavedPacks(): SavedPack[] {
|
|
|
50
51
|
let entries: string[];
|
|
51
52
|
try {
|
|
52
53
|
entries = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
53
|
-
} catch {
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// No stickers dir yet is the normal empty library; anything else
|
|
56
|
+
// (EACCES, ENOTDIR) hides every saved pack.
|
|
57
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
58
|
+
logWarn(
|
|
59
|
+
"stickers",
|
|
60
|
+
`Could not list sticker packs dir=${dir}: ${err instanceof Error ? err.message : String(err)}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
54
63
|
return [];
|
|
55
64
|
}
|
|
56
65
|
const packs: SavedPack[] = [];
|
|
@@ -82,8 +91,13 @@ export function listSavedPacks(): SavedPack[] {
|
|
|
82
91
|
stickers,
|
|
83
92
|
savedAt: typeof raw.savedAt === "string" ? raw.savedAt : "",
|
|
84
93
|
});
|
|
85
|
-
} catch {
|
|
86
|
-
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// Skip the malformed pack file, but name it — it silently drops
|
|
96
|
+
// out of the library otherwise.
|
|
97
|
+
logWarn(
|
|
98
|
+
"stickers",
|
|
99
|
+
`Skipped unreadable sticker pack file=${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
100
|
+
);
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
return packs.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
package/src/storage/triggers.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { randomUUID } from "node:crypto";
|
|
21
21
|
import { dirname, resolve } from "node:path";
|
|
22
|
-
import { log, logError } from "../util/log.js";
|
|
22
|
+
import { log, logError, logWarn } from "../util/log.js";
|
|
23
23
|
import { dirs, files } from "../util/paths.js";
|
|
24
24
|
import { importLegacyJson } from "./legacy-import.js";
|
|
25
25
|
import { inTransaction } from "./db.js";
|
|
@@ -286,8 +286,13 @@ export function deleteTrigger(id: string): boolean {
|
|
|
286
286
|
for (const path of [t.scriptPath, t.logPath]) {
|
|
287
287
|
try {
|
|
288
288
|
rmSync(path, { force: true });
|
|
289
|
-
} catch {
|
|
290
|
-
|
|
289
|
+
} catch (err) {
|
|
290
|
+
// Best effort; `force` already absorbs a missing file, so this is
|
|
291
|
+
// a real fault that leaves an orphan on disk.
|
|
292
|
+
logWarn(
|
|
293
|
+
"triggers",
|
|
294
|
+
`Could not remove trigger file id=${id} path=${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
295
|
+
);
|
|
291
296
|
}
|
|
292
297
|
}
|
|
293
298
|
return true;
|
package/src/storage/turn-meta.ts
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* actually fetch.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { inTransaction } from "./db.js";
|
|
17
|
+
import { dbErrorFields, inTransaction } from "./db.js";
|
|
18
18
|
import * as repo from "./repositories/turn-meta-repo.js";
|
|
19
19
|
import { importLegacyJson } from "./legacy-import.js";
|
|
20
20
|
import { files } from "../util/paths.js";
|
|
21
|
-
import { logError } from "../util/log.js";
|
|
21
|
+
import { logError, logWarn } from "../util/log.js";
|
|
22
22
|
|
|
23
23
|
/** Per-chat cap — matches the /history page ceiling. */
|
|
24
24
|
const MAX_PER_CHAT = 500;
|
|
@@ -83,7 +83,11 @@ export function recordTurnMeta(
|
|
|
83
83
|
repo.prune(chatId, MAX_PER_CHAT);
|
|
84
84
|
});
|
|
85
85
|
} catch (err) {
|
|
86
|
-
logError(
|
|
86
|
+
logError(
|
|
87
|
+
"db",
|
|
88
|
+
`Failed to persist turn meta chat=${chatId} msg=${msgId}${dbErrorFields(err)}`,
|
|
89
|
+
err,
|
|
90
|
+
);
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -96,14 +100,24 @@ export function getTurnMeta<T>(chatId: string, msgId: string): T | null {
|
|
|
96
100
|
} catch (err) {
|
|
97
101
|
// Reads are hydration-only — a storage fault must degrade to
|
|
98
102
|
// "no meta", not break the caller (matching recordTurnMeta).
|
|
99
|
-
logError(
|
|
103
|
+
logError(
|
|
104
|
+
"db",
|
|
105
|
+
`Failed to read turn meta chat=${chatId} msg=${msgId}${dbErrorFields(err)}`,
|
|
106
|
+
err,
|
|
107
|
+
);
|
|
100
108
|
return null;
|
|
101
109
|
}
|
|
102
110
|
if (raw === undefined) return null;
|
|
103
111
|
try {
|
|
104
112
|
return JSON.parse(raw) as T;
|
|
105
|
-
} catch {
|
|
106
|
-
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// Corrupt row — treat as absent rather than throw, but say so: the
|
|
115
|
+
// caller sees "no meta" and can't tell it from a message never tagged.
|
|
116
|
+
logWarn(
|
|
117
|
+
"db",
|
|
118
|
+
`Corrupt turn meta chat=${chatId} msg=${msgId} bytes=${raw.length}: ${err instanceof Error ? err.message : String(err)}`,
|
|
119
|
+
);
|
|
120
|
+
return null;
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
|
|
@@ -113,6 +127,10 @@ export function clearTurnMeta(chatId: string): void {
|
|
|
113
127
|
try {
|
|
114
128
|
repo.removeChat(chatId);
|
|
115
129
|
} catch (err) {
|
|
116
|
-
logError(
|
|
130
|
+
logError(
|
|
131
|
+
"db",
|
|
132
|
+
`Failed to clear turn meta chat=${chatId}${dbErrorFields(err)}`,
|
|
133
|
+
err,
|
|
134
|
+
);
|
|
117
135
|
}
|
|
118
136
|
}
|