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,470 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, extname, join, resolve } from "node:path";
5
+ import type { DiscoveredSession } from "../types.js";
6
+ import {
7
+ MAX_NATIVE_BYTES, MAX_NATIVE_RECORDS, NativeInputError, nativeByteLimit, object, readBoundedText, reportNativeError, safeNativeId,
8
+ type NativeDiscoveryOptions, type NativeReadOptions, type NativeTranscript,
9
+ } from "./common.js";
10
+ import { withReadOnlyDb } from "./sqlite-ro.js";
11
+
12
+ type Row = Record<string, unknown>;
13
+ type Message = { info: Row; parts: Row[] };
14
+ type Export = { info: Row; messages: Message[] };
15
+ const PART_TYPES = new Set([
16
+ "text", "reasoning", "tool", "file", "subtask", "step-start", "step-finish",
17
+ "snapshot", "patch", "agent", "retry", "compaction",
18
+ ]);
19
+
20
+ function check(condition: unknown, message: string): asserts condition {
21
+ if (!condition) throw new NativeInputError(`OpenCode: ${message}.`);
22
+ }
23
+
24
+ function parseJson(value: unknown): unknown {
25
+ check(typeof value === "string", "expected JSON text");
26
+ try { return JSON.parse(value); }
27
+ catch (error) {
28
+ if (error instanceof SyntaxError) throw new NativeInputError("OpenCode: malformed JSON.");
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ function jsonObject(value: unknown): Row {
34
+ const parsed = parseJson(value);
35
+ check(object(parsed), "expected a JSON object");
36
+ return parsed;
37
+ }
38
+
39
+ function timestamp(value: unknown): string {
40
+ check(typeof value === "number" && Number.isFinite(value), "invalid timestamp");
41
+ const date = new Date(value);
42
+ check(Number.isFinite(date.getTime()), "invalid timestamp");
43
+ return date.toISOString();
44
+ }
45
+
46
+ function withOpenCodeDb<T>(path: string, read: (db: Database) => T): T {
47
+ try { return withReadOnlyDb(path, read); }
48
+ catch (error) {
49
+ if (object(error) && ["SQLITE_CORRUPT", "SQLITE_NOTADB", "SQLITE_FORMAT"].includes(String(error.code))) {
50
+ throw new NativeInputError("OpenCode: corrupt or unsupported database.");
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ export function openCodeRoots(): string[] {
57
+ return [
58
+ join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "opencode"),
59
+ join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode", "sessions"),
60
+ ];
61
+ }
62
+
63
+ // Format evidence, not an upstream reader implementation:
64
+ // https://github.com/anomalyco/opencode/blob/v1.2.0/packages/opencode/src/session/session.sql.ts
65
+ function checkSchema(db: Database): Record<string, string[]> {
66
+ const required = {
67
+ session: ["id", "parent_id", "directory", "title", "time_created", "time_updated"],
68
+ message: ["id", "session_id", "time_created", "time_updated", "data"],
69
+ part: ["id", "message_id", "session_id", "time_created", "time_updated", "data"],
70
+ };
71
+ const schema: Record<string, string[]> = {};
72
+ for (const [table, fields] of Object.entries(required)) {
73
+ check(db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table), "unsupported database schema");
74
+ const columns = new Set((db.query(`PRAGMA table_info("${table}")`).all() as Row[]).map((row) => row.name));
75
+ check(fields.every((field) => columns.has(field)), "unsupported database schema");
76
+ schema[table] = [...columns] as string[];
77
+ }
78
+ return schema;
79
+ }
80
+
81
+ const PART_SELECTION = "session_id = ? OR message_id IN (SELECT id FROM message WHERE session_id = ?)";
82
+
83
+ function checkSqliteBudget(db: Database, schema: Record<string, string[]>, id: string, maxBytes: number): void {
84
+ let records = 0;
85
+ let bytes = 0;
86
+ // Measure before fetching any JSON, in the same read snapshot as the assembly.
87
+ for (const [table, where, ids] of [
88
+ ["session", "id = ?", [id]], ["message", "session_id = ?", [id]], ["part", PART_SELECTION, [id, id]],
89
+ ] as const) {
90
+ const size = schema[table].map((name) => `coalesce(length(CAST("${name.replaceAll('"', '""')}" AS BLOB)), 0)`).join(" + ");
91
+ const row = db.query(`SELECT count(*) AS records, coalesce(sum(${size}), 0) AS bytes FROM "${table}" WHERE ${where}`).get(...ids) as { records: number; bytes: number };
92
+ records += row.records;
93
+ bytes += row.bytes;
94
+ check(Number.isSafeInteger(records) && records <= MAX_NATIVE_RECORDS, "session exceeds the record limit");
95
+ check(Number.isSafeInteger(bytes) && bytes <= maxBytes, "session exceeds the byte limit");
96
+ }
97
+ }
98
+
99
+ function newestFirst(a: DiscoveredSession, b: DiscoveredSession): number {
100
+ const rank = (item: DiscoveredSession) => item.native?.kind === "opencode-sqlite" ? 0 : item.native ? 1 : 2;
101
+ return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "") || a.id.localeCompare(b.id) || rank(a) - rank(b) || a.path.localeCompare(b.path);
102
+ }
103
+
104
+ const NATIVE_DATABASE = /^opencode(?:-[a-z0-9._-]+)?\.db$/i;
105
+ const SKIP_DIRECTORIES = new Set(["node_modules", "cache", "caches", "log", "logs", "telemetry", "session_diff"]);
106
+
107
+ function exportFilename(name: string): boolean {
108
+ return !name.startsWith(".") && name.toLowerCase().endsWith(".json") &&
109
+ !/(?:^|[._-])(?:auth|credentials?|config|configuration|settings|secrets?|tokens?|package|tsconfig|opencode)(?:[._-]|$)/i.test(name);
110
+ }
111
+
112
+ export function discoverOpenCodeSessions(roots: string[], options: NativeDiscoveryOptions): DiscoveredSession[] {
113
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1) throw new RangeError("OpenCode discovery limit must be positive.");
114
+ const candidates: DiscoveredSession[] = [];
115
+ const seen = new Set<string>();
116
+ let examined = 0;
117
+ let metadataBytes = 0;
118
+ function count(amount = 1): void {
119
+ examined += amount;
120
+ check(examined <= MAX_NATIVE_RECORDS, "discovery exceeds the record limit");
121
+ }
122
+ function add(item: DiscoveredSession): void {
123
+ metadataBytes += Buffer.byteLength(JSON.stringify(item));
124
+ check(metadataBytes <= MAX_NATIVE_BYTES, "discovery exceeds the byte limit");
125
+ candidates.push(item);
126
+ }
127
+ function inspect(path: string, read: () => void): void {
128
+ if (seen.has(path)) return;
129
+ seen.add(path);
130
+ try {
131
+ count();
132
+ if (lstatSync(path).isSymbolicLink()) return;
133
+ read();
134
+ } catch { reportNativeError(options, path); }
135
+ }
136
+ function entries(path: string) {
137
+ check(lstatSync(path).isDirectory(), "expected native directory");
138
+ const result = readdirSync(path, { withFileTypes: true });
139
+ count(result.length);
140
+ return result.sort((a, b) => a.name.localeCompare(b.name));
141
+ }
142
+ function sqlite(path: string): void {
143
+ inspect(path, () => withOpenCodeDb(path, (db) => {
144
+ checkSchema(db);
145
+ const rows = db.query(`SELECT
146
+ CASE WHEN length(CAST(id AS BLOB)) <= ? THEN id END AS id,
147
+ CASE WHEN length(CAST(title AS BLOB)) <= ? THEN title END AS title,
148
+ time_updated FROM session ORDER BY time_updated DESC, id ASC LIMIT ?
149
+ `).iterate(MAX_NATIVE_BYTES, MAX_NATIVE_BYTES, MAX_NATIVE_RECORDS + 1);
150
+ let accepted = 0;
151
+ for (const row of rows as Iterable<Row>) {
152
+ count();
153
+ try {
154
+ check(safeNativeId(row.id) && typeof row.title === "string", "invalid session metadata");
155
+ add({ id: row.id, source: "opencode", path, title: row.title,
156
+ updatedAt: timestamp(row.time_updated), native: { kind: "opencode-sqlite" } });
157
+ if (++accepted >= options.limit) break;
158
+ } catch { reportNativeError(options, path); }
159
+ }
160
+ }));
161
+ }
162
+ function storage(path: string): void {
163
+ inspect(path, () => {
164
+ const sessions = join(path, "session");
165
+ if (!existsSync(sessions)) return;
166
+ for (const project of entries(sessions)) {
167
+ if (!project.isDirectory()) continue;
168
+ for (const entry of entries(join(sessions, project.name))) {
169
+ if (!entry.isFile() || !exportFilename(entry.name)) continue;
170
+ const file = join(sessions, project.name, entry.name);
171
+ inspect(file, () => {
172
+ const info = jsonObject(readBoundedText(file));
173
+ check(safeNativeId(info.id) && basename(file) === `${info.id}.json`, "invalid session ID");
174
+ check(object(info.time), "missing session time");
175
+ add({ id: info.id, source: "opencode", path: file,
176
+ updatedAt: timestamp(info.time.updated), title: typeof info.title === "string" ? info.title : undefined,
177
+ sizeBytes: statSync(file).size, native: { kind: "opencode-storage", root: path } });
178
+ });
179
+ }
180
+ }
181
+ });
182
+ }
183
+ function exported(path: string, depth = 0): void {
184
+ if (basename(path).startsWith(".") || SKIP_DIRECTORIES.has(basename(path).toLowerCase())) return;
185
+ inspect(path, () => {
186
+ const stat = lstatSync(path);
187
+ if (stat.isDirectory()) {
188
+ check(depth <= 8, "export traversal exceeds the depth limit");
189
+ for (const entry of entries(path)) {
190
+ if (entry.isDirectory() || (entry.isFile() && exportFilename(entry.name))) {
191
+ exported(join(path, entry.name), depth + 1);
192
+ }
193
+ }
194
+ } else if (stat.isFile() && exportFilename(basename(path))) {
195
+ const data = parseJson(readBoundedText(path));
196
+ if (!object(data) || !object(data.info) || !Array.isArray(data.messages)) return;
197
+ validateExport(data);
198
+ const time = object(data.info.time) ? data.info.time : {};
199
+ add({ id: data.info.id as string, source: "opencode", path,
200
+ updatedAt: time.updated == null ? stat.mtime.toISOString() : timestamp(time.updated),
201
+ title: typeof data.info.title === "string" ? data.info.title : undefined, sizeBytes: stat.size });
202
+ }
203
+ });
204
+ }
205
+ for (const input of [...new Set(roots.map((root) => resolve(root)))].sort()) {
206
+ if (!existsSync(input)) continue;
207
+ try {
208
+ const stat = lstatSync(input);
209
+ if (stat.isSymbolicLink()) continue;
210
+ if (stat.isFile()) {
211
+ if ([".db", ".sqlite", ".sqlite3"].includes(extname(input).toLowerCase())) sqlite(input);
212
+ else if (exportFilename(basename(input))) exported(input);
213
+ continue;
214
+ }
215
+ if (!stat.isDirectory()) continue;
216
+ const children = entries(input);
217
+ if (children.some((entry) => entry.name === "session" && entry.isDirectory())) {
218
+ storage(input);
219
+ } else if (basename(input) === "opencode" || children.some((entry) => NATIVE_DATABASE.test(entry.name) || entry.name === "storage")) {
220
+ for (const entry of children) {
221
+ const path = join(input, entry.name);
222
+ if (entry.isFile() && NATIVE_DATABASE.test(entry.name)) sqlite(path);
223
+ else if (entry.isDirectory() && entry.name === "storage") storage(path);
224
+ else if (entry.isDirectory() && ["exports", "sessions"].includes(entry.name)) exported(path);
225
+ }
226
+ } else {
227
+ exported(input);
228
+ }
229
+ } catch { reportNativeError(options, input); }
230
+ }
231
+ const byId = new Map<string, DiscoveredSession>();
232
+ for (const item of candidates.sort(newestFirst)) if (!byId.has(item.id)) byId.set(item.id, item);
233
+ const deduplicated = [...byId.values()];
234
+ return options.select ? options.select(deduplicated) : deduplicated.slice(0, options.limit);
235
+ }
236
+
237
+ function sessionInfo(row: Row): Row {
238
+ check(safeNativeId(row.id) && typeof row.directory === "string" && typeof row.title === "string", "invalid session metadata");
239
+ timestamp(row.time_created);
240
+ timestamp(row.time_updated);
241
+ check(row.parent_id == null || safeNativeId(row.parent_id), "invalid parent ID");
242
+ const info: Row = {
243
+ id: row.id, projectID: row.project_id, parentID: row.parent_id ?? null,
244
+ workspaceID: row.workspace_id ?? undefined, slug: row.slug, directory: row.directory, title: row.title, version: row.version,
245
+ time: { created: row.time_created, updated: row.time_updated,
246
+ compacting: row.time_compacting ?? undefined, archived: row.time_archived ?? undefined },
247
+ };
248
+ if (row.share_url != null) {
249
+ check(typeof row.share_url === "string", "invalid session share metadata");
250
+ info.share = { url: row.share_url };
251
+ }
252
+ const summary: Row = {};
253
+ for (const field of ["additions", "deletions", "files"]) {
254
+ if (row[`summary_${field}`] != null) summary[field] = row[`summary_${field}`];
255
+ }
256
+ if (row.summary_diffs != null) {
257
+ summary.diffs = parseJson(row.summary_diffs);
258
+ check(Array.isArray(summary.diffs), "invalid session summary");
259
+ }
260
+ if (Object.keys(summary).length) info.summary = summary;
261
+ for (const field of ["revert", "permission", "metadata", "model"]) {
262
+ if (row[field] == null) continue;
263
+ const value = parseJson(row[field]);
264
+ check(field === "permission" ? Array.isArray(value) : object(value), "invalid session metadata");
265
+ info[field] = value;
266
+ }
267
+ return info;
268
+ }
269
+
270
+ function transcriptResult(data: Export, maxBytes: number): NativeTranscript {
271
+ validateExport(data);
272
+ const transcript = JSON.stringify(data);
273
+ check(Buffer.byteLength(transcript) <= maxBytes, "session exceeds the byte limit");
274
+ const time = object(data.info.time) ? data.info.time : {};
275
+ const model = data.messages.map((m) => m.info.modelID).find((value) => typeof value === "string");
276
+ return {
277
+ transcript,
278
+ title: typeof data.info.title === "string" ? data.info.title : undefined,
279
+ cwd: typeof data.info.directory === "string" ? data.info.directory : undefined,
280
+ model: typeof model === "string" ? model : undefined,
281
+ startedAt: time.created == null ? undefined : timestamp(time.created),
282
+ endedAt: time.updated == null ? undefined : timestamp(time.updated),
283
+ };
284
+ }
285
+
286
+ export function readOpenCodeSession(item: DiscoveredSession, options: NativeReadOptions = {}): NativeTranscript {
287
+ const maxBytes = nativeByteLimit(options);
288
+ check(safeNativeId(item.id), "invalid session ID");
289
+ if (!item.native) {
290
+ const data = parseJson(readBoundedText(item.path, maxBytes));
291
+ validateExport(data);
292
+ check(data.info.id === item.id, "session ID mismatch");
293
+ return transcriptResult(data, maxBytes);
294
+ }
295
+ if (item.native?.kind === "opencode-storage") return readLegacy(item, maxBytes);
296
+ check(item.native?.kind === "opencode-sqlite", "unsupported source kind");
297
+ return withOpenCodeDb(item.path, (db) => {
298
+ const schema = checkSchema(db);
299
+ checkSqliteBudget(db, schema, item.id, maxBytes);
300
+ const row = db.query("SELECT * FROM session WHERE id = ?").get(item.id) as Row | null;
301
+ check(row, "session is missing");
302
+ const info = sessionInfo(row);
303
+ const messages: Message[] = [];
304
+ const byId = new Map<string, Message>();
305
+ for (const row of db.query("SELECT * FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").iterate(item.id) as Iterable<Row>) {
306
+ check(safeNativeId(row.id) && !byId.has(row.id), "invalid or duplicate message ID");
307
+ timestamp(row.time_created);
308
+ timestamp(row.time_updated);
309
+ const data = jsonObject(row.data);
310
+ check((data.id === undefined || data.id === row.id) &&
311
+ (data.sessionID === undefined || data.sessionID === item.id), "message ID mismatch");
312
+ check(data.role === "user" || data.role === "assistant", "invalid message role");
313
+ const message = { info: { ...data, id: row.id, sessionID: item.id,
314
+ time: data.time ?? { created: row.time_created } }, parts: [] as Row[] };
315
+ messages.push(message);
316
+ byId.set(row.id, message);
317
+ }
318
+ for (const row of db.query(`SELECT * FROM part WHERE ${PART_SELECTION} ORDER BY id ASC`).iterate(item.id, item.id) as Iterable<Row>) {
319
+ check(safeNativeId(row.id) && safeNativeId(row.message_id), "invalid part ID");
320
+ check(row.session_id === item.id, "part session ID mismatch");
321
+ timestamp(row.time_created);
322
+ timestamp(row.time_updated);
323
+ const message = byId.get(row.message_id);
324
+ check(message, "part references a missing message");
325
+ const data = jsonObject(row.data);
326
+ check((data.id === undefined || data.id === row.id) &&
327
+ (data.messageID === undefined || data.messageID === row.message_id) &&
328
+ (data.sessionID === undefined || data.sessionID === item.id), "part ID mismatch");
329
+ check(typeof data.type === "string" && data.type.length > 0, "invalid part type");
330
+ message.parts.push({ ...data, id: row.id, messageID: row.message_id, sessionID: item.id });
331
+ }
332
+ return transcriptResult({ info, messages }, maxBytes);
333
+ });
334
+ }
335
+
336
+ export function openCodeSessionFiles(item: DiscoveredSession): string[] {
337
+ if (item.native?.kind !== "opencode-storage") return [item.path];
338
+ const { storage, messageDir } = legacyLayout(item);
339
+ const files = [item.path, dirname(item.path), join(storage, "session"), storage,
340
+ join(storage, "message"), join(storage, "part"), messageDir];
341
+ for (const file of jsonFiles(messageDir, true)) {
342
+ files.push(file);
343
+ const id = basename(file, ".json");
344
+ check(safeNativeId(id), "invalid message filename");
345
+ const partDir = join(storage, "part", id);
346
+ files.push(partDir);
347
+ for (const part of jsonFiles(partDir, true)) files.push(part);
348
+ check(files.length <= MAX_NATIVE_RECORDS, "session exceeds the artifact limit");
349
+ }
350
+ return [...new Set(files)].sort();
351
+ }
352
+
353
+ function jsonFiles(directory: string, allowMissing = false): string[] {
354
+ try {
355
+ check(lstatSync(directory).isDirectory(), "expected artifact directory");
356
+ return readdirSync(directory, { withFileTypes: true }).filter((entry) => {
357
+ if (!entry.name.endsWith(".json")) return false;
358
+ check(entry.isFile() && safeNativeId(basename(entry.name, ".json")), "invalid artifact filename or type");
359
+ return true;
360
+ }).map((entry) => join(directory, entry.name)).sort();
361
+ } catch (error) {
362
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
363
+ if (allowMissing) return [];
364
+ throw new NativeInputError("OpenCode: required artifact directory is missing.");
365
+ }
366
+ throw error;
367
+ }
368
+ }
369
+
370
+ function legacyLayout(item: DiscoveredSession): { storage: string; messageDir: string } {
371
+ check(item.native?.kind === "opencode-storage" && safeNativeId(item.id), "invalid legacy session");
372
+ const storage = resolve(item.native.root);
373
+ check(dirname(dirname(resolve(item.path))) === join(storage, "session") &&
374
+ basename(item.path) === `${item.id}.json`, "session filename does not match its native ID");
375
+ for (const path of [storage, join(storage, "session"), dirname(item.path), join(storage, "message"), join(storage, "part")]) {
376
+ try { check(lstatSync(path).isDirectory(), "invalid artifact directory or symlink"); }
377
+ catch (error) {
378
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
379
+ }
380
+ }
381
+ return { storage, messageDir: join(storage, "message", item.id) };
382
+ }
383
+
384
+ function readLegacy(item: DiscoveredSession, maxBytes: number): NativeTranscript {
385
+ const { storage, messageDir } = legacyLayout(item);
386
+ let remaining = maxBytes;
387
+ let records = 0;
388
+ function read(path: string): Row {
389
+ check(++records <= MAX_NATIVE_RECORDS, "session exceeds the record limit");
390
+ let stat;
391
+ try { stat = lstatSync(path); }
392
+ catch (error) {
393
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new NativeInputError("OpenCode: required artifact file is missing.");
394
+ throw error;
395
+ }
396
+ check(stat.isFile(), "expected artifact file");
397
+ const text = readBoundedText(path, remaining);
398
+ remaining -= Math.max(stat.size, Buffer.byteLength(text));
399
+ check(remaining >= 0, "session exceeds the byte limit");
400
+ return jsonObject(text);
401
+ }
402
+ const info = read(item.path);
403
+ check(info.id === item.id, "session ID mismatch");
404
+ const messages: Message[] = [];
405
+ for (const file of jsonFiles(messageDir)) {
406
+ const info = read(file);
407
+ check(safeNativeId(info.id) && basename(file) === `${info.id}.json` && info.sessionID === item.id, "message ID mismatch");
408
+ check(info.role === "user" || info.role === "assistant", "invalid message role");
409
+ const parts = jsonFiles(join(storage, "part", info.id)).map((path) => {
410
+ const part = read(path);
411
+ check(safeNativeId(part.id) && basename(path) === `${part.id}.json` &&
412
+ part.sessionID === item.id && part.messageID === info.id, "part ID mismatch");
413
+ check(typeof part.type === "string" && PART_TYPES.has(part.type), "unsupported part type");
414
+ return part;
415
+ });
416
+ messages.push({ info, parts });
417
+ }
418
+ messages.sort((a, b) => {
419
+ const aTime = object(a.info.time) ? a.info.time.created : undefined;
420
+ const bTime = object(b.info.time) ? b.info.time.created : undefined;
421
+ return timestamp(aTime).localeCompare(timestamp(bTime)) || String(a.info.id).localeCompare(String(b.info.id));
422
+ });
423
+ return transcriptResult({ info, messages }, maxBytes);
424
+ }
425
+
426
+ function validateExport(value: unknown): asserts value is Export {
427
+ check(object(value) && object(value.info) && safeNativeId(value.info.id) && Array.isArray(value.messages), "incomplete export");
428
+ const session = value.info;
429
+ check(session.parentID == null || safeNativeId(session.parentID), "invalid parent ID");
430
+ if (session.time !== undefined) {
431
+ check(object(session.time), "invalid session time");
432
+ timestamp(session.time.created);
433
+ timestamp(session.time.updated);
434
+ }
435
+ const messages = new Set<string>();
436
+ const parts = new Set<string>();
437
+ let records = 1;
438
+ for (const message of value.messages) {
439
+ check(++records <= MAX_NATIVE_RECORDS, "session exceeds the record limit");
440
+ check(object(message) && object(message.info) && Array.isArray(message.parts), "incomplete message");
441
+ // OpenCode persists message info before its streamed parts; an empty array is valid.
442
+ const info = message.info;
443
+ check(safeNativeId(info.id) && !messages.has(info.id), "invalid or duplicate message ID");
444
+ messages.add(info.id);
445
+ check(info.sessionID === undefined || info.sessionID === session.id, "message session ID mismatch");
446
+ check(info.role === "user" || info.role === "assistant", "invalid message role");
447
+ if (info.time !== undefined) {
448
+ check(object(info.time), "invalid message time");
449
+ timestamp(info.time.created);
450
+ if (info.time.completed !== undefined) timestamp(info.time.completed);
451
+ }
452
+ for (const part of message.parts) {
453
+ check(++records <= MAX_NATIVE_RECORDS, "session exceeds the record limit");
454
+ check(object(part) && safeNativeId(part.id) && !parts.has(part.id), "invalid or duplicate part ID");
455
+ parts.add(part.id);
456
+ check((part.sessionID === undefined || part.sessionID === session.id) &&
457
+ (part.messageID === undefined || part.messageID === info.id), "part ID mismatch");
458
+ check(typeof part.type === "string" && PART_TYPES.has(part.type), "unsupported part type");
459
+ if (part.type === "text" || part.type === "reasoning") check(typeof part.text === "string", "missing part text");
460
+ if (part.type === "tool") {
461
+ check(typeof part.callID === "string" && part.callID.trim().length > 0 &&
462
+ typeof part.tool === "string" && part.tool.trim().length > 0 && object(part.state), "incomplete tool part");
463
+ check(["pending", "running", "completed", "error"].includes(String(part.state.status)) &&
464
+ object(part.state.input), "invalid tool state");
465
+ if (part.state.status === "completed") check(typeof part.state.output === "string", "missing tool output");
466
+ if (part.state.status === "error") check(typeof part.state.error === "string", "missing tool error");
467
+ }
468
+ }
469
+ }
470
+ }