om-memory-system 3.2.0-next.8 → 3.3.0-next.17

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 (89) hide show
  1. package/README.md +4 -1
  2. package/dist/adapters/opencode/auto-capture-summary.js +50 -8
  3. package/dist/adapters/opencode/import-command.d.ts +3 -7
  4. package/dist/adapters/opencode/import-command.js +3 -54
  5. package/dist/adapters/pi/extension.js +3 -0
  6. package/dist/adapters/pi/profile.js +2 -1
  7. package/dist/adapters/pi/provider.js +41 -14
  8. package/dist/config.d.ts +18 -1
  9. package/dist/config.js +103 -9
  10. package/dist/core/capture-context.js +1 -1
  11. package/dist/core/capture.d.ts +4 -0
  12. package/dist/core/capture.js +30 -0
  13. package/dist/core/extraction.d.ts +23 -1
  14. package/dist/core/extraction.js +41 -0
  15. package/dist/core/host.d.ts +23 -0
  16. package/dist/core/host.js +9 -1
  17. package/dist/core/profile-analysis.js +9 -1
  18. package/dist/importer/discovery.d.ts +32 -5
  19. package/dist/importer/discovery.js +88 -36
  20. package/dist/importer/import-args.d.ts +23 -1
  21. package/dist/importer/import-args.js +29 -1
  22. package/dist/importer/import-project.d.ts +16 -0
  23. package/dist/importer/import-project.js +35 -0
  24. package/dist/importer/import-readiness.d.ts +47 -0
  25. package/dist/importer/import-readiness.js +54 -0
  26. package/dist/importer/import-sessions.d.ts +98 -0
  27. package/dist/importer/import-sessions.js +222 -0
  28. package/dist/importer/import-sources.d.ts +47 -0
  29. package/dist/importer/import-sources.js +147 -0
  30. package/dist/importer/importer.d.ts +31 -1
  31. package/dist/importer/importer.js +82 -24
  32. package/dist/importer/opencode-import.d.ts +11 -1
  33. package/dist/importer/opencode-import.js +133 -69
  34. package/dist/importer/opencode-project.d.ts +1 -4
  35. package/dist/importer/opencode-project.js +3 -36
  36. package/dist/importer/opencode-reader.d.ts +25 -5
  37. package/dist/importer/opencode-reader.js +92 -106
  38. package/dist/importer/opencode-snapshot.d.ts +82 -0
  39. package/dist/importer/opencode-snapshot.js +276 -0
  40. package/dist/importer/profile-import.d.ts +1 -0
  41. package/dist/importer/profile-import.js +7 -1
  42. package/dist/importer/run-import.d.ts +10 -0
  43. package/dist/importer/run-import.js +21 -0
  44. package/dist/importer/settings-health.d.ts +25 -0
  45. package/dist/importer/settings-health.js +136 -0
  46. package/dist/importer/web-import-api.d.ts +8 -0
  47. package/dist/importer/web-import-api.js +8 -0
  48. package/dist/importer/web-import-jobs.d.ts +53 -0
  49. package/dist/importer/web-import-jobs.js +227 -0
  50. package/dist/index.js +4 -0
  51. package/dist/services/ai/live-model-choice.js +6 -3
  52. package/dist/services/ai/opencode-import-models.d.ts +11 -0
  53. package/dist/services/ai/opencode-import-models.js +55 -0
  54. package/dist/services/ai/opencode-provider.d.ts +9 -0
  55. package/dist/services/ai/opencode-provider.js +31 -4
  56. package/dist/services/ai/providers/anthropic-messages.js +14 -6
  57. package/dist/services/ai/providers/base-provider.d.ts +8 -0
  58. package/dist/services/ai/providers/base-provider.js +12 -0
  59. package/dist/services/ai/providers/google-gemini.js +21 -7
  60. package/dist/services/ai/providers/openai-chat-completion.js +20 -7
  61. package/dist/services/ai/providers/openai-responses.js +11 -0
  62. package/dist/services/capture-attempt-store.d.ts +21 -0
  63. package/dist/services/capture-attempt-store.js +108 -0
  64. package/dist/services/capture-diagnostics.d.ts +66 -0
  65. package/dist/services/capture-diagnostics.js +175 -0
  66. package/dist/services/cleanup-service.js +9 -0
  67. package/dist/services/global-config-writer.d.ts +10 -0
  68. package/dist/services/global-config-writer.js +106 -0
  69. package/dist/services/log-path.d.ts +2 -0
  70. package/dist/services/log-path.js +15 -0
  71. package/dist/services/logger.js +1 -12
  72. package/dist/services/settings-log.d.ts +5 -0
  73. package/dist/services/settings-log.js +36 -0
  74. package/dist/services/settings-models.d.ts +35 -0
  75. package/dist/services/settings-models.js +39 -0
  76. package/dist/services/settings-snapshot.d.ts +24 -0
  77. package/dist/services/settings-snapshot.js +71 -0
  78. package/dist/services/settings-traces.d.ts +7 -0
  79. package/dist/services/settings-traces.js +39 -0
  80. package/dist/services/user-memory-learning.js +2 -1
  81. package/dist/services/web-server.d.ts +5 -0
  82. package/dist/services/web-server.js +176 -3
  83. package/dist/v2/legacy-client.js +19 -2
  84. package/dist/web/assets/index-BMkPht7g.js +124 -0
  85. package/dist/web/assets/index-D5ak5KsI.css +2 -0
  86. package/dist/web/index.html +2 -2
  87. package/package.json +2 -1
  88. package/dist/web/assets/index-DPjEpD1h.css +0 -2
  89. package/dist/web/assets/index-do26c4JJ.js +0 -122
