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,121 @@
1
+ import { assertCursorPortable } from './cursor.js';
2
+ import { assertDeepAgentsPortable } from './deepagents.js';
3
+ import { assertGitHubCopilotHistory } from './github-copilot.js';
4
+ import { assertClinePortable } from './cline.js';
5
+ import { assertFxPortable } from './fx.js';
6
+ import { piCompactedContext } from './pi-compaction.js';
7
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ function unsupported(harness, reason) {
9
+ throw new Error(`Unsupported ${harness} continuation history: ${reason}. Native bytes can be archived, but this bootstrap cannot safely export their active context.`);
10
+ }
11
+ /** The upstream normalizer is an archive reader, not a native active-branch resolver. */
12
+ export function assertPortableHistory(harness, raw, fxCompanions = {}) {
13
+ if (harness === 'cursor')
14
+ return assertCursorPortable(raw);
15
+ if (harness === 'deepagents')
16
+ return assertDeepAgentsPortable(raw);
17
+ if (harness === 'github-copilot')
18
+ return assertGitHubCopilotHistory(raw);
19
+ if (harness === 'cline')
20
+ return assertClinePortable(raw);
21
+ if (harness === 'fx')
22
+ return assertFxPortable(raw, fxCompanions);
23
+ if (harness === 'opencode') {
24
+ const data = JSON.parse(raw);
25
+ if (data.info?.revert != null)
26
+ unsupported(harness, 'reverted history requires native context reconstruction');
27
+ for (const message of data.messages ?? []) {
28
+ if (message.info?.summary === true)
29
+ unsupported(harness, 'summary history requires native context reconstruction');
30
+ for (const part of message.parts ?? []) {
31
+ if (part.type === 'compaction' || part.ignored === true || part.state?.time?.compacted != null)
32
+ unsupported(harness, 'compacted or ignored history requires native context reconstruction');
33
+ }
34
+ }
35
+ return;
36
+ }
37
+ if (!['pi', 'omp', 'claude-code', 'codex'].includes(harness))
38
+ return;
39
+ const rows = raw.split('\n').filter(line => line.trim()).map((line, index) => {
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(line);
43
+ }
44
+ catch {
45
+ throw new Error(`Invalid native JSON at line ${index + 1}.`);
46
+ }
47
+ if (!object(parsed))
48
+ throw new Error(`Invalid native record at line ${index + 1}.`);
49
+ return parsed;
50
+ });
51
+ if (harness === 'codex') {
52
+ for (const row of rows) {
53
+ const payload = object(row.payload) ? row.payload : {};
54
+ if (row.type === 'compacted' || (row.type === 'event_msg' && ['thread_rolled_back', 'context_compacted'].includes(String(payload.type)))) {
55
+ unsupported(harness, 'compaction or rollback requires native context reconstruction');
56
+ }
57
+ if (row.type === 'session_meta' && payload.history_base != null)
58
+ unsupported(harness, 'inherited history lives in an external rollout prefix');
59
+ }
60
+ return;
61
+ }
62
+ if (harness === 'pi' || harness === 'omp') {
63
+ if (harness === 'pi' && piCompactedContext(raw))
64
+ return;
65
+ for (const row of rows) {
66
+ if (['compaction', 'branch_summary', 'reset_boundary'].includes(String(row.type)))
67
+ unsupported(harness, `${String(row.type)} requires native context reconstruction`);
68
+ const message = object(row.message) ? row.message : {};
69
+ if (row.type === 'custom_message' || (row.type === 'message' && ['custom', 'hookMessage'].includes(String(message.role))))
70
+ unsupported(harness, 'custom context messages require native context reconstruction');
71
+ }
72
+ const chain = rows.filter(row => typeof row.id === 'string' && Object.hasOwn(row, 'parentId'));
73
+ const messages = rows.filter(row => row.type === 'message');
74
+ assertLinearContext(harness, chain, messages, 'id', 'parentId', chain.at(-1));
75
+ return;
76
+ }
77
+ for (const row of rows) {
78
+ if (row.isCompactSummary === true || row.subtype === 'compact_boundary' || row.type === 'compact_boundary')
79
+ unsupported(harness, 'compaction requires native context reconstruction');
80
+ }
81
+ const conversational = rows.filter(row => row.type === 'user' || row.type === 'assistant');
82
+ const standaloneSidechain = conversational.length > 0 && conversational.every(row => row.isSidechain === true);
83
+ const messages = conversational.filter(row => standaloneSidechain || row.isSidechain !== true);
84
+ // Progress and attachments can participate in parent chains without becoming
85
+ // conversation messages. They must remain available when walking the chain.
86
+ const chain = rows.filter(row => typeof row.uuid === 'string');
87
+ assertLinearContext(harness, chain, messages, 'uuid', 'parentUuid', messages.at(-1));
88
+ }
89
+ function assertLinearContext(harness, chain, messages, idKey, parentKey, leaf) {
90
+ // Legacy flat transcripts have no topology to contradict their file order.
91
+ if (!chain.some(row => Object.hasOwn(row, parentKey)))
92
+ return;
93
+ const byId = new Map();
94
+ const messageIds = new Set();
95
+ for (const row of messages) {
96
+ const id = row[idKey];
97
+ if (typeof id !== 'string')
98
+ unsupported(harness, 'conversation entries mix identified and unidentified history');
99
+ if (messageIds.has(id))
100
+ unsupported(harness, 'duplicate native conversation ids');
101
+ messageIds.add(id);
102
+ }
103
+ for (const row of chain)
104
+ byId.set(row[idKey], row);
105
+ const ancestors = new Set();
106
+ let current = leaf;
107
+ while (current) {
108
+ const id = current[idKey];
109
+ if (ancestors.has(id))
110
+ unsupported(harness, 'cyclic native parent chain');
111
+ ancestors.add(id);
112
+ const parent = current[parentKey];
113
+ if (parent === null || parent === undefined)
114
+ break;
115
+ if (typeof parent !== 'string' || !byId.has(parent))
116
+ unsupported(harness, 'missing native parent entry');
117
+ current = byId.get(parent);
118
+ }
119
+ if ([...messageIds].some(id => !ancestors.has(id)))
120
+ unsupported(harness, 'branched or disconnected native conversation');
121
+ }
@@ -0,0 +1,7 @@
1
+ import { type CanonicalResult } from '@letta-ai/trajectory';
2
+ type Row = Record<string, unknown>;
3
+ /** Narrow Pi 0.84.4 buildContextEntries contract; no runtime or extension execution. */
4
+ export declare function piCompactedContext(raw: string): Row[] | undefined;
5
+ /** Preserve archive identities; only summary messages are derived from native summary entries. */
6
+ export declare function projectPiCompaction(raw: string, canonical: CanonicalResult): CanonicalResult;
7
+ export {};
@@ -0,0 +1,112 @@
1
+ import { normalizeToCanonical } from '@letta-ai/trajectory';
2
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
3
+ function fail(reason) { throw new Error(`Unsupported pi continuation history: ${reason} requires native context reconstruction.`); }
4
+ /** Narrow Pi 0.84.4 buildContextEntries contract; no runtime or extension execution. */
5
+ export function piCompactedContext(raw) {
6
+ const rows = raw.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
7
+ if (!rows.some(row => row.type === 'compaction'))
8
+ return undefined;
9
+ const header = rows[0];
10
+ if (!header || header.type !== 'session' || header.version !== 3 || typeof header.id !== 'string' || !header.id)
11
+ fail('compaction without an identified v3 session');
12
+ const entries = rows.slice(1);
13
+ const seen = new Set();
14
+ let parent = null;
15
+ for (const entry of entries) {
16
+ if (typeof entry.id !== 'string' || !entry.id || seen.has(entry.id))
17
+ fail('missing or duplicate native entry ids');
18
+ if (entry.parentId !== parent)
19
+ fail('branched, disconnected or reordered native entry chain');
20
+ seen.add(entry.id);
21
+ parent = entry.id;
22
+ switch (entry.type) {
23
+ case 'message': {
24
+ const message = entry.message;
25
+ if (!object(message) || !['user', 'assistant', 'toolResult'].includes(String(message.role)))
26
+ fail('unsupported native message role');
27
+ if (message.role === 'toolResult' && (typeof message.toolCallId !== 'string' || !message.toolCallId
28
+ || typeof message.toolName !== 'string' || !message.toolName))
29
+ fail('unidentified native tool result');
30
+ const content = message.content;
31
+ if (typeof content === 'string' && message.role === 'user')
32
+ break;
33
+ if (!Array.isArray(content))
34
+ fail('unsupported native message content');
35
+ for (const block of content) {
36
+ if (!object(block))
37
+ fail('unsupported native content block');
38
+ if (block.type === 'text' && typeof block.text === 'string')
39
+ continue;
40
+ if (message.role === 'assistant' && block.type === 'thinking' && typeof block.thinking === 'string')
41
+ continue;
42
+ if (message.role === 'assistant' && block.type === 'toolCall' && typeof block.id === 'string' && block.id
43
+ && typeof block.name === 'string' && block.name && object(block.arguments))
44
+ continue;
45
+ fail('unsupported native content block');
46
+ }
47
+ break;
48
+ }
49
+ case 'compaction': {
50
+ if (typeof entry.summary !== 'string' || !entry.summary.trim() || typeof entry.timestamp !== 'string' || !Number.isFinite(Date.parse(entry.timestamp))
51
+ || typeof entry.tokensBefore !== 'number' || !Number.isFinite(entry.tokensBefore) || entry.tokensBefore < 0 || entry.fromHook === true)
52
+ fail('unsupported compaction summary');
53
+ if (typeof entry.firstKeptEntryId !== 'string' || !seen.has(entry.firstKeptEntryId) || entry.firstKeptEntryId === entry.id)
54
+ fail('missing or non-ancestral compaction retention anchor');
55
+ break;
56
+ }
57
+ case 'model_change':
58
+ case 'thinking_level_change':
59
+ case 'session_info':
60
+ case 'label': break;
61
+ default: fail(`unsupported ${String(entry.type)} entry`);
62
+ }
63
+ }
64
+ let latest = entries.length - 1;
65
+ while (entries[latest]?.type !== 'compaction')
66
+ latest--;
67
+ const compaction = entries[latest];
68
+ const kept = entries.findIndex(entry => entry.id === compaction.firstKeptEntryId);
69
+ const selected = [compaction, ...entries.slice(kept, latest), ...entries.slice(latest + 1)];
70
+ const archive = normalizeToCanonical({ source: 'pi', transcript: raw,
71
+ bounds: { toolArguments: { maxCharacters: null }, toolResults: { maxCharacters: null } },
72
+ filters: { systemMessages: 'include', toolResults: 'include' },
73
+ });
74
+ for (const entry of selected) {
75
+ if (entry.type !== 'message')
76
+ continue;
77
+ const message = entry.message;
78
+ if (message.role === 'assistant' && ['aborted', 'error'].includes(String(message.stopReason)))
79
+ continue;
80
+ if (!archive.records.some(record => record.source_identity_kind === 'native' && record.stable_source_record_id === entry.id))
81
+ fail('selected native message omitted by normalization');
82
+ }
83
+ return selected;
84
+ }
85
+ /** Preserve archive identities; only summary messages are derived from native summary entries. */
86
+ export function projectPiCompaction(raw, canonical) {
87
+ let entries;
88
+ try {
89
+ entries = piCompactedContext(raw);
90
+ }
91
+ catch {
92
+ return canonical;
93
+ } // Unsupported archives remain capturable; exports run the strict guard.
94
+ if (!entries)
95
+ return canonical;
96
+ const group = canonical.records.find(record => record.record_type === 'meta')?.source_group_id ?? 'default';
97
+ const records = canonical.records.filter(record => record.record_type === 'meta');
98
+ const diagnostics = [...canonical.diagnostics];
99
+ for (const entry of entries) {
100
+ if (entry.type === 'message')
101
+ records.push(...canonical.records.filter(record => record.source_identity_kind === 'native' && record.stable_source_record_id === entry.id));
102
+ if (entry.type !== 'compaction')
103
+ continue;
104
+ const content = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n${entry.summary}\n</summary>`;
105
+ const derived = normalizeToCanonical({ source: 'pi', transcript: JSON.stringify({ ...entry, type: 'message', message: { role: 'user', content, timestamp: Date.parse(entry.timestamp) } }),
106
+ sourceContext: { groupId: group, partial: true }, bounds: { toolArguments: { maxCharacters: null }, toolResults: { maxCharacters: null } },
107
+ filters: { systemMessages: 'include', toolResults: 'include' }, });
108
+ records.push(...derived.records.filter(record => record.record_type === 'user'));
109
+ diagnostics.push({ code: 'noise_record_dropped', message: `[trajectories/pi-compaction-context-v1] Native Pi summary entry ${entry.id} supplies the derived user context; retained messages follow native order. Complete original history remains in snapshot.raw.` });
110
+ }
111
+ return { ...canonical, records, diagnostics };
112
+ }
@@ -0,0 +1,13 @@
1
+ import { type CanonicalResult } from '@letta-ai/trajectory';
2
+ /**
3
+ * Pi excludes aborted/error assistant drafts when building provider messages.
4
+ * Apply that narrow projection after the verified Pi compaction view, retaining raw
5
+ * bytes elsewhere and preserving every surviving canonical identity. OMP has
6
+ * different semantics and must not use this projection.
7
+ *
8
+ * Verified source and pinned identity contract: docs/pi-provider-context.md.
9
+ */
10
+ export declare function projectPiProviderContext(raw: string, canonical: CanonicalResult): CanonicalResult;
11
+ /** Full archive normalization; continuation applies the separate failed-draft filter. */
12
+ export declare function normalizePiArchive(raw: string): CanonicalResult;
13
+ export declare function projectPiDrafts(raw: string, canonical: CanonicalResult): CanonicalResult;
@@ -0,0 +1,64 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { normalizeToCanonical } from '@letta-ai/trajectory';
3
+ import { projectPiCompaction } from './pi-compaction.js';
4
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
5
+ /**
6
+ * Pi excludes aborted/error assistant drafts when building provider messages.
7
+ * Apply that narrow projection after the verified Pi compaction view, retaining raw
8
+ * bytes elsewhere and preserving every surviving canonical identity. OMP has
9
+ * different semantics and must not use this projection.
10
+ *
11
+ * Verified source and pinned identity contract: docs/pi-provider-context.md.
12
+ */
13
+ export function projectPiProviderContext(raw, canonical) {
14
+ return projectPiDrafts(raw, projectPiCompaction(raw, canonical));
15
+ }
16
+ /** Full archive normalization; continuation applies the separate failed-draft filter. */
17
+ export function normalizePiArchive(raw) {
18
+ return normalizeToCanonical({ source: 'pi', transcript: raw,
19
+ bounds: { toolArguments: { maxCharacters: null }, toolResults: { maxCharacters: null } },
20
+ filters: { systemMessages: 'include', toolResults: 'include' },
21
+ });
22
+ }
23
+ export function projectPiDrafts(raw, canonical) {
24
+ const group = canonical.records.find(record => record.record_type === 'meta')?.source_group_id ?? 'default';
25
+ const omitted = new Set();
26
+ const diagnostics = [];
27
+ let byteOffset = 0;
28
+ for (const [index, line] of raw.split('\n').entries()) {
29
+ const offset = byteOffset;
30
+ byteOffset += Buffer.byteLength(line, 'utf8') + 1;
31
+ if (!line.trim())
32
+ continue;
33
+ let row;
34
+ try {
35
+ row = JSON.parse(line);
36
+ }
37
+ catch {
38
+ continue;
39
+ } // The caller retains and rejects malformed-input diagnostics.
40
+ if (!row || typeof row !== 'object' || Array.isArray(row))
41
+ continue;
42
+ const entry = row;
43
+ if (entry.type !== 'message' || entry.message?.role !== 'assistant'
44
+ || (entry.message.stopReason !== 'aborted' && entry.message.stopReason !== 'error'))
45
+ continue;
46
+ const nativeId = typeof entry.id === 'string' && entry.id ? entry.id : undefined;
47
+ const identity = nativeId === undefined ? `location\0${sha256(`${group}|byte|${offset}`)}` : `native\0${nativeId}`;
48
+ omitted.add(identity);
49
+ diagnostics.push({
50
+ code: 'noise_record_dropped',
51
+ inputLine: index + 1,
52
+ message: `[trajectories/pi-provider-context-v1] Pi omits assistant entry ${nativeId ?? `at byte ${offset}`} with stopReason=${entry.message.stopReason} from provider context; complete native bytes remain in snapshot.raw.`,
53
+ });
54
+ }
55
+ if (!omitted.size)
56
+ return canonical;
57
+ return {
58
+ ...canonical,
59
+ // Keep tool results, even when their corresponding draft was omitted. The
60
+ // normal native-writer orphan guard must reject that ambiguous history.
61
+ records: canonical.records.filter(record => !omitted.has(`${record.source_identity_kind}\0${record.stable_source_record_id}`)),
62
+ diagnostics: [...canonical.diagnostics, ...diagnostics],
63
+ };
64
+ }
@@ -0,0 +1,25 @@
1
+ import type { Harness, Snapshot, ManagedSession, WriteOptions, NativeExport } from './types.js';
2
+ /** Validate portable prefix continuity, optionally requiring new conversation records. */
3
+ export declare function assertSnapshotContinuation(baseline: Snapshot, observed: Snapshot, options?: {
4
+ requireProgress?: boolean;
5
+ allowInitialSystem?: boolean;
6
+ }): void;
7
+ export declare function getSnapshot(root: string, id: string): Promise<Snapshot>;
8
+ export declare function getSession(root: string, id: string): Promise<ManagedSession>;
9
+ export declare function listSessions(root: string): Promise<ManagedSession[]>;
10
+ export declare function createSession(root: string, snapshot: Snapshot, options: {
11
+ workspace: string;
12
+ id?: string;
13
+ }): Promise<ManagedSession>;
14
+ /** Import observed native history. An exact replay of the latest append is idempotent. */
15
+ export declare function appendSnapshot(root: string, id: string, snapshot: Snapshot, options: {
16
+ expectedHead: string;
17
+ }): Promise<ManagedSession>;
18
+ /** Prepare a new native continuation; execution and native shutdown belong to the caller. */
19
+ export declare function switchSession(root: string, id: string, target: Harness, options: Omit<WriteOptions, 'cwd'> & {
20
+ cwd?: string;
21
+ expectedHead: string;
22
+ }): Promise<{
23
+ session: ManagedSession;
24
+ native: NativeExport;
25
+ }>;
package/dist/store.js ADDED
@@ -0,0 +1,218 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile, rename, link, rm, readdir, realpath } from 'node:fs/promises';
3
+ import { join, resolve, isAbsolute } from 'node:path';
4
+ import { assertSnapshot, assertPortableSnapshot, writeSnapshot, readSnapshot } from './convert.js';
5
+ import { piCompactedContext } from './readers/pi-compaction.js';
6
+ import { normalizePiArchive, projectPiDrafts } from './readers/pi-context.js';
7
+ import { portableConversationTokens } from './portable.js';
8
+ /** Compare the portable conversation, not timestamps, provider metadata, or native ids of messages. */
9
+ function conversation(snapshot) {
10
+ return portableConversationTokens(snapshot.canonical.records.map(record => JSON.parse(record.record_json)))
11
+ .map(token => token.semantic);
12
+ }
13
+ /** Validate portable prefix continuity, optionally requiring new conversation records. */
14
+ export function assertSnapshotContinuation(baseline, observed, options = {}) {
15
+ assertSnapshot(baseline);
16
+ assertPortableSnapshot(observed);
17
+ let before = conversation(baseline), after = conversation(observed);
18
+ if (baseline.source.harness === 'pi' && observed.source.harness === 'pi'
19
+ && baseline.source.nativeSessionId && baseline.source.nativeSessionId === observed.source.nativeSessionId && piCompactedContext(observed.raw)) {
20
+ if (!observed.raw.startsWith(baseline.raw) || (observed.raw.length > baseline.raw.length && !baseline.raw.endsWith('\n'))) {
21
+ throw new Error('Compacted Pi continuation must preserve its exact append-only native archive. The session head was not changed.');
22
+ }
23
+ before = conversation({ ...baseline, canonical: projectPiDrafts(baseline.raw, normalizePiArchive(baseline.raw)) });
24
+ after = conversation({ ...observed, canonical: projectPiDrafts(observed.raw, normalizePiArchive(observed.raw)) });
25
+ }
26
+ // A fresh target may initialize its own system instructions before the imported
27
+ // conversation. Preserve them in the snapshot; subsequent same-session appends
28
+ // must retain them exactly. Never allow replacement of existing instructions.
29
+ if (options.allowInitialSystem && before.length && JSON.parse(before[0])[0] !== 'system') {
30
+ if (after.length && JSON.parse(after[0])[0] === 'system')
31
+ after.shift();
32
+ }
33
+ if (!before.length || after.length < before.length || before.some((record, index) => record !== after[index])) {
34
+ throw new Error('Observed native history does not preserve the complete prepared conversation prefix. The session head was not changed.');
35
+ }
36
+ if (options.requireProgress && after.length === before.length) {
37
+ throw new Error('Observed native history contains no new conversation records after its prepared baseline.');
38
+ }
39
+ }
40
+ function safeId(value) {
41
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value))
42
+ throw new Error('Invalid session or snapshot id.');
43
+ return value;
44
+ }
45
+ function sessionPath(root, id) { return join(resolve(root), 'sessions', `${safeId(id)}.json`); }
46
+ function snapshotPath(root, id) { return join(resolve(root), 'snapshots', `${safeId(id)}.json`); }
47
+ async function initialize(root) {
48
+ try {
49
+ const format = JSON.parse(await readFile(join(resolve(root), 'format.json'), 'utf8'));
50
+ if (format.version === 2)
51
+ throw new Error('Version-1 writers cannot write a canonical-log store.');
52
+ }
53
+ catch (error) {
54
+ if (error.code !== 'ENOENT')
55
+ throw error;
56
+ }
57
+ await mkdir(resolve(root), { recursive: true, mode: 0o700 });
58
+ for (const kind of ['sessions', 'snapshots', 'locks'])
59
+ await mkdir(join(root, kind), { recursive: true, mode: 0o700 });
60
+ }
61
+ async function withLock(root, id, operation) {
62
+ await initialize(root);
63
+ const lock = join(root, 'locks', safeId(id));
64
+ try {
65
+ await mkdir(lock, { mode: 0o700 });
66
+ }
67
+ catch (error) {
68
+ if (error.code === 'EEXIST')
69
+ throw new Error(`Session ${id} is locked by another operation. Inspect abandoned locks before removing them.`);
70
+ throw error;
71
+ }
72
+ try {
73
+ return await operation();
74
+ }
75
+ finally {
76
+ await rm(lock, { recursive: true });
77
+ }
78
+ }
79
+ async function persistSnapshot(root, snapshot) {
80
+ assertSnapshot(snapshot);
81
+ const destination = snapshotPath(root, snapshot.id);
82
+ const temporary = `${destination}.${randomUUID()}.tmp`;
83
+ try {
84
+ await writeFile(temporary, JSON.stringify(snapshot, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
85
+ // Publish a complete file atomically without replacing another writer's
86
+ // immutable snapshot. A killed writer can leave a .tmp, never a partial id.
87
+ try {
88
+ await link(temporary, destination);
89
+ }
90
+ catch (error) {
91
+ if (error.code !== 'EEXIST')
92
+ throw error;
93
+ const existing = await getSnapshot(root, snapshot.id);
94
+ if (existing.raw !== snapshot.raw || JSON.stringify(existing.canonical) !== JSON.stringify(snapshot.canonical))
95
+ throw new Error('Conflicting immutable snapshot.');
96
+ }
97
+ }
98
+ finally {
99
+ await rm(temporary, { force: true });
100
+ }
101
+ }
102
+ async function persistSession(root, session) {
103
+ const destination = sessionPath(root, session.id);
104
+ const temporary = `${destination}.${randomUUID()}.tmp`;
105
+ try {
106
+ await writeFile(temporary, JSON.stringify(session, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
107
+ await rename(temporary, destination);
108
+ }
109
+ finally {
110
+ await rm(temporary, { force: true });
111
+ }
112
+ }
113
+ export async function getSnapshot(root, id) {
114
+ const snapshot = JSON.parse(await readFile(snapshotPath(root, id), 'utf8'));
115
+ assertSnapshot(snapshot);
116
+ if (snapshot.id !== id)
117
+ throw new Error('Snapshot filename does not match its identity.');
118
+ return snapshot;
119
+ }
120
+ export async function getSession(root, id) {
121
+ const session = JSON.parse(await readFile(sessionPath(root, id), 'utf8'));
122
+ if (session.version !== 1 || session.id !== id || !Array.isArray(session.snapshots) || !session.snapshots.includes(session.head) || !Array.isArray(session.bindings))
123
+ throw new Error('Invalid managed session.');
124
+ safeId(session.head);
125
+ return session;
126
+ }
127
+ export async function listSessions(root) {
128
+ let files;
129
+ try {
130
+ files = await readdir(join(resolve(root), 'sessions'));
131
+ }
132
+ catch (error) {
133
+ if (error.code === 'ENOENT')
134
+ return [];
135
+ throw error;
136
+ }
137
+ const sessions = await Promise.all(files.filter(file => file.endsWith('.json')).map(file => getSession(root, file.slice(0, -5))));
138
+ return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id));
139
+ }
140
+ export async function createSession(root, snapshot, options) {
141
+ if (!isAbsolute(options.workspace))
142
+ throw new Error('workspace must be an absolute path.');
143
+ const workspace = await realpath(options.workspace);
144
+ const id = safeId(options.id ?? randomUUID());
145
+ return withLock(root, id, async () => {
146
+ try {
147
+ await readFile(sessionPath(root, id));
148
+ throw new Error(`Session ${id} already exists.`);
149
+ }
150
+ catch (error) {
151
+ if (error.code !== 'ENOENT')
152
+ throw error;
153
+ }
154
+ await persistSnapshot(root, snapshot);
155
+ const now = new Date().toISOString();
156
+ const session = { version: 1, id, createdAt: now, updatedAt: now, workspace, head: snapshot.id, snapshots: [snapshot.id], bindings: [] };
157
+ await persistSession(root, session);
158
+ return session;
159
+ });
160
+ }
161
+ /** Import observed native history. An exact replay of the latest append is idempotent. */
162
+ export async function appendSnapshot(root, id, snapshot, options) {
163
+ return withLock(root, id, async () => {
164
+ const session = await getSession(root, id);
165
+ assertSnapshot(snapshot);
166
+ if (session.head === snapshot.id && (options.expectedHead === session.head || session.snapshots.at(-2) === options.expectedHead)) {
167
+ // The commit may have succeeded before a workflow step's acknowledgement
168
+ // was saved. Prove this exact transition; never accept an unrelated or
169
+ // older expected head just because the requested snapshot already exists.
170
+ await getSnapshot(root, snapshot.id);
171
+ return session;
172
+ }
173
+ if (session.head !== options.expectedHead)
174
+ throw new Error('Session head changed; read the latest session before importing.');
175
+ if (session.snapshots.includes(snapshot.id))
176
+ throw new Error('Cannot replace the head with an older snapshot. Create a separate session to branch.');
177
+ const current = await getSnapshot(root, session.head);
178
+ const nativeId = snapshot.source.nativeSessionId;
179
+ if (!nativeId)
180
+ throw new Error('A continuation requires an identified native session.');
181
+ let baseline;
182
+ let preparedTarget = false;
183
+ if (current.source.harness === snapshot.source.harness && current.source.nativeSessionId === nativeId)
184
+ baseline = current;
185
+ else {
186
+ const binding = session.bindings.find(item => item.snapshotId === session.head && item.harness === snapshot.source.harness && item.nativeSessionId === nativeId);
187
+ if (!binding)
188
+ throw new Error('Native session is not a prepared continuation of the current head.');
189
+ baseline = await getSnapshot(root, binding.exportedSnapshotId);
190
+ preparedTarget = true;
191
+ }
192
+ assertSnapshotContinuation(baseline, snapshot, { allowInitialSystem: preparedTarget });
193
+ await persistSnapshot(root, snapshot);
194
+ session.snapshots.push(snapshot.id);
195
+ session.head = snapshot.id;
196
+ session.updatedAt = new Date().toISOString();
197
+ await persistSession(root, session);
198
+ return session;
199
+ });
200
+ }
201
+ /** Prepare a new native continuation; execution and native shutdown belong to the caller. */
202
+ export async function switchSession(root, id, target, options) {
203
+ return withLock(root, id, async () => {
204
+ const session = await getSession(root, id);
205
+ if (session.head !== options.expectedHead)
206
+ throw new Error('Session head changed; refusing to export stale history.');
207
+ const snapshot = await getSnapshot(root, session.head);
208
+ const native = await writeSnapshot(snapshot, target, { ...options, cwd: options.cwd ?? session.workspace });
209
+ const exported = await readSnapshot({ harness: target, path: join(options.directory, native.entrypoint), nativeSessionId: native.sessionId });
210
+ if (exported.source.nativeSessionId !== native.sessionId || !conversation(exported).length)
211
+ throw new Error('Native export did not establish an identified conversation.');
212
+ await persistSnapshot(root, exported);
213
+ session.bindings.push({ harness: target, nativeSessionId: native.sessionId, snapshotId: snapshot.id, exportedSnapshotId: exported.id, directory: resolve(options.directory), entrypoint: native.entrypoint, createdAt: new Date().toISOString() });
214
+ session.updatedAt = new Date().toISOString();
215
+ await persistSession(root, session);
216
+ return { session, native };
217
+ });
218
+ }
@@ -0,0 +1,91 @@
1
+ import type { CanonicalResult, NormalizedRecord } from '@letta-ai/trajectory';
2
+ export declare const HARNESSES: readonly ["claude-code", "codex", "pi", "omp", "opencode", "grok-build", "cursor", "deepagents", "github-copilot", "cline", "fx"];
3
+ export type Harness = typeof HARNESSES[number];
4
+ /** Immutable history. Text-file inputs retain decoded source text;
5
+ * Cursor retains SQLite rows/blobs;
6
+ * Deep Agents retains decoded messages and metadata from one selected checkpoint. */
7
+ export interface Snapshot {
8
+ version: 1;
9
+ id: string;
10
+ createdAt: string;
11
+ source: {
12
+ harness: Harness;
13
+ nativeSessionId?: string;
14
+ path?: string;
15
+ sha256: string;
16
+ };
17
+ cwd?: string;
18
+ raw: string;
19
+ /** Native metadata sidecars needed to interpret otherwise headerless histories. */
20
+ attachments?: Array<{
21
+ name: string;
22
+ content: string;
23
+ sha256: string;
24
+ }>;
25
+ canonical: CanonicalResult;
26
+ /** Archived histories can be inspected even when active-context conversion is unsupported. */
27
+ portabilityIssues?: ConversionDiagnostic[];
28
+ }
29
+ export interface ConversionDiagnostic {
30
+ code: string;
31
+ message: string;
32
+ }
33
+ /** Explicit destination; converters never overwrite a runtime's existing store. */
34
+ export interface WriteOptions {
35
+ directory: string;
36
+ cwd: string;
37
+ sessionId?: string;
38
+ model?: string;
39
+ provider?: string;
40
+ timestamp?: string;
41
+ }
42
+ export interface NativeExport {
43
+ harness: Harness;
44
+ sessionId: string;
45
+ /** Paths are relative to WriteOptions.directory. */
46
+ files: Array<{
47
+ path: string;
48
+ content: string;
49
+ }>;
50
+ /** Relative path of the native import/resume artifact. */
51
+ entrypoint: string;
52
+ /** Argument vector, never a shell command. Does not launch automatically. */
53
+ resume: {
54
+ command: string;
55
+ args: string[];
56
+ cwd: string;
57
+ };
58
+ /** Explicit native import steps, when a harness owns a database rather than a file store. */
59
+ prepare?: Array<{
60
+ command: string;
61
+ args: string[];
62
+ cwd: string;
63
+ }>;
64
+ diagnostics: ConversionDiagnostic[];
65
+ }
66
+ export interface NativeEntropy {
67
+ uuid(): string;
68
+ bytes(length: number): Uint8Array;
69
+ }
70
+ export type NativeWriter = (records: readonly NormalizedRecord[], options: WriteOptions, entropy?: NativeEntropy) => NativeExport;
71
+ export interface SessionBinding {
72
+ harness: Harness;
73
+ nativeSessionId: string;
74
+ snapshotId: string;
75
+ /** What the target format actually carries, before that runtime continues it. */
76
+ exportedSnapshotId: string;
77
+ directory: string;
78
+ entrypoint: string;
79
+ createdAt: string;
80
+ }
81
+ /** One central conversation can have successive native runtime sessions. */
82
+ export interface ManagedSession {
83
+ version: 1;
84
+ id: string;
85
+ createdAt: string;
86
+ updatedAt: string;
87
+ workspace: string;
88
+ head: string;
89
+ snapshots: string[];
90
+ bindings: SessionBinding[];
91
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export const HARNESSES = ['claude-code', 'codex', 'pi', 'omp', 'opencode', 'grok-build', 'cursor', 'deepagents', 'github-copilot', 'cline', 'fx'];