@tangle-network/traces 0.23.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-XLS3VANY.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-3I4PJ5Q7.js +25 -0
- package/dist/chunk-3T62R7ON.js +5695 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6CLC7DKP.js +492 -0
- package/dist/chunk-7CBV5RJ2.js +535 -0
- package/dist/chunk-B62DOFGI.js +273 -0
- package/dist/chunk-CL3OQOWX.js +407 -0
- package/dist/chunk-DLGAKCQJ.js +782 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-J2PM5SVN.js +503 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-NH7HCDLB.js +759 -0
- package/dist/chunk-NZ5Y7UEL.js +1217 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TR6O4S4T.js +208 -0
- package/dist/chunk-TW43MR5D.js +259 -0
- package/dist/chunk-TYLFUX42.js +228 -0
- package/dist/chunk-UOLRCQHC.js +120 -0
- package/dist/chunk-UVE5ANVV.js +3921 -0
- package/dist/chunk-V2OCGX57.js +53 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-ZZ6KYBPP.js +16 -0
- package/dist/improvement-OJQ6365E.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-Y64AJ6MA.js +14 -0
- package/dist/mcp-ZPGTJQYQ.js +85 -0
- package/dist/report-JAMXRTCW.js +338 -0
- package/dist/run-watch-PLWTIRSV.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-R6NMKSGQ.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +8 -7
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-COSWFESO.js +0 -20979
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SessionWorkflowError,
|
|
3
|
+
buildSessionFactsReport,
|
|
4
|
+
describeSessionRelationship,
|
|
5
|
+
priceReport
|
|
6
|
+
} from "./chunk-UVE5ANVV.js";
|
|
7
|
+
import {
|
|
8
|
+
isMissingPathError,
|
|
9
|
+
locateSessions,
|
|
10
|
+
parseSession
|
|
11
|
+
} from "./chunk-3T62R7ON.js";
|
|
12
|
+
import {
|
|
13
|
+
tracesVersion,
|
|
14
|
+
writeOtlpFile
|
|
15
|
+
} from "./chunk-NZ5Y7UEL.js";
|
|
16
|
+
import {
|
|
17
|
+
SOURCE_ATTRIBUTE_PREFIX,
|
|
18
|
+
sourceFileId
|
|
19
|
+
} from "./chunk-DU534HAX.js";
|
|
20
|
+
|
|
21
|
+
// src/session-selection.ts
|
|
22
|
+
import { createHash } from "crypto";
|
|
23
|
+
import { createReadStream } from "fs";
|
|
24
|
+
|
|
25
|
+
// src/session-workflow.ts
|
|
26
|
+
function refPathKey(ref) {
|
|
27
|
+
return `${ref.harness}\0${ref.path}`;
|
|
28
|
+
}
|
|
29
|
+
function referencesFor(relationship) {
|
|
30
|
+
return [
|
|
31
|
+
...relationship.parentSessionId ? [{
|
|
32
|
+
sessionId: relationship.parentSessionId,
|
|
33
|
+
referencedBySessionId: relationship.sessionId,
|
|
34
|
+
relation: "parent"
|
|
35
|
+
}] : [],
|
|
36
|
+
...relationship.childSessionIds.map((sessionId) => ({
|
|
37
|
+
sessionId,
|
|
38
|
+
referencedBySessionId: relationship.sessionId,
|
|
39
|
+
relation: "child",
|
|
40
|
+
taskScope: relationship.spawnedChildSessionIds.includes(sessionId) ? "all" : relationship.resumedChildSessionIds.includes(sessionId) ? "latest" : "all"
|
|
41
|
+
}))
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function issueKey(issue) {
|
|
45
|
+
return JSON.stringify(issue);
|
|
46
|
+
}
|
|
47
|
+
function referenceKey(reference) {
|
|
48
|
+
return JSON.stringify(reference);
|
|
49
|
+
}
|
|
50
|
+
function findParentCycles(sessions) {
|
|
51
|
+
const parentBySession = new Map(
|
|
52
|
+
sessions.filter((session) => session.relationship.parentSessionId).map((session) => [session.relationship.sessionId, session.relationship.parentSessionId])
|
|
53
|
+
);
|
|
54
|
+
const cycles = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const start of parentBySession.keys()) {
|
|
56
|
+
const path = [];
|
|
57
|
+
const indexById = /* @__PURE__ */ new Map();
|
|
58
|
+
let current = start;
|
|
59
|
+
while (current && parentBySession.has(current)) {
|
|
60
|
+
const seenAt = indexById.get(current);
|
|
61
|
+
if (seenAt !== void 0) {
|
|
62
|
+
const cycle = path.slice(seenAt);
|
|
63
|
+
const canonicalStart = cycle.reduce(
|
|
64
|
+
(best, id, index) => id.localeCompare(cycle[best]) < 0 ? index : best,
|
|
65
|
+
0
|
|
66
|
+
);
|
|
67
|
+
const canonical = [...cycle.slice(canonicalStart), ...cycle.slice(0, canonicalStart)];
|
|
68
|
+
cycles.set(canonical.join("\0"), canonical);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
indexById.set(current, path.length);
|
|
72
|
+
path.push(current);
|
|
73
|
+
current = parentBySession.get(current);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...cycles.values()].map((sessionIds) => ({ kind: "cycle", sessionIds }));
|
|
77
|
+
}
|
|
78
|
+
function relationshipIssues(sessions) {
|
|
79
|
+
const byId = new Map(sessions.map((session) => [session.relationship.sessionId, session]));
|
|
80
|
+
const referencedParents = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const parent of sessions) {
|
|
82
|
+
for (const childId of parent.relationship.childSessionIds) {
|
|
83
|
+
if (!byId.has(childId)) continue;
|
|
84
|
+
const parents = referencedParents.get(childId) ?? /* @__PURE__ */ new Set();
|
|
85
|
+
parents.add(parent.relationship.sessionId);
|
|
86
|
+
referencedParents.set(childId, parents);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const issues = [];
|
|
90
|
+
for (const session of sessions) {
|
|
91
|
+
const referenced = [...referencedParents.get(session.relationship.sessionId) ?? []].sort();
|
|
92
|
+
const declared = session.relationship.parentSessionId;
|
|
93
|
+
const mismatched = referenced.filter((parent) => parent !== declared);
|
|
94
|
+
const missingReverseLink = Boolean(declared && byId.has(declared) && !referenced.includes(declared));
|
|
95
|
+
if (referenced.length > 1 || mismatched.length > 0 || missingReverseLink) {
|
|
96
|
+
issues.push({
|
|
97
|
+
kind: "parent-conflict",
|
|
98
|
+
sessionId: session.relationship.sessionId,
|
|
99
|
+
...declared ? { declaredParentSessionId: declared } : {},
|
|
100
|
+
referencedParentSessionIds: referenced
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...issues, ...findParentCycles(sessions)];
|
|
105
|
+
}
|
|
106
|
+
function orderSessions(sessions) {
|
|
107
|
+
const byId = new Map(sessions.map((session) => [session.relationship.sessionId, session]));
|
|
108
|
+
const children = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const session of sessions) {
|
|
110
|
+
const parentId = session.relationship.parentSessionId;
|
|
111
|
+
if (parentId && byId.has(parentId)) {
|
|
112
|
+
const childIds = children.get(parentId) ?? /* @__PURE__ */ new Set();
|
|
113
|
+
childIds.add(session.relationship.sessionId);
|
|
114
|
+
children.set(parentId, childIds);
|
|
115
|
+
}
|
|
116
|
+
for (const childId of session.relationship.childSessionIds) {
|
|
117
|
+
if (!byId.has(childId)) continue;
|
|
118
|
+
const childIds = children.get(session.relationship.sessionId) ?? /* @__PURE__ */ new Set();
|
|
119
|
+
childIds.add(childId);
|
|
120
|
+
children.set(session.relationship.sessionId, childIds);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const ordered = [];
|
|
124
|
+
const seen = /* @__PURE__ */ new Set();
|
|
125
|
+
const visit = (id) => {
|
|
126
|
+
if (seen.has(id)) return;
|
|
127
|
+
seen.add(id);
|
|
128
|
+
const session = byId.get(id);
|
|
129
|
+
if (!session) return;
|
|
130
|
+
ordered.push(session);
|
|
131
|
+
for (const childId of [...children.get(id) ?? []].sort()) visit(childId);
|
|
132
|
+
};
|
|
133
|
+
const roots = sessions.filter((session) => !session.relationship.parentSessionId || !byId.has(session.relationship.parentSessionId)).map((session) => session.relationship.sessionId).sort();
|
|
134
|
+
for (const id of roots) visit(id);
|
|
135
|
+
for (const id of [...byId.keys()].sort()) visit(id);
|
|
136
|
+
return ordered;
|
|
137
|
+
}
|
|
138
|
+
function startedAt(session) {
|
|
139
|
+
let first = Number.POSITIVE_INFINITY;
|
|
140
|
+
for (const span of session.spans) {
|
|
141
|
+
const at = Date.parse(span.start_time);
|
|
142
|
+
if (Number.isFinite(at) && at < first) first = at;
|
|
143
|
+
}
|
|
144
|
+
return first;
|
|
145
|
+
}
|
|
146
|
+
function orderFromSeeds(sessions, seedIds) {
|
|
147
|
+
const byId = new Map(sessions.map((session) => [session.relationship.sessionId, session]));
|
|
148
|
+
const ordered = [];
|
|
149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
150
|
+
const visit = (id) => {
|
|
151
|
+
const session = byId.get(id);
|
|
152
|
+
if (!session || seen.has(id)) return;
|
|
153
|
+
seen.add(id);
|
|
154
|
+
ordered.push(session);
|
|
155
|
+
const children = session.relationship.childSessionIds.map((childId) => byId.get(childId)).filter((child) => child !== void 0).sort((a, b) => startedAt(a) - startedAt(b) || a.relationship.sessionId.localeCompare(b.relationship.sessionId));
|
|
156
|
+
for (const child of children) visit(child.relationship.sessionId);
|
|
157
|
+
};
|
|
158
|
+
for (const id of seedIds) visit(id);
|
|
159
|
+
for (const session of sessions) visit(session.relationship.sessionId);
|
|
160
|
+
return ordered;
|
|
161
|
+
}
|
|
162
|
+
async function collectSessionWorkflow(adapter, seeds, options = {}) {
|
|
163
|
+
const followParents = options.parents !== false;
|
|
164
|
+
if (seeds.length === 0) {
|
|
165
|
+
throw new SessionWorkflowError(
|
|
166
|
+
"SESSION_WORKFLOW_EMPTY_SELECTION",
|
|
167
|
+
"session workflow needs at least one seed session"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const maxSessions = options.maxSessions ?? 100;
|
|
171
|
+
if (!Number.isInteger(maxSessions) || maxSessions < 1) {
|
|
172
|
+
throw new SessionWorkflowError(
|
|
173
|
+
"SESSION_WORKFLOW_INVALID_LIMIT",
|
|
174
|
+
"maxSessions must be a positive integer"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
options.signal?.throwIfAborted();
|
|
178
|
+
const refsById = /* @__PURE__ */ new Map();
|
|
179
|
+
const addRef = (ref) => {
|
|
180
|
+
const refs = refsById.get(ref.sessionId) ?? [];
|
|
181
|
+
if (!refs.some((candidate) => refPathKey(candidate) === refPathKey(ref))) refs.push(ref);
|
|
182
|
+
refsById.set(ref.sessionId, refs);
|
|
183
|
+
};
|
|
184
|
+
for (const ref of seeds) addRef(ref);
|
|
185
|
+
let fallbackLocated;
|
|
186
|
+
const resolvedIds = /* @__PURE__ */ new Set();
|
|
187
|
+
const refsForId = async (sessionId) => {
|
|
188
|
+
if (resolvedIds.has(sessionId)) return refsById.get(sessionId) ?? [];
|
|
189
|
+
if (adapter.locateBySessionId) {
|
|
190
|
+
for (const ref of await adapter.locateBySessionId(sessionId, options.home !== void 0 ? { home: options.home } : {})) addRef(ref);
|
|
191
|
+
} else {
|
|
192
|
+
fallbackLocated ??= await locateSessions(adapter, { home: options.home });
|
|
193
|
+
for (const ref of fallbackLocated) addRef(ref);
|
|
194
|
+
}
|
|
195
|
+
resolvedIds.add(sessionId);
|
|
196
|
+
options.signal?.throwIfAborted();
|
|
197
|
+
return refsById.get(sessionId) ?? [];
|
|
198
|
+
};
|
|
199
|
+
const queue = [];
|
|
200
|
+
const queuedByPath = /* @__PURE__ */ new Map();
|
|
201
|
+
const enqueue = (ref, taskScope, taskTurnId, explicit = false) => {
|
|
202
|
+
const key = refPathKey(ref);
|
|
203
|
+
const existing = queuedByPath.get(key);
|
|
204
|
+
if (existing) {
|
|
205
|
+
const conflictingExplicitTurns = existing.taskScope === "turn" && taskScope === "turn" && existing.taskTurnId !== taskTurnId && existing.explicitExactTurn && explicit;
|
|
206
|
+
if (conflictingExplicitTurns) {
|
|
207
|
+
throw new SessionWorkflowError(
|
|
208
|
+
"SESSION_WORKFLOW_TASK_CONFLICT",
|
|
209
|
+
`session ${ref.sessionId} was explicitly selected at two different exact turns`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (existing.taskScope === taskScope && existing.taskTurnId === taskTurnId) {
|
|
213
|
+
if (taskScope === "turn" && explicit) existing.explicitExactTurn = true;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
existing.taskScope = "all";
|
|
217
|
+
delete existing.taskTurnId;
|
|
218
|
+
existing.explicitExactTurn = false;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const queued = {
|
|
222
|
+
ref,
|
|
223
|
+
taskScope,
|
|
224
|
+
...taskTurnId ? { taskTurnId } : {},
|
|
225
|
+
explicitExactTurn: explicit && taskScope === "turn"
|
|
226
|
+
};
|
|
227
|
+
queuedByPath.set(key, queued);
|
|
228
|
+
queue.push(queued);
|
|
229
|
+
};
|
|
230
|
+
const seedTaskScope = options.taskScope ?? "all";
|
|
231
|
+
for (const seed of seeds) enqueue(seed, seedTaskScope, options.taskTurnId, true);
|
|
232
|
+
const sessions = [];
|
|
233
|
+
const parsedById = /* @__PURE__ */ new Map();
|
|
234
|
+
const references = [];
|
|
235
|
+
const issues = [];
|
|
236
|
+
const unresolvedReferences = /* @__PURE__ */ new Set();
|
|
237
|
+
const seedPaths = new Set(seeds.map(refPathKey));
|
|
238
|
+
const seedSessionIds = [];
|
|
239
|
+
const unreadIds = /* @__PURE__ */ new Set();
|
|
240
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
241
|
+
options.signal?.throwIfAborted();
|
|
242
|
+
if (sessions.length >= maxSessions) {
|
|
243
|
+
throw new SessionWorkflowError(
|
|
244
|
+
"SESSION_WORKFLOW_LIMIT_EXCEEDED",
|
|
245
|
+
`the selected session tree holds more than ${maxSessions} sessions, the cap on spawned children read with a session; name one child session to read it alone`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const selected = queue[index];
|
|
249
|
+
const { ref, taskScope, taskTurnId } = selected;
|
|
250
|
+
let spans;
|
|
251
|
+
try {
|
|
252
|
+
spans = await parseSession(adapter, ref, {
|
|
253
|
+
corruptionMode: options.corruptionMode,
|
|
254
|
+
taskScope,
|
|
255
|
+
taskTurnId,
|
|
256
|
+
signal: options.signal
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!options.onUnreadable || options.signal?.aborted) throw error;
|
|
260
|
+
options.onUnreadable({ ref, error, seed: seedPaths.has(refPathKey(ref)) });
|
|
261
|
+
unreadIds.add(ref.sessionId);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
options.signal?.throwIfAborted();
|
|
265
|
+
const relationship = describeSessionRelationship(ref, spans);
|
|
266
|
+
const discoveredPaths = followParents ? (await refsForId(relationship.sessionId)).map((candidate) => candidate.path).filter((path, pathIndex, paths) => paths.indexOf(path) === pathIndex) : [];
|
|
267
|
+
if (discoveredPaths.length > 1) {
|
|
268
|
+
throw new SessionWorkflowError(
|
|
269
|
+
"SESSION_WORKFLOW_DUPLICATE_ID",
|
|
270
|
+
`session ID "${relationship.sessionId}" resolves to ${discoveredPaths.join(", ")}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const previous = parsedById.get(relationship.sessionId);
|
|
274
|
+
if (previous && refPathKey(previous.ref) !== refPathKey(ref)) {
|
|
275
|
+
throw new SessionWorkflowError(
|
|
276
|
+
"SESSION_WORKFLOW_DUPLICATE_ID",
|
|
277
|
+
`session ID "${relationship.sessionId}" resolves to both ${previous.ref.path} and ${ref.path}`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (previous) continue;
|
|
281
|
+
const joinedChildIds = [];
|
|
282
|
+
if (relationship.unjoinedSpawnPaths.length > 0) {
|
|
283
|
+
const resolutions = adapter.locateSpawnedChildren ? await adapter.locateSpawnedChildren(relationship.sessionId, relationship.unjoinedSpawnPaths, {
|
|
284
|
+
corruptionMode: options.corruptionMode,
|
|
285
|
+
signal: options.signal,
|
|
286
|
+
...options.home !== void 0 ? { home: options.home } : {}
|
|
287
|
+
}) : [];
|
|
288
|
+
options.signal?.throwIfAborted();
|
|
289
|
+
if (!adapter.locateSpawnedChildren) {
|
|
290
|
+
for (const agentPath of relationship.unjoinedSpawnPaths) {
|
|
291
|
+
issues.push({
|
|
292
|
+
kind: "unjoined-child",
|
|
293
|
+
parentSessionId: relationship.sessionId,
|
|
294
|
+
agentPath,
|
|
295
|
+
reason: "unsupported"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const resolution of resolutions) {
|
|
300
|
+
if (resolution.ref) {
|
|
301
|
+
addRef(resolution.ref);
|
|
302
|
+
joinedChildIds.push(resolution.ref.sessionId);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
issues.push({
|
|
306
|
+
kind: "unjoined-child",
|
|
307
|
+
parentSessionId: relationship.sessionId,
|
|
308
|
+
agentPath: resolution.agentPath,
|
|
309
|
+
reason: resolution.reason ?? "not-found",
|
|
310
|
+
...resolution.candidates ? { candidates: resolution.candidates } : {}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const joined = joinedChildIds.length === 0 ? relationship : {
|
|
315
|
+
...relationship,
|
|
316
|
+
childSessionIds: [.../* @__PURE__ */ new Set([...relationship.childSessionIds, ...joinedChildIds])].sort(),
|
|
317
|
+
spawnedChildSessionIds: [.../* @__PURE__ */ new Set([...relationship.spawnedChildSessionIds, ...joinedChildIds])].sort()
|
|
318
|
+
};
|
|
319
|
+
const session = {
|
|
320
|
+
adapter,
|
|
321
|
+
ref,
|
|
322
|
+
spans,
|
|
323
|
+
relationship: joined,
|
|
324
|
+
taskScope,
|
|
325
|
+
...taskTurnId ? { taskTurnId } : {}
|
|
326
|
+
};
|
|
327
|
+
sessions.push(session);
|
|
328
|
+
parsedById.set(joined.sessionId, session);
|
|
329
|
+
if (seedPaths.has(refPathKey(ref))) seedSessionIds.push(joined.sessionId);
|
|
330
|
+
const discovered = referencesFor(joined).filter((reference) => followParents || reference.relation === "child");
|
|
331
|
+
references.push(...discovered);
|
|
332
|
+
for (const reference of discovered) {
|
|
333
|
+
if (parsedById.has(reference.sessionId)) continue;
|
|
334
|
+
const candidates = await refsForId(reference.sessionId);
|
|
335
|
+
if (candidates.length !== 1) continue;
|
|
336
|
+
if (reference.relation !== "parent" || taskScope !== "latest") {
|
|
337
|
+
enqueue(candidates[0], reference.taskScope ?? "all");
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const resolution = adapter.resolveParentTask ? await adapter.resolveParentTask(candidates[0], joined.sessionId, {
|
|
341
|
+
corruptionMode: options.corruptionMode,
|
|
342
|
+
signal: options.signal
|
|
343
|
+
}) : { kind: "unavailable", reason: "unsupported" };
|
|
344
|
+
options.signal?.throwIfAborted();
|
|
345
|
+
if (resolution.kind === "resolved") {
|
|
346
|
+
enqueue(candidates[0], "turn", resolution.turnId);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
unresolvedReferences.add(referenceKey(reference));
|
|
350
|
+
issues.push({
|
|
351
|
+
kind: "unresolved-parent-task",
|
|
352
|
+
parentSessionId: reference.sessionId,
|
|
353
|
+
childSessionId: joined.sessionId,
|
|
354
|
+
reason: resolution.reason,
|
|
355
|
+
..."turnIds" in resolution && resolution.turnIds ? { turnIds: resolution.turnIds } : {}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const reference of references) {
|
|
360
|
+
if (parsedById.has(reference.sessionId) || unreadIds.has(reference.sessionId)) continue;
|
|
361
|
+
if (unresolvedReferences.has(referenceKey(reference))) continue;
|
|
362
|
+
const {
|
|
363
|
+
taskScope: _taskScope,
|
|
364
|
+
...publicReference
|
|
365
|
+
} = reference;
|
|
366
|
+
const paths = (refsById.get(reference.sessionId) ?? []).map((ref) => ref.path).sort();
|
|
367
|
+
issues.push(paths.length > 1 ? { kind: "ambiguous-session", ...publicReference, paths } : { kind: "missing-session", ...publicReference });
|
|
368
|
+
}
|
|
369
|
+
issues.push(...relationshipIssues(sessions));
|
|
370
|
+
const uniqueIssues = [...new Map(issues.map((issue) => [issueKey(issue), issue])).values()];
|
|
371
|
+
const ordered = followParents ? orderSessions(sessions) : orderFromSeeds(sessions, seedSessionIds);
|
|
372
|
+
return {
|
|
373
|
+
sessions: ordered,
|
|
374
|
+
seedSessionIds: [...new Set(seedSessionIds)],
|
|
375
|
+
complete: uniqueIssues.length === 0,
|
|
376
|
+
issues: uniqueIssues
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/session-selection.ts
|
|
381
|
+
async function fileSha256(path, signal) {
|
|
382
|
+
signal?.throwIfAborted();
|
|
383
|
+
const hash = createHash("sha256");
|
|
384
|
+
try {
|
|
385
|
+
for await (const chunk of createReadStream(path, { signal })) {
|
|
386
|
+
signal?.throwIfAborted();
|
|
387
|
+
hash.update(chunk);
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
signal?.throwIfAborted();
|
|
394
|
+
return hash.digest("hex");
|
|
395
|
+
}
|
|
396
|
+
async function sourceSnapshot(adapter, ref, signal) {
|
|
397
|
+
signal?.throwIfAborted();
|
|
398
|
+
const paths = [
|
|
399
|
+
...adapter.sourcePaths ? await adapter.sourcePaths(ref, { signal }) : [ref.path]
|
|
400
|
+
].sort();
|
|
401
|
+
signal?.throwIfAborted();
|
|
402
|
+
if (paths.length === 0) throw new Error(`adapter returned no source files for ${ref.path}`);
|
|
403
|
+
if (new Set(paths).size !== paths.length) {
|
|
404
|
+
throw new Error(`adapter returned duplicate source files for ${ref.path}`);
|
|
405
|
+
}
|
|
406
|
+
const files = [];
|
|
407
|
+
for (const path of paths) {
|
|
408
|
+
signal?.throwIfAborted();
|
|
409
|
+
files.push({ path, sha256: await fileSha256(path, signal) });
|
|
410
|
+
}
|
|
411
|
+
const sha256 = files.length === 1 ? files[0].sha256 : createHash("sha256").update(JSON.stringify(files)).digest("hex");
|
|
412
|
+
return { sha256, files };
|
|
413
|
+
}
|
|
414
|
+
function summarizeWorkflows(workflows) {
|
|
415
|
+
if (workflows.length === 0) return void 0;
|
|
416
|
+
const issues = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const workflow of workflows) {
|
|
418
|
+
for (const issue of workflow.issues) issues.set(JSON.stringify(issue), issue);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
seedSessionIds: [...new Set(workflows.flatMap((workflow) => workflow.seedSessionIds))],
|
|
422
|
+
complete: workflows.every((workflow) => workflow.complete),
|
|
423
|
+
issues: [...issues.values()]
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function boundSessionRow(adapter, ref, taskScope, taskTurnId, expectedRelationship, signal) {
|
|
427
|
+
const before = await sourceSnapshot(adapter, ref, signal);
|
|
428
|
+
const spans = await parseSession(adapter, ref, { taskScope, taskTurnId, signal, captureSources: true });
|
|
429
|
+
signal?.throwIfAborted();
|
|
430
|
+
const after = await sourceSnapshot(adapter, ref, signal);
|
|
431
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
432
|
+
throw new Error(`session source changed while parsing; refusing unbound evidence: ${ref.path}`);
|
|
433
|
+
}
|
|
434
|
+
const digests = new Map(after.files.map((file) => [sourceFileId(file.path), file.sha256]));
|
|
435
|
+
for (const span of spans) {
|
|
436
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
437
|
+
if (!key.startsWith(SOURCE_ATTRIBUTE_PREFIX) || typeof value !== "string") continue;
|
|
438
|
+
const refs = JSON.parse(value);
|
|
439
|
+
span.attributes[key] = JSON.stringify(refs.map((record) => {
|
|
440
|
+
const sourceSha256 = digests.get(record.sourceId);
|
|
441
|
+
if (!sourceSha256) throw new Error("adapter source reference is outside its declared source files");
|
|
442
|
+
return { ...record, sourceSha256 };
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (expectedRelationship && JSON.stringify(describeSessionRelationship(ref, spans)) !== JSON.stringify(expectedRelationship)) {
|
|
447
|
+
throw new Error(`session relationships changed during workflow selection: ${ref.path}`);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
adapter,
|
|
451
|
+
ref,
|
|
452
|
+
spans,
|
|
453
|
+
taskScope,
|
|
454
|
+
...taskTurnId ? { taskTurnId } : {},
|
|
455
|
+
sourceSha256: after.sha256,
|
|
456
|
+
sourceFiles: after.files
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function collectSessionSelection(groups, options = {}) {
|
|
460
|
+
const rows = [];
|
|
461
|
+
const workflows = [];
|
|
462
|
+
const taskScope = options.taskScope ?? "all";
|
|
463
|
+
const read = /* @__PURE__ */ new Set();
|
|
464
|
+
const add = async (row) => {
|
|
465
|
+
const key = `${row.adapter.harness}\0${row.ref.path}`;
|
|
466
|
+
if (read.has(key)) return;
|
|
467
|
+
read.add(key);
|
|
468
|
+
if (options.onRow) await options.onRow(row);
|
|
469
|
+
else rows.push(row);
|
|
470
|
+
};
|
|
471
|
+
for (const { adapter, refs } of groups) {
|
|
472
|
+
options.signal?.throwIfAborted();
|
|
473
|
+
if (refs.length === 0) continue;
|
|
474
|
+
if (!options.workflow) {
|
|
475
|
+
for (const ref of refs) {
|
|
476
|
+
let row;
|
|
477
|
+
try {
|
|
478
|
+
row = options.bindSources ? await boundSessionRow(
|
|
479
|
+
adapter,
|
|
480
|
+
ref,
|
|
481
|
+
taskScope,
|
|
482
|
+
options.taskTurnId,
|
|
483
|
+
void 0,
|
|
484
|
+
options.signal
|
|
485
|
+
) : {
|
|
486
|
+
adapter,
|
|
487
|
+
ref,
|
|
488
|
+
spans: await parseSession(adapter, ref, {
|
|
489
|
+
taskScope,
|
|
490
|
+
taskTurnId: options.taskTurnId,
|
|
491
|
+
signal: options.signal
|
|
492
|
+
}),
|
|
493
|
+
taskScope,
|
|
494
|
+
...options.taskTurnId ? { taskTurnId: options.taskTurnId } : {}
|
|
495
|
+
};
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (!options.onUnreadable || options.signal?.aborted) throw error;
|
|
498
|
+
options.onUnreadable({ ref, error, seed: true });
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
await add(row);
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const workflow2 = await collectSessionWorkflow(adapter, refs, {
|
|
506
|
+
maxSessions: options.maxWorkflowSessions,
|
|
507
|
+
home: options.home,
|
|
508
|
+
taskScope,
|
|
509
|
+
taskTurnId: options.taskTurnId,
|
|
510
|
+
signal: options.signal,
|
|
511
|
+
...options.parents === false ? { parents: false } : {},
|
|
512
|
+
...options.onUnreadable ? { onUnreadable: options.onUnreadable } : {}
|
|
513
|
+
});
|
|
514
|
+
workflows.push(workflow2);
|
|
515
|
+
for (const session of workflow2.sessions) {
|
|
516
|
+
await add(options.bindSources ? await boundSessionRow(
|
|
517
|
+
adapter,
|
|
518
|
+
session.ref,
|
|
519
|
+
session.taskScope,
|
|
520
|
+
session.taskTurnId,
|
|
521
|
+
session.relationship,
|
|
522
|
+
options.signal
|
|
523
|
+
) : {
|
|
524
|
+
adapter,
|
|
525
|
+
ref: session.ref,
|
|
526
|
+
spans: session.spans,
|
|
527
|
+
taskScope: session.taskScope,
|
|
528
|
+
...session.taskTurnId ? { taskTurnId: session.taskTurnId } : {}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const workflow = summarizeWorkflows(workflows);
|
|
533
|
+
return { rows, ...workflow ? { workflow } : {} };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/bundle.ts
|
|
537
|
+
import { cp, copyFile, lstat, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
|
|
538
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
539
|
+
var SESSION_WINDOW_PAD_MS = 15 * 60 * 1e3;
|
|
540
|
+
function bundlePath(...parts) {
|
|
541
|
+
return parts.join("/");
|
|
542
|
+
}
|
|
543
|
+
async function statOrNull(path) {
|
|
544
|
+
try {
|
|
545
|
+
return await stat(path);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (isMissingPathError(error)) return null;
|
|
548
|
+
if (error.code === "ENOTDIR") return null;
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async function runGitLog(cwd, sinceIso, untilIso) {
|
|
553
|
+
const { execFile } = await import("child_process");
|
|
554
|
+
const { promisify } = await import("util");
|
|
555
|
+
const run = promisify(execFile);
|
|
556
|
+
try {
|
|
557
|
+
const { stdout } = await run(
|
|
558
|
+
"git",
|
|
559
|
+
["-C", cwd, "log", `--since=${sinceIso}`, `--until=${untilIso}`, "--date=iso-strict", "--pretty=format:%H %ad %an %s"],
|
|
560
|
+
{ timeout: 5e3 }
|
|
561
|
+
);
|
|
562
|
+
return { succeeded: true, stdout };
|
|
563
|
+
} catch (error) {
|
|
564
|
+
return { succeeded: false, error: error instanceof Error ? error.message.split("\n", 1)[0] : String(error) };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function listBundleFiles(root) {
|
|
568
|
+
const out = [];
|
|
569
|
+
const pending = [resolve(root)];
|
|
570
|
+
while (pending.length > 0) {
|
|
571
|
+
const dir = pending.pop();
|
|
572
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
573
|
+
const path = join(dir, entry.name);
|
|
574
|
+
if (entry.isDirectory()) pending.push(path);
|
|
575
|
+
else if (entry.isFile()) out.push(relative(resolve(root), path).split(sep).join("/"));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return out.sort();
|
|
579
|
+
}
|
|
580
|
+
async function assembleSessionBundle(opts) {
|
|
581
|
+
const { adapter, ref } = opts;
|
|
582
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
583
|
+
try {
|
|
584
|
+
await stat(ref.path);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (isMissingPathError(error)) {
|
|
587
|
+
throw new Error(`session transcript not found at ${ref.path} \u2014 a bundle cannot be assembled without its transcript`);
|
|
588
|
+
}
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
const selection = await collectSessionSelection([{ adapter, refs: [ref] }], {
|
|
592
|
+
bindSources: true,
|
|
593
|
+
signal: opts.signal
|
|
594
|
+
});
|
|
595
|
+
const row = selection.rows[0];
|
|
596
|
+
const spans = row.spans;
|
|
597
|
+
const transcriptSha256 = row.sourceFiles.find((file) => resolve(file.path) === resolve(ref.path))?.sha256 ?? row.sourceSha256;
|
|
598
|
+
const outDir = resolve(opts.outDir);
|
|
599
|
+
await mkdir(outDir, { recursive: true });
|
|
600
|
+
if ((await readdir(outDir)).length > 0) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`bundle output directory ${outDir} is not empty \u2014 pass a new directory so one bundle holds exactly one session`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const absent = [];
|
|
606
|
+
await mkdir(join(outDir, "session"), { recursive: true });
|
|
607
|
+
const subagentsSource = join(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
608
|
+
if ((await statOrNull(subagentsSource))?.isDirectory()) {
|
|
609
|
+
await cp(subagentsSource, join(outDir, "session", "subagents"), { recursive: true });
|
|
610
|
+
} else {
|
|
611
|
+
absent.push({ path: bundlePath("session", "subagents"), reason: `no subagents directory at ${subagentsSource}` });
|
|
612
|
+
}
|
|
613
|
+
const retainedSourceIds = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const file of row.sourceFiles) {
|
|
615
|
+
opts.signal?.throwIfAborted();
|
|
616
|
+
const sourceId = sourceFileId(file.path);
|
|
617
|
+
const subagentRelative = relative(subagentsSource, file.path);
|
|
618
|
+
const path = resolve(file.path) === resolve(ref.path) ? bundlePath("session", "transcript.jsonl") : !subagentRelative.startsWith("..") && !subagentRelative.startsWith(sep) ? bundlePath("session", "subagents", subagentRelative) : bundlePath("session", "sources", `${sourceId}.jsonl`);
|
|
619
|
+
const target = join(outDir, path);
|
|
620
|
+
await mkdir(dirname(target), { recursive: true });
|
|
621
|
+
await copyFile(file.path, target);
|
|
622
|
+
if (await fileSha256(target, opts.signal) !== file.sha256) {
|
|
623
|
+
throw new Error(`session source changed while retaining bundle evidence: ${file.path}`);
|
|
624
|
+
}
|
|
625
|
+
retainedSourceIds.set(path, sourceId);
|
|
626
|
+
}
|
|
627
|
+
await writeOtlpFile(spans, join(outDir, "spans.otlp.jsonl"));
|
|
628
|
+
const facts = await priceReport(buildSessionFactsReport(spans, { harness: ref.harness, generatedAt }));
|
|
629
|
+
await writeFile(join(outDir, "facts.json"), `${JSON.stringify(facts, null, 2)}
|
|
630
|
+
`, "utf8");
|
|
631
|
+
const own = facts.sessions.find((session) => session.sessionId === ref.sessionId) ?? facts.sessions[0];
|
|
632
|
+
const firstRecordAt = own?.firstRecordAt.value ?? null;
|
|
633
|
+
const lastRecordAt = own?.lastRecordAt.value ?? null;
|
|
634
|
+
const cwd = own?.cwd.value ?? ref.cwd ?? null;
|
|
635
|
+
const gitLogPath = "git-log.txt";
|
|
636
|
+
if (!cwd) {
|
|
637
|
+
absent.push({ path: gitLogPath, reason: "session has no recorded cwd to read git history from" });
|
|
638
|
+
} else if (firstRecordAt === null || lastRecordAt === null) {
|
|
639
|
+
absent.push({ path: gitLogPath, reason: "session window unavailable (no record carries a time)" });
|
|
640
|
+
} else {
|
|
641
|
+
const log = await runGitLog(
|
|
642
|
+
cwd,
|
|
643
|
+
new Date(Date.parse(firstRecordAt) - SESSION_WINDOW_PAD_MS).toISOString(),
|
|
644
|
+
new Date(Date.parse(lastRecordAt) + SESSION_WINDOW_PAD_MS).toISOString()
|
|
645
|
+
);
|
|
646
|
+
if (log.succeeded) {
|
|
647
|
+
await writeFile(join(outDir, gitLogPath), log.stdout ? `${log.stdout}
|
|
648
|
+
` : "", "utf8");
|
|
649
|
+
} else {
|
|
650
|
+
absent.push({ path: gitLogPath, reason: `git log failed in ${cwd}: ${log.error}` });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const files = [];
|
|
654
|
+
for (const path of await listBundleFiles(outDir)) {
|
|
655
|
+
const filePath = join(outDir, path);
|
|
656
|
+
const sourceId = retainedSourceIds.get(path);
|
|
657
|
+
files.push({
|
|
658
|
+
...sourceId !== void 0 ? { sourceId } : {},
|
|
659
|
+
path,
|
|
660
|
+
bytes: (await stat(filePath)).size,
|
|
661
|
+
sha256: await fileSha256(filePath)
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const manifest = {
|
|
665
|
+
schemaVersion: 3,
|
|
666
|
+
kind: "traces.session_bundle",
|
|
667
|
+
createdAt: generatedAt,
|
|
668
|
+
provenance: {
|
|
669
|
+
// The id the session recorded; a file named on the command line is its own ref.
|
|
670
|
+
sessionId: own?.sessionId ?? ref.sessionId,
|
|
671
|
+
harness: ref.harness,
|
|
672
|
+
cwd,
|
|
673
|
+
transcriptPath: resolve(ref.path),
|
|
674
|
+
transcriptSha256,
|
|
675
|
+
tracesVersion: tracesVersion(),
|
|
676
|
+
sessionWindow: { firstRecordAt, lastRecordAt, padMs: SESSION_WINDOW_PAD_MS }
|
|
677
|
+
},
|
|
678
|
+
files,
|
|
679
|
+
absent
|
|
680
|
+
};
|
|
681
|
+
const manifestPath = join(outDir, "manifest.json");
|
|
682
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
683
|
+
`, "utf8");
|
|
684
|
+
return { directory: outDir, manifestPath, manifest };
|
|
685
|
+
}
|
|
686
|
+
async function readBundleFileList(bundleDir) {
|
|
687
|
+
const path = join(bundleDir, "manifest.json");
|
|
688
|
+
let text;
|
|
689
|
+
try {
|
|
690
|
+
text = await readFile(path, "utf8");
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (isMissingPathError(error)) {
|
|
693
|
+
throw new Error(`no manifest.json at ${path} \u2014 ${bundleDir} is not a traces session bundle`);
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
let parsed;
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(text);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
702
|
+
}
|
|
703
|
+
const manifest = parsed;
|
|
704
|
+
if (manifest?.kind !== "traces.session_bundle") {
|
|
705
|
+
throw new Error(`${path} has kind ${JSON.stringify(manifest?.kind)}, expected "traces.session_bundle"`);
|
|
706
|
+
}
|
|
707
|
+
if (manifest.schemaVersion !== 3 && manifest.schemaVersion !== 2) {
|
|
708
|
+
throw new Error(`${path} is bundle schemaVersion ${JSON.stringify(manifest.schemaVersion)}; this traces reads 2 and 3`);
|
|
709
|
+
}
|
|
710
|
+
if (!Array.isArray(manifest.files)) throw new Error(`${path} lists no files`);
|
|
711
|
+
return manifest.files;
|
|
712
|
+
}
|
|
713
|
+
function unsafeBundlePath(path) {
|
|
714
|
+
if (path.length === 0) return "empty path";
|
|
715
|
+
if (path.includes("\\")) return "contains a backslash";
|
|
716
|
+
if (path.startsWith("/") || /^[A-Za-z]:/.test(path)) return "is absolute";
|
|
717
|
+
if (path.split("/").some((part) => part === ".." || part === "." || part === "")) {
|
|
718
|
+
return 'has an empty, "." or ".." segment';
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
async function nonRegularEntries(root) {
|
|
723
|
+
const out = [];
|
|
724
|
+
const pending = [resolve(root)];
|
|
725
|
+
while (pending.length > 0) {
|
|
726
|
+
const dir = pending.pop();
|
|
727
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
728
|
+
const path = join(dir, entry.name);
|
|
729
|
+
if (entry.isDirectory()) pending.push(path);
|
|
730
|
+
else if (!entry.isFile()) out.push(relative(resolve(root), path).split(sep).join("/"));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return out.sort();
|
|
734
|
+
}
|
|
735
|
+
async function verifySessionBundle(bundleDir) {
|
|
736
|
+
const directory = resolve(bundleDir);
|
|
737
|
+
const listedFiles = await readBundleFileList(directory);
|
|
738
|
+
const issues = [];
|
|
739
|
+
const listed = /* @__PURE__ */ new Set();
|
|
740
|
+
let matched = 0;
|
|
741
|
+
for (const file of listedFiles) {
|
|
742
|
+
const unsafe = unsafeBundlePath(file.path);
|
|
743
|
+
if (unsafe) {
|
|
744
|
+
issues.push({ kind: "unsafe-path", path: file.path, detail: `manifest path ${unsafe}` });
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
listed.add(file.path);
|
|
748
|
+
const info = await lstat(join(directory, file.path)).catch((error) => {
|
|
749
|
+
if (isMissingPathError(error)) return null;
|
|
750
|
+
throw error;
|
|
751
|
+
});
|
|
752
|
+
if (!info) {
|
|
753
|
+
issues.push({ kind: "missing", path: file.path, detail: "listed in the manifest, not on disk" });
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (!info.isFile()) continue;
|
|
757
|
+
const sha256 = await fileSha256(join(directory, file.path));
|
|
758
|
+
if (info.size !== file.bytes || sha256 !== file.sha256) {
|
|
759
|
+
issues.push({
|
|
760
|
+
kind: "mismatch",
|
|
761
|
+
path: file.path,
|
|
762
|
+
detail: `recorded ${file.bytes} bytes sha256 ${file.sha256}, found ${info.size} bytes sha256 ${sha256}`
|
|
763
|
+
});
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
matched += 1;
|
|
767
|
+
}
|
|
768
|
+
for (const path of await listBundleFiles(directory)) {
|
|
769
|
+
if (path === "manifest.json" || listed.has(path)) continue;
|
|
770
|
+
issues.push({ kind: "unexpected", path, detail: "on disk, not listed in the manifest" });
|
|
771
|
+
}
|
|
772
|
+
for (const path of await nonRegularEntries(directory)) {
|
|
773
|
+
issues.push({ kind: "not-a-file", path, detail: "a symbolic link or special file; a bundle holds regular files only" });
|
|
774
|
+
}
|
|
775
|
+
return { ok: issues.length === 0, directory, listed: listedFiles.length, matched, issues };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export {
|
|
779
|
+
collectSessionSelection,
|
|
780
|
+
assembleSessionBundle,
|
|
781
|
+
verifySessionBundle
|
|
782
|
+
};
|