@@ -7,6 +7,7 @@ import { tursoConnectionManager } from "../services/turso/connection-manager.js"
7
7
  import { tursoShardManager } from "../services/turso/shard-manager.js";
8
8
  import { extractPiConversationWindows, } from "../adapters/pi/conversation.js";
9
9
  import { discoverPiSessions } from "./discovery.js";
10
+ import { resolveImportProject } from "./import-project.js";
10
11
  import { PiImportLedger, importLedgerDbPath } from "./ledger.js";
11
12
  export function buildImportKey(sessionId, window, host = "pi") {
12
13
  const terminalEntryId = window.sourceEntryIds?.[window.sourceEntryIds.length - 1];
@@ -46,13 +47,6 @@ export function projectFilterTag(directory) {
46
47
  }
47
48
  return getTags(real).project.tag;
48
49
  }
49
- function resolveSessionDirectory(cwd, pathMaps) {
50
- if (!cwd)
51
- return null;
52
- const mapped = pathMaps.find((map) => map.from === cwd)?.to;
53
- const directory = mapped ?? cwd;
54
- return existsSync(directory) ? directory : null;
55
- }
56
50
  function preview(text) {
57
51
  const trimmed = text.trim().replace(/\s+/g, " ");
58
52
  return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
@@ -66,6 +60,38 @@ function windowWithinDateRange(window, filters) {
66
60
  return false;
67
61
  return true;
68
62
  }
63
+ /**
64
+ * Keep the windows a run would import: inside the date range and not newer
65
+ * than the web listing cutoff. Held-back windows are only counted; they never
66
+ * reach the ledger, so a later run imports them under the same keys.
67
+ */
68
+ export function selectImportWindows(windows, filters) {
69
+ const kept = [];
70
+ let heldBack = 0;
71
+ let untimed = 0;
72
+ for (const window of windows) {
73
+ if (!windowWithinDateRange(window, filters))
74
+ continue;
75
+ if (filters.cutoff !== undefined &&
76
+ window.userTimestamp !== undefined &&
77
+ window.userTimestamp > filters.cutoff) {
78
+ heldBack++;
79
+ continue;
80
+ }
81
+ if (window.userTimestamp === undefined)
82
+ untimed++;
83
+ kept.push(window);
84
+ }
85
+ return { kept, heldBack, untimed };
86
+ }
87
+ /** Units of one session that `importHistorySource` would consider, for a counting pass. */
88
+ export function countImportableUnits(session, host, filters) {
89
+ return session.units.filter((window) => windowWithinDateRange(window, filters) &&
90
+ buildImportKey(session.sessionId, window, host) !== null).length;
91
+ }
92
+ function isLazySource(source) {
93
+ return typeof source.open === "function";
94
+ }
69
95
  async function lookupExistingMemory(projectHash, key, options) {
70
96
  if (!options.storageAccessible)
71
97
  return null;
@@ -91,21 +117,34 @@ export async function importHistorySource(source, host, deps, filters, report) {
91
117
  throw new Error(`Memory system is not ready for import: ${embeddingInitError}. Start the extension and retry.`);
92
118
  }
93
119
  }
94
- const candidates = [];
95
- for await (const session of source) {
120
+ const sessionCandidates = (session) => {
96
121
  const { hash } = extractScopeFromContainerTag(getTags(session.directory).project.tag);
97
- for (const window of session.units) {
98
- if (!windowWithinDateRange(window, filters))
99
- continue;
100
- if (!buildImportKey(session.sessionId, window, host))
101
- continue;
102
- candidates.push({ ...session, hash, window });
103
- }
122
+ return session.units
123
+ .filter((window) => windowWithinDateRange(window, filters) &&
124
+ buildImportKey(session.sessionId, window, host) !== null)
125
+ .map((window) => ({ ...session, hash, window }));
126
+ };
127
+ // A lazy source is read session by session; an eager one is buffered as before.
128
+ let candidates;
129
+ if (isLazySource(source)) {
130
+ report.unitsTotal = source.total;
131
+ candidates = (async function* () {
132
+ for await (const session of source.open())
133
+ yield* sessionCandidates(session);
134
+ })();
135
+ }
136
+ else {
137
+ const all = [];
138
+ for await (const session of source)
139
+ all.push(...sessionCandidates(session));
140
+ report.unitsTotal = all.length;
141
+ candidates = all;
104
142
  }
105
- report.unitsTotal = candidates.length;
106
143
  const storageAccessible = ledgerFileExists;
107
144
  let processed = 0;
108
- for (const candidate of candidates) {
145
+ for await (const candidate of candidates) {
146
+ if (deps.signal?.aborted)
147
+ break;
109
148
  const { sessionId, directory, sourceFile, hash, window } = candidate;
110
149
  const key = buildImportKey(sessionId, window, host);
111
150
  processed++;
@@ -223,10 +262,19 @@ export async function importPiHistory(deps, filters) {
223
262
  skipReasons: {},
224
263
  projects: [],
225
264
  units: [],
265
+ unitsHeldBack: 0,
266
+ unitsUntimed: 0,
226
267
  };
268
+ const selected = filters.selectionKeys ? new Set(filters.selectionKeys) : null;
227
269
  const projectAggregates = new Map();
228
270
  const candidates = [];
229
271
  for (const discovered of discovery.sessions) {
272
+ if (deps.signal?.aborted)
273
+ break;
274
+ if (selected && !selected.has(discovered.key)) {
275
+ report.sessionsFilteredOut++;
276
+ continue;
277
+ }
230
278
  if (filters.session) {
231
279
  const wanted = filters.session;
232
280
  if (discovered.sessionId !== wanted && discovered.file !== wanted) {
@@ -245,7 +293,16 @@ export async function importPiHistory(deps, filters) {
245
293
  });
246
294
  continue;
247
295
  }
248
- const directory = resolveSessionDirectory(session.cwd, pathMaps);
296
+ // The header ID names the session in the page's list; a file that loads as
297
+ // another session would be imported under keys the user never saw.
298
+ if (discovered.sessionId && discovered.sessionId !== session.sessionId) {
299
+ report.loadErrors.push({
300
+ file: discovered.file,
301
+ error: "Loaded session ID does not match the file header",
302
+ });
303
+ continue;
304
+ }
305
+ const directory = resolveImportProject(session.cwd, pathMaps).directory;
249
306
  if (!directory) {
250
307
  report.unresolvableSessions.push({ file: session.sourceFile, cwd: session.cwd });
251
308
  continue;
@@ -269,10 +326,10 @@ export async function importPiHistory(deps, filters) {
269
326
  projectAggregates.set(tags.project.tag, aggregate);
270
327
  }
271
328
  aggregate.sessions.add(session.sessionId);
272
- const windows = extractPiConversationWindows(session.branch);
273
- for (const window of windows) {
274
- if (!windowWithinDateRange(window, filters))
275
- continue;
329
+ const windows = selectImportWindows(extractPiConversationWindows(session.branch), filters);
330
+ report.unitsHeldBack += windows.heldBack;
331
+ report.unitsUntimed += windows.untimed;
332
+ for (const window of windows.kept) {
276
333
  const key = buildImportKey(session.sessionId, window);
277
334
  if (!key)
278
335
  continue;
@@ -311,10 +368,11 @@ export async function importPiHistory(deps, filters) {
311
368
  sessions: aggregate.sessions.size,
312
369
  units: aggregate.units,
313
370
  }));
314
- if (deps.profile) {
371
+ if (deps.profile && !deps.signal?.aborted) {
315
372
  const { importProfileFromHistory } = await import("./profile-import.js");
316
373
  report.profile = await importProfileFromHistory(sourceSessions, {
317
374
  host: "pi",
375
+ signal: deps.signal,
318
376
  dryRun,
319
377
  model: deps.profile.model,
320
378
  batchSize: deps.profile.batchSize,
@@ -1,14 +1,22 @@
1
1
  import type { CaptureSummaryProvider } from "../core/host.js";
2
2
  import type { ModelPort } from "../core/profile-analysis.js";
3
3
  import { type ImportPathMap, type ImportReport } from "./importer.js";
4
- import { type UnresolvedProject } from "./opencode-project.js";
4
+ import type { UnresolvedProject } from "./opencode-project.js";
5
+ import { DEFAULT_OPENCODE_DB, type OpencodeOpenOptions } from "./opencode-reader.js";
5
6
  import { type ProfileImportReport } from "./profile-import.js";
7
+ export { DEFAULT_OPENCODE_DB };
6
8
  export interface OpencodeImportOptions {
7
9
  dbPath?: string;
8
10
  dryRun?: boolean;
9
11
  since?: number;
10
12
  until?: number;
11
13
  session?: string;
14
+ /** Web selection by session ID; never combined with `session` or `maxSessions`. */
15
+ selectionKeys?: string[];
16
+ /** Web selection: user turns after this epoch ms are held back for a later run. */
17
+ cutoff?: number;
18
+ /** Web jobs share the listing's snapshot of a live database. */
19
+ snapshot?: OpencodeOpenOptions["shared"];
12
20
  project?: string;
13
21
  maxSessions?: number;
14
22
  pathMaps?: ImportPathMap[];
@@ -18,6 +26,8 @@ export interface OpencodeImportOptions {
18
26
  profileBatch?: number;
19
27
  provider?: CaptureSummaryProvider;
20
28
  profileModel?: ModelPort;
29
+ onProgress?: (processed: number, total: number, promptPreview: string) => void;
30
+ signal?: AbortSignal;
21
31
  }
22
32
  export interface OpencodeImportReport extends ImportReport {
23
33
  childSessionsFolded: number;
@@ -1,11 +1,12 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
1
  import { getTags } from "../services/tags.js";
4
2
  import { extractScopeFromContainerTag } from "../services/memory-scope.js";
5
- import { importHistorySource, projectFilterTag, } from "./importer.js";
6
- import { resolveOpencodeSessions } from "./opencode-project.js";
7
- import { readOpencodeHistory } from "./opencode-reader.js";
3
+ import { countImportableUnits, importHistorySource, projectFilterTag, selectImportWindows, } from "./importer.js";
4
+ import { resolveImportProject } from "./import-project.js";
5
+ import { DEFAULT_OPENCODE_DB, openOpencodeHistory, } from "./opencode-reader.js";
8
6
  import { importProfileFromHistory } from "./profile-import.js";
7
+ export { DEFAULT_OPENCODE_DB };
8
+ /** Let the web server and live capture run between sessions of a long read. */
9
+ const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
9
10
  /** Import resolved OpenCode history through the shared memory and profile steps. */
10
11
  export async function importOpencodeHistory(options) {
11
12
  if (!options.dryRun && !options.skipMemories && !options.provider) {
@@ -14,74 +15,137 @@ export async function importOpencodeHistory(options) {
14
15
  if (!options.dryRun && !options.skipProfile && !options.profileModel) {
15
16
  throw new Error("OpenCode history import needs a profile model");
16
17
  }
17
- const dbPath = options.dbPath ?? join(homedir(), ".local/share/opencode/opencode.db");
18
- // Resolve the filter before opening the reader: the reader only closes once iterated.
18
+ const dbPath = options.dbPath ?? DEFAULT_OPENCODE_DB;
19
19
  const projectTag = options.project ? projectFilterTag(options.project) : null;
20
- const reader = readOpencodeHistory(dbPath, options);
21
- const unresolved = new Map();
22
- const sessions = [];
23
- const projects = new Map();
24
- let filtered = 0;
25
- for await (const session of resolveOpencodeSessions(reader.sessions, options.pathMaps ?? [], unresolved)) {
26
- const info = getTags(session.directory).project;
27
- if (projectTag && projectTag !== projectFilterTag(session.directory)) {
28
- filtered++;
29
- continue;
20
+ const history = await openOpencodeHistory(dbPath, {
21
+ ...(options.since !== undefined ? { since: options.since } : {}),
22
+ ...(options.until !== undefined ? { until: options.until } : {}),
23
+ ...(options.session ? { session: options.session } : {}),
24
+ ...(options.selectionKeys ? { sessionIds: options.selectionKeys } : {}),
25
+ ...(options.maxSessions ? { maxSessions: options.maxSessions } : {}),
26
+ }, {
27
+ ...(options.snapshot ? { shared: options.snapshot } : {}),
28
+ ...(options.signal ? { signal: options.signal } : {}),
29
+ });
30
+ try {
31
+ const unresolved = new Map();
32
+ const projects = new Map();
33
+ const selected = [];
34
+ let filtered = 0;
35
+ for (const session of history.sessions) {
36
+ const { directory } = resolveImportProject(session.recordedDirectory, options.pathMaps ?? [], session.projectWorktree);
37
+ if (!directory) {
38
+ const entry = unresolved.get(session.recordedDirectory) ?? {
39
+ directory: session.recordedDirectory,
40
+ sessions: 0,
41
+ units: 0,
42
+ };
43
+ entry.sessions++;
44
+ entry.units += history.loadUnits(session.sessionId).length;
45
+ unresolved.set(entry.directory, entry);
46
+ continue;
47
+ }
48
+ if (projectTag && projectTag !== projectFilterTag(directory)) {
49
+ filtered++;
50
+ continue;
51
+ }
52
+ selected.push({ ...session, directory });
30
53
  }
31
- const project = projects.get(info.tag) ?? {
32
- tag: info.tag,
33
- hash: extractScopeFromContainerTag(info.tag).hash,
34
- directory: session.directory,
35
- sessions: 0,
36
- units: 0,
37
- sessionIds: new Set(),
54
+ const build = (session) => {
55
+ const windows = selectImportWindows(history.loadUnits(session.sessionId), options);
56
+ const source = {
57
+ sessionId: session.sessionId,
58
+ directory: session.directory,
59
+ sourceFile: dbPath,
60
+ units: windows.kept,
61
+ };
62
+ return { source, windows };
38
63
  };
39
- project.sessionIds.add(session.sessionId);
40
- project.sessions = project.sessionIds.size;
41
- project.units += session.units.length;
42
- projects.set(info.tag, project);
43
- sessions.push(session);
44
- }
45
- const report = {
46
- dryRun: Boolean(options.dryRun),
47
- root: dbPath,
48
- sessionsDiscovered: reader.topLevelSessions,
49
- sessionsLoaded: sessions.length,
50
- sessionsFilteredOut: filtered,
51
- sessionsUnrecognized: 0,
52
- unresolvableSessions: [],
53
- loadErrors: [],
54
- unitsTotal: 0,
55
- unitsImported: 0,
56
- unitsWouldImport: 0,
57
- unitsSkipped: 0,
58
- unitsFailed: 0,
59
- unitsAlreadyHandled: 0,
60
- skipReasons: {},
61
- units: [],
62
- projects: [...projects.values()].map(({ sessionIds: _ids, ...project }) => project),
63
- childSessionsFolded: reader.childSessions,
64
- unresolvedProjects: [...unresolved.values()],
65
- };
66
- if (!options.skipMemories) {
67
- await importHistorySource(sessions, "opencode", {
68
- provider: options.provider ?? {
69
- summarize: async () => {
70
- throw new Error("A dry-run cannot call the capture model");
64
+ // Counting pass: totals for progress and the report, one session in memory at a time.
65
+ let total = 0;
66
+ let kept = 0;
67
+ let heldBack = 0;
68
+ let untimed = 0;
69
+ for (const session of selected) {
70
+ if (options.signal?.aborted)
71
+ break;
72
+ const { source, windows } = build(session);
73
+ const info = getTags(session.directory).project;
74
+ const project = projects.get(info.tag) ?? {
75
+ tag: info.tag,
76
+ hash: extractScopeFromContainerTag(info.tag).hash,
77
+ directory: session.directory,
78
+ sessions: 0,
79
+ units: 0,
80
+ sessionIds: new Set(),
81
+ };
82
+ project.sessionIds.add(session.sessionId);
83
+ project.sessions = project.sessionIds.size;
84
+ project.units += windows.kept.length;
85
+ projects.set(info.tag, project);
86
+ total += countImportableUnits(source, "opencode", options);
87
+ kept += windows.kept.length;
88
+ heldBack += windows.heldBack;
89
+ untimed += windows.untimed;
90
+ await yieldToEventLoop();
91
+ }
92
+ const open = () => (async function* () {
93
+ for (const session of selected) {
94
+ if (options.signal?.aborted)
95
+ return;
96
+ await yieldToEventLoop();
97
+ yield build(session).source;
98
+ }
99
+ })();
100
+ const report = {
101
+ dryRun: Boolean(options.dryRun),
102
+ root: dbPath,
103
+ sessionsDiscovered: history.topLevelSessions,
104
+ sessionsLoaded: selected.length,
105
+ sessionsFilteredOut: filtered,
106
+ sessionsUnrecognized: 0,
107
+ unresolvableSessions: [],
108
+ loadErrors: [],
109
+ unitsTotal: 0,
110
+ unitsImported: 0,
111
+ unitsWouldImport: 0,
112
+ unitsSkipped: 0,
113
+ unitsFailed: 0,
114
+ unitsAlreadyHandled: 0,
115
+ unitsHeldBack: heldBack,
116
+ unitsUntimed: untimed,
117
+ skipReasons: {},
118
+ units: [],
119
+ projects: [...projects.values()].map(({ sessionIds: _ids, ...project }) => project),
120
+ childSessionsFolded: history.childSessions,
121
+ unresolvedProjects: [...unresolved.values()],
122
+ };
123
+ if (!options.skipMemories) {
124
+ await importHistorySource({ total, open }, "opencode", {
125
+ provider: options.provider ?? {
126
+ summarize: async () => {
127
+ throw new Error("A dry-run cannot call the capture model");
128
+ },
71
129
  },
72
- },
73
- }, options, report);
74
- }
75
- else {
76
- report.unitsTotal = sessions.reduce((total, session) => total + session.units.length, 0);
130
+ ...(options.signal ? { signal: options.signal } : {}),
131
+ ...(options.onProgress ? { onProgress: options.onProgress } : {}),
132
+ }, options, report);
133
+ }
134
+ else {
135
+ report.unitsTotal = kept;
136
+ }
137
+ if (!options.skipProfile && !options.signal?.aborted) {
138
+ report.profile = await importProfileFromHistory(open(), {
139
+ host: "opencode",
140
+ signal: options.signal,
141
+ dryRun: options.dryRun,
142
+ batchSize: options.profileBatch,
143
+ model: options.profileModel,
144
+ });
145
+ }
146
+ return report;
77
147
  }
78
- if (!options.skipProfile) {
79
- report.profile = await importProfileFromHistory(sessions, {
80
- host: "opencode",
81
- dryRun: options.dryRun,
82
- batchSize: options.profileBatch,
83
- model: options.profileModel,
84
- });
148
+ finally {
149
+ await history.close();
85
150
  }
86
- return report;
87
151
  }
@@ -1,11 +1,8 @@
1
1
  import type { ImportPathMap } from "./importer.js";
2
- import type { OpencodeSourceSession } from "./opencode-reader.js";
3
- /** Prefer the recorded directory, then an explicit map, then the project root. */
2
+ /** A directory map first, then the recorded directory, then the project root. */
4
3
  export declare function resolveOpencodeProject(recordedDirectory: string, projectWorktree: string | null, maps?: ImportPathMap[]): string | null;
5
4
  export interface UnresolvedProject {
6
5
  directory: string;
7
6
  sessions: number;
8
7
  units: number;
9
8
  }
10
- /** Resolve each session without buffering its conversation windows. */
11
- export declare function resolveOpencodeSessions(sessions: AsyncIterable<OpencodeSourceSession>, maps: ImportPathMap[], unresolved: Map<string, UnresolvedProject>): AsyncGenerator<OpencodeSourceSession>;
@@ -1,38 +1,5 @@
1
- import { statSync } from "node:fs";
2
- function isDirectory(path) {
3
- if (!path)
4
- return false;
5
- try {
6
- return statSync(path).isDirectory();
7
- }
8
- catch {
9
- return false;
10
- }
11
- }
12
- /** Prefer the recorded directory, then an explicit map, then the project root. */
1
+ import { resolveImportProject } from "./import-project.js";
2
+ /** A directory map first, then the recorded directory, then the project root. */
13
3
  export function resolveOpencodeProject(recordedDirectory, projectWorktree, maps = []) {
14
- if (isDirectory(recordedDirectory))
15
- return recordedDirectory;
16
- const mapped = maps.find((item) => item.from === recordedDirectory)?.to;
17
- if (isDirectory(mapped))
18
- return mapped;
19
- return projectWorktree !== "/" && isDirectory(projectWorktree) ? projectWorktree : null;
20
- }
21
- /** Resolve each session without buffering its conversation windows. */
22
- export async function* resolveOpencodeSessions(sessions, maps, unresolved) {
23
- for await (const session of sessions) {
24
- const directory = resolveOpencodeProject(session.recordedDirectory, session.projectWorktree, maps);
25
- if (directory) {
26
- yield { ...session, directory };
27
- continue;
28
- }
29
- const entry = unresolved.get(session.recordedDirectory) ?? {
30
- directory: session.recordedDirectory,
31
- sessions: 0,
32
- units: 0,
33
- };
34
- entry.sessions++;
35
- entry.units += session.units.length;
36
- unresolved.set(entry.directory, entry);
37
- }
4
+ return resolveImportProject(recordedDirectory, maps, projectWorktree).directory;
38
5
  }
@@ -1,19 +1,39 @@
1
- import type { ImportSourceSession } from "./importer.js";
2
- export interface OpencodeSourceSession extends ImportSourceSession {
1
+ import type { ImportWindow } from "./importer.js";
2
+ export declare const DEFAULT_OPENCODE_DB: string;
3
+ /** One top-level session's metadata; its turns are built on demand. */
4
+ export interface OpencodeSessionMeta {
5
+ sessionId: string;
3
6
  recordedDirectory: string;
4
7
  projectWorktree: string | null;
5
8
  timeCreated: number;
6
9
  }
7
- export interface OpencodeReader {
10
+ export interface OpencodeHistory {
8
11
  childSessions: number;
9
12
  topLevelSessions: number;
10
- sessions: AsyncIterable<OpencodeSourceSession>;
13
+ /** Top-level sessions matching the filters, oldest first. */
14
+ sessions: OpencodeSessionMeta[];
15
+ /** Build one session's work units; call only before `close`. */
16
+ loadUnits: (sessionId: string) => ImportWindow[];
17
+ close: () => Promise<void>;
11
18
  }
12
19
  export interface OpencodeReaderFilters {
13
20
  session?: string;
21
+ /** Web selection; never combined with `session` or `maxSessions`. */
22
+ sessionIds?: string[];
14
23
  maxSessions?: number;
15
24
  since?: number;
16
25
  until?: number;
17
26
  }
27
+ export interface OpencodeOpenOptions {
28
+ /**
29
+ * Share the snapshot with the web listing and jobs under this source key.
30
+ * `fresh` copies again, `reuse` fails when no copy exists, `any` does either.
31
+ */
32
+ shared?: {
33
+ key: string;
34
+ mode: "fresh" | "reuse" | "any";
35
+ };
36
+ signal?: AbortSignal;
37
+ }
18
38
  /** Read V1 OpenCode history without changing its database or WAL sidecars. */
19
- export declare function readOpencodeHistory(dbPath: string, filters?: OpencodeReaderFilters): OpencodeReader;
39
+ export declare function openOpencodeHistory(dbPath: string, filters?: OpencodeReaderFilters, options?: OpencodeOpenOptions): Promise<OpencodeHistory>;