atom-agent 1.1.0 → 1.3.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/CHANGELOG.md +106 -0
- package/README.md +18 -8
- package/atom.example.json +11 -0
- package/dist/App.js +1637 -255
- package/dist/adapters.js +112 -21
- package/dist/agent/gates.js +14 -1
- package/dist/agent/goal-evaluator.js +69 -0
- package/dist/agent/loop-guard.js +11 -13
- package/dist/agent/loop.js +716 -132
- package/dist/agent/normalize.js +9 -2
- package/dist/cli.js +25 -3
- package/dist/compact.js +169 -17
- package/dist/config.js +43 -7
- package/dist/context-manager.js +16 -198
- package/dist/context-windows.js +4 -2
- package/dist/env-block.js +46 -8
- package/dist/extension-commands.js +196 -0
- package/dist/extension-ui.js +153 -0
- package/dist/extensions.js +1571 -0
- package/dist/goal.js +583 -0
- package/dist/project-trust.js +96 -0
- package/dist/providers.js +6 -6
- package/dist/scheduler.js +159 -41
- package/dist/session.js +23 -5
- package/dist/sessions.js +543 -0
- package/dist/system.js +89 -13
- package/dist/telemetry-dashboard.js +28 -0
- package/dist/telemetry.js +39 -0
- package/dist/tools/compaction-hooks.js +165 -0
- package/dist/tools/custom.js +189 -0
- package/dist/tools/dir-cache.js +7 -0
- package/dist/tools/filesystem.js +3 -2
- package/dist/tools/intercept.js +145 -0
- package/dist/tools/overrides.js +105 -0
- package/dist/tools/provider-hooks.js +224 -0
- package/dist/tools/registry.js +247 -17
- package/dist/tools/ripgrep.js +256 -0
- package/dist/tools/search.js +119 -58
- package/dist/tools/shared.js +39 -0
- package/dist/tools/shell.js +7 -5
- package/dist/tools/web.js +6 -6
- package/dist/tools.js +45 -0
- package/dist/ui/diff-view.js +7 -2
- package/dist/ui/live-host.js +18 -0
- package/dist/ui/live-tail.js +9 -3
- package/dist/ui/markdown.js +26 -2
- package/dist/ui/palette.js +3 -1
- package/dist/ui/side-by-side.js +2 -2
- package/dist/ui/status-bar.js +80 -5
- package/dist/ui/status-host.js +22 -0
- package/dist/ui/stream-store.js +48 -0
- package/dist/ui/tool-inspector.js +7 -1
- package/dist/ui/transcript.js +92 -38
- package/dist/zen.js +370 -87
- package/documentation/architecture.md +114 -0
- package/documentation/cli.md +82 -0
- package/documentation/compaction.md +50 -0
- package/documentation/configuration.md +111 -0
- package/documentation/development.md +62 -0
- package/documentation/extensions.md +160 -0
- package/documentation/getting-started.md +63 -0
- package/documentation/goals.md +41 -0
- package/documentation/index.md +41 -0
- package/documentation/observability.md +70 -0
- package/documentation/permissions.md +66 -0
- package/documentation/providers.md +78 -0
- package/documentation/sessions.md +92 -0
- package/documentation/skills.md +57 -0
- package/documentation/tools.md +94 -0
- package/documentation/troubleshooting.md +54 -0
- package/examples/extensions/01-audit-gate.js +24 -0
- package/examples/extensions/02-notes-tool.js +32 -0
- package/examples/extensions/03-custom-command.js +32 -0
- package/package.json +6 -2
package/dist/providers.js
CHANGED
|
@@ -68,7 +68,7 @@ export const PROVIDERS = [
|
|
|
68
68
|
"minimax-m2.7",
|
|
69
69
|
"big-pickle",
|
|
70
70
|
],
|
|
71
|
-
notes: "OpenAI-compatible chat/completions. reasoning_effort
|
|
71
|
+
notes: "OpenAI-compatible chat/completions. /effort sends reasoning_effort (Auto omits it).",
|
|
72
72
|
cache: {
|
|
73
73
|
explicitBreakpoints: false,
|
|
74
74
|
implicitPrefix: true,
|
|
@@ -90,7 +90,7 @@ export const PROVIDERS = [
|
|
|
90
90
|
"gpt-5.6-terra",
|
|
91
91
|
"gpt-5.6-luna",
|
|
92
92
|
],
|
|
93
|
-
notes: "OpenAI-compatible chat/completions. reasoning_effort
|
|
93
|
+
notes: "OpenAI-compatible chat/completions. /effort sends reasoning_effort (Auto omits it).",
|
|
94
94
|
cache: {
|
|
95
95
|
explicitBreakpoints: false,
|
|
96
96
|
implicitPrefix: true,
|
|
@@ -112,7 +112,7 @@ export const PROVIDERS = [
|
|
|
112
112
|
"claude-3-5-sonnet-20241022",
|
|
113
113
|
"claude-3-5-haiku-20241022",
|
|
114
114
|
],
|
|
115
|
-
notes: "Messages API with tool_use blocks. max_tokens 4096.",
|
|
115
|
+
notes: "Messages API with tool_use blocks. max_tokens 4096. /effort maps to the thinking budget (Auto omits it).",
|
|
116
116
|
cache: {
|
|
117
117
|
explicitBreakpoints: true,
|
|
118
118
|
implicitPrefix: true,
|
|
@@ -134,7 +134,7 @@ export const PROVIDERS = [
|
|
|
134
134
|
"deepseek-v4-flash",
|
|
135
135
|
"deepseek-v4-pro",
|
|
136
136
|
],
|
|
137
|
-
notes: "OpenAI-compatible (no /v1 prefix). reasoning_effort
|
|
137
|
+
notes: "OpenAI-compatible (no /v1 prefix). /effort sends reasoning_effort (Auto omits it).",
|
|
138
138
|
cache: {
|
|
139
139
|
explicitBreakpoints: false,
|
|
140
140
|
implicitPrefix: true,
|
|
@@ -156,7 +156,7 @@ export const PROVIDERS = [
|
|
|
156
156
|
"mistral-small-latest",
|
|
157
157
|
"open-mistral-nemo",
|
|
158
158
|
],
|
|
159
|
-
notes: "OpenAI-compatible chat/completions. reasoning_effort
|
|
159
|
+
notes: "OpenAI-compatible chat/completions. /effort sends reasoning_effort (Auto omits it).",
|
|
160
160
|
cache: {
|
|
161
161
|
explicitBreakpoints: false,
|
|
162
162
|
implicitPrefix: false,
|
|
@@ -179,7 +179,7 @@ export const PROVIDERS = [
|
|
|
179
179
|
"gemini-2.0-flash",
|
|
180
180
|
"gemini-1.5-flash",
|
|
181
181
|
],
|
|
182
|
-
notes: "streamGenerateContent SSE; :generateContent fallback.",
|
|
182
|
+
notes: "streamGenerateContent SSE; :generateContent fallback. /effort maps to thinkingLevel (Auto omits it; Max rides high).",
|
|
183
183
|
cache: {
|
|
184
184
|
explicitBreakpoints: false,
|
|
185
185
|
implicitPrefix: true,
|
package/dist/scheduler.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
// Effect-aware tool scheduler: data-driven parallelism
|
|
2
|
-
// ing every tool.
|
|
1
|
+
// Effect-aware tool scheduler: data-driven parallelism, parallel by default.
|
|
3
2
|
//
|
|
4
|
-
// The
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// The contract: batchable calls run concurrently, conflicting calls stay
|
|
4
|
+
// serialized, results commit in original call order, cancel stops between
|
|
5
|
+
// batches, approval happens per call in order before execution. This module
|
|
6
|
+
// only PLANS batches; execution (agent/loop.ts runLoopWithChat) is untouched.
|
|
7
|
+
//
|
|
8
|
+
// Parallel-by-default posture: side-effect-free reads batch unconditionally
|
|
9
|
+
// (same target included — two reads can never race each other); only real
|
|
10
|
+
// conflicts serialize: same-file write order, read/write on the same file,
|
|
11
|
+
// invisible footprints (bash), shared ambient state (todos), interactive
|
|
12
|
+
// prompts, and calls the planner cannot see (unknown/malformed/invalid).
|
|
8
13
|
//
|
|
9
14
|
// How it reasons (per tool, from the TOOL_EFFECTS table — the single
|
|
10
15
|
// coupling point; the algorithm below has no per-tool branches):
|
|
@@ -16,24 +21,35 @@
|
|
|
16
21
|
// become inline-error singletons downstream, exactly as before).
|
|
17
22
|
// - interactive (ask_question) or exclusive (shared ambient state the effect
|
|
18
23
|
// model cannot see: todowrite/todo_update/todo_get) → serial singleton.
|
|
19
|
-
// -
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
24
|
+
// - process SPAWN or network WRITE (bash: unbounded footprint) → serial
|
|
25
|
+
// singleton, splitting the block globally.
|
|
26
|
+
// - filesystem writes (write/edit) batch on disjoint canonical file keys
|
|
27
|
+
// (see canonicalFileKey): same-file mutations never share a batch, so they
|
|
28
|
+
// stay strictly ordered in program order; different files run concurrently.
|
|
29
|
+
// An unresolvable target stays serial (never batch what you cannot see).
|
|
30
|
+
// - reads (filesystem or network) always batch: a pure read has no observable
|
|
31
|
+
// footprint, so even the same tool + same target runs concurrently. A path
|
|
32
|
+
// read against an open write to the same canonical file still stays ordered
|
|
33
|
+
// (read-after-write): it splits the batch — and a write conflicts with any
|
|
34
|
+
// open read on its key (write-after-read stays ordered). Directory-scoped
|
|
35
|
+
// scans (grep/glob) vs concurrent writes to unscanned-listed files are out
|
|
36
|
+
// of scope — same exposure as an editor saving mid-scan; only
|
|
37
|
+
// same-canonical-path pairs are ordered.
|
|
38
|
+
// - deterministic is informational only (same output for same args); it no
|
|
39
|
+
// longer drives batching — not even same-task bash_output polls serialize,
|
|
40
|
+
// since concurrent polls are side-effect-free reads whose results commit in
|
|
41
|
+
// call order anyway.
|
|
31
42
|
//
|
|
32
43
|
// Pure module except the shared arg validators (same imports zen.ts already
|
|
33
|
-
// carries — no new coupling class)
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
44
|
+
// carries — no new coupling class) plus best-effort path canonicalization
|
|
45
|
+
// (node:fs/node:path only, so the architecture boundary is unchanged:
|
|
46
|
+
// scheduler still reasons from metadata, never from tool branches).
|
|
47
|
+
// Covered by tests/scheduler.test.ts; the end-to-end ordering/cancel
|
|
48
|
+
// behavior stays pinned by tests/parallel-calls.test.ts and
|
|
49
|
+
// tests/parallel-writes.test.ts.
|
|
50
|
+
import * as fs from "node:fs";
|
|
51
|
+
import * as path from "node:path";
|
|
52
|
+
import { isCustomTool, toolExecutionMode, toolNames, validateToolArgs } from "./tools.js";
|
|
37
53
|
const str = (v) => (typeof v === "string" ? v : "");
|
|
38
54
|
const targetOf = (key) => (args) => {
|
|
39
55
|
const t = str(args[key]);
|
|
@@ -83,8 +99,8 @@ export const TOOL_EFFECTS = {
|
|
|
83
99
|
process: "none",
|
|
84
100
|
interactive: false,
|
|
85
101
|
exclusive: false,
|
|
86
|
-
// Live search results vary call to call; same-query
|
|
87
|
-
//
|
|
102
|
+
// Live search results vary call to call; concurrent same-query searches
|
|
103
|
+
// are merely redundant, never incorrect — reads always batch.
|
|
88
104
|
deterministic: false,
|
|
89
105
|
target: targetOf("query"),
|
|
90
106
|
},
|
|
@@ -95,8 +111,8 @@ export const TOOL_EFFECTS = {
|
|
|
95
111
|
process: "none",
|
|
96
112
|
interactive: false,
|
|
97
113
|
exclusive: false,
|
|
98
|
-
// A running task's output grows between polls — same-task
|
|
99
|
-
//
|
|
114
|
+
// A running task's output grows between polls — concurrent same-task
|
|
115
|
+
// polls are side-effect-free reads whose results commit in call order.
|
|
100
116
|
deterministic: false,
|
|
101
117
|
target: targetOf("taskId"),
|
|
102
118
|
},
|
|
@@ -166,22 +182,93 @@ export const TOOL_EFFECTS = {
|
|
|
166
182
|
target: () => null,
|
|
167
183
|
},
|
|
168
184
|
};
|
|
185
|
+
// Canonical per-file mutation key: the identity two mutation calls compare
|
|
186
|
+
// before sharing a batch. Lexical resolve against the process cwd (the same
|
|
187
|
+
// base the executors default to), then best-effort symlink resolution, then
|
|
188
|
+
// case-folding on case-insensitive filesystems. Null when the target is
|
|
189
|
+
// missing, empty, or unresolvable — unknown footprints stay serial. Sync and
|
|
190
|
+
// best-effort by design: a handful of calls per tool block, and a miss only
|
|
191
|
+
// costs parallelism, never correctness.
|
|
192
|
+
export function canonicalFileKey(rawPath, cwd = process.cwd()) {
|
|
193
|
+
if (typeof rawPath !== "string" || rawPath.length === 0)
|
|
194
|
+
return null;
|
|
195
|
+
if (rawPath.includes("\0"))
|
|
196
|
+
return null;
|
|
197
|
+
let abs;
|
|
198
|
+
try {
|
|
199
|
+
abs = path.resolve(cwd, rawPath);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
abs = fs.realpathSync(abs);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Fresh write target or unreadable link: the lexical path stands.
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
abs = path.normalize(abs);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
217
|
+
abs = abs.toLowerCase();
|
|
218
|
+
}
|
|
219
|
+
return abs;
|
|
220
|
+
}
|
|
169
221
|
// Partition one assistant message's tool_calls into commit batches,
|
|
170
|
-
// preserving program order: consecutive batchable calls
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
222
|
+
// preserving program order: consecutive batchable calls form one batch; any
|
|
223
|
+
// serial-only call closes the batch and runs as a strict serial singleton.
|
|
224
|
+
// Filesystem writes join a batch only on a disjoint canonical file key
|
|
225
|
+
// (same-file mutations split into sequential batches, never concurrent); a
|
|
226
|
+
// path read conflicting with an open write — or a write conflicting with any
|
|
227
|
+
// open member — on the same canonical key also splits, so per-file program
|
|
228
|
+
// order always holds. Reads never split on each other. A later batch never
|
|
229
|
+
// moves ahead of an earlier serial call, and batches never span the block
|
|
230
|
+
// boundary.
|
|
176
231
|
export function planBatches(calls) {
|
|
177
232
|
const batches = [];
|
|
178
233
|
let open = [];
|
|
179
|
-
|
|
234
|
+
// Canonical file keys of the open batch ("read" and/or "write" per key).
|
|
235
|
+
// This is the ONLY cross-call ordering state: same-file read/write pairs
|
|
236
|
+
// stay in program order; everything else batches freely.
|
|
237
|
+
const openFiles = new Map();
|
|
238
|
+
// Lenient parse for classification/fallback only (mirrors the loop: {}
|
|
239
|
+
// when the JSON is malformed — the error result commits downstream).
|
|
240
|
+
const lenientParse = (call) => {
|
|
241
|
+
try {
|
|
242
|
+
const raw = call?.function?.arguments ?? "{}";
|
|
243
|
+
const v = JSON.parse(typeof raw === "string" ? raw : "{}");
|
|
244
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// Extension sequential hint (ticket 06): a tool declaring sequential
|
|
251
|
+
// execution forces its whole sibling batch one-at-a-time (Pi-style). This
|
|
252
|
+
// only ever ADDS serialization — same-file order, bash/todo/prompt
|
|
253
|
+
// isolation, ordered commits, and the approval pre-pass are untouched
|
|
254
|
+
// (singletons satisfy every one of them; the loop below handles them
|
|
255
|
+
// exactly as before, one call per batch).
|
|
256
|
+
if (calls.some((call) => {
|
|
257
|
+
const name = typeof call?.function?.name === "string" ? call.function.name : "";
|
|
258
|
+
try {
|
|
259
|
+
return toolExecutionMode(name) === "sequential";
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
})) {
|
|
265
|
+
return calls.map((call) => [{ call, parsed: lenientParse(call), parallelKey: null }]);
|
|
266
|
+
}
|
|
180
267
|
const flush = () => {
|
|
181
268
|
if (open.length > 0) {
|
|
182
269
|
batches.push(open);
|
|
183
270
|
open = [];
|
|
184
|
-
|
|
271
|
+
openFiles.clear();
|
|
185
272
|
}
|
|
186
273
|
};
|
|
187
274
|
const singleton = (call, parsed) => {
|
|
@@ -201,6 +288,15 @@ export function planBatches(calls) {
|
|
|
201
288
|
malformed = true;
|
|
202
289
|
}
|
|
203
290
|
const name = typeof call?.function?.name === "string" ? call.function.name : "(unknown)";
|
|
291
|
+
// Extension tools carry no scheduler effect metadata: their footprint is
|
|
292
|
+
// invisible, so they always run as serial singletons (fail safe, exactly
|
|
293
|
+
// like the old allowlist-miss). Batch planning is never corrupted by
|
|
294
|
+
// what it cannot see; validation still runs in the loop, where failures
|
|
295
|
+
// become inline-error results.
|
|
296
|
+
if (isCustomTool(name)) {
|
|
297
|
+
singleton(call, parsed);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
204
300
|
// Missing metadata fails safe to serial (never batch the unknown).
|
|
205
301
|
const meta = TOOL_EFFECTS[name];
|
|
206
302
|
if (malformed || !meta || !toolNames().includes(name)) {
|
|
@@ -212,17 +308,31 @@ export function planBatches(calls) {
|
|
|
212
308
|
singleton(call, parsed);
|
|
213
309
|
continue;
|
|
214
310
|
}
|
|
215
|
-
// Interactive, ambient-state,
|
|
311
|
+
// Interactive, ambient-state, spawning, or network-writing calls
|
|
312
|
+
// serialize (bash has no statically visible footprint: global conflict).
|
|
216
313
|
if (meta.interactive ||
|
|
217
314
|
meta.exclusive ||
|
|
218
|
-
meta.filesystem === "write" ||
|
|
219
315
|
meta.network === "write" ||
|
|
220
316
|
meta.process !== "none") {
|
|
221
317
|
singleton(call, parsed);
|
|
222
318
|
continue;
|
|
223
319
|
}
|
|
224
|
-
//
|
|
225
|
-
//
|
|
320
|
+
// Filesystem writes batch on disjoint canonical file keys: the
|
|
321
|
+
// per-file mutation queue. Same-file mutations split into sequential
|
|
322
|
+
// batches (never interleave); disjoint files run concurrently.
|
|
323
|
+
if (meta.filesystem === "write") {
|
|
324
|
+
const fileKey = canonicalFileKey(parsed["path"]);
|
|
325
|
+
if (!fileKey || openFiles.has(fileKey)) {
|
|
326
|
+
singleton(call, parsed);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
openFiles.set(fileKey, "write");
|
|
330
|
+
open.push({ call, parsed, parallelKey: `${name} ${fileKey}` });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Reads always batch (parallel by default): a pure read has no
|
|
334
|
+
// observable footprint, so even the same tool + same target runs
|
|
335
|
+
// concurrently. Empty target = unknown footprint = serial.
|
|
226
336
|
let target = null;
|
|
227
337
|
try {
|
|
228
338
|
target = meta.target(parsed);
|
|
@@ -235,11 +345,19 @@ export function planBatches(calls) {
|
|
|
235
345
|
continue;
|
|
236
346
|
}
|
|
237
347
|
const key = `${name} ${target}`;
|
|
238
|
-
|
|
239
|
-
|
|
348
|
+
// A path read against an open write to the same canonical file stays
|
|
349
|
+
// ordered (read-after-write): split the batch.
|
|
350
|
+
if (name === "read") {
|
|
351
|
+
const fileKey = canonicalFileKey(target);
|
|
352
|
+
if (fileKey && openFiles.get(fileKey) === "write") {
|
|
353
|
+
singleton(call, parsed);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (fileKey && !openFiles.has(fileKey))
|
|
357
|
+
openFiles.set(fileKey, "read");
|
|
358
|
+
open.push({ call, parsed, parallelKey: key });
|
|
240
359
|
continue;
|
|
241
360
|
}
|
|
242
|
-
keys.add(key);
|
|
243
361
|
open.push({ call, parsed, parallelKey: key });
|
|
244
362
|
}
|
|
245
363
|
flush();
|
package/dist/session.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
// cannot corrupt or clobber the last good save.
|
|
7
7
|
//
|
|
8
8
|
// Shape: {version:1, savedAt, provider, model, effort, mode, usageTotals,
|
|
9
|
+
// goal (ticket 07: the live session goal plus cumulative stats, or null),
|
|
9
10
|
// history (full API history incl. system + tool pairs), turns (display
|
|
10
11
|
// transcript)}. Writes are atomic (temp file + rename) to survive kills
|
|
11
12
|
// mid-write. Loads never throw: missing -> "missing", anything malformed ->
|
|
12
|
-
// "corrupt" (caller shows a one-line notice and starts fresh).
|
|
13
|
+
// "corrupt" (caller shows a one-line notice and starts fresh). A missing or
|
|
14
|
+
// corrupt goal degrades to no-goal (null) WITHOUT failing the load — the
|
|
15
|
+
// conversation still restores.
|
|
13
16
|
//
|
|
14
17
|
// Privacy: the file can contain pasted secrets if the user typed them as
|
|
15
18
|
// chat. Never print its contents; never commit it (it lives under ~/.atom,
|
|
@@ -19,6 +22,7 @@ import * as path from "node:path";
|
|
|
19
22
|
import { atomDir, getStoredBaseURL, loadAuth, resolveApiKey } from "./auth.js";
|
|
20
23
|
import { chatEndpointFor, isProviderId, openaiCompatibleChatEndpoint, } from "./providers.js";
|
|
21
24
|
import { EFFORT_OPTIONS, } from "./zen.js";
|
|
25
|
+
import { restoreGoalFromPersist, serializeGoalForPersist, } from "./goal.js";
|
|
22
26
|
export const SESSION_VERSION = 1;
|
|
23
27
|
export const SESSION_FILENAME = "session.json";
|
|
24
28
|
export function sessionFilePath(home) {
|
|
@@ -74,6 +78,9 @@ export function saveSession(snapshot, home) {
|
|
|
74
78
|
effort: snapshot.effort,
|
|
75
79
|
mode: snapshot.mode,
|
|
76
80
|
usageTotals: snapshot.usageTotals,
|
|
81
|
+
// Piggyback: the live goal rides every completed-turn save (no new save
|
|
82
|
+
// cadence — compaction and clean exit flow through here too).
|
|
83
|
+
goal: serializeGoalForPersist(snapshot.goal ?? null),
|
|
77
84
|
history: snapshot.history.map((m) => ({ ...m })),
|
|
78
85
|
turns: snapshot.turns.map((t) => ({ ...t })),
|
|
79
86
|
};
|
|
@@ -210,9 +217,16 @@ function validateSession(data) {
|
|
|
210
217
|
const model = data["model"];
|
|
211
218
|
if (!isNonEmptyString(model))
|
|
212
219
|
return null;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
// "default" is the pre-auto name for the same level: old saves map to
|
|
221
|
+
// "auto" instead of failing the load.
|
|
222
|
+
const rawEffort = data["effort"];
|
|
223
|
+
const effort = rawEffort === "default"
|
|
224
|
+
? "auto"
|
|
225
|
+
: typeof rawEffort === "string" &&
|
|
226
|
+
EFFORT_OPTIONS.includes(rawEffort)
|
|
227
|
+
? rawEffort
|
|
228
|
+
: null;
|
|
229
|
+
if (effort === null) {
|
|
216
230
|
return null;
|
|
217
231
|
}
|
|
218
232
|
const mode = data["mode"];
|
|
@@ -241,9 +255,13 @@ function validateSession(data) {
|
|
|
241
255
|
savedAt,
|
|
242
256
|
provider,
|
|
243
257
|
model,
|
|
244
|
-
effort
|
|
258
|
+
effort,
|
|
245
259
|
mode,
|
|
246
260
|
usageTotals: validateUsageTotals(data["usageTotals"]),
|
|
261
|
+
// Tolerant: a trashed goal degrades to no-goal (null) without failing
|
|
262
|
+
// the load — the conversation still restores. Re-serialized so the
|
|
263
|
+
// loaded record always carries concrete stats.
|
|
264
|
+
goal: serializeGoalForPersist(restoreGoalFromPersist(data["goal"])),
|
|
247
265
|
history: history,
|
|
248
266
|
turns: turns,
|
|
249
267
|
};
|