trajectories 0.1.2 → 0.1.3
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 +12 -0
- package/README.md +21 -5
- package/dist/cli.js +25984 -24326
- package/dist/convert.d.ts +8 -0
- package/dist/convert.js +26 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1205 -125
- package/dist/log-stage.js +3 -1
- package/dist/log-state.d.ts +1 -1
- package/dist/log-switch.js +4 -3
- package/dist/readers/atif.d.ts +10 -0
- package/dist/readers/atif.js +54 -0
- package/dist/shared-managed.d.ts +31 -0
- package/dist/shared-managed.js +333 -0
- package/dist/shared-observed.d.ts +96 -0
- package/dist/shared-observed.js +286 -0
- package/dist/shared.d.ts +40 -0
- package/dist/shared.js +357 -0
- package/dist/types.d.ts +41 -3
- package/dist/types.js +7 -1
- package/docs/shared-history.md +133 -0
- package/legacy/src/db.ts +11 -7
- package/legacy/src/reader.ts +21 -0
- package/legacy/src/shared.ts +217 -0
- package/package.json +47 -10
package/dist/index.js
CHANGED
|
@@ -1,14 +1,153 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var __esm = (fn, res, err) => () => {
|
|
2
|
+
if (fn)
|
|
3
|
+
try {
|
|
4
|
+
res = fn(fn = 0);
|
|
5
|
+
} catch (e) {
|
|
6
|
+
err = [e];
|
|
7
|
+
}
|
|
8
|
+
if (err)
|
|
9
|
+
throw err[0];
|
|
10
|
+
return res;
|
|
11
|
+
};
|
|
12
|
+
|
|
3
13
|
// src/log-types.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
var TrajectoriesError;
|
|
15
|
+
var init_log_types = __esm(() => {
|
|
16
|
+
TrajectoriesError = class TrajectoriesError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message, options) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = "TrajectoriesError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/log-io.ts
|
|
27
|
+
import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
|
|
28
|
+
import { link, mkdir as mkdir2, open, readFile as readFile4, rename, rm } from "node:fs/promises";
|
|
29
|
+
import { dirname as dirname3, join as join5, resolve as resolve8 } from "node:path";
|
|
30
|
+
function stableJson(value) {
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return `[${value.map((item) => item === undefined ? "null" : stableJson(item)).join(",")}]`;
|
|
33
|
+
if (value && typeof value === "object")
|
|
34
|
+
return `{${Object.keys(value).sort().filter((key) => value[key] !== undefined).map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
35
|
+
const result = JSON.stringify(value);
|
|
36
|
+
if (result === undefined)
|
|
37
|
+
throw new TrajectoriesError("INVALID_INPUT", "Input must be JSON serializable.");
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function safeId(value) {
|
|
41
|
+
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value))
|
|
42
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid session or intent identifier.");
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function objectPath(store, id) {
|
|
46
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
47
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid content reference.");
|
|
48
|
+
return join5(resolve8(store), "objects", id.slice(0, 2), id);
|
|
49
|
+
}
|
|
50
|
+
async function syncDirectory(path) {
|
|
51
|
+
const file = await open(path, "r");
|
|
52
|
+
try {
|
|
53
|
+
await file.sync();
|
|
54
|
+
} finally {
|
|
55
|
+
await file.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function ensureDirectory(path) {
|
|
59
|
+
try {
|
|
60
|
+
await mkdir2(path, { mode: 448 });
|
|
61
|
+
await syncDirectory(dirname3(path));
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
if (cause.code === "EEXIST")
|
|
64
|
+
return;
|
|
65
|
+
if (cause.code !== "ENOENT")
|
|
66
|
+
throw cause;
|
|
67
|
+
await ensureDirectory(dirname3(path));
|
|
68
|
+
await ensureDirectory(path);
|
|
10
69
|
}
|
|
11
70
|
}
|
|
71
|
+
async function atomicWrite(path, content, immutable = false) {
|
|
72
|
+
await ensureDirectory(dirname3(path));
|
|
73
|
+
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
74
|
+
const file = await open(temporary, "wx", 384);
|
|
75
|
+
try {
|
|
76
|
+
await file.writeFile(content);
|
|
77
|
+
await file.sync();
|
|
78
|
+
} finally {
|
|
79
|
+
await file.close();
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
if (immutable)
|
|
83
|
+
await link(temporary, path);
|
|
84
|
+
else
|
|
85
|
+
await rename(temporary, path);
|
|
86
|
+
await syncDirectory(dirname3(path));
|
|
87
|
+
} finally {
|
|
88
|
+
await rm(temporary, { force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function putObject(store, content) {
|
|
92
|
+
const id = digest2(content), path = objectPath(store, id);
|
|
93
|
+
try {
|
|
94
|
+
await atomicWrite(path, content, true);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.code !== "EEXIST")
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const existing = await getObject(store, id);
|
|
100
|
+
if (digest2(existing) !== id)
|
|
101
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Stored content is corrupt.");
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
async function getObject(store, id, derived) {
|
|
105
|
+
let bytes;
|
|
106
|
+
try {
|
|
107
|
+
bytes = await readFile4(objectPath(store, id));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const fallback = derived?.get(id);
|
|
110
|
+
if (error.code !== "ENOENT" || !fallback)
|
|
111
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", `Missing retained content ${id}.`, { cause: error });
|
|
112
|
+
bytes = fallback;
|
|
113
|
+
}
|
|
114
|
+
if (digest2(bytes) !== id)
|
|
115
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", `Content hash mismatch ${id}.`);
|
|
116
|
+
return bytes;
|
|
117
|
+
}
|
|
118
|
+
async function withSessionLock(store, id, action) {
|
|
119
|
+
const locks = join5(resolve8(store), "locks");
|
|
120
|
+
await ensureDirectory(locks);
|
|
121
|
+
const path = join5(locks, safeId(id));
|
|
122
|
+
try {
|
|
123
|
+
await mkdir2(path, { mode: 448 });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error.code === "EEXIST")
|
|
126
|
+
throw new TrajectoriesError("OPERATION_PENDING", "A writer holds this session; reconcile interrupted writers before retrying.");
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await action();
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(path, { recursive: true });
|
|
133
|
+
await syncDirectory(locks);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
var digest2 = (content) => createHash9("sha256").update(content).digest("hex");
|
|
137
|
+
var init_log_io = __esm(() => {
|
|
138
|
+
init_log_types();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// src/types.ts
|
|
142
|
+
var NATIVE_HARNESSSES = ["claude-code", "codex", "pi", "omp", "opencode", "grok-build", "cursor", "deepagents", "github-copilot", "cline", "fx"];
|
|
143
|
+
var HARNESSES = [...NATIVE_HARNESSSES, "atif"];
|
|
144
|
+
function isNativeHarness(value) {
|
|
145
|
+
return NATIVE_HARNESSSES.includes(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/index.ts
|
|
149
|
+
init_log_types();
|
|
150
|
+
|
|
12
151
|
// src/convert.ts
|
|
13
152
|
import { createHash as createHash8 } from "node:crypto";
|
|
14
153
|
import { readFile as readFile3, mkdir, writeFile, realpath } from "node:fs/promises";
|
|
@@ -5064,6 +5203,47 @@ function normalizeGrokBuild(transcript, metadata = {}) {
|
|
|
5064
5203
|
};
|
|
5065
5204
|
}
|
|
5066
5205
|
|
|
5206
|
+
// src/readers/atif.ts
|
|
5207
|
+
var STEP_RECORD_ID = /^(?:trajectory:.+:)?step:([1-9]\d*)$/;
|
|
5208
|
+
var isObject4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5209
|
+
var present = (value) => value !== undefined && value !== null;
|
|
5210
|
+
function injectedOrigin(step) {
|
|
5211
|
+
if (step.source !== "user")
|
|
5212
|
+
return;
|
|
5213
|
+
const role = isObject4(step.extra) ? step.extra.role : undefined;
|
|
5214
|
+
return typeof role === "string" && role ? "harness" : undefined;
|
|
5215
|
+
}
|
|
5216
|
+
function enrichAtifCanonical(raw, canonical) {
|
|
5217
|
+
let document;
|
|
5218
|
+
try {
|
|
5219
|
+
document = JSON.parse(raw);
|
|
5220
|
+
} catch (cause) {
|
|
5221
|
+
throw new Error("ATIF evidence is not the JSON trajectory that produced this canonical form.", { cause });
|
|
5222
|
+
}
|
|
5223
|
+
const steps = isObject4(document) && Array.isArray(document.steps) ? document.steps : undefined;
|
|
5224
|
+
if (!steps)
|
|
5225
|
+
throw new Error("ATIF evidence has no steps array.");
|
|
5226
|
+
const byStep = new Map;
|
|
5227
|
+
for (const step of steps) {
|
|
5228
|
+
if (!isObject4(step) || typeof step.step_id !== "number" || !Number.isInteger(step.step_id))
|
|
5229
|
+
continue;
|
|
5230
|
+
const { metrics, extra } = step;
|
|
5231
|
+
const metadata = present(metrics) || present(extra) ? { ...present(metrics) ? { metrics } : {}, ...present(extra) ? { extra } : {} } : undefined;
|
|
5232
|
+
const origin = injectedOrigin(step);
|
|
5233
|
+
if (!metadata && !origin)
|
|
5234
|
+
continue;
|
|
5235
|
+
byStep.set(step.step_id, { ...metadata ? { metadata } : {}, ...origin ? { origin } : {} });
|
|
5236
|
+
}
|
|
5237
|
+
if (!byStep.size)
|
|
5238
|
+
return canonical;
|
|
5239
|
+
const records = canonical.records.map((record) => {
|
|
5240
|
+
const match = STEP_RECORD_ID.exec(record.stable_source_record_id);
|
|
5241
|
+
const enrichment = match ? byStep.get(Number(match[1])) : undefined;
|
|
5242
|
+
return enrichment ? { ...record, ...enrichment } : record;
|
|
5243
|
+
});
|
|
5244
|
+
return { ...canonical, records };
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5067
5247
|
// src/readers/cursor.ts
|
|
5068
5248
|
import { createHash as createHash5 } from "node:crypto";
|
|
5069
5249
|
import { readFile as readFile2, stat } from "node:fs/promises";
|
|
@@ -6830,7 +7010,7 @@ function finalizeSnapshot(input, archived, clineCompaction = false) {
|
|
|
6830
7010
|
const invalid = archived.diagnostics.filter((d) => d.code === "invalid_json_line" || d.code === "non_object_json_line");
|
|
6831
7011
|
if (invalid.length)
|
|
6832
7012
|
throw new Error(`Invalid native transcript: ${invalid.map((d) => d.message).join("; ")}`);
|
|
6833
|
-
const canonical = input.harness === "pi" ? projectPiProviderContext(raw, archived) : archived;
|
|
7013
|
+
const canonical = input.harness === "pi" ? projectPiProviderContext(raw, archived) : input.harness === "atif" ? enrichAtifCanonical(raw, archived) : archived;
|
|
6834
7014
|
const metadata = provenance(input.harness, canonical);
|
|
6835
7015
|
if (input.nativeSessionId && metadata.nativeSessionId !== input.nativeSessionId)
|
|
6836
7016
|
throw new Error("Selected native session does not match source provenance.");
|
|
@@ -6968,12 +7148,30 @@ function assertSnapshot(snapshot) {
|
|
|
6968
7148
|
if (sha2562(attachment.content) !== attachment.sha256)
|
|
6969
7149
|
throw new Error("Snapshot attachment integrity check failed.");
|
|
6970
7150
|
}
|
|
7151
|
+
for (const annotation of snapshot.annotations ?? []) {
|
|
7152
|
+
if (typeof annotation.kind !== "string" || !annotation.kind || typeof annotation.content !== "string" || sha2562(annotation.content) !== annotation.sha256)
|
|
7153
|
+
throw new Error("Snapshot annotation integrity check failed.");
|
|
7154
|
+
}
|
|
6971
7155
|
for (const record of canonical.records) {
|
|
6972
7156
|
if (sha2562(record.record_json) !== record.record_hash)
|
|
6973
7157
|
throw new Error("Snapshot canonical record integrity check failed.");
|
|
6974
7158
|
}
|
|
6975
7159
|
validateTranscript(canonical.records.map((r) => JSON.parse(r.record_json)));
|
|
6976
7160
|
}
|
|
7161
|
+
function annotateSnapshot(snapshot, annotation) {
|
|
7162
|
+
assertSnapshot(snapshot);
|
|
7163
|
+
if (typeof annotation?.kind !== "string" || !annotation.kind.trim())
|
|
7164
|
+
throw new Error("Snapshot annotation requires a kind.");
|
|
7165
|
+
if (typeof annotation.content !== "string")
|
|
7166
|
+
throw new Error("Snapshot annotation content must be a string.");
|
|
7167
|
+
const entry = {
|
|
7168
|
+
kind: annotation.kind,
|
|
7169
|
+
content: annotation.content,
|
|
7170
|
+
sha256: sha2562(annotation.content),
|
|
7171
|
+
createdAt: new Date().toISOString()
|
|
7172
|
+
};
|
|
7173
|
+
return { ...snapshot, annotations: [...snapshot.annotations ?? [], entry] };
|
|
7174
|
+
}
|
|
6977
7175
|
function assertPortableSnapshot(snapshot) {
|
|
6978
7176
|
assertSnapshot(snapshot);
|
|
6979
7177
|
const clineCompaction = snapshot.source.harness === "cline" && snapshot.attachments?.some((attachment) => attachment.name.endsWith(".compaction.json") || attachment.name.endsWith(".json") && JSON.parse(attachment.content).compaction_path);
|
|
@@ -6998,8 +7196,9 @@ function convertManaged(snapshot, target, options, entropySeed, records) {
|
|
|
6998
7196
|
}
|
|
6999
7197
|
function convertWithEntropy(snapshot, target, options, entropy, retainedRecords) {
|
|
7000
7198
|
assertPortableSnapshot(snapshot);
|
|
7001
|
-
if (!
|
|
7002
|
-
throw new Error(`Unsupported target: ${target}`);
|
|
7199
|
+
if (!isNativeHarness(target)) {
|
|
7200
|
+
throw new Error(HARNESSES.includes(target) ? `Unsupported target: ${target} is a read-only interchange format.` : `Unsupported target: ${target}`);
|
|
7201
|
+
}
|
|
7003
7202
|
if (!isAbsolute2(options.cwd) || !isAbsolute2(options.directory))
|
|
7004
7203
|
throw new Error("cwd and directory must be absolute paths.");
|
|
7005
7204
|
const records = retainedRecords ?? snapshot.canonical.records.map((r) => JSON.parse(r.record_json));
|
|
@@ -7040,119 +7239,13 @@ async function writeSnapshot(snapshot, target, options) {
|
|
|
7040
7239
|
await writeNativeExport(output, options.directory);
|
|
7041
7240
|
return output;
|
|
7042
7241
|
}
|
|
7043
|
-
// src/log
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
import { dirname as dirname3, join as join5, resolve as resolve8 } from "node:path";
|
|
7047
|
-
function stableJson(value) {
|
|
7048
|
-
if (Array.isArray(value))
|
|
7049
|
-
return `[${value.map((item) => item === undefined ? "null" : stableJson(item)).join(",")}]`;
|
|
7050
|
-
if (value && typeof value === "object")
|
|
7051
|
-
return `{${Object.keys(value).sort().filter((key) => value[key] !== undefined).map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
7052
|
-
const result = JSON.stringify(value);
|
|
7053
|
-
if (result === undefined)
|
|
7054
|
-
throw new TrajectoriesError("INVALID_INPUT", "Input must be JSON serializable.");
|
|
7055
|
-
return result;
|
|
7056
|
-
}
|
|
7057
|
-
var digest2 = (content) => createHash9("sha256").update(content).digest("hex");
|
|
7058
|
-
function safeId(value) {
|
|
7059
|
-
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value))
|
|
7060
|
-
throw new TrajectoriesError("INVALID_INPUT", "Invalid session or intent identifier.");
|
|
7061
|
-
return value;
|
|
7062
|
-
}
|
|
7063
|
-
function objectPath(store, id) {
|
|
7064
|
-
if (!/^[a-f0-9]{64}$/.test(id))
|
|
7065
|
-
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid content reference.");
|
|
7066
|
-
return join5(resolve8(store), "objects", id.slice(0, 2), id);
|
|
7067
|
-
}
|
|
7068
|
-
async function syncDirectory(path) {
|
|
7069
|
-
const file = await open(path, "r");
|
|
7070
|
-
try {
|
|
7071
|
-
await file.sync();
|
|
7072
|
-
} finally {
|
|
7073
|
-
await file.close();
|
|
7074
|
-
}
|
|
7075
|
-
}
|
|
7076
|
-
async function ensureDirectory(path) {
|
|
7077
|
-
try {
|
|
7078
|
-
await mkdir2(path, { mode: 448 });
|
|
7079
|
-
await syncDirectory(dirname3(path));
|
|
7080
|
-
} catch (cause) {
|
|
7081
|
-
if (cause.code === "EEXIST")
|
|
7082
|
-
return;
|
|
7083
|
-
if (cause.code !== "ENOENT")
|
|
7084
|
-
throw cause;
|
|
7085
|
-
await ensureDirectory(dirname3(path));
|
|
7086
|
-
await ensureDirectory(path);
|
|
7087
|
-
}
|
|
7088
|
-
}
|
|
7089
|
-
async function atomicWrite(path, content, immutable = false) {
|
|
7090
|
-
await ensureDirectory(dirname3(path));
|
|
7091
|
-
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
7092
|
-
const file = await open(temporary, "wx", 384);
|
|
7093
|
-
try {
|
|
7094
|
-
await file.writeFile(content);
|
|
7095
|
-
await file.sync();
|
|
7096
|
-
} finally {
|
|
7097
|
-
await file.close();
|
|
7098
|
-
}
|
|
7099
|
-
try {
|
|
7100
|
-
if (immutable)
|
|
7101
|
-
await link(temporary, path);
|
|
7102
|
-
else
|
|
7103
|
-
await rename(temporary, path);
|
|
7104
|
-
await syncDirectory(dirname3(path));
|
|
7105
|
-
} finally {
|
|
7106
|
-
await rm(temporary, { force: true });
|
|
7107
|
-
}
|
|
7108
|
-
}
|
|
7109
|
-
async function putObject(store, content) {
|
|
7110
|
-
const id = digest2(content), path = objectPath(store, id);
|
|
7111
|
-
try {
|
|
7112
|
-
await atomicWrite(path, content, true);
|
|
7113
|
-
} catch (error) {
|
|
7114
|
-
if (error.code !== "EEXIST")
|
|
7115
|
-
throw error;
|
|
7116
|
-
}
|
|
7117
|
-
const existing = await getObject(store, id);
|
|
7118
|
-
if (digest2(existing) !== id)
|
|
7119
|
-
throw new TrajectoriesError("INTEGRITY_ERROR", "Stored content is corrupt.");
|
|
7120
|
-
return id;
|
|
7121
|
-
}
|
|
7122
|
-
async function getObject(store, id, derived) {
|
|
7123
|
-
let bytes;
|
|
7124
|
-
try {
|
|
7125
|
-
bytes = await readFile4(objectPath(store, id));
|
|
7126
|
-
} catch (error) {
|
|
7127
|
-
const fallback = derived?.get(id);
|
|
7128
|
-
if (error.code !== "ENOENT" || !fallback)
|
|
7129
|
-
throw new TrajectoriesError("INTEGRITY_ERROR", `Missing retained content ${id}.`, { cause: error });
|
|
7130
|
-
bytes = fallback;
|
|
7131
|
-
}
|
|
7132
|
-
if (digest2(bytes) !== id)
|
|
7133
|
-
throw new TrajectoriesError("INTEGRITY_ERROR", `Content hash mismatch ${id}.`);
|
|
7134
|
-
return bytes;
|
|
7135
|
-
}
|
|
7136
|
-
async function withSessionLock(store, id, action) {
|
|
7137
|
-
const locks = join5(resolve8(store), "locks");
|
|
7138
|
-
await ensureDirectory(locks);
|
|
7139
|
-
const path = join5(locks, safeId(id));
|
|
7140
|
-
try {
|
|
7141
|
-
await mkdir2(path, { mode: 448 });
|
|
7142
|
-
} catch (error) {
|
|
7143
|
-
if (error.code === "EEXIST")
|
|
7144
|
-
throw new TrajectoriesError("OPERATION_PENDING", "A writer holds this session; reconcile interrupted writers before retrying.");
|
|
7145
|
-
throw error;
|
|
7146
|
-
}
|
|
7147
|
-
try {
|
|
7148
|
-
return await action();
|
|
7149
|
-
} finally {
|
|
7150
|
-
await rm(path, { recursive: true });
|
|
7151
|
-
await syncDirectory(locks);
|
|
7152
|
-
}
|
|
7153
|
-
}
|
|
7242
|
+
// src/log.ts
|
|
7243
|
+
init_log_types();
|
|
7244
|
+
init_log_io();
|
|
7154
7245
|
|
|
7155
7246
|
// src/log-state.ts
|
|
7247
|
+
init_log_types();
|
|
7248
|
+
init_log_io();
|
|
7156
7249
|
import { open as open2, readFile as readFile5, readdir } from "node:fs/promises";
|
|
7157
7250
|
import { dirname as dirname4, join as join6, resolve as resolve9 } from "node:path";
|
|
7158
7251
|
var logPath = (store, id) => join6(resolve9(store), "logs", `${safeId(id)}.jsonl`);
|
|
@@ -7240,10 +7333,14 @@ async function publishFrame(store, body) {
|
|
|
7240
7333
|
}
|
|
7241
7334
|
|
|
7242
7335
|
// src/log-recovery.ts
|
|
7336
|
+
init_log_types();
|
|
7337
|
+
init_log_io();
|
|
7243
7338
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
7244
7339
|
import { join as join8, resolve as resolve11 } from "node:path";
|
|
7245
7340
|
|
|
7246
7341
|
// src/log-content.ts
|
|
7342
|
+
init_log_io();
|
|
7343
|
+
init_log_types();
|
|
7247
7344
|
function textChunks(text) {
|
|
7248
7345
|
const bytes = Buffer.from(text), chunks = [];
|
|
7249
7346
|
let start = 0, rolling = 0;
|
|
@@ -7320,9 +7417,15 @@ async function getText(store, ref, derived) {
|
|
|
7320
7417
|
}
|
|
7321
7418
|
|
|
7322
7419
|
// src/log-read.ts
|
|
7420
|
+
init_log_types();
|
|
7421
|
+
init_log_io();
|
|
7323
7422
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
7324
7423
|
import { join as join7, resolve as resolve10 } from "node:path";
|
|
7325
7424
|
|
|
7425
|
+
// src/log-records.ts
|
|
7426
|
+
init_log_io();
|
|
7427
|
+
init_log_types();
|
|
7428
|
+
|
|
7326
7429
|
// src/portable.ts
|
|
7327
7430
|
function canonicalJson2(value) {
|
|
7328
7431
|
if (Array.isArray(value))
|
|
@@ -7676,10 +7779,13 @@ async function recoverEvidence(store, evidence, frames, ownId, cache = recoveryC
|
|
|
7676
7779
|
}
|
|
7677
7780
|
|
|
7678
7781
|
// src/log-stage.ts
|
|
7782
|
+
init_log_types();
|
|
7679
7783
|
import { readFile as readFile8, realpath as realpath3 } from "node:fs/promises";
|
|
7680
7784
|
import { resolve as resolve13 } from "node:path";
|
|
7785
|
+
init_log_io();
|
|
7681
7786
|
|
|
7682
7787
|
// src/log-baseline.ts
|
|
7788
|
+
init_log_types();
|
|
7683
7789
|
var semantic = (record) => portableConversationTokens([record])[0]?.semantic;
|
|
7684
7790
|
var isTool = (record) => record.role === "tool" || record.role === "assistant" && ("tool_calls" in record);
|
|
7685
7791
|
var omittedRole = (record) => ["system", "reasoning", "observation"].includes(record.role);
|
|
@@ -7971,7 +8077,7 @@ async function stageCapture(store, snapshot, requested) {
|
|
|
7971
8077
|
allowInitialSystem: baseline?.firstTarget,
|
|
7972
8078
|
origin: { harness: binding.harness, ...binding.nativeSessionId ? { nativeSessionId: binding.nativeSessionId } : {}, bindingId: binding.id }
|
|
7973
8079
|
});
|
|
7974
|
-
const { raw, canonical, attachments, ...header } = snapshot;
|
|
8080
|
+
const { raw, canonical, attachments, annotations: _annotations, ...header } = snapshot;
|
|
7975
8081
|
const generated = baseline?.derivedBinding ? readNativeExportSnapshot(await regenerateExport(store, frames, baseline.derivedBinding)) : undefined;
|
|
7976
8082
|
const derived = generated ? derivedChunks([generated.raw, ...(generated.attachments ?? []).map((a) => a.content)]) : undefined;
|
|
7977
8083
|
const prefix = baseline && raw.startsWith(baseline.snapshot.raw) ? {
|
|
@@ -8027,6 +8133,8 @@ async function validateStageBacking(store, stage, frames) {
|
|
|
8027
8133
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8028
8134
|
import { lstat, realpath as realpath4 } from "node:fs/promises";
|
|
8029
8135
|
import { resolve as resolve14 } from "node:path";
|
|
8136
|
+
init_log_types();
|
|
8137
|
+
init_log_io();
|
|
8030
8138
|
var manifestOf = (native) => ({ ...native, files: native.files.map((file) => ({
|
|
8031
8139
|
path: file.path,
|
|
8032
8140
|
sha256: sha2562(file.content),
|
|
@@ -8040,8 +8148,9 @@ async function preparedReceipt(store, frame) {
|
|
|
8040
8148
|
async function switchSession(store, id, target, requested) {
|
|
8041
8149
|
if (!requested || typeof requested !== "object" || !requested.write || typeof requested.write !== "object")
|
|
8042
8150
|
throw new TrajectoriesError("INVALID_INPUT", "Export options are required.");
|
|
8043
|
-
if (!
|
|
8044
|
-
throw new TrajectoriesError("INVALID_INPUT", "Unsupported target harness.");
|
|
8151
|
+
if (!isNativeHarness(target)) {
|
|
8152
|
+
throw new TrajectoriesError("INVALID_INPUT", HARNESSES.includes(target) ? `Unsupported target harness: ${target} is a read-only interchange format.` : "Unsupported target harness.");
|
|
8153
|
+
}
|
|
8045
8154
|
let cwd;
|
|
8046
8155
|
try {
|
|
8047
8156
|
cwd = await realpath4(resolve14(requested.write.cwd));
|
|
@@ -8202,8 +8311,10 @@ async function appendCapture(store, id, input, options) {
|
|
|
8202
8311
|
return commitCapture(store, ref, stage);
|
|
8203
8312
|
}
|
|
8204
8313
|
// src/migrate.ts
|
|
8314
|
+
init_log_types();
|
|
8205
8315
|
import { lstat as lstat2, mkdir as mkdir4, readFile as readFile9, realpath as realpath5 } from "node:fs/promises";
|
|
8206
8316
|
import { dirname as dirname5, join as join10, resolve as resolve15 } from "node:path";
|
|
8317
|
+
init_log_io();
|
|
8207
8318
|
function validateSnapshot(snapshot) {
|
|
8208
8319
|
try {
|
|
8209
8320
|
assertPortableSnapshot(snapshot);
|
|
@@ -8379,15 +8490,983 @@ async function migrateLegacyStore(source, destination, options) {
|
|
|
8379
8490
|
throw new TrajectoriesError("INTEGRITY_ERROR", cause instanceof Error ? cause.message : "Legacy migration failed; preserve the source and partial destination.", { cause });
|
|
8380
8491
|
}
|
|
8381
8492
|
}
|
|
8493
|
+
// src/shared.ts
|
|
8494
|
+
init_log_types();
|
|
8495
|
+
init_log_io();
|
|
8496
|
+
import { timingSafeEqual } from "node:crypto";
|
|
8497
|
+
|
|
8498
|
+
// src/shared-managed.ts
|
|
8499
|
+
init_log_types();
|
|
8500
|
+
init_log_io();
|
|
8501
|
+
import { open as open3, readFile as readFile10, rm as rm3 } from "node:fs/promises";
|
|
8502
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4 } from "node:path";
|
|
8503
|
+
function fail5(message) {
|
|
8504
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", message);
|
|
8505
|
+
}
|
|
8506
|
+
var equal = (a, b) => stableJson(a) === stableJson(b);
|
|
8507
|
+
function hash2(value) {
|
|
8508
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value))
|
|
8509
|
+
fail5("Invalid shared content hash.");
|
|
8510
|
+
}
|
|
8511
|
+
async function managedObjectIds(store, frames, lineage) {
|
|
8512
|
+
const ids = new Set;
|
|
8513
|
+
async function object(id, derived) {
|
|
8514
|
+
hash2(id);
|
|
8515
|
+
try {
|
|
8516
|
+
const bytes = await getObject(store, id);
|
|
8517
|
+
ids.add(id);
|
|
8518
|
+
return bytes;
|
|
8519
|
+
} catch (error) {
|
|
8520
|
+
if (error.cause?.code !== "ENOENT" || !derived?.has(id))
|
|
8521
|
+
throw error;
|
|
8522
|
+
return getObject(store, id, derived);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
async function text(ref, derived) {
|
|
8526
|
+
if (!ref?.root)
|
|
8527
|
+
fail5("Missing text reference.");
|
|
8528
|
+
await getText(store, ref, derived);
|
|
8529
|
+
const visiting = new Set;
|
|
8530
|
+
async function visit(part) {
|
|
8531
|
+
if (!part || visiting.has(part.id))
|
|
8532
|
+
fail5("Invalid shared content tree.");
|
|
8533
|
+
const bytes = await object(part.id, derived);
|
|
8534
|
+
if (part.tree) {
|
|
8535
|
+
visiting.add(part.id);
|
|
8536
|
+
const children = JSON.parse(bytes.toString());
|
|
8537
|
+
if (!Array.isArray(children) || !children.length || children.length > 32)
|
|
8538
|
+
fail5("Invalid shared content tree.");
|
|
8539
|
+
for (const child of children)
|
|
8540
|
+
await visit(child);
|
|
8541
|
+
visiting.delete(part.id);
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
await visit(ref.root);
|
|
8545
|
+
}
|
|
8546
|
+
let accepted = lineage;
|
|
8547
|
+
async function evidence(value) {
|
|
8548
|
+
let derived;
|
|
8549
|
+
if (value.derivedBinding) {
|
|
8550
|
+
accepted ??= await readFrames(store, frames[0].body.session.id);
|
|
8551
|
+
const native = await regenerateExport(store, accepted, value.derivedBinding);
|
|
8552
|
+
const snapshot = readNativeExportSnapshot(native);
|
|
8553
|
+
derived = derivedChunks([snapshot.raw, ...(snapshot.attachments ?? []).map((item) => item.content)]);
|
|
8554
|
+
}
|
|
8555
|
+
await text(value.raw, derived);
|
|
8556
|
+
for (const attachment of value.attachments)
|
|
8557
|
+
await text(attachment.text, derived);
|
|
8558
|
+
}
|
|
8559
|
+
for (const frame of frames) {
|
|
8560
|
+
await text(frame.body.diagnostics);
|
|
8561
|
+
for (const record of frame.body.records)
|
|
8562
|
+
await text(JSON.parse((await object(record.object)).toString()));
|
|
8563
|
+
if (frame.body.capture) {
|
|
8564
|
+
hash2(frame.body.stage);
|
|
8565
|
+
const stage = JSON.parse((await object(frame.body.stage)).toString());
|
|
8566
|
+
await evidence(stage.evidence);
|
|
8567
|
+
await text(stage.diagnostics);
|
|
8568
|
+
for (const record of stage.records)
|
|
8569
|
+
await text(JSON.parse((await object(record.object)).toString()));
|
|
8570
|
+
}
|
|
8571
|
+
if (frame.body.legacyBaseline)
|
|
8572
|
+
await evidence(frame.body.legacyBaseline);
|
|
8573
|
+
}
|
|
8574
|
+
return [...ids].sort();
|
|
8575
|
+
}
|
|
8576
|
+
async function exportManagedDelta(store, id, options = {}) {
|
|
8577
|
+
safeId(id);
|
|
8578
|
+
const accepted = await readFrames(store, id), current = accepted.at(-1);
|
|
8579
|
+
if (!current)
|
|
8580
|
+
throw new TrajectoriesError("NOT_FOUND", "Managed session does not exist.");
|
|
8581
|
+
const afterHead = options.afterHead ?? null;
|
|
8582
|
+
const index = afterHead === null ? -1 : accepted.findIndex((frame) => frame.head === afterHead);
|
|
8583
|
+
if (afterHead !== null && index < 0)
|
|
8584
|
+
throw new TrajectoriesError("STALE_HEAD", "Requested shared baseline is not an ancestor.");
|
|
8585
|
+
const frames = accepted.slice(index + 1);
|
|
8586
|
+
const captures = [], intents = [];
|
|
8587
|
+
for (const frame of frames) {
|
|
8588
|
+
const data = await readIntent(store, id, frame.body.intentId);
|
|
8589
|
+
if (!data || data.input !== frame.body.input)
|
|
8590
|
+
fail5("Committed frame has no matching durable intent.");
|
|
8591
|
+
intents.push({ intentId: frame.body.intentId, data });
|
|
8592
|
+
if (frame.body.capture) {
|
|
8593
|
+
const pointer = JSON.parse(await readFile10(capturePath(store, frame.body.capture.id), "utf8"));
|
|
8594
|
+
if (pointer.object !== frame.body.stage)
|
|
8595
|
+
fail5("Capture pointer disagrees with its committed frame.");
|
|
8596
|
+
captures.push({ id: frame.body.capture.id, object: pointer.object });
|
|
8597
|
+
}
|
|
8598
|
+
}
|
|
8599
|
+
return {
|
|
8600
|
+
version: 1,
|
|
8601
|
+
sessionId: id,
|
|
8602
|
+
afterHead,
|
|
8603
|
+
head: current.head,
|
|
8604
|
+
frames,
|
|
8605
|
+
captures,
|
|
8606
|
+
intents,
|
|
8607
|
+
objectIds: await managedObjectIds(store, frames)
|
|
8608
|
+
};
|
|
8609
|
+
}
|
|
8610
|
+
function validateEnvelope(delta, expectedHead) {
|
|
8611
|
+
if (!delta || delta.version !== 1 || !Array.isArray(delta.frames) || !Array.isArray(delta.captures) || !Array.isArray(delta.intents) || !Array.isArray(delta.objectIds))
|
|
8612
|
+
fail5("Invalid managed transfer.");
|
|
8613
|
+
safeId(delta.sessionId);
|
|
8614
|
+
hash2(delta.head);
|
|
8615
|
+
if (expectedHead !== null)
|
|
8616
|
+
hash2(expectedHead);
|
|
8617
|
+
if (delta.afterHead !== expectedHead)
|
|
8618
|
+
throw new TrajectoriesError("STALE_HEAD", "Transfer baseline differs from requested head.");
|
|
8619
|
+
let parent = delta.afterHead;
|
|
8620
|
+
const intents = new Set;
|
|
8621
|
+
for (const frame of delta.frames) {
|
|
8622
|
+
if (!frame?.body || frame.body.session?.id !== delta.sessionId || frame.body.parent !== parent || digest2(stableJson(frame.body)) !== frame.head)
|
|
8623
|
+
fail5("Shared log chain is corrupt.");
|
|
8624
|
+
hash2(frame.head);
|
|
8625
|
+
hash2(frame.body.input);
|
|
8626
|
+
safeId(frame.body.intentId);
|
|
8627
|
+
if (intents.has(frame.body.intentId))
|
|
8628
|
+
fail5("Repeated shared intent.");
|
|
8629
|
+
intents.add(frame.body.intentId);
|
|
8630
|
+
if (!isAbsolute4(frame.body.session.workspace) || !Number.isFinite(Date.parse(frame.body.session.createdAt)) || !Number.isFinite(Date.parse(frame.body.time)) || !Array.isArray(frame.body.records) || typeof frame.body.progress !== "boolean" || !HARNESSES.includes(frame.body.binding?.harness) || typeof frame.body.binding.nativeSessionId !== "string")
|
|
8631
|
+
fail5("Invalid managed frame metadata.");
|
|
8632
|
+
safeId(frame.body.binding.id);
|
|
8633
|
+
checkpointLimits(frame.body.session.nativeCheckpoints);
|
|
8634
|
+
if (frame.body.kind !== "capture" && frame.body.kind !== "binding")
|
|
8635
|
+
fail5("Unknown managed frame kind.");
|
|
8636
|
+
parent = frame.head;
|
|
8637
|
+
}
|
|
8638
|
+
if (parent !== delta.head)
|
|
8639
|
+
fail5("Transfer head does not match its suffix.");
|
|
8640
|
+
if (new Set(delta.objectIds).size !== delta.objectIds.length)
|
|
8641
|
+
fail5("Duplicate shared object declaration.");
|
|
8642
|
+
for (const id of delta.objectIds)
|
|
8643
|
+
hash2(id);
|
|
8644
|
+
if (delta.intents.length !== delta.frames.length || new Set(delta.intents.map((item) => item.intentId)).size !== delta.intents.length)
|
|
8645
|
+
fail5("Transfer intent inventory is incomplete.");
|
|
8646
|
+
const captures = delta.frames.filter((frame) => frame.body.capture);
|
|
8647
|
+
if (delta.captures.length !== captures.length || new Set(delta.captures.map((item) => item.id)).size !== delta.captures.length)
|
|
8648
|
+
fail5("Transfer capture inventory is incomplete.");
|
|
8649
|
+
for (const frame of delta.frames) {
|
|
8650
|
+
const intent = delta.intents.find((item) => item.intentId === frame.body.intentId)?.data;
|
|
8651
|
+
if (!intent || intent.input !== frame.body.input)
|
|
8652
|
+
fail5("Transfer intent differs from its frame.");
|
|
8653
|
+
if (frame.body.kind === "capture") {
|
|
8654
|
+
const ref = frame.body.capture;
|
|
8655
|
+
if (!ref || !frame.body.stage || intent.stage !== frame.body.stage || intent.export || ref.sessionId !== delta.sessionId || ref.parent !== frame.body.parent || ref.intentId !== frame.body.intentId || ref.integrity !== frame.body.input || ref.bindingId !== frame.body.binding.id || ref.source.harness !== frame.body.binding.harness || (ref.source.nativeSessionId ?? "") !== frame.body.binding.nativeSessionId)
|
|
8656
|
+
fail5("Capture frame has inconsistent provenance.");
|
|
8657
|
+
safeId(ref.id);
|
|
8658
|
+
hash2(frame.body.stage);
|
|
8659
|
+
if (delta.captures.find((item) => item.id === ref.id)?.object !== frame.body.stage)
|
|
8660
|
+
fail5("Capture pointer differs from its frame.");
|
|
8661
|
+
} else if (frame.body.capture || frame.body.stage || frame.body.records.length || frame.body.edit || frame.body.progress || !!frame.body.recipe === !!frame.body.legacyBaseline || !equal(intent.export ?? null, frame.body.recipe ?? null) || intent.stage) {
|
|
8662
|
+
fail5("Prepared binding frame has inconsistent retained input.");
|
|
8663
|
+
}
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8666
|
+
function validateManagedDelta(delta, expectedHead) {
|
|
8667
|
+
validateEnvelope(delta, expectedHead);
|
|
8668
|
+
}
|
|
8669
|
+
async function validateCapture(store, frame, before) {
|
|
8670
|
+
const stage = await loadStage(store, frame.body.capture);
|
|
8671
|
+
if (stage.version !== 2 || stage.options.sessionId !== frame.body.session.id || stage.options.intentId !== frame.body.intentId || stage.options.expectedHead !== frame.body.parent || stage.input !== frame.body.input || !equal(stage.ref, frame.body.capture) || !equal(stage.records, frame.body.records) || !equal(stage.edit, frame.body.edit) || !equal(stage.binding, frame.body.binding) || stage.progress !== frame.body.progress || !equal(stage.diagnostics, frame.body.diagnostics) || !equal(stage.nativeMeta ?? null, frame.body.nativeMeta ?? null))
|
|
8672
|
+
fail5("Committed capture does not match its retained stage.");
|
|
8673
|
+
if (frame.body.parent === null && (stage.options.expectedHead !== null || stage.options.workspace !== frame.body.session.workspace || !equal(stage.options.nativeCheckpoints, frame.body.session.nativeCheckpoints)))
|
|
8674
|
+
fail5("Initial capture session options differ.");
|
|
8675
|
+
if (typeof stage.options.requireProgress !== "boolean")
|
|
8676
|
+
fail5("Invalid capture progress policy.");
|
|
8677
|
+
await validateStageBacking(store, stage, [...before, frame]);
|
|
8678
|
+
const snapshot = await recoverSnapshot(store, stage, [...before, frame]);
|
|
8679
|
+
assertSnapshot(snapshot);
|
|
8680
|
+
const source = {
|
|
8681
|
+
harness: snapshot.source.harness,
|
|
8682
|
+
sha256: digest2(snapshot.raw),
|
|
8683
|
+
...snapshot.source.nativeSessionId ? { nativeSessionId: snapshot.source.nativeSessionId } : {}
|
|
8684
|
+
};
|
|
8685
|
+
if (!equal(stage.ref.source, source))
|
|
8686
|
+
fail5("Capture source fingerprint differs from recovered native evidence.");
|
|
8687
|
+
const diagnostics = JSON.parse(await getText(store, stage.diagnostics));
|
|
8688
|
+
const meta = snapshot.canonical.records.find((record) => record.record_type === "meta");
|
|
8689
|
+
if (!equal(diagnostics, snapshot.canonical.diagnostics) || !equal(stage.nativeMeta ?? null, meta ? JSON.parse(meta.record_json) : null))
|
|
8690
|
+
fail5("Capture diagnostics or native metadata differ from recovered evidence.");
|
|
8691
|
+
const operation = frame.body.parent === null ? "create" : "append";
|
|
8692
|
+
if (digest2(stableJson({ operation, options: stage.options, snapshot: snapshot.id })) !== stage.input)
|
|
8693
|
+
fail5("Capture intent does not bind its recovered native observation.");
|
|
8694
|
+
const baseline = before.length ? await captureBaseline(store, before, snapshot, stage.options.expectedHead === null ? undefined : stage.options.bindingId) : undefined;
|
|
8695
|
+
if (baseline)
|
|
8696
|
+
assertSnapshotContinuation(baseline.snapshot, snapshot, { requireProgress: stage.options.requireProgress, allowInitialSystem: baseline.firstTarget });
|
|
8697
|
+
const planned = await planRecords({
|
|
8698
|
+
store,
|
|
8699
|
+
sessionId: frame.body.session.id,
|
|
8700
|
+
intentId: frame.body.intentId,
|
|
8701
|
+
observed: snapshot,
|
|
8702
|
+
before: baseline?.before ?? [],
|
|
8703
|
+
beforeIds: baseline?.ids ?? [],
|
|
8704
|
+
beforeActive: baseline ? units(baseline.snapshot) : [],
|
|
8705
|
+
beforeActiveIds: baseline?.activeIds ?? [],
|
|
8706
|
+
context: selection(before).context,
|
|
8707
|
+
allowInitialSystem: baseline?.firstTarget,
|
|
8708
|
+
origin: { harness: stage.binding.harness, ...stage.binding.nativeSessionId ? { nativeSessionId: stage.binding.nativeSessionId } : {}, bindingId: stage.binding.id }
|
|
8709
|
+
});
|
|
8710
|
+
const mapping = { ...baseline?.mapping, added: planned.added, ...planned.leading ? { leading: planned.leading } : {} };
|
|
8711
|
+
if (!equal(planned.records, stage.records) || !equal(planned.edit, stage.edit) || planned.progress !== stage.progress || !equal(mapping, stage.mapping) || baseline && !equal(baseline.binding, stage.binding))
|
|
8712
|
+
fail5("Capture records or context do not reproduce from native evidence.");
|
|
8713
|
+
}
|
|
8714
|
+
async function importManagedDelta(store, delta, options) {
|
|
8715
|
+
validateEnvelope(delta, options?.expectedHead);
|
|
8716
|
+
return withSessionLock(store, delta.sessionId, async () => {
|
|
8717
|
+
await initializeLog(store);
|
|
8718
|
+
const existing = await readFrames(store, delta.sessionId), current = existing.at(-1);
|
|
8719
|
+
const replay = current?.head === delta.head;
|
|
8720
|
+
if (replay) {
|
|
8721
|
+
const index = delta.afterHead === null ? -1 : existing.findIndex((frame) => frame.head === delta.afterHead);
|
|
8722
|
+
if (delta.afterHead !== null && index < 0 || !equal(existing.slice(index + 1), delta.frames))
|
|
8723
|
+
throw new TrajectoriesError("INTENT_CONFLICT", "Shared replay does not match the exact committed transition.");
|
|
8724
|
+
} else if ((current?.head ?? null) !== options.expectedHead) {
|
|
8725
|
+
throw new TrajectoriesError("STALE_HEAD", "Shared session advanced or diverged.");
|
|
8726
|
+
}
|
|
8727
|
+
const before = replay ? existing.slice(0, existing.length - delta.frames.length) : existing;
|
|
8728
|
+
const seen = new Set(before.map((frame) => frame.body.intentId));
|
|
8729
|
+
const combined = [...before];
|
|
8730
|
+
for (const frame of delta.frames) {
|
|
8731
|
+
if (seen.has(frame.body.intentId))
|
|
8732
|
+
throw new TrajectoriesError("INTENT_CONFLICT", "Intent already belongs to another accepted frame.");
|
|
8733
|
+
seen.add(frame.body.intentId);
|
|
8734
|
+
if (combined[0] && !equal(combined[0].body.session, frame.body.session))
|
|
8735
|
+
fail5("Immutable session metadata changed.");
|
|
8736
|
+
if (!combined.length && (frame.body.kind !== "capture" || frame.body.session.createdAt !== frame.body.time))
|
|
8737
|
+
fail5("Invalid initial session frame.");
|
|
8738
|
+
combined.push(frame);
|
|
8739
|
+
}
|
|
8740
|
+
selection(combined);
|
|
8741
|
+
for (const id of delta.objectIds)
|
|
8742
|
+
await getObject(store, id);
|
|
8743
|
+
const createdPointers = [];
|
|
8744
|
+
let publicationStarted = false;
|
|
8745
|
+
try {
|
|
8746
|
+
for (const pointer of delta.captures) {
|
|
8747
|
+
const path = capturePath(store, pointer.id);
|
|
8748
|
+
try {
|
|
8749
|
+
if (!equal(JSON.parse(await readFile10(path, "utf8")), { object: pointer.object }))
|
|
8750
|
+
fail5("Immutable capture pointer conflicts.");
|
|
8751
|
+
} catch (error) {
|
|
8752
|
+
if (error.code !== "ENOENT")
|
|
8753
|
+
throw error;
|
|
8754
|
+
await atomicWrite(path, stableJson({ object: pointer.object }), true);
|
|
8755
|
+
createdPointers.push(path);
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8758
|
+
for (const item of delta.intents) {
|
|
8759
|
+
const previous = await readIntent(store, delta.sessionId, item.intentId);
|
|
8760
|
+
if (previous && !equal(previous, item.data))
|
|
8761
|
+
throw new TrajectoriesError("INTENT_CONFLICT", "Durable intent has different retained input.");
|
|
8762
|
+
}
|
|
8763
|
+
await managedObjectIds(store, combined, combined);
|
|
8764
|
+
const validated = [...before];
|
|
8765
|
+
for (const frame of delta.frames) {
|
|
8766
|
+
if (frame.body.kind === "capture")
|
|
8767
|
+
await validateCapture(store, frame, validated);
|
|
8768
|
+
else {
|
|
8769
|
+
if (frame.body.recipe && (frame.body.recipe.atHead !== frame.body.parent || frame.body.recipe.write.cwd !== frame.body.session.workspace || frame.body.recipe.seed !== digest2(stableJson([frame.body.input, "native-entropy"]))))
|
|
8770
|
+
fail5("Prepared export recipe is inconsistent.");
|
|
8771
|
+
const snapshot = await bindingSnapshot(store, [...validated, frame], frame.body.binding.id);
|
|
8772
|
+
assertSnapshot(snapshot);
|
|
8773
|
+
if (snapshot.source.harness !== frame.body.binding.harness || snapshot.source.nativeSessionId !== frame.body.binding.nativeSessionId)
|
|
8774
|
+
fail5("Prepared binding native identity differs.");
|
|
8775
|
+
const native = frame.body.recipe ? await regenerateExport(store, [...validated, frame], frame.body.binding.id) : undefined;
|
|
8776
|
+
const diagnostics = JSON.parse(await getText(store, frame.body.diagnostics));
|
|
8777
|
+
const meta = snapshot.canonical.records.find((record) => record.record_type === "meta");
|
|
8778
|
+
if (!equal(diagnostics, native?.diagnostics ?? snapshot.canonical.diagnostics) || !equal(frame.body.nativeMeta ?? null, meta ? JSON.parse(meta.record_json) : null))
|
|
8779
|
+
fail5("Prepared binding diagnostics or native metadata differ from retained evidence.");
|
|
8780
|
+
const selected = selection(validated), records = new Map(selected.history.map((record) => [record.id, record]));
|
|
8781
|
+
const context = await Promise.all(selected.context.map(async (id) => ({ id, record: await loadRecord(store, records.get(id)) })));
|
|
8782
|
+
mapPreparedRecords(context, units(snapshot, true));
|
|
8783
|
+
}
|
|
8784
|
+
validated.push(frame);
|
|
8785
|
+
}
|
|
8786
|
+
if (!validated.length)
|
|
8787
|
+
fail5("An empty transfer cannot create a session.");
|
|
8788
|
+
if (!replay && delta.frames.length) {
|
|
8789
|
+
for (const item of delta.intents)
|
|
8790
|
+
await saveIntent(store, delta.sessionId, item.intentId, item.data);
|
|
8791
|
+
publicationStarted = true;
|
|
8792
|
+
const path = logPath(store, delta.sessionId);
|
|
8793
|
+
await ensureDirectory(dirname6(path));
|
|
8794
|
+
const file = await open3(path, "a", 384);
|
|
8795
|
+
try {
|
|
8796
|
+
await file.writeFile(delta.frames.map((frame) => stableJson(frame) + `
|
|
8797
|
+
`).join(""));
|
|
8798
|
+
await file.sync();
|
|
8799
|
+
} finally {
|
|
8800
|
+
await file.close();
|
|
8801
|
+
}
|
|
8802
|
+
await syncDirectory(dirname6(path));
|
|
8803
|
+
}
|
|
8804
|
+
return sessionOf(validated.at(-1));
|
|
8805
|
+
} catch (error) {
|
|
8806
|
+
if (!publicationStarted)
|
|
8807
|
+
for (const path of createdPointers)
|
|
8808
|
+
await rm3(path);
|
|
8809
|
+
if (error instanceof TrajectoriesError)
|
|
8810
|
+
throw error;
|
|
8811
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared history validation or publication failed.", { cause: error });
|
|
8812
|
+
}
|
|
8813
|
+
});
|
|
8814
|
+
}
|
|
8815
|
+
|
|
8816
|
+
// src/shared-observed.ts
|
|
8817
|
+
init_log_types();
|
|
8818
|
+
import { readdir as readdir4, readFile as readFile11 } from "node:fs/promises";
|
|
8819
|
+
import { join as join11, resolve as resolve16 } from "node:path";
|
|
8820
|
+
init_log_io();
|
|
8821
|
+
function sharedObservationId(machineId, sessionId) {
|
|
8822
|
+
safeId(machineId);
|
|
8823
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 4096)
|
|
8824
|
+
throw new TrajectoriesError("INVALID_INPUT", "An indexed session identity is required.");
|
|
8825
|
+
return digest2(stableJson([machineId, sessionId]));
|
|
8826
|
+
}
|
|
8827
|
+
var hash3 = (value) => {
|
|
8828
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value))
|
|
8829
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared content identity.");
|
|
8830
|
+
return value;
|
|
8831
|
+
};
|
|
8832
|
+
var observationPath = (store, id) => join11(resolve16(store), "observations", `${hash3(id)}.jsonl`);
|
|
8833
|
+
function assertSession(value) {
|
|
8834
|
+
if (!value || typeof value !== "object" || typeof value.id !== "string" || !value.id || typeof value.source !== "string" || !value.source || typeof value.source_path !== "string" || typeof value.content_hash !== "string" || !Number.isSafeInteger(value.record_count) || value.record_count < 0 || typeof value.created_at !== "string" || typeof value.updated_at !== "string")
|
|
8835
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid canonical observed session metadata.");
|
|
8836
|
+
for (const key of ["title", "cwd", "model", "started_at", "ended_at"])
|
|
8837
|
+
if (value[key] !== null && typeof value[key] !== "string")
|
|
8838
|
+
throw new TrajectoriesError("INVALID_INPUT", `Invalid session ${key}.`);
|
|
8839
|
+
for (const key of ["parent_session_id", "parent_tool_use_id", "agent_nickname", "thread_role"])
|
|
8840
|
+
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string")
|
|
8841
|
+
throw new TrajectoriesError("INVALID_INPUT", `Invalid session ${key}.`);
|
|
8842
|
+
if (value.content_hash_version !== undefined && ![1, 2].includes(value.content_hash_version))
|
|
8843
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid observed hash version.");
|
|
8844
|
+
if (value.diagnostics !== undefined) {
|
|
8845
|
+
if (!Array.isArray(value.diagnostics))
|
|
8846
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid observed diagnostics.");
|
|
8847
|
+
const codes = new Set;
|
|
8848
|
+
for (const item of value.diagnostics) {
|
|
8849
|
+
if (!item || typeof item.code !== "string" || !item.code || codes.has(item.code) || !Number.isSafeInteger(item.occurrenceCount) || item.occurrenceCount < 0 || item.sample !== null && typeof item.sample !== "string" || item.firstSeenAt !== undefined && typeof item.firstSeenAt !== "string")
|
|
8850
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid observed diagnostic aggregate.");
|
|
8851
|
+
codes.add(item.code);
|
|
8852
|
+
}
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
function assertRecord(value, sessionId) {
|
|
8856
|
+
if (!value || typeof value !== "object" || typeof value.id !== "string" || !value.id || value.session_id !== sessionId || typeof value.source_type !== "string" || typeof value.record_type !== "string" || typeof value.source_order_id !== "string" || !Number.isSafeInteger(value.component_index) || value.component_index < 0 || typeof value.record_hash !== "string" || typeof value.content_hash !== "string" || typeof value.record_json !== "string")
|
|
8857
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid canonical observed record or session identity.");
|
|
8858
|
+
for (const key of [
|
|
8859
|
+
"content",
|
|
8860
|
+
"tool_call_id",
|
|
8861
|
+
"tool_name",
|
|
8862
|
+
"tool_arguments_json",
|
|
8863
|
+
"tool_result_json",
|
|
8864
|
+
"record_timestamp",
|
|
8865
|
+
"source_timestamp",
|
|
8866
|
+
"stable_source_record_id",
|
|
8867
|
+
"source_identity_kind"
|
|
8868
|
+
])
|
|
8869
|
+
if (value[key] !== null && typeof value[key] !== "string")
|
|
8870
|
+
throw new TrajectoriesError("INVALID_INPUT", `Invalid record ${key}.`);
|
|
8871
|
+
if (![null, 0, 1].includes(value.tool_result_ok))
|
|
8872
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid tool outcome.");
|
|
8873
|
+
if (value.source_identity_kind !== null && !["native", "location", "content", "synthetic"].includes(value.source_identity_kind))
|
|
8874
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid source identity kind.");
|
|
8875
|
+
for (const key of ["native_session_id", "binding_id"])
|
|
8876
|
+
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string")
|
|
8877
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid native lineage identity.");
|
|
8878
|
+
let parsed;
|
|
8879
|
+
try {
|
|
8880
|
+
parsed = JSON.parse(value.record_json);
|
|
8881
|
+
} catch {
|
|
8882
|
+
throw new TrajectoriesError("INVALID_INPUT", "Malformed canonical record JSON.");
|
|
8883
|
+
}
|
|
8884
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
8885
|
+
throw new TrajectoriesError("INVALID_INPUT", "Canonical record JSON must be an object.");
|
|
8886
|
+
try {
|
|
8887
|
+
validateTranscript([parsed], { partial: true });
|
|
8888
|
+
} catch {
|
|
8889
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid normalized canonical record.");
|
|
8890
|
+
}
|
|
8891
|
+
const record = parsed;
|
|
8892
|
+
if (value.record_type !== (record.role === "assistant" && record.tool_calls ? "assistant-tool-call" : record.role) || digest2(value.record_json) !== value.record_hash)
|
|
8893
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Canonical record type or digest disagrees with its payload.");
|
|
8894
|
+
}
|
|
8895
|
+
function assertObservedSummary(value, id) {
|
|
8896
|
+
if (!value || typeof value !== "object")
|
|
8897
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared observation.");
|
|
8898
|
+
assertSession(value.session);
|
|
8899
|
+
hash3(value.head);
|
|
8900
|
+
if (id !== undefined && value.id !== id || value.id !== sharedObservationId(value.machineId, value.session.id))
|
|
8901
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared observation identity mismatch.");
|
|
8902
|
+
}
|
|
8903
|
+
function assertObservedManifest(value, id) {
|
|
8904
|
+
assertObservedSummary(value, id);
|
|
8905
|
+
if (!Array.isArray(value.recordObjects))
|
|
8906
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared record manifest.");
|
|
8907
|
+
const ids = new Set;
|
|
8908
|
+
for (const ref of value.recordObjects) {
|
|
8909
|
+
if (!ref || typeof ref.id !== "string" || !ref.id || ids.has(ref.id))
|
|
8910
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared record identities.");
|
|
8911
|
+
ids.add(ref.id);
|
|
8912
|
+
hash3(ref.object);
|
|
8913
|
+
}
|
|
8914
|
+
if (ids.size !== value.session.record_count)
|
|
8915
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared manifest count mismatch.");
|
|
8916
|
+
}
|
|
8917
|
+
function assertObservedState(value, id) {
|
|
8918
|
+
assertObservedSummary(value, id);
|
|
8919
|
+
if (!Array.isArray(value.records))
|
|
8920
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared canonical records.");
|
|
8921
|
+
const ids = new Set;
|
|
8922
|
+
for (const record of value.records) {
|
|
8923
|
+
assertRecord(record, value.session.id);
|
|
8924
|
+
if (ids.has(record.id))
|
|
8925
|
+
throw new TrajectoriesError("INVALID_INPUT", "Duplicate shared canonical record.");
|
|
8926
|
+
ids.add(record.id);
|
|
8927
|
+
}
|
|
8928
|
+
if (ids.size !== value.session.record_count)
|
|
8929
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared record count mismatch.");
|
|
8930
|
+
}
|
|
8931
|
+
async function frames(store, id) {
|
|
8932
|
+
let text;
|
|
8933
|
+
try {
|
|
8934
|
+
text = await readFile11(observationPath(store, id), "utf8");
|
|
8935
|
+
} catch (error) {
|
|
8936
|
+
if (error.code === "ENOENT")
|
|
8937
|
+
return [];
|
|
8938
|
+
throw error;
|
|
8939
|
+
}
|
|
8940
|
+
if (!text.endsWith(`
|
|
8941
|
+
`))
|
|
8942
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Partial observed-history log.");
|
|
8943
|
+
const output = [];
|
|
8944
|
+
for (const line of text.trimEnd().split(`
|
|
8945
|
+
`)) {
|
|
8946
|
+
let frame;
|
|
8947
|
+
try {
|
|
8948
|
+
frame = JSON.parse(line);
|
|
8949
|
+
} catch {
|
|
8950
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid observed-history JSON.");
|
|
8951
|
+
}
|
|
8952
|
+
const body = frame?.body;
|
|
8953
|
+
if (!body || body.version !== 1 || body.id !== id || sharedObservationId(body.machineId, body.sessionId) !== id || body.parent !== (output.at(-1)?.head ?? null) || digest2(stableJson(body)) !== frame.head || !Array.isArray(body.set) || !Array.isArray(body.remove))
|
|
8954
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid observed-history chain.");
|
|
8955
|
+
output.push(frame);
|
|
8956
|
+
}
|
|
8957
|
+
return output;
|
|
8958
|
+
}
|
|
8959
|
+
async function materialize(store, id, history) {
|
|
8960
|
+
const current = history.at(-1);
|
|
8961
|
+
if (!current)
|
|
8962
|
+
throw new TrajectoriesError("NOT_FOUND", "Shared observed session not found.");
|
|
8963
|
+
const refs = new Map;
|
|
8964
|
+
for (const { body } of history) {
|
|
8965
|
+
for (const key of body.remove)
|
|
8966
|
+
refs.delete(key);
|
|
8967
|
+
for (const item of body.set)
|
|
8968
|
+
refs.set(item.id, hash3(item.object));
|
|
8969
|
+
}
|
|
8970
|
+
const session = JSON.parse((await getObject(store, hash3(current.body.sessionObject))).toString());
|
|
8971
|
+
assertSession(session);
|
|
8972
|
+
if (session.id !== current.body.sessionId || session.record_count !== refs.size)
|
|
8973
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Observed session metadata disagrees with its record view.");
|
|
8974
|
+
return {
|
|
8975
|
+
id,
|
|
8976
|
+
head: current.head,
|
|
8977
|
+
machineId: current.body.machineId,
|
|
8978
|
+
session,
|
|
8979
|
+
recordObjects: [...refs].map(([recordId, object]) => ({ id: recordId, object }))
|
|
8980
|
+
};
|
|
8981
|
+
}
|
|
8982
|
+
async function getObservedManifest(store, id) {
|
|
8983
|
+
return materialize(store, id, await frames(store, id));
|
|
8984
|
+
}
|
|
8985
|
+
async function readObservedHistory(store, id) {
|
|
8986
|
+
const { recordObjects, ...summary } = await getObservedManifest(store, id);
|
|
8987
|
+
const records = [];
|
|
8988
|
+
for (const ref of recordObjects) {
|
|
8989
|
+
const record = JSON.parse((await getObject(store, ref.object)).toString());
|
|
8990
|
+
assertRecord(record, summary.session.id);
|
|
8991
|
+
if (record.id !== ref.id)
|
|
8992
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Observed record identity changed.");
|
|
8993
|
+
records.push(record);
|
|
8994
|
+
}
|
|
8995
|
+
records.sort((a, b) => a.source_order_id < b.source_order_id ? -1 : a.source_order_id > b.source_order_id ? 1 : a.component_index - b.component_index || a.id.localeCompare(b.id));
|
|
8996
|
+
return { ...summary, records };
|
|
8997
|
+
}
|
|
8998
|
+
async function listObservedHistories(store) {
|
|
8999
|
+
let names;
|
|
9000
|
+
try {
|
|
9001
|
+
names = await readdir4(join11(resolve16(store), "observations"));
|
|
9002
|
+
} catch (error) {
|
|
9003
|
+
if (error.code === "ENOENT")
|
|
9004
|
+
return [];
|
|
9005
|
+
throw error;
|
|
9006
|
+
}
|
|
9007
|
+
const output = [];
|
|
9008
|
+
for (const name of names.sort()) {
|
|
9009
|
+
if (!/^[a-f0-9]{64}\.jsonl$/.test(name))
|
|
9010
|
+
continue;
|
|
9011
|
+
const { recordObjects, ...summary } = await getObservedManifest(store, name.slice(0, -6));
|
|
9012
|
+
output.push(summary);
|
|
9013
|
+
}
|
|
9014
|
+
return output;
|
|
9015
|
+
}
|
|
9016
|
+
async function commitObservedDelta(store, delta) {
|
|
9017
|
+
if (!delta || delta.version !== 1 || delta.id !== sharedObservationId(delta.machineId, delta.sessionId) || delta.parent !== null && hash3(delta.parent) !== delta.parent || !Array.isArray(delta.set) || !Array.isArray(delta.remove))
|
|
9018
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid observed-history delta.");
|
|
9019
|
+
hash3(delta.sessionObject);
|
|
9020
|
+
const keys = new Set;
|
|
9021
|
+
for (const item of delta.set) {
|
|
9022
|
+
if (!item || typeof item.id !== "string" || !item.id || keys.has(item.id))
|
|
9023
|
+
throw new TrajectoriesError("INVALID_INPUT", "Duplicate or invalid record identity.");
|
|
9024
|
+
keys.add(item.id);
|
|
9025
|
+
hash3(item.object);
|
|
9026
|
+
}
|
|
9027
|
+
for (const key of delta.remove) {
|
|
9028
|
+
if (typeof key !== "string" || !key || keys.has(key))
|
|
9029
|
+
throw new TrajectoriesError("INVALID_INPUT", "Duplicate or contradictory record changes.");
|
|
9030
|
+
keys.add(key);
|
|
9031
|
+
}
|
|
9032
|
+
return withSessionLock(store, delta.id, async () => {
|
|
9033
|
+
const history = await frames(store, delta.id);
|
|
9034
|
+
const head = digest2(stableJson(delta));
|
|
9035
|
+
if (history.at(-1)?.head === head) {
|
|
9036
|
+
const { recordObjects, ...summary } = await materialize(store, delta.id, history);
|
|
9037
|
+
return summary;
|
|
9038
|
+
}
|
|
9039
|
+
if ((history.at(-1)?.head ?? null) !== delta.parent)
|
|
9040
|
+
throw new TrajectoriesError("STALE_HEAD", "Shared observed history advanced; refresh before publishing.");
|
|
9041
|
+
for (const item of delta.set) {
|
|
9042
|
+
const record = JSON.parse((await getObject(store, item.object)).toString());
|
|
9043
|
+
assertRecord(record, delta.sessionId);
|
|
9044
|
+
if (record.id !== item.id)
|
|
9045
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Observed record reference has a different identity.");
|
|
9046
|
+
}
|
|
9047
|
+
const prospective = [...history, { head, body: delta }];
|
|
9048
|
+
const manifest = await materialize(store, delta.id, prospective);
|
|
9049
|
+
const { open } = await import("node:fs/promises");
|
|
9050
|
+
await Promise.resolve().then(() => init_log_io());
|
|
9051
|
+
const path = observationPath(store, delta.id);
|
|
9052
|
+
await ensureDirectory(join11(resolve16(store), "observations"));
|
|
9053
|
+
const file = await open(path, "a", 384);
|
|
9054
|
+
try {
|
|
9055
|
+
await file.writeFile(stableJson({ head, body: delta }) + `
|
|
9056
|
+
`);
|
|
9057
|
+
await file.sync();
|
|
9058
|
+
} finally {
|
|
9059
|
+
await file.close();
|
|
9060
|
+
}
|
|
9061
|
+
await syncDirectory(join11(resolve16(store), "observations"));
|
|
9062
|
+
const { recordObjects, ...summary } = manifest;
|
|
9063
|
+
return summary;
|
|
9064
|
+
});
|
|
9065
|
+
}
|
|
9066
|
+
async function prepareObservedHistory(input, previous) {
|
|
9067
|
+
assertSession(input.session);
|
|
9068
|
+
const id = sharedObservationId(input.machineId, input.session.id);
|
|
9069
|
+
if (previous)
|
|
9070
|
+
assertObservedManifest(previous, id);
|
|
9071
|
+
if (previous && (previous.id !== id || previous.machineId !== input.machineId || previous.session.id !== input.session.id))
|
|
9072
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Remote observation identity mismatch.");
|
|
9073
|
+
const prior = new Map(previous?.recordObjects.map((item) => [item.id, item.object]) ?? []);
|
|
9074
|
+
const objects = new Map;
|
|
9075
|
+
const add = (value) => {
|
|
9076
|
+
const bytes = Buffer.from(stableJson(value));
|
|
9077
|
+
const key = digest2(bytes);
|
|
9078
|
+
objects.set(key, bytes);
|
|
9079
|
+
return key;
|
|
9080
|
+
};
|
|
9081
|
+
const sessionObject = add(input.session);
|
|
9082
|
+
const seen = new Set;
|
|
9083
|
+
const set = [];
|
|
9084
|
+
for (const record of input.records) {
|
|
9085
|
+
assertRecord(record, input.session.id);
|
|
9086
|
+
if (seen.has(record.id))
|
|
9087
|
+
throw new TrajectoriesError("INVALID_INPUT", "Duplicate canonical record ID.");
|
|
9088
|
+
seen.add(record.id);
|
|
9089
|
+
const key = digest2(stableJson(record));
|
|
9090
|
+
if (prior.get(record.id) !== key) {
|
|
9091
|
+
add(record);
|
|
9092
|
+
set.push({ id: record.id, object: key });
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
if (input.session.record_count !== seen.size)
|
|
9096
|
+
throw new TrajectoriesError("INVALID_INPUT", "Session record count disagrees with canonical records.");
|
|
9097
|
+
const remove = [...prior.keys()].filter((key) => !seen.has(key));
|
|
9098
|
+
if (previous && !set.length && !remove.length && stableJson(previous.session) === stableJson(input.session))
|
|
9099
|
+
return { delta: null, objects: new Map, changedRecords: 0, removedRecords: 0 };
|
|
9100
|
+
return { delta: {
|
|
9101
|
+
version: 1,
|
|
9102
|
+
id,
|
|
9103
|
+
parent: previous?.head ?? null,
|
|
9104
|
+
machineId: input.machineId,
|
|
9105
|
+
sessionId: input.session.id,
|
|
9106
|
+
sessionObject,
|
|
9107
|
+
set,
|
|
9108
|
+
remove
|
|
9109
|
+
}, objects, changedRecords: set.length, removedRecords: remove.length };
|
|
9110
|
+
}
|
|
9111
|
+
|
|
9112
|
+
// src/shared.ts
|
|
9113
|
+
var MAX_BYTES = 64 * 1024 * 1024;
|
|
9114
|
+
function tokenBytes(token) {
|
|
9115
|
+
if (typeof token !== "string" || !token || /[\s\x00-\x1f\x7f]/u.test(token) || token.length > 4096)
|
|
9116
|
+
throw new TrajectoriesError("INVALID_INPUT", "A nonempty shared-store token is required.");
|
|
9117
|
+
return Buffer.from(`Bearer ${token}`);
|
|
9118
|
+
}
|
|
9119
|
+
async function boundedBody(message, limit) {
|
|
9120
|
+
const declared = message.headers.get("content-length");
|
|
9121
|
+
if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > limit))
|
|
9122
|
+
throw new TrajectoriesError("CAPACITY_EXCEEDED", "Shared transfer exceeds the admitted byte limit.");
|
|
9123
|
+
const reader = message.body?.getReader();
|
|
9124
|
+
if (!reader)
|
|
9125
|
+
return new Uint8Array;
|
|
9126
|
+
let size = 0;
|
|
9127
|
+
const chunks = [];
|
|
9128
|
+
try {
|
|
9129
|
+
for (;; ) {
|
|
9130
|
+
const item = await reader.read();
|
|
9131
|
+
if (item.done)
|
|
9132
|
+
break;
|
|
9133
|
+
size += item.value.byteLength;
|
|
9134
|
+
if (size > limit)
|
|
9135
|
+
throw new TrajectoriesError("CAPACITY_EXCEEDED", "Shared transfer exceeds the admitted byte limit.");
|
|
9136
|
+
chunks.push(item.value);
|
|
9137
|
+
}
|
|
9138
|
+
} catch (error) {
|
|
9139
|
+
await reader.cancel().catch(() => {});
|
|
9140
|
+
throw error;
|
|
9141
|
+
} finally {
|
|
9142
|
+
reader.releaseLock();
|
|
9143
|
+
}
|
|
9144
|
+
return Buffer.concat(chunks, size);
|
|
9145
|
+
}
|
|
9146
|
+
var json2 = (value, status = 200) => new Response(JSON.stringify(value), {
|
|
9147
|
+
status,
|
|
9148
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" }
|
|
9149
|
+
});
|
|
9150
|
+
var decode3 = (bytes) => {
|
|
9151
|
+
try {
|
|
9152
|
+
return JSON.parse(Buffer.from(bytes).toString());
|
|
9153
|
+
} catch {
|
|
9154
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared-store JSON.");
|
|
9155
|
+
}
|
|
9156
|
+
};
|
|
9157
|
+
function missingObject(error) {
|
|
9158
|
+
return error.cause?.code === "ENOENT";
|
|
9159
|
+
}
|
|
9160
|
+
function assertManagedSummary(value, id) {
|
|
9161
|
+
if (!value || value.version !== 2 || typeof value.workspace !== "string" || !value.workspace || typeof value.head !== "string" || !/^[a-f0-9]{64}$/.test(value.head) || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string" || !value.nativeCheckpoints || !Number.isSafeInteger(value.nativeCheckpoints.maxCount) || value.nativeCheckpoints.maxCount < 0 || !Number.isSafeInteger(value.nativeCheckpoints.maxBytes) || value.nativeCheckpoints.maxBytes < 0)
|
|
9162
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid shared managed session metadata.");
|
|
9163
|
+
safeId(value.id);
|
|
9164
|
+
if (id !== undefined && value.id !== id)
|
|
9165
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared managed session identity mismatch.");
|
|
9166
|
+
}
|
|
9167
|
+
function createSharedHandler(options) {
|
|
9168
|
+
const expected = tokenBytes(options.token), limit = options.maxBytes ?? MAX_BYTES;
|
|
9169
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
9170
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared-store byte limit.");
|
|
9171
|
+
return async (request) => {
|
|
9172
|
+
const supplied = Buffer.from(request.headers.get("authorization") ?? "");
|
|
9173
|
+
if (supplied.byteLength !== expected.byteLength || !timingSafeEqual(supplied, expected))
|
|
9174
|
+
return json2({ error: { code: "UNAUTHORIZED", message: "Shared-store authentication required." } }, 401);
|
|
9175
|
+
try {
|
|
9176
|
+
const url = new URL(request.url), path = url.pathname;
|
|
9177
|
+
if (path === "/v1/objects/check" && request.method === "POST") {
|
|
9178
|
+
const keys = decode3(await boundedBody(request, limit));
|
|
9179
|
+
if (!Array.isArray(keys) || keys.length > 512 || new Set(keys).size !== keys.length || !keys.every((key) => typeof key === "string" && /^[a-f0-9]{64}$/.test(key)))
|
|
9180
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared object inventory.");
|
|
9181
|
+
const missing = [];
|
|
9182
|
+
for (const key of keys) {
|
|
9183
|
+
try {
|
|
9184
|
+
await getObject(options.store, key);
|
|
9185
|
+
} catch (error) {
|
|
9186
|
+
if (missingObject(error))
|
|
9187
|
+
missing.push(key);
|
|
9188
|
+
else
|
|
9189
|
+
throw error;
|
|
9190
|
+
}
|
|
9191
|
+
}
|
|
9192
|
+
return json2({ missing });
|
|
9193
|
+
}
|
|
9194
|
+
if (path === "/v1/objects/batch" && request.method === "POST") {
|
|
9195
|
+
const items = decode3(await boundedBody(request, limit));
|
|
9196
|
+
if (!Array.isArray(items) || items.length > 512)
|
|
9197
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared object batch.");
|
|
9198
|
+
const verified = new Map;
|
|
9199
|
+
for (const item of items) {
|
|
9200
|
+
if (!item || typeof item.object !== "string" || !/^[a-f0-9]{64}$/.test(item.object) || typeof item.base64 !== "string" || verified.has(item.object))
|
|
9201
|
+
throw new TrajectoriesError("INVALID_INPUT", "Invalid shared batch item.");
|
|
9202
|
+
const bytes = Buffer.from(item.base64, "base64");
|
|
9203
|
+
if (bytes.toString("base64") !== item.base64 || digest2(bytes) !== item.object)
|
|
9204
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared batch contains corrupt content.");
|
|
9205
|
+
verified.set(item.object, bytes);
|
|
9206
|
+
}
|
|
9207
|
+
for (const bytes of verified.values())
|
|
9208
|
+
await putObject(options.store, bytes);
|
|
9209
|
+
return json2({ objects: [...verified.keys()] });
|
|
9210
|
+
}
|
|
9211
|
+
const object = /^\/v1\/objects\/([a-f0-9]{64})$/.exec(path);
|
|
9212
|
+
if (object && ["GET", "HEAD", "PUT"].includes(request.method)) {
|
|
9213
|
+
const key = object[1];
|
|
9214
|
+
if (request.method === "PUT") {
|
|
9215
|
+
const bytes = await boundedBody(request, limit);
|
|
9216
|
+
if (digest2(bytes) !== key)
|
|
9217
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared object does not match its declared digest.");
|
|
9218
|
+
await putObject(options.store, bytes);
|
|
9219
|
+
return json2({ object: key });
|
|
9220
|
+
}
|
|
9221
|
+
let bytes;
|
|
9222
|
+
try {
|
|
9223
|
+
bytes = await getObject(options.store, key);
|
|
9224
|
+
} catch (error) {
|
|
9225
|
+
if (missingObject(error))
|
|
9226
|
+
return json2({ error: { code: "NOT_FOUND", message: "Shared object not found." } }, 404);
|
|
9227
|
+
throw error;
|
|
9228
|
+
}
|
|
9229
|
+
if (bytes.byteLength > limit)
|
|
9230
|
+
throw new TrajectoriesError("CAPACITY_EXCEEDED", "Shared object exceeds the admitted byte limit.");
|
|
9231
|
+
return new Response(request.method === "HEAD" ? null : new Uint8Array(bytes), {
|
|
9232
|
+
headers: { "content-type": "application/octet-stream", "content-length": String(bytes.byteLength), "cache-control": "no-store" }
|
|
9233
|
+
});
|
|
9234
|
+
}
|
|
9235
|
+
if (path === "/v1/observed" && request.method === "GET")
|
|
9236
|
+
return json2(await listObservedHistories(options.store));
|
|
9237
|
+
const observed = /^\/v1\/observed\/([a-f0-9]{64})$/.exec(path);
|
|
9238
|
+
if (observed && request.method === "GET")
|
|
9239
|
+
return json2(url.searchParams.get("manifest") === "1" ? await getObservedManifest(options.store, observed[1]) : await readObservedHistory(options.store, observed[1]));
|
|
9240
|
+
if (observed && request.method === "POST") {
|
|
9241
|
+
const delta = decode3(await boundedBody(request, limit));
|
|
9242
|
+
if (delta?.id !== observed[1])
|
|
9243
|
+
throw new TrajectoriesError("INVALID_INPUT", "Shared route and observation identity differ.");
|
|
9244
|
+
return json2(await commitObservedDelta(options.store, delta));
|
|
9245
|
+
}
|
|
9246
|
+
if (path === "/v1/managed" && request.method === "GET") {
|
|
9247
|
+
const sessions = [];
|
|
9248
|
+
let cursor;
|
|
9249
|
+
do {
|
|
9250
|
+
const page = await listSessions(options.store, { cursor, limit: 1000 });
|
|
9251
|
+
sessions.push(...page.items);
|
|
9252
|
+
cursor = page.nextCursor ?? undefined;
|
|
9253
|
+
} while (cursor);
|
|
9254
|
+
return json2(sessions);
|
|
9255
|
+
}
|
|
9256
|
+
const managed = /^\/v1\/managed\/([A-Za-z0-9][A-Za-z0-9_-]{0,127})$/.exec(path);
|
|
9257
|
+
if (managed && request.method === "GET") {
|
|
9258
|
+
if (url.searchParams.get("summary") === "1")
|
|
9259
|
+
return json2(await getSession(options.store, managed[1]));
|
|
9260
|
+
const after = url.searchParams.get("after");
|
|
9261
|
+
return json2(await exportManagedDelta(options.store, managed[1], { afterHead: after }));
|
|
9262
|
+
}
|
|
9263
|
+
if (managed && request.method === "POST") {
|
|
9264
|
+
const input = decode3(await boundedBody(request, limit));
|
|
9265
|
+
if (input?.delta?.sessionId !== managed[1])
|
|
9266
|
+
throw new TrajectoriesError("INVALID_INPUT", "Shared route and managed identity differ.");
|
|
9267
|
+
return json2(await importManagedDelta(options.store, input.delta, { expectedHead: input.expectedHead }));
|
|
9268
|
+
}
|
|
9269
|
+
return json2({ error: { code: "NOT_FOUND", message: "Shared-store route not found." } }, 404);
|
|
9270
|
+
} catch (error) {
|
|
9271
|
+
const code = error instanceof TrajectoriesError ? error.code : "INTEGRITY_ERROR";
|
|
9272
|
+
const status = code === "NOT_FOUND" ? 404 : code === "STALE_HEAD" || code === "INTENT_CONFLICT" || code === "OPERATION_PENDING" ? 409 : code === "CAPACITY_EXCEEDED" ? 413 : 422;
|
|
9273
|
+
return json2({ error: { code, message: error instanceof TrajectoriesError ? error.message : "Shared history could not be validated." } }, status);
|
|
9274
|
+
}
|
|
9275
|
+
};
|
|
9276
|
+
}
|
|
9277
|
+
function createSharedClient(options) {
|
|
9278
|
+
tokenBytes(options.token);
|
|
9279
|
+
const base = new URL(options.url);
|
|
9280
|
+
if (!["http:", "https:"].includes(base.protocol) || base.username || base.password || base.search || base.hash)
|
|
9281
|
+
throw new TrajectoriesError("INVALID_INPUT", "Shared endpoint must be an HTTP(S) URL without embedded credentials, query or fragment.");
|
|
9282
|
+
if (!base.pathname.endsWith("/"))
|
|
9283
|
+
base.pathname += "/";
|
|
9284
|
+
async function request(path, init = {}) {
|
|
9285
|
+
const response = await fetch(new URL(path, base), {
|
|
9286
|
+
...init,
|
|
9287
|
+
redirect: "error",
|
|
9288
|
+
signal: options.signal,
|
|
9289
|
+
headers: { ...Object.fromEntries(new Headers(init.headers)), authorization: `Bearer ${options.token}` }
|
|
9290
|
+
});
|
|
9291
|
+
if (!response.ok) {
|
|
9292
|
+
if (init.method === "HEAD")
|
|
9293
|
+
throw new TrajectoriesError(response.status === 404 ? "NOT_FOUND" : "INTEGRITY_ERROR", "Shared object lookup failed.");
|
|
9294
|
+
const data = decode3(await boundedBody(response, MAX_BYTES));
|
|
9295
|
+
const code = data.error?.code;
|
|
9296
|
+
const allowed = ["INVALID_INPUT", "NOT_FOUND", "INTEGRITY_ERROR", "UNSUPPORTED_CONTEXT", "STALE_HEAD", "INTENT_CONFLICT", "OPERATION_PENDING", "OUTCOME_UNKNOWN", "CAPACITY_EXCEEDED"];
|
|
9297
|
+
throw new TrajectoriesError(allowed.includes(code ?? "") ? code : "INVALID_INPUT", data.error?.message ?? "Shared request failed.");
|
|
9298
|
+
}
|
|
9299
|
+
return response;
|
|
9300
|
+
}
|
|
9301
|
+
const read = async (path) => decode3(await boundedBody(await request(path), MAX_BYTES));
|
|
9302
|
+
async function upload(objects) {
|
|
9303
|
+
let objectsTransferred = 0, bytesTransferred = 0;
|
|
9304
|
+
const entries = [...objects];
|
|
9305
|
+
for (let offset = 0;offset < entries.length; offset += 512) {
|
|
9306
|
+
const group = entries.slice(offset, offset + 512), keys = group.map(([key]) => key);
|
|
9307
|
+
const response = await request("v1/objects/check", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(keys) });
|
|
9308
|
+
const inventory = decode3(await boundedBody(response, MAX_BYTES));
|
|
9309
|
+
if (!inventory || !Array.isArray(inventory.missing) || new Set(inventory.missing).size !== inventory.missing.length || !inventory.missing.every((key) => keys.includes(key)))
|
|
9310
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid shared object inventory response.");
|
|
9311
|
+
const missing = new Set(inventory.missing);
|
|
9312
|
+
let batch = [], batchBytes = 0;
|
|
9313
|
+
const flush = async () => {
|
|
9314
|
+
if (!batch.length)
|
|
9315
|
+
return;
|
|
9316
|
+
const reply = decode3(await boundedBody(await request("v1/objects/batch", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(batch) }), MAX_BYTES));
|
|
9317
|
+
if (!reply || stableJson(reply.objects) !== stableJson(batch.map((item) => item.object)))
|
|
9318
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared batch acknowledgment differs.");
|
|
9319
|
+
objectsTransferred += batch.length;
|
|
9320
|
+
bytesTransferred += batchBytes;
|
|
9321
|
+
batch = [];
|
|
9322
|
+
batchBytes = 0;
|
|
9323
|
+
};
|
|
9324
|
+
for (const [key, bytes] of group) {
|
|
9325
|
+
if (!missing.has(key))
|
|
9326
|
+
continue;
|
|
9327
|
+
if (digest2(bytes) !== key)
|
|
9328
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Local shared object is corrupt.");
|
|
9329
|
+
if (bytes.byteLength > 1024 * 1024) {
|
|
9330
|
+
await flush();
|
|
9331
|
+
await request(`v1/objects/${key}`, { method: "PUT", body: new Uint8Array(bytes) });
|
|
9332
|
+
objectsTransferred++;
|
|
9333
|
+
bytesTransferred += bytes.byteLength;
|
|
9334
|
+
continue;
|
|
9335
|
+
}
|
|
9336
|
+
if (batchBytes + bytes.byteLength > 1024 * 1024)
|
|
9337
|
+
await flush();
|
|
9338
|
+
batch.push({ object: key, base64: Buffer.from(bytes).toString("base64") });
|
|
9339
|
+
batchBytes += bytes.byteLength;
|
|
9340
|
+
}
|
|
9341
|
+
await flush();
|
|
9342
|
+
}
|
|
9343
|
+
return { objectsTransferred, bytesTransferred };
|
|
9344
|
+
}
|
|
9345
|
+
return {
|
|
9346
|
+
async listObserved() {
|
|
9347
|
+
const values = await read("v1/observed");
|
|
9348
|
+
if (!Array.isArray(values))
|
|
9349
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid shared session list.");
|
|
9350
|
+
const ids = new Set;
|
|
9351
|
+
for (const value of values) {
|
|
9352
|
+
assertObservedSummary(value);
|
|
9353
|
+
if (ids.has(value.id))
|
|
9354
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Duplicate shared session.");
|
|
9355
|
+
ids.add(value.id);
|
|
9356
|
+
}
|
|
9357
|
+
return values;
|
|
9358
|
+
},
|
|
9359
|
+
async getObserved(id) {
|
|
9360
|
+
const value = await read(`v1/observed/${id}`);
|
|
9361
|
+
assertObservedState(value, id);
|
|
9362
|
+
return value;
|
|
9363
|
+
},
|
|
9364
|
+
async listManaged() {
|
|
9365
|
+
const values = await read("v1/managed");
|
|
9366
|
+
if (!Array.isArray(values))
|
|
9367
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Invalid managed session list.");
|
|
9368
|
+
const ids = new Set;
|
|
9369
|
+
for (const value of values) {
|
|
9370
|
+
assertManagedSummary(value);
|
|
9371
|
+
if (ids.has(value.id))
|
|
9372
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Duplicate managed session.");
|
|
9373
|
+
ids.add(value.id);
|
|
9374
|
+
}
|
|
9375
|
+
return values;
|
|
9376
|
+
},
|
|
9377
|
+
async pushObserved(input) {
|
|
9378
|
+
const id = sharedObservationId(input.machineId, input.session.id);
|
|
9379
|
+
let previous;
|
|
9380
|
+
try {
|
|
9381
|
+
previous = await read(`v1/observed/${id}?manifest=1`);
|
|
9382
|
+
} catch (error) {
|
|
9383
|
+
if (!(error instanceof TrajectoriesError) || error.code !== "NOT_FOUND")
|
|
9384
|
+
throw error;
|
|
9385
|
+
}
|
|
9386
|
+
const prepared = await prepareObservedHistory(input, previous);
|
|
9387
|
+
if (!prepared.delta)
|
|
9388
|
+
return { id, head: previous.head, changedRecords: 0, removedRecords: 0, objectsTransferred: 0, bytesTransferred: 0 };
|
|
9389
|
+
const transferred = await upload(prepared.objects);
|
|
9390
|
+
const response = await request(`v1/observed/${id}`, { method: "POST", headers: { "content-type": "application/json" }, body: stableJson(prepared.delta) });
|
|
9391
|
+
const committed = decode3(await boundedBody(response, MAX_BYTES));
|
|
9392
|
+
assertObservedSummary(committed, id);
|
|
9393
|
+
if (committed.head !== digest2(stableJson(prepared.delta)) || stableJson(committed.session) !== stableJson(input.session))
|
|
9394
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared observation acknowledgment does not match the published delta.");
|
|
9395
|
+
return { id, head: committed.head, ...transferred, changedRecords: prepared.changedRecords, removedRecords: prepared.removedRecords };
|
|
9396
|
+
},
|
|
9397
|
+
async pushManaged(store, id) {
|
|
9398
|
+
let remote;
|
|
9399
|
+
try {
|
|
9400
|
+
remote = await read(`v1/managed/${id}?summary=1`);
|
|
9401
|
+
} catch (error) {
|
|
9402
|
+
if (!(error instanceof TrajectoriesError) || error.code !== "NOT_FOUND")
|
|
9403
|
+
throw error;
|
|
9404
|
+
}
|
|
9405
|
+
if (remote)
|
|
9406
|
+
assertManagedSummary(remote, id);
|
|
9407
|
+
const delta = await exportManagedDelta(store, id, { afterHead: remote?.head ?? null });
|
|
9408
|
+
const objects = new Map;
|
|
9409
|
+
for (const key of delta.objectIds)
|
|
9410
|
+
objects.set(key, await getObject(store, key));
|
|
9411
|
+
const transferred = await upload(objects);
|
|
9412
|
+
if (delta.frames.length) {
|
|
9413
|
+
const committed = decode3(await boundedBody(await request(`v1/managed/${id}`, {
|
|
9414
|
+
method: "POST",
|
|
9415
|
+
headers: { "content-type": "application/json" },
|
|
9416
|
+
body: stableJson({ expectedHead: remote?.head ?? null, delta })
|
|
9417
|
+
}), MAX_BYTES));
|
|
9418
|
+
assertManagedSummary(committed, id);
|
|
9419
|
+
if (committed.head !== delta.head)
|
|
9420
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared managed acknowledgment differs from the submitted head.");
|
|
9421
|
+
}
|
|
9422
|
+
return { id, head: delta.head, framesTransferred: delta.frames.length, ...transferred };
|
|
9423
|
+
},
|
|
9424
|
+
async pullManaged(store, id) {
|
|
9425
|
+
let local;
|
|
9426
|
+
try {
|
|
9427
|
+
local = await getSession(store, id);
|
|
9428
|
+
} catch (error) {
|
|
9429
|
+
if (!(error instanceof TrajectoriesError) || error.code !== "NOT_FOUND")
|
|
9430
|
+
throw error;
|
|
9431
|
+
}
|
|
9432
|
+
const delta = await read(`v1/managed/${id}${local ? `?after=${encodeURIComponent(local.head)}` : ""}`);
|
|
9433
|
+
if (delta?.sessionId !== id)
|
|
9434
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Shared managed transfer belongs to another session.");
|
|
9435
|
+
validateManagedDelta(delta, local?.head ?? null);
|
|
9436
|
+
let objectsTransferred = 0, bytesTransferred = 0;
|
|
9437
|
+
for (const key of delta.objectIds) {
|
|
9438
|
+
try {
|
|
9439
|
+
await getObject(store, key);
|
|
9440
|
+
continue;
|
|
9441
|
+
} catch (error) {
|
|
9442
|
+
if (!missingObject(error))
|
|
9443
|
+
throw error;
|
|
9444
|
+
}
|
|
9445
|
+
const bytes = await boundedBody(await request(`v1/objects/${key}`), MAX_BYTES);
|
|
9446
|
+
if (digest2(bytes) !== key)
|
|
9447
|
+
throw new TrajectoriesError("INTEGRITY_ERROR", "Remote shared object is corrupt.");
|
|
9448
|
+
await putObject(store, bytes);
|
|
9449
|
+
objectsTransferred++;
|
|
9450
|
+
bytesTransferred += bytes.byteLength;
|
|
9451
|
+
}
|
|
9452
|
+
await importManagedDelta(store, delta, { expectedHead: local?.head ?? null });
|
|
9453
|
+
return { id, head: delta.head, framesTransferred: delta.frames.length, objectsTransferred, bytesTransferred };
|
|
9454
|
+
}
|
|
9455
|
+
};
|
|
9456
|
+
}
|
|
8382
9457
|
export {
|
|
8383
9458
|
HARNESSES,
|
|
9459
|
+
NATIVE_HARNESSSES,
|
|
8384
9460
|
TrajectoriesError,
|
|
9461
|
+
annotateSnapshot,
|
|
8385
9462
|
appendCapture,
|
|
8386
9463
|
assertPortableSnapshot,
|
|
8387
9464
|
assertSnapshot,
|
|
8388
9465
|
assertSnapshotContinuation,
|
|
8389
9466
|
convert,
|
|
8390
9467
|
createSession,
|
|
9468
|
+
createSharedClient,
|
|
9469
|
+
createSharedHandler,
|
|
8391
9470
|
getHeadMetadata,
|
|
8392
9471
|
getNativeBinding,
|
|
8393
9472
|
getSession,
|
|
@@ -8395,6 +9474,7 @@ export {
|
|
|
8395
9474
|
migrateLegacyStore,
|
|
8396
9475
|
readSession,
|
|
8397
9476
|
readSnapshot,
|
|
9477
|
+
sharedObservationId,
|
|
8398
9478
|
stageCapture,
|
|
8399
9479
|
switchSession,
|
|
8400
9480
|
writeNativeExport,
|