trajectories 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +201 -0
  3. package/README.md +274 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +8542 -0
  6. package/dist/convert.d.ts +31 -0
  7. package/dist/convert.js +431 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1219 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +120 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-release.sh +49 -0
  197. package/ops/linux/install-systemd-user.sh +103 -0
  198. package/ops/macos/install-launchd.sh +72 -0
  199. package/ops/sync-settings.sh +37 -0
  200. package/package.json +19 -5
  201. package/skills/trajectories/SKILL.md +138 -0
  202. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  203. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  204. package/index.js +0 -1
@@ -0,0 +1,26 @@
1
+ import type { Snapshot } from './types.js';
2
+ import type { BindingId, CaptureId, NativeBinding, RecordId } from './log-types.js';
3
+ import { type Unit } from './log-records.js';
4
+ import type { Frame, StageData } from './log-state.js';
5
+ import type { NormalizedRecord } from '@letta-ai/trajectory';
6
+ export declare function mapPreparedRecords(source: readonly {
7
+ id: RecordId;
8
+ record: {
9
+ payload: NormalizedRecord;
10
+ };
11
+ }[], target: readonly Unit[]): RecordId[];
12
+ export declare function captureMapping(store: string, frames: Frame[], stage: StageData, visited?: Set<CaptureId>): Promise<RecordId[]>;
13
+ export interface Baseline {
14
+ snapshot: Snapshot;
15
+ before: Unit[];
16
+ ids: RecordId[];
17
+ activeIds: RecordId[];
18
+ binding: NativeBinding;
19
+ mapping: {
20
+ capture?: CaptureId;
21
+ binding?: BindingId;
22
+ };
23
+ derivedBinding?: BindingId;
24
+ firstTarget: boolean;
25
+ }
26
+ export declare function captureBaseline(store: string, frames: Frame[], observed: Snapshot, requestedBinding?: BindingId): Promise<Baseline>;
@@ -0,0 +1,104 @@
1
+ import { TrajectoriesError } from './log-types.js';
2
+ import { atHead, selection } from './log-read.js';
3
+ import { loadRecord, units } from './log-records.js';
4
+ import { bindingSnapshot, loadStage, recoverSnapshot } from './log-recovery.js';
5
+ import { portableConversationTokens } from './portable.js';
6
+ const semantic = (record) => portableConversationTokens([record])[0]?.semantic;
7
+ const isTool = (record) => record.role === 'tool' || (record.role === 'assistant' && 'tool_calls' in record);
8
+ const omittedRole = (record) => ['system', 'reasoning', 'observation'].includes(record.role);
9
+ // Native codecs can serialize a completed batch in call order and omit a tool's
10
+ // success flag. Its call identity, arguments and complete result must survive.
11
+ const bindingSemantic = (record) => record.role === 'tool'
12
+ ? JSON.stringify(['tool', record.tool_call_id, record.content]) : semantic(record);
13
+ export function mapPreparedRecords(source, target) {
14
+ const ids = [];
15
+ let index = 0, next = 0;
16
+ const fail = () => { throw new TrajectoriesError('UNSUPPORTED_CONTEXT', 'Prepared baseline does not preserve its canonical conversation.'); };
17
+ while (next < target.length) {
18
+ const wanted = target[next];
19
+ while (index < source.length && omittedRole(source[index].record.payload)
20
+ && semantic(source[index].record.payload) !== wanted.semantic)
21
+ index++;
22
+ if (index >= source.length)
23
+ fail();
24
+ if (isTool(wanted.payload)) {
25
+ const batch = [];
26
+ while (index < source.length && (isTool(source[index].record.payload) || omittedRole(source[index].record.payload))) {
27
+ const item = source[index++];
28
+ if (isTool(item.record.payload))
29
+ batch.push({ id: item.id, key: bindingSemantic(item.record.payload) });
30
+ }
31
+ if (!batch.length)
32
+ fail();
33
+ while (next < target.length && isTool(target[next].payload)) {
34
+ const key = bindingSemantic(target[next++].payload), match = batch.findIndex(item => item.key === key);
35
+ if (match < 0)
36
+ fail();
37
+ ids.push(batch.splice(match, 1)[0].id);
38
+ }
39
+ if (batch.length)
40
+ fail();
41
+ }
42
+ else {
43
+ if (semantic(source[index].record.payload) !== wanted.semantic)
44
+ fail();
45
+ ids.push(source[index++].id);
46
+ next++;
47
+ }
48
+ }
49
+ if (source.slice(index).some(item => !omittedRole(item.record.payload)))
50
+ fail();
51
+ return ids;
52
+ }
53
+ async function bindingMapping(store, frames, id) {
54
+ const frame = frames.find(frame => frame.body.kind === 'binding' && frame.body.binding.id === id);
55
+ if (!frame?.body.parent)
56
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Prepared binding baseline is missing.');
57
+ const snapshot = await bindingSnapshot(store, frames, id);
58
+ const selected = selection(atHead(frames, frame.body.parent)), byId = new Map(selected.history.map(record => [record.id, record]));
59
+ const portable = await Promise.all(selected.context.map(async (id) => ({ id, record: await loadRecord(store, byId.get(id)) })));
60
+ const ids = mapPreparedRecords(portable, units(snapshot, true));
61
+ return { snapshot, ids };
62
+ }
63
+ export async function captureMapping(store, frames, stage, visited = new Set()) {
64
+ if (visited.has(stage.ref.id))
65
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Native baseline reference cycle.');
66
+ visited.add(stage.ref.id);
67
+ let prefix = [];
68
+ if (stage.mapping.capture) {
69
+ const ref = frames.find(frame => frame.body.capture?.id === stage.mapping.capture)?.body.capture;
70
+ if (!ref)
71
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Accepted native baseline capture is missing.');
72
+ prefix = await captureMapping(store, frames, await loadStage(store, ref), visited);
73
+ }
74
+ else if (stage.mapping.binding)
75
+ prefix = (await bindingMapping(store, frames, stage.mapping.binding)).ids;
76
+ return [...(stage.mapping.leading ? [stage.mapping.leading] : []), ...prefix, ...stage.mapping.added];
77
+ }
78
+ export async function captureBaseline(store, frames, observed, requestedBinding) {
79
+ const current = frames.at(-1);
80
+ const latest = [...frames].reverse().find(frame => frame.body.capture);
81
+ if (!latest?.body.capture)
82
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Current accepted native source is missing.');
83
+ const selected = selection(frames);
84
+ if (!requestedBinding || requestedBinding === latest.body.binding.id) {
85
+ if (requestedBinding && current.body.binding.id !== requestedBinding)
86
+ throw new TrajectoriesError('STALE_HEAD', 'Requested binding is not current at this head.');
87
+ const stage = await loadStage(store, latest.body.capture);
88
+ if (stage.ref.source.harness !== observed.source.harness || !observed.source.nativeSessionId
89
+ || stage.ref.source.nativeSessionId !== observed.source.nativeSessionId)
90
+ throw new TrajectoriesError('INVALID_INPUT', 'Another native lineage must name its committed prepared binding.');
91
+ const snapshot = await recoverSnapshot(store, stage, frames);
92
+ return { snapshot, before: units(snapshot, true), ids: await captureMapping(store, frames, stage), activeIds: selected.context,
93
+ binding: stage.binding, mapping: { capture: stage.ref.id }, firstTarget: false,
94
+ ...(stage.evidence.derivedBinding ? { derivedBinding: stage.evidence.derivedBinding } : {}) };
95
+ }
96
+ if (current.body.binding.id !== requestedBinding || current.body.binding.harness !== observed.source.harness
97
+ || current.body.binding.nativeSessionId !== observed.source.nativeSessionId)
98
+ throw new TrajectoriesError('STALE_HEAD', 'Target capture does not belong to the exact committed binding.');
99
+ const prepared = await bindingMapping(store, frames, requestedBinding);
100
+ const bindingFrame = frames.find(frame => frame.body.kind === 'binding' && frame.body.binding.id === requestedBinding);
101
+ return { snapshot: prepared.snapshot, before: units(prepared.snapshot, true), ids: prepared.ids, activeIds: prepared.ids,
102
+ binding: current.body.binding, mapping: { binding: requestedBinding },
103
+ ...(!bindingFrame.body.legacyBaseline ? { derivedBinding: requestedBinding } : {}), firstTarget: true };
104
+ }
@@ -0,0 +1,16 @@
1
+ interface Part {
2
+ id: string;
3
+ bytes: number;
4
+ tree?: true;
5
+ }
6
+ export interface TextRef {
7
+ root: Part;
8
+ sha256: string;
9
+ bytes: number;
10
+ }
11
+ /** Content-defined boundaries keep unchanged ranges shared after native JSON rewrites. */
12
+ export declare function textChunks(text: string): Buffer[];
13
+ export declare function derivedChunks(texts: readonly string[]): Map<string, Buffer>;
14
+ export declare function putText(store: string, text: string, derived?: ReadonlyMap<string, Buffer>): Promise<TextRef>;
15
+ export declare function getText(store: string, ref: TextRef, derived?: ReadonlyMap<string, Buffer>): Promise<string>;
16
+ export {};
@@ -0,0 +1,80 @@
1
+ import { digest, getObject, putObject, stableJson } from './log-io.js';
2
+ import { TrajectoriesError } from './log-types.js';
3
+ /** Content-defined boundaries keep unchanged ranges shared after native JSON rewrites. */
4
+ export function textChunks(text) {
5
+ const bytes = Buffer.from(text), chunks = [];
6
+ let start = 0, rolling = 0;
7
+ for (let i = 0; i < bytes.length; i++) {
8
+ rolling = ((rolling << 1) + Math.imul(bytes[i] + 1, 0x9e3779b1)) >>> 0;
9
+ const length = i + 1 - start;
10
+ if (length >= 16384 || (length >= 2048 && (rolling & 4095) === 0)) {
11
+ chunks.push(bytes.subarray(start, i + 1));
12
+ start = i + 1;
13
+ rolling = 0;
14
+ }
15
+ }
16
+ if (start < bytes.length || !chunks.length)
17
+ chunks.push(bytes.subarray(start));
18
+ return chunks;
19
+ }
20
+ export function derivedChunks(texts) {
21
+ const result = new Map();
22
+ for (const text of texts)
23
+ for (const bytes of textChunks(text))
24
+ result.set(digest(bytes), bytes);
25
+ return result;
26
+ }
27
+ export async function putText(store, text, derived) {
28
+ let parts = [];
29
+ for (const bytes of textChunks(text)) {
30
+ const id = digest(bytes);
31
+ if (!derived?.has(id))
32
+ await putObject(store, bytes);
33
+ parts.push({ id, bytes: bytes.length });
34
+ }
35
+ while (parts.length > 1) {
36
+ const next = [];
37
+ for (let i = 0; i < parts.length; i += 32) {
38
+ const group = parts.slice(i, i + 32);
39
+ next.push({ id: await putObject(store, stableJson(group)), bytes: group.reduce((sum, p) => sum + p.bytes, 0), tree: true });
40
+ }
41
+ parts = next;
42
+ }
43
+ return { root: parts[0], sha256: digest(text), bytes: Buffer.byteLength(text) };
44
+ }
45
+ export async function getText(store, ref, derived) {
46
+ if (!ref || !Number.isSafeInteger(ref.bytes) || ref.bytes < 0)
47
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid text reference.');
48
+ const chunks = [], ancestors = new Set();
49
+ async function visit(part) {
50
+ if (!part || !Number.isSafeInteger(part.bytes) || part.bytes < 0 || ancestors.has(part.id))
51
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid content tree.');
52
+ const bytes = await getObject(store, part.id, derived);
53
+ if (part.tree) {
54
+ ancestors.add(part.id);
55
+ let children;
56
+ try {
57
+ children = JSON.parse(bytes.toString());
58
+ }
59
+ catch (cause) {
60
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid content-tree node.', { cause });
61
+ }
62
+ if (!Array.isArray(children) || !children.length || children.length > 32
63
+ || children.reduce((sum, child) => sum + child.bytes, 0) !== part.bytes)
64
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid content-tree size.');
65
+ for (const child of children)
66
+ await visit(child);
67
+ ancestors.delete(part.id);
68
+ }
69
+ else {
70
+ if (bytes.length !== part.bytes)
71
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid content size.');
72
+ chunks.push(bytes);
73
+ }
74
+ }
75
+ await visit(ref.root);
76
+ const bytes = Buffer.concat(chunks), text = bytes.toString('utf8');
77
+ if (bytes.length !== ref.bytes || digest(bytes) !== ref.sha256 || !Buffer.from(text).equals(bytes))
78
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Text integrity mismatch.');
79
+ return text;
80
+ }
@@ -0,0 +1,10 @@
1
+ export declare function stableJson(value: unknown): string;
2
+ export declare const digest: (content: string | Uint8Array) => string;
3
+ export declare function safeId(value: string): string;
4
+ export declare function objectPath(store: string, id: string): string;
5
+ export declare function syncDirectory(path: string): Promise<void>;
6
+ export declare function ensureDirectory(path: string): Promise<void>;
7
+ export declare function atomicWrite(path: string, content: string | Uint8Array, immutable?: boolean): Promise<void>;
8
+ export declare function putObject(store: string, content: string | Uint8Array): Promise<string>;
9
+ export declare function getObject(store: string, id: string, derived?: ReadonlyMap<string, Buffer>): Promise<Buffer>;
10
+ export declare function withSessionLock<T>(store: string, id: string, action: () => Promise<T>): Promise<T>;
package/dist/log-io.js ADDED
@@ -0,0 +1,121 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { link, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { TrajectoriesError } from './log-types.js';
5
+ export function stableJson(value) {
6
+ if (Array.isArray(value))
7
+ return `[${value.map(item => item === undefined ? 'null' : stableJson(item)).join(',')}]`;
8
+ if (value && typeof value === 'object')
9
+ return `{${Object.keys(value).sort()
10
+ .filter(key => value[key] !== undefined)
11
+ .map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
12
+ const result = JSON.stringify(value);
13
+ if (result === undefined)
14
+ throw new TrajectoriesError('INVALID_INPUT', 'Input must be JSON serializable.');
15
+ return result;
16
+ }
17
+ export const digest = (content) => createHash('sha256').update(content).digest('hex');
18
+ export function safeId(value) {
19
+ if (typeof value !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value))
20
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid session or intent identifier.');
21
+ return value;
22
+ }
23
+ export function objectPath(store, id) {
24
+ if (!/^[a-f0-9]{64}$/.test(id))
25
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid content reference.');
26
+ return join(resolve(store), 'objects', id.slice(0, 2), id);
27
+ }
28
+ export async function syncDirectory(path) {
29
+ const file = await open(path, 'r');
30
+ try {
31
+ await file.sync();
32
+ }
33
+ finally {
34
+ await file.close();
35
+ }
36
+ }
37
+ export async function ensureDirectory(path) {
38
+ try {
39
+ await mkdir(path, { mode: 0o700 });
40
+ await syncDirectory(dirname(path));
41
+ }
42
+ catch (cause) {
43
+ if (cause.code === 'EEXIST')
44
+ return;
45
+ if (cause.code !== 'ENOENT')
46
+ throw cause;
47
+ await ensureDirectory(dirname(path));
48
+ await ensureDirectory(path);
49
+ }
50
+ }
51
+ export async function atomicWrite(path, content, immutable = false) {
52
+ await ensureDirectory(dirname(path));
53
+ const temporary = `${path}.${randomUUID()}.tmp`;
54
+ const file = await open(temporary, 'wx', 0o600);
55
+ try {
56
+ await file.writeFile(content);
57
+ await file.sync();
58
+ }
59
+ finally {
60
+ await file.close();
61
+ }
62
+ try {
63
+ if (immutable)
64
+ await link(temporary, path);
65
+ else
66
+ await rename(temporary, path);
67
+ await syncDirectory(dirname(path));
68
+ }
69
+ finally {
70
+ await rm(temporary, { force: true });
71
+ }
72
+ }
73
+ export async function putObject(store, content) {
74
+ const id = digest(content), path = objectPath(store, id);
75
+ try {
76
+ await atomicWrite(path, content, true);
77
+ }
78
+ catch (error) {
79
+ if (error.code !== 'EEXIST')
80
+ throw error;
81
+ }
82
+ const existing = await getObject(store, id);
83
+ if (digest(existing) !== id)
84
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Stored content is corrupt.');
85
+ return id;
86
+ }
87
+ export async function getObject(store, id, derived) {
88
+ let bytes;
89
+ try {
90
+ bytes = await readFile(objectPath(store, id));
91
+ }
92
+ catch (error) {
93
+ const fallback = derived?.get(id);
94
+ if (error.code !== 'ENOENT' || !fallback)
95
+ throw new TrajectoriesError('INTEGRITY_ERROR', `Missing retained content ${id}.`, { cause: error });
96
+ bytes = fallback;
97
+ }
98
+ if (digest(bytes) !== id)
99
+ throw new TrajectoriesError('INTEGRITY_ERROR', `Content hash mismatch ${id}.`);
100
+ return bytes;
101
+ }
102
+ export async function withSessionLock(store, id, action) {
103
+ const locks = join(resolve(store), 'locks');
104
+ await ensureDirectory(locks);
105
+ const path = join(locks, safeId(id));
106
+ try {
107
+ await mkdir(path, { mode: 0o700 });
108
+ }
109
+ catch (error) {
110
+ if (error.code === 'EEXIST')
111
+ throw new TrajectoriesError('OPERATION_PENDING', 'A writer holds this session; reconcile interrupted writers before retrying.');
112
+ throw error;
113
+ }
114
+ try {
115
+ return await action();
116
+ }
117
+ finally {
118
+ await rm(path, { recursive: true });
119
+ await syncDirectory(locks);
120
+ }
121
+ }
@@ -0,0 +1,24 @@
1
+ import type { CommitReceipt, ConversationRecord, Head, HeadMetadata, ManagedSession, NativeBinding, Page, PageCursor, ReadOptions, RecordId } from './log-types.js';
2
+ import { type Frame, type StoredRecord } from './log-state.js';
3
+ import { type CanonicalConversationRecord } from './log-records.js';
4
+ export declare function sessionOf(frame: Frame): ManagedSession;
5
+ export declare function receiptOf(store: string, frame: Frame): Promise<CommitReceipt>;
6
+ export declare function atHead(frames: Frame[], head: Head): Frame[];
7
+ export declare function selection(frames: Frame[]): {
8
+ context: RecordId[];
9
+ history: StoredRecord[];
10
+ };
11
+ export declare function getSession(store: string, id: string): Promise<ManagedSession>;
12
+ export declare function getHeadMetadata(store: string, id: string, options: {
13
+ atHead: Head;
14
+ }): Promise<HeadMetadata>;
15
+ export declare function getNativeBinding(store: string, id: string, options: {
16
+ atHead: Head;
17
+ bindingId?: NativeBinding['id'];
18
+ }): Promise<NativeBinding>;
19
+ export declare function readCanonicalSession(store: string, id: string, options: ReadOptions): Promise<Page<CanonicalConversationRecord>>;
20
+ export declare function readSession(store: string, id: string, options: ReadOptions): Promise<Page<ConversationRecord>>;
21
+ export declare function listSessions(store: string, options?: {
22
+ cursor?: PageCursor;
23
+ limit?: number;
24
+ }): Promise<Page<ManagedSession>>;
@@ -0,0 +1,117 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { TrajectoriesError } from './log-types.js';
4
+ import { digest, stableJson } from './log-io.js';
5
+ import { getText } from './log-content.js';
6
+ import { assertLogStore, readFrames } from './log-state.js';
7
+ import { applyEdit, loadRecord } from './log-records.js';
8
+ export function sessionOf(frame) {
9
+ return { version: 2, ...frame.body.session, head: frame.head, updatedAt: frame.body.time };
10
+ }
11
+ export async function receiptOf(store, frame) {
12
+ return { intentId: frame.body.intentId, session: sessionOf(frame), parent: frame.body.parent,
13
+ diagnostics: JSON.parse(await getText(store, frame.body.diagnostics)),
14
+ ...(frame.body.capture ? { capture: frame.body.capture } : {}) };
15
+ }
16
+ export function atHead(frames, head) {
17
+ const index = frames.findIndex(frame => frame.head === head);
18
+ if (index < 0)
19
+ throw new TrajectoriesError('NOT_FOUND', 'Unknown committed head.');
20
+ return frames.slice(0, index + 1);
21
+ }
22
+ export function selection(frames) {
23
+ let context = [];
24
+ const history = [], known = new Set();
25
+ for (const frame of frames) {
26
+ for (const record of frame.body.records) {
27
+ if (known.has(record.id))
28
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Duplicate central record identity.');
29
+ known.add(record.id);
30
+ history.push(record);
31
+ }
32
+ context = applyEdit(context, frame.body.edit);
33
+ if (context.some(id => !known.has(id)))
34
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Context references an uncommitted record.');
35
+ }
36
+ return { context, history };
37
+ }
38
+ function pageLimit(value = 100) {
39
+ if (!Number.isSafeInteger(value) || value < 1 || value > 1000)
40
+ throw new TrajectoriesError('INVALID_INPUT', 'Page limit must be between 1 and 1000.');
41
+ return value;
42
+ }
43
+ function encode(value) { const body = stableJson(value); return Buffer.from(body).toString('base64url') + '.' + digest(body); }
44
+ function decode(cursor) {
45
+ try {
46
+ const [encoded, hash, extra] = cursor.split('.'), text = Buffer.from(encoded, 'base64url').toString();
47
+ if (extra || digest(text) !== hash)
48
+ throw new Error('hash');
49
+ return JSON.parse(text);
50
+ }
51
+ catch (cause) {
52
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid page cursor.', { cause });
53
+ }
54
+ }
55
+ export async function getSession(store, id) {
56
+ const frame = (await readFrames(store, id)).at(-1);
57
+ if (!frame)
58
+ throw new TrajectoriesError('NOT_FOUND', 'Managed session does not exist.');
59
+ return sessionOf(frame);
60
+ }
61
+ export async function getHeadMetadata(store, id, options) {
62
+ const frame = atHead(await readFrames(store, id), options.atHead).at(-1);
63
+ return { head: frame.head, parent: frame.body.parent, kind: frame.body.kind, progress: frame.body.progress,
64
+ diagnostics: (await receiptOf(store, frame)).diagnostics, binding: frame.body.binding,
65
+ ...(frame.body.capture ? { capture: frame.body.capture } : {}),
66
+ ...(frame.body.nativeMeta ? { nativeMeta: frame.body.nativeMeta } : {}) };
67
+ }
68
+ export async function getNativeBinding(store, id, options) {
69
+ const frames = atHead(await readFrames(store, id), options.atHead), binding = frames.at(-1).body.binding;
70
+ if (options.bindingId && binding.id !== options.bindingId)
71
+ throw new TrajectoriesError('STALE_HEAD', 'Binding is not active at the pinned head.');
72
+ return binding;
73
+ }
74
+ export async function readCanonicalSession(store, id, options) {
75
+ const limit = pageLimit(options.limit);
76
+ if (options.view !== 'history' && options.view !== 'context')
77
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid conversation view.');
78
+ const selected = selection(atHead(await readFrames(store, id), options.atHead));
79
+ const byId = new Map(selected.history.map(record => [record.id, record]));
80
+ const records = options.view === 'history' ? selected.history : selected.context.map(id => byId.get(id));
81
+ let offset = 0;
82
+ if (options.cursor) {
83
+ const cursor = decode(options.cursor);
84
+ if (cursor.id !== id || cursor.head !== options.atHead || cursor.view !== options.view || !Number.isSafeInteger(cursor.offset)
85
+ || cursor.offset < 1 || cursor.offset >= records.length)
86
+ throw new TrajectoriesError('INVALID_INPUT', 'Cursor does not belong to this session, head and view.');
87
+ offset = cursor.offset;
88
+ }
89
+ const items = await Promise.all(records.slice(offset, offset + limit).map(record => loadRecord(store, record)));
90
+ return { items, nextCursor: offset + limit < records.length ? encode({ id, head: options.atHead, view: options.view, offset: offset + limit }) : null };
91
+ }
92
+ export async function readSession(store, id, options) {
93
+ const page = await readCanonicalSession(store, id, options);
94
+ return { ...page, items: page.items.map(({ canonical: _, ...record }) => record) };
95
+ }
96
+ export async function listSessions(store, options = {}) {
97
+ await assertLogStore(store);
98
+ const limit = pageLimit(options.limit);
99
+ let names;
100
+ try {
101
+ names = (await readdir(join(resolve(store), 'logs'))).filter(name => name.endsWith('.jsonl')).map(name => name.slice(0, -6)).sort();
102
+ }
103
+ catch (error) {
104
+ if (error.code !== 'ENOENT')
105
+ throw error;
106
+ names = [];
107
+ }
108
+ let offset = 0;
109
+ if (options.cursor) {
110
+ const c = decode(options.cursor), index = names.indexOf(c.after);
111
+ if (c.view !== 'sessions' || c.store !== resolve(store) || index < 0)
112
+ throw new TrajectoriesError('INVALID_INPUT', 'Unknown session-list cursor.');
113
+ offset = index + 1;
114
+ }
115
+ const ids = names.slice(offset, offset + limit), items = await Promise.all(ids.map(id => getSession(store, id)));
116
+ return { items, nextCursor: offset + limit < names.length ? encode({ view: 'sessions', store: resolve(store), after: ids.at(-1) }) : null };
117
+ }
@@ -0,0 +1,5 @@
1
+ /** Host-only recovery after its own writer/process guard proves all writers stopped.
2
+ * Never guesses from elapsed time, discards partial tails, or resolves native effects. */
3
+ export declare function reconcileStoppedWriter(store: string, id: string, proof: {
4
+ writersStopped: true;
5
+ }): Promise<boolean>;
@@ -0,0 +1,38 @@
1
+ import { lstat, readdir, rmdir } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { safeId, syncDirectory } from './log-io.js';
4
+ import { TrajectoriesError } from './log-types.js';
5
+ import { readFrames } from './log-state.js';
6
+ import { loadStage, bindingSnapshot } from './log-recovery.js';
7
+ import { validateStageBacking } from './log-stage.js';
8
+ import { selection } from './log-read.js';
9
+ import { loadRecord } from './log-records.js';
10
+ /** Host-only recovery after its own writer/process guard proves all writers stopped.
11
+ * Never guesses from elapsed time, discards partial tails, or resolves native effects. */
12
+ export async function reconcileStoppedWriter(store, id, proof) {
13
+ if (proof?.writersStopped !== true)
14
+ throw new TrajectoriesError('INVALID_INPUT', 'The host must prove that every writer has stopped.');
15
+ const frames = await readFrames(store, id);
16
+ selection(frames);
17
+ for (const frame of frames) {
18
+ if (frame.body.capture)
19
+ await validateStageBacking(store, await loadStage(store, frame.body.capture), frames);
20
+ else if (frame.body.kind === 'binding')
21
+ await bindingSnapshot(store, frames, frame.body.binding.id);
22
+ for (const record of frame.body.records)
23
+ await loadRecord(store, record);
24
+ }
25
+ const path = join(resolve(store), 'locks', safeId(id));
26
+ try {
27
+ if (!(await lstat(path)).isDirectory() || (await readdir(path)).length)
28
+ throw new TrajectoriesError('OUTCOME_UNKNOWN', 'The writer lock is not the expected empty lock directory.');
29
+ await rmdir(path);
30
+ await syncDirectory(join(resolve(store), 'locks'));
31
+ return true;
32
+ }
33
+ catch (cause) {
34
+ if (cause.code === 'ENOENT')
35
+ return false;
36
+ throw cause;
37
+ }
38
+ }
@@ -0,0 +1,35 @@
1
+ import type { NormalizedRecord } from '@letta-ai/trajectory';
2
+ import type { Snapshot } from './types.js';
3
+ import type { ConversationRecord, RecordId } from './log-types.js';
4
+ import type { ContextEdit, StoredRecord } from './log-state.js';
5
+ export interface CanonicalConversationRecord extends ConversationRecord {
6
+ canonical: Snapshot['canonical']['records'][number];
7
+ }
8
+ export interface Unit {
9
+ semantic: string;
10
+ payload: NormalizedRecord;
11
+ canonical: CanonicalConversationRecord['canonical'];
12
+ }
13
+ export declare function units(snapshot: Snapshot, archive?: boolean): Unit[];
14
+ export declare function contextEdit(before: readonly RecordId[], after: readonly RecordId[]): ContextEdit;
15
+ export declare function applyEdit(context: readonly RecordId[], edit?: ContextEdit): RecordId[];
16
+ export declare function loadRecord(store: string, record: StoredRecord): Promise<CanonicalConversationRecord>;
17
+ export declare function planRecords(input: {
18
+ store: string;
19
+ sessionId: string;
20
+ intentId: string;
21
+ observed: Snapshot;
22
+ before: Unit[];
23
+ beforeIds: RecordId[];
24
+ beforeActive: Unit[];
25
+ beforeActiveIds: RecordId[];
26
+ context: RecordId[];
27
+ origin: ConversationRecord['origin'];
28
+ allowInitialSystem?: boolean;
29
+ }): Promise<{
30
+ records: StoredRecord[];
31
+ edit: ContextEdit;
32
+ added: RecordId[];
33
+ leading?: RecordId;
34
+ progress: boolean;
35
+ }>;