@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.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/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
chmod,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
readlink,
|
|
12
|
+
rename,
|
|
13
|
+
rm,
|
|
14
|
+
symlink,
|
|
15
|
+
utimes,
|
|
16
|
+
writeFile,
|
|
17
|
+
} from "node:fs/promises";
|
|
4
18
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
19
|
import { promisify } from "node:util";
|
|
6
20
|
import { gunzip, gzip } from "node:zlib";
|
|
7
21
|
import type { NornRunCheckpoint } from "@vimhead.dev/norn";
|
|
8
22
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
9
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
writeJsonAtomically,
|
|
25
|
+
writeTextAtomically,
|
|
26
|
+
} from "@vimhead.dev/norn-core/atomic-files";
|
|
10
27
|
|
|
11
28
|
const gzipBuffer = promisify(gzip);
|
|
12
29
|
const gunzipBuffer = promisify(gunzip);
|
|
@@ -50,42 +67,62 @@ export class NornRunStore {
|
|
|
50
67
|
|
|
51
68
|
async snapshotCurrent(message: string): Promise<NornRunCheckpoint> {
|
|
52
69
|
const previous = await this.readCheckpointHistory();
|
|
53
|
-
const checkpoint = this.createCheckpoint(
|
|
70
|
+
const checkpoint = this.createCheckpoint(
|
|
71
|
+
message,
|
|
72
|
+
previous.checkpoints.length + 1,
|
|
73
|
+
);
|
|
54
74
|
const checkpoints = [...previous.checkpoints, checkpoint];
|
|
55
75
|
await this.createSnapshot(checkpoint, checkpoints);
|
|
56
76
|
await writeJsonAtomically(this.checkpointsPath, checkpoints);
|
|
57
77
|
try {
|
|
58
78
|
await writeTextAtomically(this.currentRefPath, `${checkpoint.id}\n`);
|
|
59
79
|
} catch (error) {
|
|
60
|
-
if (previous.content === undefined)
|
|
80
|
+
if (previous.content === undefined)
|
|
81
|
+
await rm(this.checkpointsPath, { force: true });
|
|
61
82
|
else await writeTextAtomically(this.checkpointsPath, previous.content);
|
|
62
83
|
throw error;
|
|
63
84
|
}
|
|
64
85
|
return checkpoint;
|
|
65
86
|
}
|
|
66
87
|
|
|
67
|
-
async restoreSnapshot(
|
|
68
|
-
|
|
88
|
+
async restoreSnapshot(
|
|
89
|
+
ref: string,
|
|
90
|
+
prepare: ((stagedRunRoot: string) => Promise<void>) | undefined,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const checkpoint = (await this.listCheckpoints()).find(
|
|
93
|
+
(entry) => entry.id === ref,
|
|
94
|
+
);
|
|
69
95
|
if (!checkpoint) throw new Error(`Unknown active run checkpoint: ${ref}`);
|
|
70
|
-
await this.restoreSnapshotManifest(
|
|
96
|
+
await this.restoreSnapshotManifest(
|
|
97
|
+
await this.readSnapshot(checkpoint.id),
|
|
98
|
+
prepare,
|
|
99
|
+
);
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
async restoreCurrentSnapshot(): Promise<void> {
|
|
74
|
-
await this.restoreSnapshotManifest(
|
|
103
|
+
await this.restoreSnapshotManifest(
|
|
104
|
+
await this.readSnapshot(await this.currentSnapshotRef()),
|
|
105
|
+
undefined,
|
|
106
|
+
);
|
|
75
107
|
}
|
|
76
108
|
|
|
77
109
|
async listCheckpoints(): Promise<NornRunCheckpoint[]> {
|
|
78
110
|
return (await this.readCheckpointHistory()).checkpoints;
|
|
79
111
|
}
|
|
80
112
|
|
|
81
|
-
private async readCheckpointHistory(): Promise<{
|
|
113
|
+
private async readCheckpointHistory(): Promise<{
|
|
114
|
+
readonly checkpoints: NornRunCheckpoint[];
|
|
115
|
+
readonly content?: string;
|
|
116
|
+
}> {
|
|
82
117
|
try {
|
|
83
118
|
const content = await readFile(this.checkpointsPath, "utf8");
|
|
84
119
|
const checkpoints = parseNornRunCheckpoints(JSON.parse(content));
|
|
85
|
-
for (const checkpoint of checkpoints)
|
|
120
|
+
for (const checkpoint of checkpoints)
|
|
121
|
+
this.assertCheckpointPathMatchesId(checkpoint);
|
|
86
122
|
return { checkpoints, content };
|
|
87
123
|
} catch (error) {
|
|
88
|
-
if (isNodeError(error) && error.code === "ENOENT")
|
|
124
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
125
|
+
return { checkpoints: [] };
|
|
89
126
|
throw error;
|
|
90
127
|
}
|
|
91
128
|
}
|
|
@@ -114,15 +151,36 @@ export class NornRunStore {
|
|
|
114
151
|
|
|
115
152
|
private createCheckpoint(message: string, index: number): NornRunCheckpoint {
|
|
116
153
|
const id = `cp_${randomUUID().replaceAll("-", "")}`;
|
|
117
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
id,
|
|
156
|
+
path: this.snapshotRelativePath(id),
|
|
157
|
+
index,
|
|
158
|
+
message,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
};
|
|
118
161
|
}
|
|
119
162
|
|
|
120
|
-
private async createSnapshot(
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
private async createSnapshot(
|
|
164
|
+
checkpoint: NornRunCheckpoint,
|
|
165
|
+
checkpoints: readonly NornRunCheckpoint[],
|
|
166
|
+
): Promise<RunSnapshotManifest> {
|
|
167
|
+
const entries = (await this.snapshotEntries(this.currentRoot)).filter(
|
|
168
|
+
(entry) => entry.path !== CHECKPOINTS_FILE_NAME,
|
|
169
|
+
);
|
|
170
|
+
const historyBytes = Buffer.from(
|
|
171
|
+
`${JSON.stringify(checkpoints, null, 2)}\n`,
|
|
172
|
+
);
|
|
123
173
|
const historyHash = hashBuffer(historyBytes);
|
|
124
174
|
await this.storeObject(historyHash, historyBytes);
|
|
125
|
-
entries.push({
|
|
175
|
+
entries.push({
|
|
176
|
+
path: CHECKPOINTS_FILE_NAME,
|
|
177
|
+
type: "file",
|
|
178
|
+
sha256: historyHash,
|
|
179
|
+
size: historyBytes.length,
|
|
180
|
+
mode: 0o600,
|
|
181
|
+
mtimeMs: Date.now(),
|
|
182
|
+
compression: "gzip",
|
|
183
|
+
});
|
|
126
184
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
127
185
|
const snapshot: RunSnapshotManifest = {
|
|
128
186
|
version: SNAPSHOT_VERSION,
|
|
@@ -143,25 +201,52 @@ export class NornRunStore {
|
|
|
143
201
|
return entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
144
202
|
}
|
|
145
203
|
|
|
146
|
-
private async collectSnapshotEntries(
|
|
204
|
+
private async collectSnapshotEntries(
|
|
205
|
+
absolutePath: string,
|
|
206
|
+
snapshotPath: string,
|
|
207
|
+
entries: RunSnapshotEntry[],
|
|
208
|
+
): Promise<void> {
|
|
147
209
|
const stat = await lstat(absolutePath);
|
|
148
210
|
if (stat.isDirectory()) {
|
|
149
|
-
if (snapshotPath.length > 0)
|
|
211
|
+
if (snapshotPath.length > 0)
|
|
212
|
+
entries.push({
|
|
213
|
+
path: snapshotPath,
|
|
214
|
+
type: "directory",
|
|
215
|
+
mode: stat.mode,
|
|
216
|
+
});
|
|
150
217
|
const children = await readdir(absolutePath, { withFileTypes: true });
|
|
151
|
-
for (const child of children.sort((left, right) =>
|
|
152
|
-
|
|
218
|
+
for (const child of children.sort((left, right) =>
|
|
219
|
+
left.name.localeCompare(right.name),
|
|
220
|
+
)) {
|
|
221
|
+
await this.collectSnapshotEntries(
|
|
222
|
+
join(absolutePath, child.name),
|
|
223
|
+
joinSnapshotPath(snapshotPath, child.name),
|
|
224
|
+
entries,
|
|
225
|
+
);
|
|
153
226
|
}
|
|
154
227
|
return;
|
|
155
228
|
}
|
|
156
229
|
if (stat.isSymbolicLink()) {
|
|
157
|
-
entries.push({
|
|
230
|
+
entries.push({
|
|
231
|
+
path: snapshotPath,
|
|
232
|
+
type: "symlink",
|
|
233
|
+
target: await readlink(absolutePath),
|
|
234
|
+
});
|
|
158
235
|
return;
|
|
159
236
|
}
|
|
160
237
|
if (!stat.isFile()) return;
|
|
161
238
|
const bytes = await readFile(absolutePath);
|
|
162
239
|
const sha256 = hashBuffer(bytes);
|
|
163
240
|
await this.storeObject(sha256, bytes);
|
|
164
|
-
entries.push({
|
|
241
|
+
entries.push({
|
|
242
|
+
path: snapshotPath,
|
|
243
|
+
type: "file",
|
|
244
|
+
sha256,
|
|
245
|
+
size: stat.size,
|
|
246
|
+
mode: stat.mode,
|
|
247
|
+
mtimeMs: stat.mtimeMs,
|
|
248
|
+
compression: "gzip",
|
|
249
|
+
});
|
|
165
250
|
}
|
|
166
251
|
|
|
167
252
|
private async storeObject(sha256: string, bytes: Buffer): Promise<void> {
|
|
@@ -178,7 +263,10 @@ export class NornRunStore {
|
|
|
178
263
|
await rename(tmpPath, path);
|
|
179
264
|
}
|
|
180
265
|
|
|
181
|
-
private async restoreSnapshotManifest(
|
|
266
|
+
private async restoreSnapshotManifest(
|
|
267
|
+
snapshot: RunSnapshotManifest,
|
|
268
|
+
prepare: ((stagedRunRoot: string) => Promise<void>) | undefined,
|
|
269
|
+
): Promise<void> {
|
|
182
270
|
const stagedRunRoot = await mkdtemp(join(this.runRoot, ".restore-"));
|
|
183
271
|
const stagedCurrent = runCurrentRoot(stagedRunRoot);
|
|
184
272
|
const previousCurrent = join(this.runRoot, `.previous-${randomUUID()}`);
|
|
@@ -200,26 +288,38 @@ export class NornRunStore {
|
|
|
200
288
|
await rename(previousCurrent, this.currentRoot);
|
|
201
289
|
throw error;
|
|
202
290
|
}
|
|
203
|
-
await rm(previousCurrent, { recursive: true, force: true }).catch(
|
|
291
|
+
await rm(previousCurrent, { recursive: true, force: true }).catch(
|
|
292
|
+
() => undefined,
|
|
293
|
+
);
|
|
204
294
|
} finally {
|
|
205
|
-
await rm(stagedRunRoot, { recursive: true, force: true }).catch(
|
|
295
|
+
await rm(stagedRunRoot, { recursive: true, force: true }).catch(
|
|
296
|
+
() => undefined,
|
|
297
|
+
);
|
|
206
298
|
}
|
|
207
299
|
}
|
|
208
300
|
|
|
209
|
-
private async materializeSnapshot(
|
|
301
|
+
private async materializeSnapshot(
|
|
302
|
+
snapshot: RunSnapshotManifest,
|
|
303
|
+
destination: string,
|
|
304
|
+
): Promise<void> {
|
|
210
305
|
const entriesByPath = new Map<string, RunSnapshotEntry>();
|
|
211
306
|
for (const entry of snapshot.entries) {
|
|
212
307
|
this.materializedPath(destination, entry.path);
|
|
213
|
-
if (entriesByPath.has(entry.path))
|
|
308
|
+
if (entriesByPath.has(entry.path))
|
|
309
|
+
throw new Error(`Duplicate snapshot path: ${entry.path}`);
|
|
214
310
|
entriesByPath.set(entry.path, entry);
|
|
215
311
|
}
|
|
216
312
|
for (const entry of snapshot.entries) {
|
|
217
313
|
let parent = dirname(entry.path);
|
|
218
314
|
while (parent !== ".") {
|
|
219
|
-
if (entriesByPath.get(parent)?.type !== "directory")
|
|
315
|
+
if (entriesByPath.get(parent)?.type !== "directory")
|
|
316
|
+
throw new Error(`Snapshot parent is not a directory: ${parent}`);
|
|
220
317
|
parent = dirname(parent);
|
|
221
318
|
}
|
|
222
|
-
if (entry.type === "directory")
|
|
319
|
+
if (entry.type === "directory")
|
|
320
|
+
await mkdir(this.materializedPath(destination, entry.path), {
|
|
321
|
+
recursive: true,
|
|
322
|
+
});
|
|
223
323
|
}
|
|
224
324
|
for (const entry of snapshot.entries) {
|
|
225
325
|
if (entry.type === "directory") continue;
|
|
@@ -228,28 +328,44 @@ export class NornRunStore {
|
|
|
228
328
|
await symlink(entry.target, path);
|
|
229
329
|
continue;
|
|
230
330
|
}
|
|
231
|
-
const bytes = await gunzipBuffer(
|
|
331
|
+
const bytes = await gunzipBuffer(
|
|
332
|
+
await readFile(this.objectPath(entry.sha256)),
|
|
333
|
+
);
|
|
232
334
|
const actualSha256 = hashBuffer(bytes);
|
|
233
|
-
if (actualSha256 !== entry.sha256)
|
|
335
|
+
if (actualSha256 !== entry.sha256)
|
|
336
|
+
throw new Error(`CAS object checksum mismatch: ${entry.sha256}`);
|
|
234
337
|
await writeFile(path, bytes);
|
|
235
338
|
await chmod(path, entry.mode);
|
|
236
339
|
await utimes(path, new Date(entry.mtimeMs), new Date(entry.mtimeMs));
|
|
237
340
|
}
|
|
238
341
|
for (const entry of [...snapshot.entries].reverse()) {
|
|
239
|
-
if (entry.type === "directory")
|
|
342
|
+
if (entry.type === "directory")
|
|
343
|
+
await chmod(this.materializedPath(destination, entry.path), entry.mode);
|
|
240
344
|
}
|
|
241
345
|
}
|
|
242
346
|
|
|
243
347
|
private materializedPath(destination: string, snapshotPath: string): string {
|
|
244
|
-
if (
|
|
348
|
+
if (
|
|
349
|
+
snapshotPath.length === 0 ||
|
|
350
|
+
isAbsolute(snapshotPath) ||
|
|
351
|
+
snapshotPath
|
|
352
|
+
.split(/[\\/]/)
|
|
353
|
+
.some((part) => part === ".." || part === "." || part === "")
|
|
354
|
+
)
|
|
355
|
+
throw new Error(`Invalid snapshot path: ${snapshotPath}`);
|
|
245
356
|
const path = resolve(destination, ...snapshotPath.split("/"));
|
|
246
357
|
const pathFromCurrent = relative(destination, path);
|
|
247
|
-
if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`))
|
|
358
|
+
if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`))
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Snapshot path escapes current checkout: ${snapshotPath}`,
|
|
361
|
+
);
|
|
248
362
|
return path;
|
|
249
363
|
}
|
|
250
364
|
|
|
251
365
|
private async readSnapshot(id: string): Promise<RunSnapshotManifest> {
|
|
252
|
-
return parseRunSnapshotManifest(
|
|
366
|
+
return parseRunSnapshotManifest(
|
|
367
|
+
JSON.parse(await readFile(this.snapshotPath(id), "utf8")),
|
|
368
|
+
);
|
|
253
369
|
}
|
|
254
370
|
|
|
255
371
|
private snapshotPath(id: string): string {
|
|
@@ -262,11 +378,19 @@ export class NornRunStore {
|
|
|
262
378
|
|
|
263
379
|
private assertCheckpointPathMatchesId(checkpoint: NornRunCheckpoint): void {
|
|
264
380
|
const expectedPath = this.snapshotRelativePath(checkpoint.id);
|
|
265
|
-
if (checkpoint.path !== expectedPath)
|
|
381
|
+
if (checkpoint.path !== expectedPath)
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Run checkpoint path does not match id: ${checkpoint.id}`,
|
|
384
|
+
);
|
|
266
385
|
}
|
|
267
386
|
|
|
268
387
|
private objectPath(sha256: string): string {
|
|
269
|
-
return join(
|
|
388
|
+
return join(
|
|
389
|
+
this.objectsRoot,
|
|
390
|
+
sha256.slice(0, 2),
|
|
391
|
+
sha256.slice(2, 4),
|
|
392
|
+
`${sha256}.gz`,
|
|
393
|
+
);
|
|
270
394
|
}
|
|
271
395
|
}
|
|
272
396
|
|
|
@@ -282,8 +406,20 @@ type RunSnapshotManifest = {
|
|
|
282
406
|
|
|
283
407
|
type RunSnapshotEntry =
|
|
284
408
|
| { readonly path: string; readonly type: "directory"; readonly mode: number }
|
|
285
|
-
| {
|
|
286
|
-
|
|
409
|
+
| {
|
|
410
|
+
readonly path: string;
|
|
411
|
+
readonly type: "file";
|
|
412
|
+
readonly sha256: string;
|
|
413
|
+
readonly size: number;
|
|
414
|
+
readonly mode: number;
|
|
415
|
+
readonly mtimeMs: number;
|
|
416
|
+
readonly compression: "gzip";
|
|
417
|
+
}
|
|
418
|
+
| {
|
|
419
|
+
readonly path: string;
|
|
420
|
+
readonly type: "symlink";
|
|
421
|
+
readonly target: string;
|
|
422
|
+
};
|
|
287
423
|
|
|
288
424
|
function hashBuffer(bytes: Buffer): string {
|
|
289
425
|
return createHash("sha256").update(bytes).digest("hex");
|
|
@@ -294,14 +430,27 @@ function joinSnapshotPath(parent: string, child: string): string {
|
|
|
294
430
|
}
|
|
295
431
|
|
|
296
432
|
function parseRunSnapshotManifest(value: unknown): RunSnapshotManifest {
|
|
297
|
-
if (!value || typeof value !== "object")
|
|
433
|
+
if (!value || typeof value !== "object")
|
|
434
|
+
throw new Error("Invalid run snapshot manifest");
|
|
298
435
|
const snapshot = value as Partial<RunSnapshotManifest>;
|
|
299
|
-
if (snapshot.version !== SNAPSHOT_VERSION)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (typeof snapshot.
|
|
304
|
-
|
|
436
|
+
if (snapshot.version !== SNAPSHOT_VERSION)
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Unsupported run snapshot version: ${String(snapshot.version)}`,
|
|
439
|
+
);
|
|
440
|
+
if (typeof snapshot.id !== "string" || snapshot.id.length === 0)
|
|
441
|
+
throw new Error("Invalid run snapshot id");
|
|
442
|
+
if (
|
|
443
|
+
typeof snapshot.index !== "number" ||
|
|
444
|
+
!Number.isInteger(snapshot.index) ||
|
|
445
|
+
snapshot.index < 1
|
|
446
|
+
)
|
|
447
|
+
throw new Error("Invalid run snapshot index");
|
|
448
|
+
if (typeof snapshot.message !== "string")
|
|
449
|
+
throw new Error("Invalid run snapshot message");
|
|
450
|
+
if (typeof snapshot.createdAt !== "string")
|
|
451
|
+
throw new Error("Invalid run snapshot timestamp");
|
|
452
|
+
if (!Array.isArray(snapshot.entries))
|
|
453
|
+
throw new Error("Invalid run snapshot entries");
|
|
305
454
|
return snapshot as RunSnapshotManifest;
|
|
306
455
|
}
|
|
307
456
|
|
|
@@ -311,13 +460,27 @@ function parseNornRunCheckpoints(value: unknown): NornRunCheckpoint[] {
|
|
|
311
460
|
}
|
|
312
461
|
|
|
313
462
|
function parseNornRunCheckpoint(value: unknown): NornRunCheckpoint {
|
|
314
|
-
if (!value || typeof value !== "object")
|
|
463
|
+
if (!value || typeof value !== "object")
|
|
464
|
+
throw new Error("Invalid run checkpoint");
|
|
315
465
|
const checkpoint = value as Partial<NornRunCheckpoint>;
|
|
316
|
-
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0)
|
|
317
|
-
|
|
318
|
-
if (checkpoint.path
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
|
|
466
|
+
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0)
|
|
467
|
+
throw new Error("Invalid run checkpoint id");
|
|
468
|
+
if (typeof checkpoint.path !== "string" || checkpoint.path.length === 0)
|
|
469
|
+
throw new Error("Invalid run checkpoint path");
|
|
470
|
+
if (
|
|
471
|
+
checkpoint.path.split(/[\\/]/).includes("..") ||
|
|
472
|
+
resolve(checkpoint.path) === checkpoint.path
|
|
473
|
+
)
|
|
474
|
+
throw new Error(`Invalid run checkpoint path: ${checkpoint.path}`);
|
|
475
|
+
if (
|
|
476
|
+
typeof checkpoint.index !== "number" ||
|
|
477
|
+
!Number.isInteger(checkpoint.index) ||
|
|
478
|
+
checkpoint.index < 1
|
|
479
|
+
)
|
|
480
|
+
throw new Error("Invalid run checkpoint index");
|
|
481
|
+
if (typeof checkpoint.message !== "string")
|
|
482
|
+
throw new Error("Invalid run checkpoint message");
|
|
483
|
+
if (typeof checkpoint.createdAt !== "string")
|
|
484
|
+
throw new Error("Invalid run checkpoint timestamp");
|
|
322
485
|
return checkpoint as NornRunCheckpoint;
|
|
323
486
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { NornAgentUsage } from "@vimhead.dev/norn";
|
|
2
2
|
|
|
3
|
-
export function agentUsageFromValue(
|
|
3
|
+
export function agentUsageFromValue(
|
|
4
|
+
value: unknown,
|
|
5
|
+
): NornAgentUsage | undefined {
|
|
4
6
|
if (!value || typeof value !== "object") return undefined;
|
|
5
7
|
const usage = value as Record<string, unknown>;
|
|
6
8
|
const input = numberField(usage.input);
|
|
@@ -8,8 +10,12 @@ export function agentUsageFromValue(value: unknown): NornAgentUsage | undefined
|
|
|
8
10
|
const cacheRead = numberField(usage.cacheRead);
|
|
9
11
|
const cacheWrite = numberField(usage.cacheWrite);
|
|
10
12
|
const reasoning = optionalNumberField(usage.reasoning);
|
|
11
|
-
const totalTokens =
|
|
12
|
-
|
|
13
|
+
const totalTokens =
|
|
14
|
+
numberField(usage.totalTokens) || input + output + cacheRead + cacheWrite;
|
|
15
|
+
const costValue =
|
|
16
|
+
usage.cost && typeof usage.cost === "object"
|
|
17
|
+
? (usage.cost as Record<string, unknown>)
|
|
18
|
+
: {};
|
|
13
19
|
return {
|
|
14
20
|
input,
|
|
15
21
|
output,
|
|
@@ -27,12 +33,20 @@ export function agentUsageFromValue(value: unknown): NornAgentUsage | undefined
|
|
|
27
33
|
};
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
export function totalAgentUsage(
|
|
36
|
+
export function totalAgentUsage(
|
|
37
|
+
usages: readonly NornAgentUsage[],
|
|
38
|
+
): NornAgentUsage {
|
|
31
39
|
return usages.reduce(addAgentUsage, emptyAgentUsage());
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
export function addAgentUsage(
|
|
35
|
-
|
|
42
|
+
export function addAgentUsage(
|
|
43
|
+
left: NornAgentUsage,
|
|
44
|
+
right: NornAgentUsage,
|
|
45
|
+
): NornAgentUsage {
|
|
46
|
+
const reasoning =
|
|
47
|
+
left.reasoning === undefined && right.reasoning === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: (left.reasoning ?? 0) + (right.reasoning ?? 0);
|
|
36
50
|
return {
|
|
37
51
|
input: left.input + right.input,
|
|
38
52
|
output: left.output + right.output,
|
|
@@ -66,5 +80,7 @@ function numberField(value: unknown): number {
|
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
function optionalNumberField(value: unknown): number | undefined {
|
|
69
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
83
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
84
|
+
? value
|
|
85
|
+
: undefined;
|
|
70
86
|
}
|
|
@@ -2,7 +2,9 @@ import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
4
|
|
|
5
|
-
export async function prepareRunWorkerDirectory(
|
|
5
|
+
export async function prepareRunWorkerDirectory(
|
|
6
|
+
runRoot: string,
|
|
7
|
+
): Promise<string> {
|
|
6
8
|
const directory = join(runRoot, "worker");
|
|
7
9
|
try {
|
|
8
10
|
await mkdir(directory, { mode: 0o555 });
|
|
@@ -10,8 +12,12 @@ export async function prepareRunWorkerDirectory(runRoot: string): Promise<string
|
|
|
10
12
|
if (!isNodeError(error) || error.code !== "EEXIST") throw error;
|
|
11
13
|
}
|
|
12
14
|
const metadata = await lstat(directory);
|
|
13
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
14
|
-
|
|
15
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Run worker cwd must be a directory, not a symlink: ${directory}`,
|
|
18
|
+
);
|
|
19
|
+
if ((await readdir(directory)).length !== 0)
|
|
20
|
+
throw new Error(`Run worker cwd must be empty: ${directory}`);
|
|
15
21
|
await chmod(directory, 0o555);
|
|
16
22
|
return directory;
|
|
17
23
|
}
|