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/dist/log-stage.js CHANGED
@@ -106,7 +106,9 @@ export async function stageCapture(store, snapshot, requested) {
106
106
  before: baseline?.before ?? [], beforeIds: baseline?.ids ?? [], beforeActive: baseline ? units(baseline.snapshot) : [],
107
107
  beforeActiveIds: baseline?.activeIds ?? [], context: selection(frames).context, allowInitialSystem: baseline?.firstTarget,
108
108
  origin: { harness: binding.harness, ...(binding.nativeSessionId ? { nativeSessionId: binding.nativeSessionId } : {}), bindingId: binding.id } });
109
- const { raw, canonical, attachments, ...header } = snapshot;
109
+ // Annotations are post-hoc by definition; a capture stages normalization-time
110
+ // evidence only, so they are not retained as header state.
111
+ const { raw, canonical, attachments, annotations: _annotations, ...header } = snapshot;
110
112
  const generated = baseline?.derivedBinding ? readNativeExportSnapshot(await regenerateExport(store, frames, baseline.derivedBinding)) : undefined;
111
113
  const derived = generated ? derivedChunks([generated.raw, ...(generated.attachments ?? []).map(a => a.content)]) : undefined;
112
114
  const prefix = baseline && raw.startsWith(baseline.snapshot.raw) ? { ...baseline.mapping,
@@ -53,7 +53,7 @@ export interface StageData {
53
53
  input: string;
54
54
  options: StageOptions;
55
55
  evidence: {
56
- header: Omit<Snapshot, 'raw' | 'canonical' | 'attachments'>;
56
+ header: Omit<Snapshot, 'raw' | 'canonical' | 'attachments' | 'annotations'>;
57
57
  raw: TextRef;
58
58
  attachments: Array<{
59
59
  name: string;
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { lstat, realpath } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
- import { HARNESSES } from './types.js';
4
+ import { HARNESSES, isNativeHarness } from './types.js';
5
5
  import { TrajectoriesError } from './log-types.js';
6
6
  import { convertManaged, readNativeExportSnapshot, sha256, writeNativeExport } from './convert.js';
7
7
  import { digest, stableJson, withSessionLock } from './log-io.js';
@@ -21,8 +21,9 @@ async function preparedReceipt(store, frame) {
21
21
  export async function switchSession(store, id, target, requested) {
22
22
  if (!requested || typeof requested !== 'object' || !requested.write || typeof requested.write !== 'object')
23
23
  throw new TrajectoriesError('INVALID_INPUT', 'Export options are required.');
24
- if (!HARNESSES.includes(target))
25
- throw new TrajectoriesError('INVALID_INPUT', 'Unsupported target harness.');
24
+ if (!isNativeHarness(target)) {
25
+ throw new TrajectoriesError('INVALID_INPUT', HARNESSES.includes(target) ? `Unsupported target harness: ${target} is a read-only interchange format.` : 'Unsupported target harness.');
26
+ }
26
27
  let cwd;
27
28
  try {
28
29
  cwd = await realpath(resolve(requested.write.cwd));
@@ -0,0 +1,10 @@
1
+ import type { SnapshotCanonical } from '../types.js';
2
+ /**
3
+ * Carry ATIF per-step `metrics` and `extra` onto the canonical records the
4
+ * upstream normalizer emits for that step, and mark harness-injected user turns
5
+ * with `origin: 'harness'`. The upstream adapter drops both kinds of signal;
6
+ * they are re-attached here as wrapper properties. `record_json`, `record_hash`,
7
+ * `record_id` and `content_hash` stay untouched, so enrichment never perturbs
8
+ * existing record identity — the snapshot id deterministically covers the wrapper.
9
+ */
10
+ export declare function enrichAtifCanonical(raw: string, canonical: SnapshotCanonical): SnapshotCanonical;
@@ -0,0 +1,54 @@
1
+ /** Matches the ATIF adapter's source record ids: `step:<n>` or `trajectory:<id>:step:<n>`. */
2
+ const STEP_RECORD_ID = /^(?:trajectory:.+:)?step:([1-9]\d*)$/;
3
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ const present = (value) => value !== undefined && value !== null;
5
+ /** ATIF cannot express a harness author: injected prompts (final-answer
6
+ * instructions, malformed-call retries, fallback final-answer calls) are recorded
7
+ * as `user` steps tagged with a synthetic `extra.role`. */
8
+ function injectedOrigin(step) {
9
+ if (step.source !== 'user')
10
+ return undefined;
11
+ const role = isObject(step.extra) ? step.extra.role : undefined;
12
+ return typeof role === 'string' && role ? 'harness' : undefined;
13
+ }
14
+ /**
15
+ * Carry ATIF per-step `metrics` and `extra` onto the canonical records the
16
+ * upstream normalizer emits for that step, and mark harness-injected user turns
17
+ * with `origin: 'harness'`. The upstream adapter drops both kinds of signal;
18
+ * they are re-attached here as wrapper properties. `record_json`, `record_hash`,
19
+ * `record_id` and `content_hash` stay untouched, so enrichment never perturbs
20
+ * existing record identity — the snapshot id deterministically covers the wrapper.
21
+ */
22
+ export function enrichAtifCanonical(raw, canonical) {
23
+ let document;
24
+ try {
25
+ document = JSON.parse(raw);
26
+ }
27
+ catch (cause) {
28
+ throw new Error('ATIF evidence is not the JSON trajectory that produced this canonical form.', { cause });
29
+ }
30
+ const steps = isObject(document) && Array.isArray(document.steps) ? document.steps : undefined;
31
+ if (!steps)
32
+ throw new Error('ATIF evidence has no steps array.');
33
+ const byStep = new Map();
34
+ for (const step of steps) {
35
+ if (!isObject(step) || typeof step.step_id !== 'number' || !Number.isInteger(step.step_id))
36
+ continue;
37
+ const { metrics, extra } = step;
38
+ const metadata = present(metrics) || present(extra)
39
+ ? { ...(present(metrics) ? { metrics } : {}), ...(present(extra) ? { extra } : {}) }
40
+ : undefined;
41
+ const origin = injectedOrigin(step);
42
+ if (!metadata && !origin)
43
+ continue;
44
+ byStep.set(step.step_id, { ...(metadata ? { metadata } : {}), ...(origin ? { origin } : {}) });
45
+ }
46
+ if (!byStep.size)
47
+ return canonical;
48
+ const records = canonical.records.map(record => {
49
+ const match = STEP_RECORD_ID.exec(record.stable_source_record_id);
50
+ const enrichment = match ? byStep.get(Number(match[1])) : undefined;
51
+ return enrichment ? { ...record, ...enrichment } : record;
52
+ });
53
+ return { ...canonical, records };
54
+ }
@@ -0,0 +1,31 @@
1
+ import type { CaptureId, Head, ManagedSession } from './log-types.js';
2
+ import { type Frame } from './log-state.js';
3
+ import { type IntentData } from './log-stage.js';
4
+ /** Immutable history transfer. Native paths are provenance; importing never writes native files. */
5
+ export interface ManagedDelta {
6
+ version: 1;
7
+ sessionId: string;
8
+ afterHead: Head | null;
9
+ head: Head;
10
+ frames: Frame[];
11
+ captures: Array<{
12
+ id: CaptureId;
13
+ object: string;
14
+ }>;
15
+ intents: Array<{
16
+ intentId: string;
17
+ data: IntentData;
18
+ }>;
19
+ objectIds: string[];
20
+ }
21
+ /** Enumerate only typed retained references. Generated export chunks remain reproducible from recipes. */
22
+ export declare function managedObjectIds(store: string, frames: Frame[], lineage?: Frame[]): Promise<string[]>;
23
+ export declare function exportManagedDelta(store: string, id: string, options?: {
24
+ afterHead?: Head | null;
25
+ }): Promise<ManagedDelta>;
26
+ /** Validate wire metadata before fetching any remote content references. */
27
+ export declare function validateManagedDelta(delta: ManagedDelta, expectedHead: Head | null): void;
28
+ /** Storage CAS, not an execution lease: importing cannot establish that a native process stopped. */
29
+ export declare function importManagedDelta(store: string, delta: ManagedDelta, options: {
30
+ expectedHead: Head | null;
31
+ }): Promise<ManagedSession>;
@@ -0,0 +1,333 @@
1
+ import { open, readFile, rm } from 'node:fs/promises';
2
+ import { dirname, isAbsolute } from 'node:path';
3
+ import { TrajectoriesError } from './log-types.js';
4
+ import { atomicWrite, digest, ensureDirectory, getObject, safeId, stableJson, syncDirectory, withSessionLock } from './log-io.js';
5
+ import { derivedChunks, getText } from './log-content.js';
6
+ import { initializeLog, logPath, readFrames } from './log-state.js';
7
+ import { capturePath, loadStage, recoverSnapshot, regenerateExport, bindingSnapshot } from './log-recovery.js';
8
+ import { readIntent, saveIntent, validateStageBacking, checkpointLimits } from './log-stage.js';
9
+ import { selection, sessionOf } from './log-read.js';
10
+ import { captureBaseline, mapPreparedRecords } from './log-baseline.js';
11
+ import { planRecords, loadRecord, units } from './log-records.js';
12
+ import { assertSnapshotContinuation } from './store.js';
13
+ import { HARNESSES } from './types.js';
14
+ import { assertSnapshot, readNativeExportSnapshot } from './convert.js';
15
+ function fail(message) { throw new TrajectoriesError('INTEGRITY_ERROR', message); }
16
+ const equal = (a, b) => stableJson(a) === stableJson(b);
17
+ function hash(value) {
18
+ if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value))
19
+ fail('Invalid shared content hash.');
20
+ }
21
+ /** Enumerate only typed retained references. Generated export chunks remain reproducible from recipes. */
22
+ export async function managedObjectIds(store, frames, lineage) {
23
+ const ids = new Set();
24
+ async function object(id, derived) {
25
+ hash(id);
26
+ try {
27
+ const bytes = await getObject(store, id);
28
+ ids.add(id);
29
+ return bytes;
30
+ }
31
+ catch (error) {
32
+ if (error.cause?.code !== 'ENOENT' || !derived?.has(id))
33
+ throw error;
34
+ return getObject(store, id, derived);
35
+ }
36
+ }
37
+ async function text(ref, derived) {
38
+ if (!ref?.root)
39
+ fail('Missing text reference.');
40
+ await getText(store, ref, derived);
41
+ const visiting = new Set();
42
+ async function visit(part) {
43
+ if (!part || visiting.has(part.id))
44
+ fail('Invalid shared content tree.');
45
+ const bytes = await object(part.id, derived);
46
+ if (part.tree) {
47
+ visiting.add(part.id);
48
+ const children = JSON.parse(bytes.toString());
49
+ if (!Array.isArray(children) || !children.length || children.length > 32)
50
+ fail('Invalid shared content tree.');
51
+ for (const child of children)
52
+ await visit(child);
53
+ visiting.delete(part.id);
54
+ }
55
+ }
56
+ await visit(ref.root);
57
+ }
58
+ let accepted = lineage;
59
+ async function evidence(value) {
60
+ let derived;
61
+ if (value.derivedBinding) {
62
+ accepted ??= await readFrames(store, frames[0].body.session.id);
63
+ const native = await regenerateExport(store, accepted, value.derivedBinding);
64
+ const snapshot = readNativeExportSnapshot(native);
65
+ derived = derivedChunks([snapshot.raw, ...(snapshot.attachments ?? []).map(item => item.content)]);
66
+ }
67
+ await text(value.raw, derived);
68
+ for (const attachment of value.attachments)
69
+ await text(attachment.text, derived);
70
+ }
71
+ for (const frame of frames) {
72
+ await text(frame.body.diagnostics);
73
+ for (const record of frame.body.records)
74
+ await text(JSON.parse((await object(record.object)).toString()));
75
+ if (frame.body.capture) {
76
+ hash(frame.body.stage);
77
+ const stage = JSON.parse((await object(frame.body.stage)).toString());
78
+ await evidence(stage.evidence);
79
+ await text(stage.diagnostics);
80
+ for (const record of stage.records)
81
+ await text(JSON.parse((await object(record.object)).toString()));
82
+ }
83
+ if (frame.body.legacyBaseline)
84
+ await evidence(frame.body.legacyBaseline);
85
+ }
86
+ return [...ids].sort();
87
+ }
88
+ export async function exportManagedDelta(store, id, options = {}) {
89
+ safeId(id);
90
+ const accepted = await readFrames(store, id), current = accepted.at(-1);
91
+ if (!current)
92
+ throw new TrajectoriesError('NOT_FOUND', 'Managed session does not exist.');
93
+ const afterHead = options.afterHead ?? null;
94
+ const index = afterHead === null ? -1 : accepted.findIndex(frame => frame.head === afterHead);
95
+ if (afterHead !== null && index < 0)
96
+ throw new TrajectoriesError('STALE_HEAD', 'Requested shared baseline is not an ancestor.');
97
+ const frames = accepted.slice(index + 1);
98
+ const captures = [], intents = [];
99
+ for (const frame of frames) {
100
+ const data = await readIntent(store, id, frame.body.intentId);
101
+ if (!data || data.input !== frame.body.input)
102
+ fail('Committed frame has no matching durable intent.');
103
+ intents.push({ intentId: frame.body.intentId, data });
104
+ if (frame.body.capture) {
105
+ const pointer = JSON.parse(await readFile(capturePath(store, frame.body.capture.id), 'utf8'));
106
+ if (pointer.object !== frame.body.stage)
107
+ fail('Capture pointer disagrees with its committed frame.');
108
+ captures.push({ id: frame.body.capture.id, object: pointer.object });
109
+ }
110
+ }
111
+ return { version: 1, sessionId: id, afterHead, head: current.head, frames, captures, intents,
112
+ objectIds: await managedObjectIds(store, frames) };
113
+ }
114
+ function validateEnvelope(delta, expectedHead) {
115
+ if (!delta || delta.version !== 1 || !Array.isArray(delta.frames) || !Array.isArray(delta.captures)
116
+ || !Array.isArray(delta.intents) || !Array.isArray(delta.objectIds))
117
+ fail('Invalid managed transfer.');
118
+ safeId(delta.sessionId);
119
+ hash(delta.head);
120
+ if (expectedHead !== null)
121
+ hash(expectedHead);
122
+ if (delta.afterHead !== expectedHead)
123
+ throw new TrajectoriesError('STALE_HEAD', 'Transfer baseline differs from requested head.');
124
+ let parent = delta.afterHead;
125
+ const intents = new Set();
126
+ for (const frame of delta.frames) {
127
+ if (!frame?.body || frame.body.session?.id !== delta.sessionId || frame.body.parent !== parent
128
+ || digest(stableJson(frame.body)) !== frame.head)
129
+ fail('Shared log chain is corrupt.');
130
+ hash(frame.head);
131
+ hash(frame.body.input);
132
+ safeId(frame.body.intentId);
133
+ if (intents.has(frame.body.intentId))
134
+ fail('Repeated shared intent.');
135
+ intents.add(frame.body.intentId);
136
+ if (!isAbsolute(frame.body.session.workspace) || !Number.isFinite(Date.parse(frame.body.session.createdAt))
137
+ || !Number.isFinite(Date.parse(frame.body.time)) || !Array.isArray(frame.body.records)
138
+ || typeof frame.body.progress !== 'boolean' || !HARNESSES.includes(frame.body.binding?.harness)
139
+ || typeof frame.body.binding.nativeSessionId !== 'string')
140
+ fail('Invalid managed frame metadata.');
141
+ safeId(frame.body.binding.id);
142
+ checkpointLimits(frame.body.session.nativeCheckpoints);
143
+ if (frame.body.kind !== 'capture' && frame.body.kind !== 'binding')
144
+ fail('Unknown managed frame kind.');
145
+ parent = frame.head;
146
+ }
147
+ if (parent !== delta.head)
148
+ fail('Transfer head does not match its suffix.');
149
+ if (new Set(delta.objectIds).size !== delta.objectIds.length)
150
+ fail('Duplicate shared object declaration.');
151
+ for (const id of delta.objectIds)
152
+ hash(id);
153
+ if (delta.intents.length !== delta.frames.length || new Set(delta.intents.map(item => item.intentId)).size !== delta.intents.length)
154
+ fail('Transfer intent inventory is incomplete.');
155
+ const captures = delta.frames.filter(frame => frame.body.capture);
156
+ if (delta.captures.length !== captures.length || new Set(delta.captures.map(item => item.id)).size !== delta.captures.length)
157
+ fail('Transfer capture inventory is incomplete.');
158
+ for (const frame of delta.frames) {
159
+ const intent = delta.intents.find(item => item.intentId === frame.body.intentId)?.data;
160
+ if (!intent || intent.input !== frame.body.input)
161
+ fail('Transfer intent differs from its frame.');
162
+ if (frame.body.kind === 'capture') {
163
+ const ref = frame.body.capture;
164
+ if (!ref || !frame.body.stage || intent.stage !== frame.body.stage || intent.export
165
+ || ref.sessionId !== delta.sessionId || ref.parent !== frame.body.parent || ref.intentId !== frame.body.intentId
166
+ || ref.integrity !== frame.body.input || ref.bindingId !== frame.body.binding.id
167
+ || ref.source.harness !== frame.body.binding.harness || (ref.source.nativeSessionId ?? '') !== frame.body.binding.nativeSessionId)
168
+ fail('Capture frame has inconsistent provenance.');
169
+ safeId(ref.id);
170
+ hash(frame.body.stage);
171
+ if (delta.captures.find(item => item.id === ref.id)?.object !== frame.body.stage)
172
+ fail('Capture pointer differs from its frame.');
173
+ }
174
+ else if (frame.body.capture || frame.body.stage || frame.body.records.length || frame.body.edit || frame.body.progress
175
+ || (!!frame.body.recipe === !!frame.body.legacyBaseline) || !equal(intent.export ?? null, frame.body.recipe ?? null) || intent.stage) {
176
+ fail('Prepared binding frame has inconsistent retained input.');
177
+ }
178
+ }
179
+ }
180
+ /** Validate wire metadata before fetching any remote content references. */
181
+ export function validateManagedDelta(delta, expectedHead) {
182
+ validateEnvelope(delta, expectedHead);
183
+ }
184
+ async function validateCapture(store, frame, before) {
185
+ const stage = await loadStage(store, frame.body.capture);
186
+ if (stage.version !== 2 || stage.options.sessionId !== frame.body.session.id || stage.options.intentId !== frame.body.intentId
187
+ || stage.options.expectedHead !== frame.body.parent || stage.input !== frame.body.input
188
+ || !equal(stage.ref, frame.body.capture) || !equal(stage.records, frame.body.records) || !equal(stage.edit, frame.body.edit)
189
+ || !equal(stage.binding, frame.body.binding) || stage.progress !== frame.body.progress
190
+ || !equal(stage.diagnostics, frame.body.diagnostics) || !equal(stage.nativeMeta ?? null, frame.body.nativeMeta ?? null))
191
+ fail('Committed capture does not match its retained stage.');
192
+ if (frame.body.parent === null && (stage.options.expectedHead !== null || stage.options.workspace !== frame.body.session.workspace
193
+ || !equal(stage.options.nativeCheckpoints, frame.body.session.nativeCheckpoints)))
194
+ fail('Initial capture session options differ.');
195
+ if (typeof stage.options.requireProgress !== 'boolean')
196
+ fail('Invalid capture progress policy.');
197
+ await validateStageBacking(store, stage, [...before, frame]);
198
+ const snapshot = await recoverSnapshot(store, stage, [...before, frame]);
199
+ assertSnapshot(snapshot);
200
+ const source = { harness: snapshot.source.harness, sha256: digest(snapshot.raw),
201
+ ...(snapshot.source.nativeSessionId ? { nativeSessionId: snapshot.source.nativeSessionId } : {}) };
202
+ if (!equal(stage.ref.source, source))
203
+ fail('Capture source fingerprint differs from recovered native evidence.');
204
+ const diagnostics = JSON.parse(await getText(store, stage.diagnostics));
205
+ const meta = snapshot.canonical.records.find(record => record.record_type === 'meta');
206
+ if (!equal(diagnostics, snapshot.canonical.diagnostics) || !equal(stage.nativeMeta ?? null, meta ? JSON.parse(meta.record_json) : null))
207
+ fail('Capture diagnostics or native metadata differ from recovered evidence.');
208
+ const operation = frame.body.parent === null ? 'create' : 'append';
209
+ if (digest(stableJson({ operation, options: stage.options, snapshot: snapshot.id })) !== stage.input)
210
+ fail('Capture intent does not bind its recovered native observation.');
211
+ const baseline = before.length ? await captureBaseline(store, before, snapshot, stage.options.expectedHead === null ? undefined : stage.options.bindingId) : undefined;
212
+ if (baseline)
213
+ assertSnapshotContinuation(baseline.snapshot, snapshot, { requireProgress: stage.options.requireProgress, allowInitialSystem: baseline.firstTarget });
214
+ const planned = await planRecords({ store, sessionId: frame.body.session.id, intentId: frame.body.intentId, observed: snapshot,
215
+ before: baseline?.before ?? [], beforeIds: baseline?.ids ?? [], beforeActive: baseline ? units(baseline.snapshot) : [],
216
+ beforeActiveIds: baseline?.activeIds ?? [], context: selection(before).context, allowInitialSystem: baseline?.firstTarget,
217
+ origin: { harness: stage.binding.harness, ...(stage.binding.nativeSessionId ? { nativeSessionId: stage.binding.nativeSessionId } : {}), bindingId: stage.binding.id } });
218
+ const mapping = { ...baseline?.mapping, added: planned.added, ...(planned.leading ? { leading: planned.leading } : {}) };
219
+ if (!equal(planned.records, stage.records) || !equal(planned.edit, stage.edit) || planned.progress !== stage.progress || !equal(mapping, stage.mapping)
220
+ || (baseline && !equal(baseline.binding, stage.binding)))
221
+ fail('Capture records or context do not reproduce from native evidence.');
222
+ }
223
+ /** Storage CAS, not an execution lease: importing cannot establish that a native process stopped. */
224
+ export async function importManagedDelta(store, delta, options) {
225
+ validateEnvelope(delta, options?.expectedHead);
226
+ return withSessionLock(store, delta.sessionId, async () => {
227
+ await initializeLog(store);
228
+ const existing = await readFrames(store, delta.sessionId), current = existing.at(-1);
229
+ const replay = current?.head === delta.head;
230
+ if (replay) {
231
+ const index = delta.afterHead === null ? -1 : existing.findIndex(frame => frame.head === delta.afterHead);
232
+ if ((delta.afterHead !== null && index < 0) || !equal(existing.slice(index + 1), delta.frames))
233
+ throw new TrajectoriesError('INTENT_CONFLICT', 'Shared replay does not match the exact committed transition.');
234
+ }
235
+ else if ((current?.head ?? null) !== options.expectedHead) {
236
+ throw new TrajectoriesError('STALE_HEAD', 'Shared session advanced or diverged.');
237
+ }
238
+ const before = replay ? existing.slice(0, existing.length - delta.frames.length) : existing;
239
+ const seen = new Set(before.map(frame => frame.body.intentId));
240
+ const combined = [...before];
241
+ for (const frame of delta.frames) {
242
+ if (seen.has(frame.body.intentId))
243
+ throw new TrajectoriesError('INTENT_CONFLICT', 'Intent already belongs to another accepted frame.');
244
+ seen.add(frame.body.intentId);
245
+ if (combined[0] && !equal(combined[0].body.session, frame.body.session))
246
+ fail('Immutable session metadata changed.');
247
+ if (!combined.length && (frame.body.kind !== 'capture' || frame.body.session.createdAt !== frame.body.time))
248
+ fail('Invalid initial session frame.');
249
+ combined.push(frame);
250
+ }
251
+ selection(combined);
252
+ for (const id of delta.objectIds)
253
+ await getObject(store, id);
254
+ const createdPointers = [];
255
+ let publicationStarted = false;
256
+ try {
257
+ for (const pointer of delta.captures) {
258
+ const path = capturePath(store, pointer.id);
259
+ try {
260
+ if (!equal(JSON.parse(await readFile(path, 'utf8')), { object: pointer.object }))
261
+ fail('Immutable capture pointer conflicts.');
262
+ }
263
+ catch (error) {
264
+ if (error.code !== 'ENOENT')
265
+ throw error;
266
+ await atomicWrite(path, stableJson({ object: pointer.object }), true);
267
+ createdPointers.push(path);
268
+ }
269
+ }
270
+ for (const item of delta.intents) {
271
+ const previous = await readIntent(store, delta.sessionId, item.intentId);
272
+ if (previous && !equal(previous, item.data))
273
+ throw new TrajectoriesError('INTENT_CONFLICT', 'Durable intent has different retained input.');
274
+ }
275
+ // Validate actual typed closure, including retained prefixes. A sender's
276
+ // declared object inventory cannot establish complete backing.
277
+ await managedObjectIds(store, combined, combined);
278
+ const validated = [...before];
279
+ for (const frame of delta.frames) {
280
+ if (frame.body.kind === 'capture')
281
+ await validateCapture(store, frame, validated);
282
+ else {
283
+ if (frame.body.recipe && (frame.body.recipe.atHead !== frame.body.parent || frame.body.recipe.write.cwd !== frame.body.session.workspace
284
+ || frame.body.recipe.seed !== digest(stableJson([frame.body.input, 'native-entropy']))))
285
+ fail('Prepared export recipe is inconsistent.');
286
+ const snapshot = await bindingSnapshot(store, [...validated, frame], frame.body.binding.id);
287
+ assertSnapshot(snapshot);
288
+ if (snapshot.source.harness !== frame.body.binding.harness || snapshot.source.nativeSessionId !== frame.body.binding.nativeSessionId)
289
+ fail('Prepared binding native identity differs.');
290
+ const native = frame.body.recipe ? await regenerateExport(store, [...validated, frame], frame.body.binding.id) : undefined;
291
+ const diagnostics = JSON.parse(await getText(store, frame.body.diagnostics));
292
+ const meta = snapshot.canonical.records.find(record => record.record_type === 'meta');
293
+ if (!equal(diagnostics, native?.diagnostics ?? snapshot.canonical.diagnostics)
294
+ || !equal(frame.body.nativeMeta ?? null, meta ? JSON.parse(meta.record_json) : null))
295
+ fail('Prepared binding diagnostics or native metadata differ from retained evidence.');
296
+ const selected = selection(validated), records = new Map(selected.history.map(record => [record.id, record]));
297
+ const context = await Promise.all(selected.context.map(async (id) => ({ id, record: await loadRecord(store, records.get(id)) })));
298
+ mapPreparedRecords(context, units(snapshot, true));
299
+ }
300
+ validated.push(frame);
301
+ }
302
+ if (!validated.length)
303
+ fail('An empty transfer cannot create a session.');
304
+ if (!replay && delta.frames.length) {
305
+ for (const item of delta.intents)
306
+ await saveIntent(store, delta.sessionId, item.intentId, item.data);
307
+ publicationStarted = true;
308
+ const path = logPath(store, delta.sessionId);
309
+ await ensureDirectory(dirname(path));
310
+ const file = await open(path, 'a', 0o600);
311
+ try {
312
+ await file.writeFile(delta.frames.map(frame => stableJson(frame) + '\n').join(''));
313
+ await file.sync();
314
+ }
315
+ finally {
316
+ await file.close();
317
+ }
318
+ await syncDirectory(dirname(path));
319
+ }
320
+ return sessionOf(validated.at(-1));
321
+ }
322
+ catch (error) {
323
+ // Failed validation reserves no capture identity. After an append starts,
324
+ // preserve backing for every potentially committed frame; partial tails fail closed.
325
+ if (!publicationStarted)
326
+ for (const path of createdPointers)
327
+ await rm(path);
328
+ if (error instanceof TrajectoriesError)
329
+ throw error;
330
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Shared history validation or publication failed.', { cause: error });
331
+ }
332
+ });
333
+ }
@@ -0,0 +1,96 @@
1
+ import type { CanonicalRecordType, SourceIdentityKind } from '@letta-ai/trajectory';
2
+ /** Read-only native-history observations, not proof of accepted native execution. */
3
+ export interface ObservedSession {
4
+ id: string;
5
+ source: string;
6
+ source_path: string;
7
+ content_hash: string;
8
+ content_hash_version?: number;
9
+ title: string | null;
10
+ cwd: string | null;
11
+ model: string | null;
12
+ started_at: string | null;
13
+ ended_at: string | null;
14
+ record_count: number;
15
+ created_at: string;
16
+ updated_at: string;
17
+ parent_session_id?: string | null;
18
+ parent_tool_use_id?: string | null;
19
+ agent_nickname?: string | null;
20
+ thread_role?: string | null;
21
+ diagnostics?: Array<{
22
+ code: string;
23
+ occurrenceCount: number;
24
+ sample: string | null;
25
+ firstSeenAt?: string;
26
+ }>;
27
+ }
28
+ export interface ObservedRecord {
29
+ id: string;
30
+ session_id: string;
31
+ source_type: string;
32
+ native_session_id?: string | null;
33
+ binding_id?: string | null;
34
+ record_type: CanonicalRecordType;
35
+ stable_source_record_id: string | null;
36
+ source_identity_kind: SourceIdentityKind | null;
37
+ source_order_id: string;
38
+ component_index: number;
39
+ record_timestamp: string | null;
40
+ source_timestamp: string | null;
41
+ content: string | null;
42
+ tool_call_id: string | null;
43
+ tool_name: string | null;
44
+ tool_arguments_json: string | null;
45
+ tool_result_json: string | null;
46
+ tool_result_ok: number | null;
47
+ record_json: string;
48
+ content_hash: string;
49
+ record_hash: string;
50
+ }
51
+ export interface ObservedSummary {
52
+ id: string;
53
+ head: string;
54
+ machineId: string;
55
+ session: ObservedSession;
56
+ }
57
+ export interface ObservedState extends ObservedSummary {
58
+ records: ObservedRecord[];
59
+ }
60
+ export interface ObservedManifest extends ObservedSummary {
61
+ recordObjects: Array<{
62
+ id: string;
63
+ object: string;
64
+ }>;
65
+ }
66
+ export interface ObservedDelta {
67
+ version: 1;
68
+ id: string;
69
+ parent: string | null;
70
+ machineId: string;
71
+ sessionId: string;
72
+ sessionObject: string;
73
+ set: Array<{
74
+ id: string;
75
+ object: string;
76
+ }>;
77
+ remove: string[];
78
+ }
79
+ export declare function sharedObservationId(machineId: string, sessionId: string): string;
80
+ export declare function assertObservedSummary(value: ObservedSummary, id?: string): void;
81
+ export declare function assertObservedManifest(value: ObservedManifest, id?: string): void;
82
+ export declare function assertObservedState(value: ObservedState, id?: string): void;
83
+ export declare function getObservedManifest(store: string, id: string): Promise<ObservedManifest>;
84
+ export declare function readObservedHistory(store: string, id: string): Promise<ObservedState>;
85
+ export declare function listObservedHistories(store: string): Promise<ObservedSummary[]>;
86
+ export declare function commitObservedDelta(store: string, delta: ObservedDelta): Promise<ObservedSummary>;
87
+ export declare function prepareObservedHistory(input: {
88
+ machineId: string;
89
+ session: ObservedSession;
90
+ records: ObservedRecord[];
91
+ }, previous?: ObservedManifest): Promise<{
92
+ delta: ObservedDelta | null;
93
+ objects: Map<string, Uint8Array>;
94
+ changedRecords: number;
95
+ removedRecords: number;
96
+ }>;