@vimhead.dev/norn-cli 0.1.0-tip.36679386408.1 → 0.1.0-tip.37307659560.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 +3 -2
- package/assets/docs/agents.md +7 -2
- package/assets/docs/cli.md +25 -0
- package/assets/docs/providers.md +3 -3
- package/assets/docs/recovery.md +4 -0
- package/assets/examples/agent-then-analysis/README.md +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +2 -0
- package/assets/examples/coordinating-multiple-agents/README.md +2 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +1 -0
- package/assets/examples/getting-started/README.md +3 -3
- package/assets/examples/getting-started/plugin.ts +1 -0
- package/assets/examples/shared-state/README.md +1 -1
- package/assets/examples/shared-state/plugin.ts +1 -0
- package/assets/examples/worktree-development-loop/README.md +1 -1
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +1 -0
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -0
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +1 -0
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +37 -0
- package/assets/packages/cli/src/client.ts +16 -0
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agents.ts +42 -2
- package/assets/packages/cli/src/internal/execution-context.ts +0 -2
- package/assets/packages/cli/src/internal/run-lease.ts +24 -2
- package/assets/packages/cli/src/internal/run-pruning.ts +169 -0
- package/assets/packages/cli/src/internal/run-store.ts +76 -4
- package/assets/packages/core/src/atomic-files.ts +8 -1
- package/assets/packages/sdk/src/api.ts +29 -1
- package/dist/cli.js +34 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +8 -0
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agents.d.ts +1 -1
- package/dist/internal/agents.js +33 -2
- package/dist/internal/execution-context.d.ts +0 -1
- package/dist/internal/execution-context.js +0 -1
- package/dist/internal/file-coordinator.js +4 -1
- package/dist/internal/launch-request.js +4 -1
- package/dist/internal/run-lease.d.ts +1 -0
- package/dist/internal/run-lease.js +27 -3
- package/dist/internal/run-log.js +4 -1
- package/dist/internal/run-pruning.d.ts +14 -0
- package/dist/internal/run-pruning.js +141 -0
- package/dist/internal/run-state.js +4 -1
- package/dist/internal/run-store.d.ts +5 -0
- package/dist/internal/run-store.js +66 -5
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/internal/run-lease.ts
|
|
2
2
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3
|
-
import { mkdir as mkdir2, readFile, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
3
|
+
import { mkdir as mkdir2, readFile, rename as rename2, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
4
|
+
import { dirname as dirname2, join } from "node:path";
|
|
5
5
|
|
|
6
6
|
// ../core/src/errors.ts
|
|
7
7
|
function isNodeError(error) {
|
|
@@ -17,6 +17,9 @@ async function writeJsonAtomically(path, value) {
|
|
|
17
17
|
`);
|
|
18
18
|
}
|
|
19
19
|
async function writeTextAtomically(path, content) {
|
|
20
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
async function writeBytesAtomically(path, content) {
|
|
20
23
|
await mkdir(dirname(path), { recursive: true });
|
|
21
24
|
let existingMode;
|
|
22
25
|
try {
|
|
@@ -26,7 +29,7 @@ async function writeTextAtomically(path, content) {
|
|
|
26
29
|
}
|
|
27
30
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
28
31
|
try {
|
|
29
|
-
await writeFile(tmpPath, content, {
|
|
32
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
30
33
|
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
31
34
|
await rename(tmpPath, path);
|
|
32
35
|
} catch (error) {
|
|
@@ -83,6 +86,27 @@ var NornRunLease = class _NornRunLease {
|
|
|
83
86
|
if (!owner || owner.token !== this.token)
|
|
84
87
|
throw new Error("Run lease was lost");
|
|
85
88
|
}
|
|
89
|
+
async deleteRunDirectory() {
|
|
90
|
+
await this.assertOwned();
|
|
91
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
92
|
+
await this.heartbeatChain;
|
|
93
|
+
await this.assertOwned();
|
|
94
|
+
const runRoot = dirname2(this.lockRoot);
|
|
95
|
+
const retiredRoot = join(
|
|
96
|
+
dirname2(dirname2(runRoot)),
|
|
97
|
+
`.pruned-run-${randomUUID2()}`
|
|
98
|
+
);
|
|
99
|
+
await rename2(runRoot, retiredRoot);
|
|
100
|
+
this.isReleased = true;
|
|
101
|
+
try {
|
|
102
|
+
await rm2(retiredRoot, { recursive: true, force: true });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Run was removed from the run list but directory cleanup failed: ${retiredRoot}`,
|
|
106
|
+
{ cause: error }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
86
110
|
async release() {
|
|
87
111
|
if (this.isReleased) return;
|
|
88
112
|
this.isReleased = true;
|
package/dist/internal/run-log.js
CHANGED
|
@@ -15,6 +15,9 @@ async function writeJsonAtomically(path, value) {
|
|
|
15
15
|
`);
|
|
16
16
|
}
|
|
17
17
|
async function writeTextAtomically(path, content) {
|
|
18
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
async function writeBytesAtomically(path, content) {
|
|
18
21
|
await mkdir(dirname(path), { recursive: true });
|
|
19
22
|
let existingMode;
|
|
20
23
|
try {
|
|
@@ -24,7 +27,7 @@ async function writeTextAtomically(path, content) {
|
|
|
24
27
|
}
|
|
25
28
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
26
29
|
try {
|
|
27
|
-
await writeFile(tmpPath, content, {
|
|
30
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
28
31
|
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
29
32
|
await rename(tmpPath, path);
|
|
30
33
|
} catch (error) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NornRunPruneOptions, NornRunPruneResult } from "@vimhead.dev/norn";
|
|
2
|
+
export declare function parseRunPruneOptions(args: readonly string[]): NornRunPruneOptions;
|
|
3
|
+
export declare class NornRunPruner {
|
|
4
|
+
private readonly input;
|
|
5
|
+
private readonly olderThan;
|
|
6
|
+
private readonly cutoff;
|
|
7
|
+
constructor(input: {
|
|
8
|
+
readonly projectRoot: string;
|
|
9
|
+
readonly options: NornRunPruneOptions;
|
|
10
|
+
readonly now: Date;
|
|
11
|
+
});
|
|
12
|
+
prune(): Promise<NornRunPruneResult>;
|
|
13
|
+
private findIneligibilityReason;
|
|
14
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/internal/run-pruning.ts
|
|
2
|
+
import { errorMessage } from "./errors.js";
|
|
3
|
+
import { getRunLeaseOwner, NornRunLease } from "./run-lease.js";
|
|
4
|
+
import { getRunInfo, listRuns } from "./run-state.js";
|
|
5
|
+
import { NornRunStore } from "./run-store.js";
|
|
6
|
+
var DURATION_UNITS = {
|
|
7
|
+
ms: 1,
|
|
8
|
+
s: 1e3,
|
|
9
|
+
m: 6e4,
|
|
10
|
+
h: 36e5,
|
|
11
|
+
d: 864e5,
|
|
12
|
+
w: 6048e5
|
|
13
|
+
};
|
|
14
|
+
function parseRunPruneOptions(args) {
|
|
15
|
+
const flags = /* @__PURE__ */ new Set();
|
|
16
|
+
let olderThan = null;
|
|
17
|
+
for (let index = 0; index < args.length; index++) {
|
|
18
|
+
const flag = args[index];
|
|
19
|
+
if (!["--delete-runs", "--all", "--dry-run", "--older-than"].includes(flag))
|
|
20
|
+
throw new Error(`runs prune: unknown argument ${flag}`);
|
|
21
|
+
if (flags.has(flag))
|
|
22
|
+
throw new Error(`runs prune: duplicate option ${flag}`);
|
|
23
|
+
flags.add(flag);
|
|
24
|
+
if (flag === "--older-than") {
|
|
25
|
+
const value = args[++index];
|
|
26
|
+
if (value === void 0 || value.startsWith("--"))
|
|
27
|
+
throw new Error(
|
|
28
|
+
"runs prune: --older-than requires a duration such as 24h or 7d"
|
|
29
|
+
);
|
|
30
|
+
olderThan = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
deleteRuns: flags.has("--delete-runs"),
|
|
35
|
+
all: flags.has("--all"),
|
|
36
|
+
olderThan,
|
|
37
|
+
dryRun: flags.has("--dry-run")
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function parsePruneDuration(value) {
|
|
41
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(value);
|
|
42
|
+
const duration = match ? Number(match[1]) * DURATION_UNITS[match[2]] : NaN;
|
|
43
|
+
if (!Number.isFinite(duration) || duration < 0 || duration > Number.MAX_SAFE_INTEGER)
|
|
44
|
+
throw new Error(
|
|
45
|
+
`runs prune: invalid duration ${value}; use a non-negative number followed by ms, s, m, h, d, or w`
|
|
46
|
+
);
|
|
47
|
+
return duration;
|
|
48
|
+
}
|
|
49
|
+
var NornRunPruner = class {
|
|
50
|
+
constructor(input) {
|
|
51
|
+
this.input = input;
|
|
52
|
+
if (input.options.all && !input.options.deleteRuns)
|
|
53
|
+
throw new Error("runs prune: --all requires --delete-runs");
|
|
54
|
+
if (input.options.all && input.options.olderThan !== null)
|
|
55
|
+
throw new Error(
|
|
56
|
+
"runs prune: --all and --older-than are mutually exclusive"
|
|
57
|
+
);
|
|
58
|
+
if (!Number.isFinite(input.now.getTime()))
|
|
59
|
+
throw new Error("Invalid pruning time");
|
|
60
|
+
this.olderThan = input.options.all ? null : input.options.olderThan ?? "24h";
|
|
61
|
+
this.cutoff = this.olderThan === null ? null : input.now.getTime() - parsePruneDuration(this.olderThan);
|
|
62
|
+
}
|
|
63
|
+
input;
|
|
64
|
+
olderThan;
|
|
65
|
+
cutoff;
|
|
66
|
+
async prune() {
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const run of await listRuns(this.input.projectRoot)) {
|
|
69
|
+
const identity = { id: run.id, name: run.name, path: run.path };
|
|
70
|
+
let lease;
|
|
71
|
+
try {
|
|
72
|
+
let reason = this.findIneligibilityReason(run);
|
|
73
|
+
if (!reason && await getRunLeaseOwner(run.path))
|
|
74
|
+
reason = "Run is owned by an executor";
|
|
75
|
+
if (reason) {
|
|
76
|
+
results.push({ ...identity, status: "skipped", reason });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (this.input.options.dryRun) {
|
|
80
|
+
results.push({ ...identity, status: "planned" });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
lease = await NornRunLease.acquire(run.path);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Error && error.message.startsWith("Run is already active:")) {
|
|
87
|
+
results.push({
|
|
88
|
+
...identity,
|
|
89
|
+
status: "skipped",
|
|
90
|
+
reason: "Run is owned by an executor"
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
reason = this.findIneligibilityReason(await getRunInfo(run.path));
|
|
97
|
+
if (reason) {
|
|
98
|
+
results.push({ ...identity, status: "skipped", reason });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
await lease.assertOwned();
|
|
102
|
+
if (this.input.options.deleteRuns) {
|
|
103
|
+
await lease.deleteRunDirectory();
|
|
104
|
+
} else {
|
|
105
|
+
const store = await NornRunStore.open(run.path);
|
|
106
|
+
await store.pruneCheckpointHistory(this.input.now.toISOString());
|
|
107
|
+
}
|
|
108
|
+
results.push({ ...identity, status: "pruned" });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
results.push({
|
|
111
|
+
...identity,
|
|
112
|
+
status: "failed",
|
|
113
|
+
reason: errorMessage(error)
|
|
114
|
+
});
|
|
115
|
+
} finally {
|
|
116
|
+
await lease?.release();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
mode: this.input.options.deleteRuns ? "runs" : "checkpoints",
|
|
121
|
+
dryRun: this.input.options.dryRun,
|
|
122
|
+
olderThan: this.olderThan,
|
|
123
|
+
runs: results
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
findIneligibilityReason(run) {
|
|
127
|
+
if (run.status !== "completed" && run.status !== "failed")
|
|
128
|
+
return `Run status is ${run.status}; only completed or failed runs are eligible`;
|
|
129
|
+
const finishedAt = run.status === "failed" ? run.failed?.failedAt ?? run.outcome?.completedAt ?? run.updatedAt : run.outcome?.completedAt ?? run.updatedAt;
|
|
130
|
+
const finishedTime = Date.parse(finishedAt);
|
|
131
|
+
if (!Number.isFinite(finishedTime))
|
|
132
|
+
return "Run completion/failure time is invalid";
|
|
133
|
+
if (this.cutoff !== null && finishedTime >= this.cutoff)
|
|
134
|
+
return `Run finished less than or exactly ${this.olderThan} ago`;
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
export {
|
|
139
|
+
NornRunPruner,
|
|
140
|
+
parseRunPruneOptions
|
|
141
|
+
};
|
|
@@ -24,6 +24,9 @@ async function writeJsonAtomically(path, value) {
|
|
|
24
24
|
`);
|
|
25
25
|
}
|
|
26
26
|
async function writeTextAtomically(path, content) {
|
|
27
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
async function writeBytesAtomically(path, content) {
|
|
27
30
|
await mkdir(dirname(path), { recursive: true });
|
|
28
31
|
let existingMode;
|
|
29
32
|
try {
|
|
@@ -33,7 +36,7 @@ async function writeTextAtomically(path, content) {
|
|
|
33
36
|
}
|
|
34
37
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
35
38
|
try {
|
|
36
|
-
await writeFile(tmpPath, content, {
|
|
39
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
37
40
|
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
38
41
|
await rename(tmpPath, path);
|
|
39
42
|
} catch (error) {
|
|
@@ -12,6 +12,10 @@ export declare class NornRunStore {
|
|
|
12
12
|
restoreSnapshot(ref: string, prepare: ((stagedRunRoot: string) => Promise<void>) | undefined): Promise<void>;
|
|
13
13
|
restoreCurrentSnapshot(): Promise<void>;
|
|
14
14
|
listCheckpoints(): Promise<NornRunCheckpoint[]>;
|
|
15
|
+
pruneCheckpointHistory(prunedAt: string): Promise<void>;
|
|
16
|
+
private get prunedHistoryPath();
|
|
17
|
+
private isCheckpointHistoryPruned;
|
|
18
|
+
private assertCheckpointHistoryAvailable;
|
|
15
19
|
private readCheckpointHistory;
|
|
16
20
|
assertWorkspaceCanBeSnapshotted(_workspace: string): Promise<void>;
|
|
17
21
|
private get objectsRoot();
|
|
@@ -29,6 +33,7 @@ export declare class NornRunStore {
|
|
|
29
33
|
private materializedPath;
|
|
30
34
|
private readSnapshot;
|
|
31
35
|
private snapshotPath;
|
|
36
|
+
private getLegacySnapshotPath;
|
|
32
37
|
private snapshotRelativePath;
|
|
33
38
|
private assertCheckpointPathMatchesId;
|
|
34
39
|
private objectPath;
|
|
@@ -34,6 +34,9 @@ async function writeJsonAtomically(path, value) {
|
|
|
34
34
|
`);
|
|
35
35
|
}
|
|
36
36
|
async function writeTextAtomically(path, content) {
|
|
37
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
async function writeBytesAtomically(path, content) {
|
|
37
40
|
await mkdir(dirname(path), { recursive: true });
|
|
38
41
|
let existingMode;
|
|
39
42
|
try {
|
|
@@ -43,7 +46,7 @@ async function writeTextAtomically(path, content) {
|
|
|
43
46
|
}
|
|
44
47
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
45
48
|
try {
|
|
46
|
-
await writeFile(tmpPath, content, {
|
|
49
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
47
50
|
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
48
51
|
await rename(tmpPath, path);
|
|
49
52
|
} catch (error) {
|
|
@@ -59,6 +62,7 @@ var SNAPSHOT_VERSION = 1;
|
|
|
59
62
|
var CURRENT_DIR_NAME = "current";
|
|
60
63
|
var STORE_DIR_NAME = "store";
|
|
61
64
|
var CHECKPOINTS_FILE_NAME = "checkpoints.json";
|
|
65
|
+
var PRUNED_HISTORY_FILE_NAME = "checkpoint-history-pruned.json";
|
|
62
66
|
function runCurrentRoot(runRoot) {
|
|
63
67
|
return join(runRoot, CURRENT_DIR_NAME);
|
|
64
68
|
}
|
|
@@ -82,13 +86,16 @@ var NornRunStore = class _NornRunStore {
|
|
|
82
86
|
static async open(root) {
|
|
83
87
|
const runStore = new _NornRunStore(root);
|
|
84
88
|
await access(runStore.currentRoot, constants.R_OK | constants.W_OK);
|
|
85
|
-
await
|
|
89
|
+
if (!await runStore.isCheckpointHistoryPruned())
|
|
90
|
+
await access(runStore.storeRoot, constants.R_OK | constants.W_OK);
|
|
86
91
|
return runStore;
|
|
87
92
|
}
|
|
88
93
|
async currentSnapshotRef() {
|
|
94
|
+
await this.assertCheckpointHistoryAvailable();
|
|
89
95
|
return readFile(this.currentRefPath, "utf8").then((value) => value.trim());
|
|
90
96
|
}
|
|
91
97
|
async snapshotCurrent(message) {
|
|
98
|
+
await this.assertCheckpointHistoryAvailable();
|
|
92
99
|
const previous = await this.readCheckpointHistory();
|
|
93
100
|
const checkpoint = this.createCheckpoint(
|
|
94
101
|
message,
|
|
@@ -109,6 +116,7 @@ var NornRunStore = class _NornRunStore {
|
|
|
109
116
|
return checkpoint;
|
|
110
117
|
}
|
|
111
118
|
async restoreSnapshot(ref, prepare) {
|
|
119
|
+
await this.assertCheckpointHistoryAvailable();
|
|
112
120
|
const checkpoint = (await this.listCheckpoints()).find(
|
|
113
121
|
(entry) => entry.id === ref
|
|
114
122
|
);
|
|
@@ -125,8 +133,42 @@ var NornRunStore = class _NornRunStore {
|
|
|
125
133
|
);
|
|
126
134
|
}
|
|
127
135
|
async listCheckpoints() {
|
|
136
|
+
if (await this.isCheckpointHistoryPruned()) return [];
|
|
128
137
|
return (await this.readCheckpointHistory()).checkpoints;
|
|
129
138
|
}
|
|
139
|
+
async pruneCheckpointHistory(prunedAt) {
|
|
140
|
+
if (!await this.isCheckpointHistoryPruned())
|
|
141
|
+
await writeJsonAtomically(this.prunedHistoryPath, {
|
|
142
|
+
version: 1,
|
|
143
|
+
prunedAt
|
|
144
|
+
});
|
|
145
|
+
await writeJsonAtomically(this.checkpointsPath, []);
|
|
146
|
+
await rm2(this.storeRoot, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
get prunedHistoryPath() {
|
|
149
|
+
return join(this.runRoot, PRUNED_HISTORY_FILE_NAME);
|
|
150
|
+
}
|
|
151
|
+
async isCheckpointHistoryPruned() {
|
|
152
|
+
let content;
|
|
153
|
+
try {
|
|
154
|
+
content = await readFile(this.prunedHistoryPath, "utf8");
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
const marker = JSON.parse(content);
|
|
160
|
+
if (!marker || typeof marker !== "object" || !("version" in marker) || marker.version !== 1 || !("prunedAt" in marker) || typeof marker.prunedAt !== "string" || !Number.isFinite(Date.parse(marker.prunedAt)))
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Invalid pruned checkpoint history marker: ${this.prunedHistoryPath}`
|
|
163
|
+
);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
async assertCheckpointHistoryAvailable() {
|
|
167
|
+
if (await this.isCheckpointHistoryPruned())
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Checkpoint history was pruned for this run: ${this.runRoot}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
130
172
|
async readCheckpointHistory() {
|
|
131
173
|
try {
|
|
132
174
|
const content = await readFile(this.checkpointsPath, "utf8");
|
|
@@ -196,7 +238,10 @@ var NornRunStore = class _NornRunStore {
|
|
|
196
238
|
createdAt: checkpoint.createdAt,
|
|
197
239
|
entries
|
|
198
240
|
};
|
|
199
|
-
await
|
|
241
|
+
await writeBytesAtomically(
|
|
242
|
+
this.snapshotPath(snapshot.id),
|
|
243
|
+
await gzipBuffer(Buffer.from(JSON.stringify(snapshot)))
|
|
244
|
+
);
|
|
200
245
|
return snapshot;
|
|
201
246
|
}
|
|
202
247
|
async snapshotEntries(root) {
|
|
@@ -346,11 +391,23 @@ var NornRunStore = class _NornRunStore {
|
|
|
346
391
|
return path;
|
|
347
392
|
}
|
|
348
393
|
async readSnapshot(id) {
|
|
394
|
+
let compressed;
|
|
395
|
+
try {
|
|
396
|
+
compressed = await readFile(this.snapshotPath(id));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
399
|
+
return parseRunSnapshotManifest(
|
|
400
|
+
JSON.parse(await readFile(this.getLegacySnapshotPath(id), "utf8"))
|
|
401
|
+
);
|
|
402
|
+
}
|
|
349
403
|
return parseRunSnapshotManifest(
|
|
350
|
-
JSON.parse(await
|
|
404
|
+
JSON.parse((await gunzipBuffer(compressed)).toString("utf8"))
|
|
351
405
|
);
|
|
352
406
|
}
|
|
353
407
|
snapshotPath(id) {
|
|
408
|
+
return join(this.snapshotsRoot, `${id}.json.gz`);
|
|
409
|
+
}
|
|
410
|
+
getLegacySnapshotPath(id) {
|
|
354
411
|
return join(this.snapshotsRoot, `${id}.json`);
|
|
355
412
|
}
|
|
356
413
|
snapshotRelativePath(id) {
|
|
@@ -358,7 +415,11 @@ var NornRunStore = class _NornRunStore {
|
|
|
358
415
|
}
|
|
359
416
|
assertCheckpointPathMatchesId(checkpoint) {
|
|
360
417
|
const expectedPath = this.snapshotRelativePath(checkpoint.id);
|
|
361
|
-
|
|
418
|
+
const legacyPath = relative(
|
|
419
|
+
this.runRoot,
|
|
420
|
+
this.getLegacySnapshotPath(checkpoint.id)
|
|
421
|
+
);
|
|
422
|
+
if (checkpoint.path !== expectedPath && checkpoint.path !== legacyPath)
|
|
362
423
|
throw new Error(
|
|
363
424
|
`Run checkpoint path does not match id: ${checkpoint.id}`
|
|
364
425
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vimhead.dev/norn-cli",
|
|
3
|
-
"version": "0.1.0-tip.
|
|
3
|
+
"version": "0.1.0-tip.37307659560.1",
|
|
4
4
|
"description": "Harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
44
44
|
"jiti": "^2.7.0",
|
|
45
45
|
"typebox": "^1.3.34",
|
|
46
|
-
"@vimhead.dev/norn": "0.1.0-tip.
|
|
46
|
+
"@vimhead.dev/norn": "0.1.0-tip.37307659560.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "node ../../scripts/build-package.ts"
|