trajectories 0.0.0 → 0.1.2

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 (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -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 +1233 -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 +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,573 @@
1
+ import { canonicalFromRecords } from './canonical.js';
2
+ function fail(message) {
3
+ throw new Error(`FX native session: ${message}`);
4
+ }
5
+ function object(value, label) {
6
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
7
+ fail(`${label} must be an object`);
8
+ return value;
9
+ }
10
+ function string(value, label) {
11
+ if (typeof value !== 'string')
12
+ fail(`${label} must be a string`);
13
+ return value;
14
+ }
15
+ function array(value, label) {
16
+ if (!Array.isArray(value))
17
+ fail(`${label} must be an array`);
18
+ return value;
19
+ }
20
+ function integer(value, label) {
21
+ if (!Number.isSafeInteger(value) || value < 0)
22
+ fail(`${label} must be a non-negative safe integer`);
23
+ return value;
24
+ }
25
+ function iso(milliseconds, label) {
26
+ const value = new Date(milliseconds).toISOString();
27
+ if (!Number.isFinite(Date.parse(value)))
28
+ fail(`${label} is invalid`);
29
+ return value;
30
+ }
31
+ function parseJson(raw, label) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ return fail(`${label} is invalid JSON`);
38
+ }
39
+ return object(parsed, label);
40
+ }
41
+ function parseIdentity(value, label) {
42
+ const result = string(value, label);
43
+ if (!result || new TextEncoder().encode(result).byteLength > 256 || /[\x00-\x1f]/.test(result))
44
+ fail(`${label} is invalid`);
45
+ return result;
46
+ }
47
+ function exactKeys(value, allowed, label) {
48
+ const permitted = new Set(allowed);
49
+ const unknown = Object.keys(value).find((key) => !permitted.has(key));
50
+ if (unknown !== undefined)
51
+ fail(`${label} has unknown field ${JSON.stringify(unknown)}`);
52
+ }
53
+ function parseProvider(value, label) {
54
+ if (typeof value === 'string') {
55
+ if (!['gateway', 'codex', 'grok'].includes(value.toLowerCase()))
56
+ fail(`${label} builtin name is invalid`);
57
+ return value;
58
+ }
59
+ const configured = object(value, label);
60
+ exactKeys(configured, ['name', 'binding'], label);
61
+ if (typeof configured.name !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(configured.name) ||
62
+ ['gateway', 'codex', 'grok'].includes(configured.name.toLowerCase()) ||
63
+ typeof configured.binding !== 'string' || !/^[0-9a-f]{64}$/.test(configured.binding)) {
64
+ fail(`${label} configured binding is invalid`);
65
+ }
66
+ return { name: configured.name, binding: configured.binding };
67
+ }
68
+ function parseSession(root) {
69
+ if (root.schema_version !== 1 && root.schema_version !== 2)
70
+ fail(`unsupported legacy schema_version ${String(root.schema_version)}`);
71
+ const id = parseIdentity(root.id, 'id');
72
+ if (id === '.' || id === '..' || /[/\\]/.test(id))
73
+ fail('id is unsafe');
74
+ const workspace = root.workspace_root === undefined || root.workspace_root === null
75
+ ? undefined : string(root.workspace_root, 'workspace_root');
76
+ if (workspace !== undefined && !workspace.startsWith('/'))
77
+ fail('workspace_root must be absolute');
78
+ string(root.conversation_language, 'conversation_language');
79
+ const createdMs = integer(root.created_at_ms, 'created_at_ms');
80
+ const updatedMs = integer(root.updated_at_ms, 'updated_at_ms');
81
+ const history = array(root.history, 'history');
82
+ if (integer(root.history_len, 'history_len') !== history.length)
83
+ fail('history_len does not match history');
84
+ return { id, workspace, created: iso(createdMs, 'created_at_ms'), updated: iso(updatedMs, 'updated_at_ms'), history };
85
+ }
86
+ function decodeLegacy(root) {
87
+ const session = parseSession(root);
88
+ const diagnostics = [];
89
+ const portabilityIssues = [];
90
+ const entries = [{
91
+ record: { role: 'meta', source: 'fx', ...(session.workspace ? { cwd: session.workspace } : {}) },
92
+ nativeId: 'meta', sourceSequence: 0,
93
+ }];
94
+ let sequence = 1;
95
+ if (session.history.length) {
96
+ diagnostics.push({
97
+ code: 'timestamps_synthesized',
98
+ message: '[trajectories/fx-native-session-json-v1] FX session.json has session and tool-result timestamps but no per-message timestamps; user and assistant record timestamps use the session bounds.',
99
+ });
100
+ }
101
+ const issue = (code, message) => {
102
+ diagnostics.push({ code: 'noise_record_dropped', message: `[trajectories/fx-native-session-json-v1:${code}] ${message}` });
103
+ portabilityIssues.push(message);
104
+ };
105
+ const emit = (record, nativeId, sourceTimestamp) => {
106
+ entries.push({ record, nativeId, sourceTimestamp, sourceSequence: sequence++ });
107
+ };
108
+ for (const [turnIndex, value] of session.history.entries()) {
109
+ const turn = object(value, `history[${turnIndex}]`);
110
+ const kind = string(turn.kind, `history[${turnIndex}].kind`);
111
+ if (kind === 'compacted_summary') {
112
+ issue('fx-unsupported-compaction', `history[${turnIndex}] is a compacted summary whose removed turns cannot be reconstructed.`);
113
+ continue;
114
+ }
115
+ if (kind === 'interrupted') {
116
+ issue('fx-unsupported-interrupted-turn', `history[${turnIndex}] is interrupted and is not a completed portable turn.`);
117
+ continue;
118
+ }
119
+ if (kind !== 'assistant') {
120
+ issue('fx-unsupported-history-kind', `history[${turnIndex}] has unsupported kind ${JSON.stringify(kind)}.`);
121
+ continue;
122
+ }
123
+ const user = object(turn.user, `history[${turnIndex}].user`);
124
+ const userText = string(user.text, `history[${turnIndex}].user.text`);
125
+ const images = user.images === undefined ? [] : array(user.images, `history[${turnIndex}].user.images`);
126
+ if (images.length)
127
+ issue('fx-unsupported-images', `history[${turnIndex}] contains ${images.length} image attachment(s).`);
128
+ emit({ role: 'user', content: userText, timestamp: session.created }, `turn:${turnIndex}:user`, session.created);
129
+ const execution = turn.execution;
130
+ if (execution !== undefined && execution !== null) {
131
+ const memory = object(execution, `history[${turnIndex}].execution`);
132
+ const schema = integer(memory.schema_version ?? 1, `history[${turnIndex}].execution.schema_version`);
133
+ if (schema < 1 || schema > 3) {
134
+ issue('fx-unsupported-execution-schema', `history[${turnIndex}] uses execution schema ${schema}; this codec supports presentation schemas 1 through 3.`);
135
+ }
136
+ else {
137
+ const files = memory.files === undefined ? [] : array(memory.files, `history[${turnIndex}].execution.files`);
138
+ const steering = memory.steering === undefined ? [] : array(memory.steering, `history[${turnIndex}].execution.steering`);
139
+ if (files.length)
140
+ issue('fx-unsupported-file-evidence', `history[${turnIndex}] contains ${files.length} execution file-evidence record(s).`);
141
+ if (steering.length)
142
+ issue('fx-unsupported-steering', `history[${turnIndex}] contains ${steering.length} steering record(s).`);
143
+ const steps = memory.tool_steps === undefined ? [] : array(memory.tool_steps, `history[${turnIndex}].execution.tool_steps`);
144
+ for (const [stepIndex, stepValue] of steps.entries()) {
145
+ const step = object(stepValue, `history[${turnIndex}].execution.tool_steps[${stepIndex}]`);
146
+ const calls = array(step.tool_calls, `history[${turnIndex}].execution.tool_steps[${stepIndex}].tool_calls`);
147
+ const results = array(step.tool_results, `history[${turnIndex}].execution.tool_steps[${stepIndex}].tool_results`);
148
+ const normalizedCalls = [];
149
+ const callNames = new Map();
150
+ let portableStep = true;
151
+ for (const [callIndex, callValue] of calls.entries()) {
152
+ const call = object(callValue, `history[${turnIndex}].execution.tool_steps[${stepIndex}].tool_calls[${callIndex}]`);
153
+ const id = string(call.id, `tool call ${callIndex} id`);
154
+ const name = string(call.name, `tool call ${callIndex} name`);
155
+ const args = string(call.arguments_json, `tool call ${callIndex} arguments_json`);
156
+ let parsedArgs;
157
+ try {
158
+ parsedArgs = JSON.parse(args);
159
+ }
160
+ catch {
161
+ parsedArgs = null;
162
+ }
163
+ if (parsedArgs === null || typeof parsedArgs !== 'object' || Array.isArray(parsedArgs)) {
164
+ issue('fx-unsupported-tool-arguments', `history[${turnIndex}] tool call ${JSON.stringify(id)} has arguments that are not a JSON object.`);
165
+ portableStep = false;
166
+ }
167
+ if (!id || !name || callNames.has(id))
168
+ fail(`history[${turnIndex}] has an invalid or duplicate tool call id`);
169
+ callNames.set(id, name);
170
+ normalizedCalls.push({ id, name, args });
171
+ if (call.provider_result !== undefined && call.provider_result !== null) {
172
+ issue('fx-unsupported-provider-result', `history[${turnIndex}] tool call ${JSON.stringify(id)} contains provider replay state.`);
173
+ }
174
+ }
175
+ const normalizedResults = [];
176
+ const seenResults = new Set();
177
+ for (const [resultIndex, resultValue] of results.entries()) {
178
+ const result = object(resultValue, `history[${turnIndex}].execution.tool_steps[${stepIndex}].tool_results[${resultIndex}]`);
179
+ const id = string(result.tool_call_id, `tool result ${resultIndex} tool_call_id`);
180
+ const name = string(result.tool_name, `tool result ${resultIndex} tool_name`);
181
+ const content = string(result.output, `tool result ${resultIndex} output`);
182
+ if (!callNames.has(id) || callNames.get(id) !== name || seenResults.has(id)) {
183
+ issue('fx-incomplete-tool-step', `history[${turnIndex}] tool result ${JSON.stringify(id)} is orphaned, duplicated, or names a different tool.`);
184
+ portableStep = false;
185
+ }
186
+ seenResults.add(id);
187
+ if (result.status !== 'success' && result.status !== 'failure')
188
+ fail(`tool result ${JSON.stringify(id)} has invalid status`);
189
+ const createdMs = integer(result.created_at_ms, `tool result ${JSON.stringify(id)} created_at_ms`);
190
+ normalizedResults.push({ id, content, ok: result.status === 'success', timestamp: iso(createdMs, 'tool result timestamp') });
191
+ const permission = result.permission_feedback === undefined ? [] : array(result.permission_feedback, `tool result ${JSON.stringify(id)} permission_feedback`);
192
+ if (permission.length || result.provider_native === true || result.truncated === true || result.output_handle != null || result.preview != null || result.committed_file_presentation != null) {
193
+ issue('fx-unsupported-tool-result-metadata', `history[${turnIndex}] tool result ${JSON.stringify(id)} contains runtime-only result metadata.`);
194
+ }
195
+ }
196
+ if (seenResults.size !== callNames.size || [...callNames.keys()].some((id) => !seenResults.has(id))) {
197
+ issue('fx-incomplete-tool-step', `history[${turnIndex}] execution step ${stepIndex} does not contain one terminal result per call.`);
198
+ portableStep = false;
199
+ }
200
+ if (!portableStep)
201
+ continue;
202
+ const assistant = step.assistant === undefined || step.assistant === null
203
+ ? null : string(step.assistant, `history[${turnIndex}].execution.tool_steps[${stepIndex}].assistant`);
204
+ if (assistant)
205
+ emit({ role: 'assistant', content: assistant, timestamp: session.updated }, `turn:${turnIndex}:step:${stepIndex}:assistant`, session.updated);
206
+ if (normalizedCalls.length)
207
+ emit({ role: 'assistant', content: null, tool_calls: normalizedCalls, timestamp: session.updated }, `turn:${turnIndex}:step:${stepIndex}:calls`, session.updated);
208
+ for (const result of normalizedResults) {
209
+ emit({ role: 'tool', tool_call_id: result.id, content: result.content, ok: result.ok, timestamp: result.timestamp }, `turn:${turnIndex}:step:${stepIndex}:result:${result.id}`, result.timestamp);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ const assistant = string(turn.assistant, `history[${turnIndex}].assistant`);
215
+ emit({ role: 'assistant', content: assistant, timestamp: session.updated }, `turn:${turnIndex}:assistant`, session.updated);
216
+ }
217
+ return { session, entries, diagnostics, portabilityIssues };
218
+ }
219
+ function parseV4Session(root) {
220
+ const allowed = new Set(['schema_version', 'id', 'origin_workspace_root', 'workspace_root', 'created_at_ms', 'updated_at_ms', 'conversation_language', 'provider', 'model', 'effort', 'fast_mode', 'ultrafast_mode', 'title', 'subagent_child']);
221
+ if (Object.keys(root).some((key) => !allowed.has(key)))
222
+ fail('session.json schema 4 has unknown fields');
223
+ const id = parseIdentity(root.id, 'id');
224
+ if (id === '.' || id === '..' || /[/\\]/.test(id))
225
+ fail('id is unsafe');
226
+ const origin = string(root.origin_workspace_root, 'origin_workspace_root');
227
+ const workspace = string(root.workspace_root, 'workspace_root');
228
+ if (!origin.startsWith('/') || !workspace.startsWith('/'))
229
+ fail('workspace roots must be absolute');
230
+ const createdMs = integer(root.created_at_ms, 'created_at_ms');
231
+ const updatedMs = integer(root.updated_at_ms, 'updated_at_ms');
232
+ if (updatedMs < createdMs)
233
+ fail('updated_at_ms precedes created_at_ms');
234
+ string(root.conversation_language, 'conversation_language');
235
+ const provider = parseProvider(root.provider, 'provider');
236
+ const model = string(root.model, 'model');
237
+ if (!model)
238
+ fail('model is empty');
239
+ string(root.effort, 'effort');
240
+ if (typeof root.fast_mode !== 'boolean')
241
+ fail('fast_mode must be a boolean');
242
+ if (root.ultrafast_mode !== undefined && root.ultrafast_mode !== null && typeof root.ultrafast_mode !== 'boolean')
243
+ fail('ultrafast_mode must be a boolean or null');
244
+ if (root.title !== undefined && root.title !== null && (typeof root.title !== 'string' || !root.title))
245
+ fail('title is invalid');
246
+ if (root.subagent_child !== undefined && typeof root.subagent_child !== 'boolean')
247
+ fail('subagent_child must be a boolean');
248
+ return { id, workspace, model, provider, created: iso(createdMs, 'created_at_ms'), updated: iso(updatedMs, 'updated_at_ms'), history: [] };
249
+ }
250
+ function decodeV4(root, companions) {
251
+ const session = parseV4Session(root);
252
+ if (companions.events === undefined)
253
+ fail('schema_version 4 requires sibling events.jsonl');
254
+ const diagnostics = [];
255
+ const portabilityIssues = [];
256
+ const entries = [{
257
+ record: { role: 'meta', source: 'fx', ...(session.workspace ? { cwd: session.workspace } : {}), ...(session.model ? { model: session.model } : {}) },
258
+ nativeId: 'meta', sourceSequence: 0,
259
+ }];
260
+ const pending = new Map();
261
+ let pendingReplay = null;
262
+ let turnEntries = [];
263
+ let turnOpen = false;
264
+ let toolResultsStarted = false;
265
+ let expectedSeq = 1;
266
+ let latestCheckpointCoverage = 0;
267
+ const note = (code, message, blocksPortability = false) => {
268
+ diagnostics.push({ code: 'noise_record_dropped', message: `[trajectories/fx-native-session-json-v4:${code}] ${message}` });
269
+ if (blocksPortability)
270
+ portabilityIssues.push(message);
271
+ };
272
+ const emit = (record, nativeId, timestamp, sequence) => {
273
+ turnEntries.push({ record, nativeId, sourceTimestamp: timestamp, sourceSequence: sequence });
274
+ };
275
+ const finishReplayAssociation = () => {
276
+ if (!pendingReplay)
277
+ return;
278
+ const replay = pendingReplay;
279
+ pendingReplay = null;
280
+ const matches = replay.callIds.length === replay.seenCallIds.length &&
281
+ replay.callIds.every((id, index) => id === replay.seenCallIds[index]);
282
+ if (!matches) {
283
+ note('fx-unsupported-provider-replay', `assistant event ${replay.seq} provider replay call association does not exactly match its following tool-call batch.`, true);
284
+ return;
285
+ }
286
+ turnEntries.splice(replay.insertionIndex, 0, ...replay.reasoning);
287
+ note('fx-provider-replay-projected', `assistant event ${replay.seq} provider replay contains visible reasoning projected into canonical history; its original bytes remain in events.jsonl.`);
288
+ };
289
+ if (!companions.events.endsWith('\n'))
290
+ fail('events.jsonl final frame is unterminated');
291
+ const lines = companions.events.split('\n');
292
+ lines.pop();
293
+ for (const [lineIndex, line] of lines.entries()) {
294
+ if (!line.trim())
295
+ fail(`events.jsonl line ${lineIndex + 1} is blank`);
296
+ const envelope = parseJson(line, `events.jsonl line ${lineIndex + 1}`);
297
+ exactKeys(envelope, ['schema_version', 'seq', 'timestamp_ms', 'event'], `events.jsonl line ${lineIndex + 1}`);
298
+ const eventSchema = integer(envelope.schema_version, `events.jsonl line ${lineIndex + 1} schema_version`);
299
+ if (eventSchema < 1 || eventSchema > 3)
300
+ fail(`events.jsonl line ${lineIndex + 1} has unsupported conversation schema ${eventSchema}`);
301
+ const seq = integer(envelope.seq, `events.jsonl line ${lineIndex + 1} seq`);
302
+ if (seq !== expectedSeq)
303
+ fail(`events.jsonl sequence ${seq} is not contiguous after ${expectedSeq - 1}`);
304
+ expectedSeq += 1;
305
+ const timestampMs = integer(envelope.timestamp_ms, `events.jsonl line ${lineIndex + 1} timestamp_ms`);
306
+ const timestamp = iso(timestampMs, `events.jsonl line ${lineIndex + 1} timestamp_ms`);
307
+ const event = object(envelope.event, `events.jsonl line ${lineIndex + 1} event`);
308
+ const kinds = Object.keys(event);
309
+ if (kinds.length !== 1)
310
+ fail(`events.jsonl line ${lineIndex + 1} must contain exactly one event variant`);
311
+ const kind = kinds[0];
312
+ const value = object(event[kind], `events.jsonl line ${lineIndex + 1} ${kind}`);
313
+ if (pendingReplay && kind !== 'tool_call')
314
+ finishReplayAssociation();
315
+ if (kind === 'user') {
316
+ exactKeys(value, ['text', 'images', 'work_id'], `user event ${seq}`);
317
+ if (turnOpen || pending.size)
318
+ fail(`user event ${seq} begins before the previous turn completed`);
319
+ const content = string(value.text, `user event ${seq} text`);
320
+ const images = value.images === undefined ? [] : array(value.images, `user event ${seq} images`);
321
+ if (images.length)
322
+ note('fx-unsupported-images', `user event ${seq} contains ${images.length} image attachment(s).`, true);
323
+ if (value.work_id != null)
324
+ note('fx-work-id', `user event ${seq} carries native work provenance retained in events.jsonl.`);
325
+ turnOpen = true;
326
+ toolResultsStarted = false;
327
+ turnEntries = [];
328
+ emit({ role: 'user', content, timestamp }, `event:${seq}:user`, timestamp, seq);
329
+ continue;
330
+ }
331
+ if (kind === 'context_checkpoint') {
332
+ exactKeys(value, ['covers_through_seq', 'summary'], `context_checkpoint event ${seq}`);
333
+ const coverage = integer(value.covers_through_seq, `context_checkpoint event ${seq} covers_through_seq`);
334
+ if (coverage < latestCheckpointCoverage || coverage >= seq || [...pending.values()].some((call) => call.seq <= coverage)) {
335
+ fail(`context_checkpoint event ${seq} has invalid coverage`);
336
+ }
337
+ latestCheckpointCoverage = coverage;
338
+ string(value.summary, `context_checkpoint event ${seq} summary`);
339
+ note('fx-unsupported-compaction', `context_checkpoint event ${seq} replaces earlier active context with a summary.`, true);
340
+ continue;
341
+ }
342
+ if (!turnOpen)
343
+ fail(`${kind} event ${seq} occurs outside a turn`);
344
+ if (kind === 'assistant') {
345
+ exactKeys(value, ['text', 'provider_replay', 'standalone_response'], `assistant event ${seq}`);
346
+ if (pending.size)
347
+ fail(`assistant event ${seq} occurs before all tool results`);
348
+ const content = string(value.text, `assistant event ${seq} text`);
349
+ if (eventSchema === 1 && (!content || value.provider_replay != null))
350
+ fail(`assistant event ${seq} violates conversation schema 1`);
351
+ if (value.provider_replay != null) {
352
+ const replay = object(value.provider_replay, `assistant event ${seq} provider_replay`);
353
+ exactKeys(replay, ['source', 'parts_json'], `assistant event ${seq} provider_replay`);
354
+ const replaySource = object(replay.source, `assistant event ${seq} provider_replay source`);
355
+ exactKeys(replaySource, ['provider', 'model'], `assistant event ${seq} provider_replay source`);
356
+ const replayModel = parseIdentity(replaySource.model, `assistant event ${seq} provider replay model`);
357
+ const replayProvider = parseProvider(replaySource.provider, `assistant event ${seq} provider replay provider`);
358
+ const partsRaw = string(replay.parts_json, `assistant event ${seq} provider replay parts_json`);
359
+ if (!partsRaw)
360
+ fail(`assistant event ${seq} provider replay parts_json is empty`);
361
+ const configuredSource = typeof session.provider !== 'string' && typeof replayProvider !== 'string' &&
362
+ session.provider?.name === replayProvider.name && session.provider.binding === replayProvider.binding && session.model === replayModel;
363
+ let parts = null;
364
+ let canonicalParts = false;
365
+ if (configuredSource) {
366
+ try {
367
+ const parsed = JSON.parse(partsRaw);
368
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
369
+ parts = parsed;
370
+ // The pinned chat adapter emits compact JSON and rejects duplicate
371
+ // keys. Re-encoding equality keeps this narrow projection from
372
+ // accepting bytes the native replay parser would refuse.
373
+ canonicalParts = JSON.stringify(parsed) === partsRaw;
374
+ }
375
+ }
376
+ catch {
377
+ // Conversation events deliberately retain provider replay as opaque
378
+ // UTF-8. Only the configured chat subset below interprets it.
379
+ }
380
+ }
381
+ const allowedPartKeys = new Set(['reasoning', 'reasoning_content', 'reasoning_details', '_tool_call_ids']);
382
+ const knownKeys = parts !== null && Object.keys(parts).every((key) => allowedPartKeys.has(key));
383
+ const rawCallIds = parts?._tool_call_ids;
384
+ const callIds = Array.isArray(rawCallIds) && rawCallIds.length <= 256 && rawCallIds.every((id) => typeof id === 'string' && id.length > 0 && new TextEncoder().encode(id).byteLength <= 256 && !/[\x00-\x1f]/.test(id))
385
+ ? rawCallIds : null;
386
+ const distinctCallIds = callIds === null ? null : new Set(callIds);
387
+ const reasoningFields = ['reasoning', 'reasoning_content'];
388
+ const reasoningShape = parts !== null && reasoningFields.every((field) => parts[field] === undefined || parts[field] === null || typeof parts[field] === 'string');
389
+ const visible = parts === null || !reasoningShape ? [] : reasoningFields.flatMap((field) => {
390
+ const part = parts[field];
391
+ return typeof part === 'string' && part ? [{ field, content: part }] : [];
392
+ });
393
+ const portableSubset = configuredSource && new TextEncoder().encode(partsRaw).byteLength <= 4 * 1024 * 1024 &&
394
+ parts !== null && canonicalParts && knownKeys && callIds !== null &&
395
+ distinctCallIds?.size === callIds.length && reasoningShape && parts.reasoning_details == null && visible.length === 1;
396
+ if (!portableSubset) {
397
+ note('fx-unsupported-provider-replay', `assistant event ${seq} carries provider replay state outside the lossless configured-provider visible-reasoning subset.`, true);
398
+ }
399
+ else {
400
+ pendingReplay = {
401
+ seq,
402
+ callIds: callIds,
403
+ seenCallIds: [],
404
+ insertionIndex: turnEntries.length,
405
+ reasoning: visible.map(({ field, content: reasoning }) => ({
406
+ record: { role: 'reasoning', content: reasoning, timestamp },
407
+ nativeId: `event:${seq}:provider_replay:${field}`,
408
+ sourceTimestamp: timestamp,
409
+ sourceSequence: seq,
410
+ })),
411
+ };
412
+ }
413
+ }
414
+ if (value.standalone_response !== undefined && typeof value.standalone_response !== 'boolean')
415
+ fail(`assistant event ${seq} standalone_response must be a boolean`);
416
+ if (value.standalone_response === true)
417
+ finishReplayAssociation();
418
+ if (content)
419
+ emit({ role: 'assistant', content, timestamp }, `event:${seq}:assistant`, timestamp, seq);
420
+ toolResultsStarted = false;
421
+ continue;
422
+ }
423
+ if (kind === 'tool_call') {
424
+ exactKeys(value, ['call_id', 'tool_name', 'arguments_json', 'argument_integrity', 'provisional_id', 'provider_result', 'final_identity', 'provenance'], `tool_call event ${seq}`);
425
+ const id = parseIdentity(value.call_id, `tool_call event ${seq} call_id`);
426
+ const name = parseIdentity(value.tool_name, `tool_call event ${seq} tool_name`);
427
+ if (toolResultsStarted)
428
+ fail(`tool_call event ${seq} occurs after results started for its assistant step`);
429
+ if (pending.has(id))
430
+ fail(`tool_call event ${seq} duplicates call ${JSON.stringify(id)}`);
431
+ if (pendingReplay)
432
+ pendingReplay.seenCallIds.push(id);
433
+ const args = string(value.arguments_json, `tool_call event ${seq} arguments_json`);
434
+ let parsedArgs;
435
+ try {
436
+ parsedArgs = JSON.parse(args);
437
+ }
438
+ catch {
439
+ parsedArgs = null;
440
+ }
441
+ if (parsedArgs === null || typeof parsedArgs !== 'object' || Array.isArray(parsedArgs))
442
+ fail(`tool_call event ${seq} arguments_json is not a JSON object`);
443
+ const argumentIntegrity = string(value.argument_integrity ?? 'valid', `tool_call event ${seq} argument_integrity`);
444
+ const finalIdentity = string(value.final_identity ?? 'valid', `tool_call event ${seq} final_identity`);
445
+ const provenance = string(value.provenance ?? 'fx_local', `tool_call event ${seq} provenance`);
446
+ if (argumentIntegrity !== 'valid')
447
+ note('fx-tool-argument-integrity', `tool_call event ${seq} marks arguments as ${JSON.stringify(argumentIntegrity)}.`, true);
448
+ if (finalIdentity !== 'valid')
449
+ note('fx-tool-final-identity', `tool_call event ${seq} marks its final identity as ${JSON.stringify(finalIdentity)}.`, true);
450
+ if (value.provisional_id != null || value.provider_result != null || provenance !== 'fx_local') {
451
+ note('fx-tool-call-provenance', `tool_call event ${seq} carries native execution provenance retained in events.jsonl.`);
452
+ }
453
+ pending.set(id, { name, seq });
454
+ emit({ role: 'assistant', content: null, tool_calls: [{ id, name, args }], timestamp }, `event:${seq}:tool_call:${id}`, timestamp, seq);
455
+ continue;
456
+ }
457
+ if (kind === 'tool_result') {
458
+ exactKeys(value, ['call_id', 'tool_name', 'status', 'artifact_ref', 'tool_image_handle', 'output_bytes', 'stored_bytes', 'completeness', 'preview', 'provider_native', 'review_feedback', 'created_at_ms', 'permission_feedback', 'committed_file_presentation', 'command_replay_ref', 'command_replay_bytes', 'command_process_presentation', 'terminal_action_presentation'], `tool_result event ${seq}`);
459
+ const id = parseIdentity(value.call_id, `tool_result event ${seq} call_id`);
460
+ toolResultsStarted = true;
461
+ const name = parseIdentity(value.tool_name, `tool_result event ${seq} tool_name`);
462
+ if (pending.get(id)?.name !== name)
463
+ fail(`tool_result event ${seq} is orphaned or names a different tool`);
464
+ const status = string(value.status, `tool_result event ${seq} status`);
465
+ if (status !== 'success' && status !== 'failure')
466
+ fail(`tool_result event ${seq} has invalid status`);
467
+ const providerNative = value.provider_native ?? false;
468
+ const reviewFeedback = value.review_feedback ?? false;
469
+ if (typeof providerNative !== 'boolean')
470
+ fail(`tool_result event ${seq} provider_native must be a boolean`);
471
+ if (typeof reviewFeedback !== 'boolean')
472
+ fail(`tool_result event ${seq} review_feedback must be a boolean`);
473
+ if (reviewFeedback && (status !== 'failure' || providerNative))
474
+ fail(`tool_result event ${seq} has invalid review_feedback state`);
475
+ const artifactRef = parseIdentity(value.artifact_ref, `tool_result event ${seq} artifact_ref`);
476
+ if (artifactRef === '.' || artifactRef === '..' || /[/\\]/.test(artifactRef))
477
+ fail(`tool_result event ${seq} artifact_ref is unsafe`);
478
+ const content = companions.toolResults && Object.hasOwn(companions.toolResults, artifactRef)
479
+ ? companions.toolResults[artifactRef] : undefined;
480
+ if (typeof content !== 'string')
481
+ fail(`tool_result event ${seq} requires tool-results/${artifactRef}; preview fallback is not lossless`);
482
+ const storedBytes = integer(value.stored_bytes, `tool_result event ${seq} stored_bytes`);
483
+ if (new TextEncoder().encode(content).byteLength !== storedBytes)
484
+ fail(`tool-results/${artifactRef} byte length does not match event ${seq}`);
485
+ if (value.output_bytes !== undefined && value.output_bytes !== null)
486
+ integer(value.output_bytes, `tool_result event ${seq} output_bytes`);
487
+ const completeness = string(value.completeness, `tool_result event ${seq} completeness`);
488
+ if (!['complete', 'partial', 'unknown'].includes(completeness))
489
+ fail(`tool_result event ${seq} has invalid completeness`);
490
+ if (completeness !== 'complete')
491
+ note('fx-result-completeness', `tool_result event ${seq} records ${JSON.stringify(completeness)} completeness; the exact stored artifact is retained.`);
492
+ const permission = value.permission_feedback === undefined ? [] : array(value.permission_feedback, `tool_result event ${seq} permission_feedback`);
493
+ if (permission.some((feedback) => typeof feedback !== 'string'))
494
+ fail(`tool_result event ${seq} permission_feedback must contain only strings`);
495
+ if (value.preview !== undefined && value.preview !== null && typeof value.preview !== 'string')
496
+ fail(`tool_result event ${seq} preview must be a string or null`);
497
+ if (typeof value.preview === 'string' && new TextEncoder().encode(value.preview).byteLength > 4096)
498
+ fail(`tool_result event ${seq} preview is too large`);
499
+ if (value.tool_image_handle !== undefined && value.tool_image_handle !== null)
500
+ parseIdentity(value.tool_image_handle, `tool_result event ${seq} tool_image_handle`);
501
+ if (permission.length || providerNative || reviewFeedback || value.preview != null || value.committed_file_presentation != null) {
502
+ note('fx-tool-result-provenance', `tool_result event ${seq} carries native result provenance retained in events.jsonl.`);
503
+ }
504
+ if (value.tool_image_handle != null)
505
+ note('fx-unsupported-tool-image', `tool_result event ${seq} references a native tool image.`, true);
506
+ if (value.command_replay_ref != null || value.command_replay_bytes != null || value.command_process_presentation != null || value.terminal_action_presentation != null) {
507
+ if ((value.command_replay_ref == null) !== (value.command_replay_bytes == null))
508
+ fail(`tool_result event ${seq} has an incomplete command replay pair`);
509
+ note('fx-unsupported-command-replay', `tool_result event ${seq} carries command replay or terminal state.`, true);
510
+ }
511
+ integer(value.created_at_ms ?? 0, `tool_result event ${seq} created_at_ms`);
512
+ pending.delete(id);
513
+ emit({ role: 'tool', tool_call_id: id, content, ok: status === 'success', timestamp }, `event:${seq}:tool_result:${id}`, timestamp, seq);
514
+ continue;
515
+ }
516
+ if (kind === 'turn_completed') {
517
+ exactKeys(value, ['files', 'turn_summary'], `turn_completed event ${seq}`);
518
+ if (pending.size)
519
+ fail(`turn_completed event ${seq} has unresolved tool calls`);
520
+ const files = value.files === undefined ? [] : array(value.files, `turn_completed event ${seq} files`);
521
+ if (files.length || value.turn_summary != null)
522
+ note('fx-turn-provenance', `turn_completed event ${seq} carries native file evidence or a turn summary retained in events.jsonl.`);
523
+ entries.push(...turnEntries);
524
+ turnEntries = [];
525
+ turnOpen = false;
526
+ toolResultsStarted = false;
527
+ continue;
528
+ }
529
+ if (kind === 'steering') {
530
+ exactKeys(value, ['text'], `steering event ${seq}`);
531
+ string(value.text, `steering event ${seq} text`);
532
+ note('fx-unsupported-steering', `steering event ${seq} affects active context and has no canonical role.`, true);
533
+ continue;
534
+ }
535
+ if (kind === 'interrupted') {
536
+ exactKeys(value, ['reason', 'partial_text', 'command_replay_ref', 'command_replay_bytes', 'command_artifact_ref', 'files', 'turn_summary', 'cancellation_origin'], `interrupted event ${seq}`);
537
+ note('fx-unsupported-interrupted-turn', `interrupted event ${seq} does not complete a portable turn.`, true);
538
+ pending.clear();
539
+ turnEntries = [];
540
+ turnOpen = false;
541
+ toolResultsStarted = false;
542
+ continue;
543
+ }
544
+ fail(`events.jsonl line ${lineIndex + 1} has unknown event variant ${JSON.stringify(kind)}`);
545
+ }
546
+ finishReplayAssociation();
547
+ if (turnOpen || pending.size) {
548
+ portabilityIssues.push('events.jsonl ends with an incomplete turn.');
549
+ turnEntries = [];
550
+ }
551
+ return { session, entries, diagnostics, portabilityIssues };
552
+ }
553
+ function decode(raw, companions) {
554
+ const root = parseJson(raw, 'session.json');
555
+ if (root.schema_version === 1 || root.schema_version === 2)
556
+ return decodeLegacy(root);
557
+ if (root.schema_version === 4)
558
+ return decodeV4(root, companions);
559
+ if (root.schema_version === 3)
560
+ fail('schema_version 3 is the event_log_v1 manifest and requires its generation log/checkpoint reader');
561
+ return fail(`unsupported schema_version ${String(root.schema_version)}`);
562
+ }
563
+ /** Parse a native FX session for archival inspection, retaining unsupported-feature diagnostics. */
564
+ export function readFx(raw, companions = {}) {
565
+ const decoded = decode(raw, companions);
566
+ return canonicalFromRecords('fx', decoded.session.id, decoded.entries, decoded.diagnostics);
567
+ }
568
+ /** Fail closed before converting FX history whose native context cannot be represented completely. */
569
+ export function assertFxPortable(raw, companions = {}) {
570
+ const issues = decode(raw, companions).portabilityIssues;
571
+ if (issues.length)
572
+ throw new Error(`FX native session is not portable: ${issues.join(' ')}`);
573
+ }
@@ -0,0 +1,8 @@
1
+ import { type CanonicalResult } from '@letta-ai/trajectory';
2
+ /** Extend Letta's archive projection with native direct prompts and system text.
3
+ * A hook is only a duplicate when its next submission carries the same prompt.
4
+ * Repeated user messages remain distinct and keep their own native identities.
5
+ */
6
+ export declare function readGitHubCopilot(raw: string): CanonicalResult;
7
+ /** Reject native state that the supported text projection cannot continue. */
8
+ export declare function assertGitHubCopilotHistory(raw: string): void;