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.
- package/README.md +4 -1
- package/dist/adapters/opencode/auto-capture-summary.js +50 -8
- package/dist/adapters/opencode/import-command.d.ts +3 -7
- package/dist/adapters/opencode/import-command.js +3 -54
- package/dist/adapters/pi/extension.js +3 -0
- package/dist/adapters/pi/profile.js +2 -1
- package/dist/adapters/pi/provider.js +41 -14
- package/dist/config.d.ts +18 -1
- package/dist/config.js +103 -9
- package/dist/core/capture-context.js +1 -1
- package/dist/core/capture.d.ts +4 -0
- package/dist/core/capture.js +30 -0
- package/dist/core/extraction.d.ts +23 -1
- package/dist/core/extraction.js +41 -0
- package/dist/core/host.d.ts +23 -0
- package/dist/core/host.js +9 -1
- package/dist/core/profile-analysis.js +9 -1
- package/dist/importer/discovery.d.ts +32 -5
- package/dist/importer/discovery.js +88 -36
- package/dist/importer/import-args.d.ts +23 -1
- package/dist/importer/import-args.js +29 -1
- package/dist/importer/import-project.d.ts +16 -0
- package/dist/importer/import-project.js +35 -0
- package/dist/importer/import-readiness.d.ts +47 -0
- package/dist/importer/import-readiness.js +54 -0
- package/dist/importer/import-sessions.d.ts +98 -0
- package/dist/importer/import-sessions.js +222 -0
- package/dist/importer/import-sources.d.ts +47 -0
- package/dist/importer/import-sources.js +147 -0
- package/dist/importer/importer.d.ts +31 -1
- package/dist/importer/importer.js +82 -24
- package/dist/importer/opencode-import.d.ts +11 -1
- package/dist/importer/opencode-import.js +133 -69
- package/dist/importer/opencode-project.d.ts +1 -4
- package/dist/importer/opencode-project.js +3 -36
- package/dist/importer/opencode-reader.d.ts +25 -5
- package/dist/importer/opencode-reader.js +92 -106
- package/dist/importer/opencode-snapshot.d.ts +82 -0
- package/dist/importer/opencode-snapshot.js +276 -0
- package/dist/importer/profile-import.d.ts +1 -0
- package/dist/importer/profile-import.js +7 -1
- package/dist/importer/run-import.d.ts +10 -0
- package/dist/importer/run-import.js +21 -0
- package/dist/importer/settings-health.d.ts +25 -0
- package/dist/importer/settings-health.js +136 -0
- package/dist/importer/web-import-api.d.ts +8 -0
- package/dist/importer/web-import-api.js +8 -0
- package/dist/importer/web-import-jobs.d.ts +53 -0
- package/dist/importer/web-import-jobs.js +227 -0
- package/dist/index.js +4 -0
- package/dist/services/ai/live-model-choice.js +6 -3
- package/dist/services/ai/opencode-import-models.d.ts +11 -0
- package/dist/services/ai/opencode-import-models.js +55 -0
- package/dist/services/ai/opencode-provider.d.ts +9 -0
- package/dist/services/ai/opencode-provider.js +31 -4
- package/dist/services/ai/providers/anthropic-messages.js +14 -6
- package/dist/services/ai/providers/base-provider.d.ts +8 -0
- package/dist/services/ai/providers/base-provider.js +12 -0
- package/dist/services/ai/providers/google-gemini.js +21 -7
- package/dist/services/ai/providers/openai-chat-completion.js +20 -7
- package/dist/services/ai/providers/openai-responses.js +11 -0
- package/dist/services/capture-attempt-store.d.ts +21 -0
- package/dist/services/capture-attempt-store.js +108 -0
- package/dist/services/capture-diagnostics.d.ts +66 -0
- package/dist/services/capture-diagnostics.js +175 -0
- package/dist/services/cleanup-service.js +9 -0
- package/dist/services/global-config-writer.d.ts +10 -0
- package/dist/services/global-config-writer.js +106 -0
- package/dist/services/log-path.d.ts +2 -0
- package/dist/services/log-path.js +15 -0
- package/dist/services/logger.js +1 -12
- package/dist/services/settings-log.d.ts +5 -0
- package/dist/services/settings-log.js +36 -0
- package/dist/services/settings-models.d.ts +35 -0
- package/dist/services/settings-models.js +39 -0
- package/dist/services/settings-snapshot.d.ts +24 -0
- package/dist/services/settings-snapshot.js +71 -0
- package/dist/services/settings-traces.d.ts +7 -0
- package/dist/services/settings-traces.js +39 -0
- package/dist/services/user-memory-learning.js +2 -1
- package/dist/services/web-server.d.ts +5 -0
- package/dist/services/web-server.js +176 -3
- package/dist/v2/legacy-client.js +19 -2
- package/dist/web/assets/index-BMkPht7g.js +124 -0
- package/dist/web/assets/index-D5ak5KsI.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/web/assets/index-DPjEpD1h.css +0 -2
- 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
|
|
95
|
-
for await (const session of source) {
|
|
120
|
+
const sessionCandidates = (session) => {
|
|
96
121
|
const { hash } = extractScopeFromContainerTag(getTags(session.directory).project.tag);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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 {
|
|
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 {
|
|
7
|
-
import {
|
|
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 ??
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export
|
|
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
|
|
10
|
+
export interface OpencodeHistory {
|
|
8
11
|
childSessions: number;
|
|
9
12
|
topLevelSessions: number;
|
|
10
|
-
sessions
|
|
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
|
|
39
|
+
export declare function openOpencodeHistory(dbPath: string, filters?: OpencodeReaderFilters, options?: OpencodeOpenOptions): Promise<OpencodeHistory>;
|