om-memory-system 3.2.0-next.9 → 3.3.0-next.18
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 +3 -5
- package/dist/adapters/opencode/auto-capture-summary.js +50 -8
- package/dist/adapters/opencode/backfill-models.d.ts +25 -0
- package/dist/adapters/opencode/backfill-models.js +39 -0
- package/dist/adapters/opencode/backfill-startup.d.ts +7 -0
- package/dist/adapters/opencode/backfill-startup.js +12 -0
- package/dist/adapters/opencode/import-command.d.ts +3 -7
- package/dist/adapters/opencode/import-command.js +6 -54
- package/dist/adapters/pi/backfill-models.d.ts +9 -0
- package/dist/adapters/pi/backfill-models.js +30 -0
- package/dist/adapters/pi/extension.js +53 -0
- package/dist/adapters/pi/import-command.js +3 -0
- package/dist/adapters/pi/profile.js +2 -1
- package/dist/adapters/pi/provider.js +41 -14
- package/dist/cli/index.js +13 -1
- package/dist/cli/web-command.d.ts +3 -0
- package/dist/cli/web-command.js +72 -0
- package/dist/config.d.ts +30 -1
- package/dist/config.js +131 -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/auto-backfill.d.ts +20 -0
- package/dist/importer/auto-backfill.js +165 -0
- package/dist/importer/backfill-lock.d.ts +3 -0
- package/dist/importer/backfill-lock.js +44 -0
- package/dist/importer/backfill-model.d.ts +10 -0
- package/dist/importer/backfill-model.js +13 -0
- 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 +96 -24
- package/dist/importer/manual-import-guard.d.ts +4 -0
- package/dist/importer/manual-import-guard.js +9 -0
- 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 +24 -0
- package/dist/importer/settings-health.js +127 -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 +34 -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/backfill-state.d.ts +25 -0
- package/dist/services/backfill-state.js +68 -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 +110 -0
- package/dist/services/live-captured-entries.d.ts +3 -0
- package/dist/services/live-captured-entries.js +34 -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/safe-health-error.d.ts +2 -0
- package/dist/services/safe-health-error.js +11 -0
- 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 +83 -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-autostart.d.ts +29 -0
- package/dist/services/web-autostart.js +208 -0
- package/dist/services/web-server.d.ts +5 -0
- package/dist/services/web-server.js +187 -3
- package/dist/v2/legacy-client.js +19 -2
- package/dist/web/assets/index-B-_gMSn9.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
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { captureConversation } from "../core/capture.js";
|
|
3
3
|
import { memoryClient } from "../services/client.js";
|
|
4
|
+
import { findLiveCapturedEntryIds } from "../services/live-captured-entries.js";
|
|
4
5
|
import { extractScopeFromContainerTag } from "../services/memory-scope.js";
|
|
5
6
|
import { getTags } from "../services/tags.js";
|
|
6
7
|
import { tursoConnectionManager } from "../services/turso/connection-manager.js";
|
|
7
8
|
import { tursoShardManager } from "../services/turso/shard-manager.js";
|
|
8
9
|
import { extractPiConversationWindows, } from "../adapters/pi/conversation.js";
|
|
9
10
|
import { discoverPiSessions } from "./discovery.js";
|
|
11
|
+
import { resolveImportProject } from "./import-project.js";
|
|
10
12
|
import { PiImportLedger, importLedgerDbPath } from "./ledger.js";
|
|
11
13
|
export function buildImportKey(sessionId, window, host = "pi") {
|
|
12
14
|
const terminalEntryId = window.sourceEntryIds?.[window.sourceEntryIds.length - 1];
|
|
@@ -46,13 +48,6 @@ export function projectFilterTag(directory) {
|
|
|
46
48
|
}
|
|
47
49
|
return getTags(real).project.tag;
|
|
48
50
|
}
|
|
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
51
|
function preview(text) {
|
|
57
52
|
const trimmed = text.trim().replace(/\s+/g, " ");
|
|
58
53
|
return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
|
|
@@ -66,6 +61,38 @@ function windowWithinDateRange(window, filters) {
|
|
|
66
61
|
return false;
|
|
67
62
|
return true;
|
|
68
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Keep the windows a run would import: inside the date range and not newer
|
|
66
|
+
* than the web listing cutoff. Held-back windows are only counted; they never
|
|
67
|
+
* reach the ledger, so a later run imports them under the same keys.
|
|
68
|
+
*/
|
|
69
|
+
export function selectImportWindows(windows, filters) {
|
|
70
|
+
const kept = [];
|
|
71
|
+
let heldBack = 0;
|
|
72
|
+
let untimed = 0;
|
|
73
|
+
for (const window of windows) {
|
|
74
|
+
if (!windowWithinDateRange(window, filters))
|
|
75
|
+
continue;
|
|
76
|
+
if (filters.cutoff !== undefined &&
|
|
77
|
+
window.userTimestamp !== undefined &&
|
|
78
|
+
window.userTimestamp > filters.cutoff) {
|
|
79
|
+
heldBack++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (window.userTimestamp === undefined)
|
|
83
|
+
untimed++;
|
|
84
|
+
kept.push(window);
|
|
85
|
+
}
|
|
86
|
+
return { kept, heldBack, untimed };
|
|
87
|
+
}
|
|
88
|
+
/** Units of one session that `importHistorySource` would consider, for a counting pass. */
|
|
89
|
+
export function countImportableUnits(session, host, filters) {
|
|
90
|
+
return session.units.filter((window) => windowWithinDateRange(window, filters) &&
|
|
91
|
+
buildImportKey(session.sessionId, window, host) !== null).length;
|
|
92
|
+
}
|
|
93
|
+
function isLazySource(source) {
|
|
94
|
+
return typeof source.open === "function";
|
|
95
|
+
}
|
|
69
96
|
async function lookupExistingMemory(projectHash, key, options) {
|
|
70
97
|
if (!options.storageAccessible)
|
|
71
98
|
return null;
|
|
@@ -91,21 +118,34 @@ export async function importHistorySource(source, host, deps, filters, report) {
|
|
|
91
118
|
throw new Error(`Memory system is not ready for import: ${embeddingInitError}. Start the extension and retry.`);
|
|
92
119
|
}
|
|
93
120
|
}
|
|
94
|
-
const
|
|
95
|
-
for await (const session of source) {
|
|
121
|
+
const sessionCandidates = (session) => {
|
|
96
122
|
const { hash } = extractScopeFromContainerTag(getTags(session.directory).project.tag);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
return session.units
|
|
124
|
+
.filter((window) => windowWithinDateRange(window, filters) &&
|
|
125
|
+
buildImportKey(session.sessionId, window, host) !== null)
|
|
126
|
+
.map((window) => ({ ...session, hash, window }));
|
|
127
|
+
};
|
|
128
|
+
// A lazy source is read session by session; an eager one is buffered as before.
|
|
129
|
+
let candidates;
|
|
130
|
+
if (isLazySource(source)) {
|
|
131
|
+
report.unitsTotal = source.total;
|
|
132
|
+
candidates = (async function* () {
|
|
133
|
+
for await (const session of source.open())
|
|
134
|
+
yield* sessionCandidates(session);
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const all = [];
|
|
139
|
+
for await (const session of source)
|
|
140
|
+
all.push(...sessionCandidates(session));
|
|
141
|
+
report.unitsTotal = all.length;
|
|
142
|
+
candidates = all;
|
|
104
143
|
}
|
|
105
|
-
report.unitsTotal = candidates.length;
|
|
106
144
|
const storageAccessible = ledgerFileExists;
|
|
107
145
|
let processed = 0;
|
|
108
|
-
for (const candidate of candidates) {
|
|
146
|
+
for await (const candidate of candidates) {
|
|
147
|
+
if (deps.signal?.aborted)
|
|
148
|
+
break;
|
|
109
149
|
const { sessionId, directory, sourceFile, hash, window } = candidate;
|
|
110
150
|
const key = buildImportKey(sessionId, window, host);
|
|
111
151
|
processed++;
|
|
@@ -132,6 +172,19 @@ export async function importHistorySource(source, host, deps, filters, report) {
|
|
|
132
172
|
report.units.push(unit);
|
|
133
173
|
continue;
|
|
134
174
|
}
|
|
175
|
+
const liveEntryIds = await findLiveCapturedEntryIds(hash, host, sessionId);
|
|
176
|
+
if (liveEntryIds.has(window.userEntryId)) {
|
|
177
|
+
if (!dryRun) {
|
|
178
|
+
await ledger.begin({ key, sessionId, sourceFile, projectHash: hash });
|
|
179
|
+
await ledger.skip(key, "live-captured");
|
|
180
|
+
}
|
|
181
|
+
unit.status = "skipped";
|
|
182
|
+
unit.reason = "live-captured";
|
|
183
|
+
report.unitsSkipped++;
|
|
184
|
+
report.skipReasons["live-captured"] = (report.skipReasons["live-captured"] ?? 0) + 1;
|
|
185
|
+
report.units.push(unit);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
135
188
|
// Crash reconciliation: the memory landed but the ledger never committed.
|
|
136
189
|
const storedMemoryId = await lookupExistingMemory(hash, key, { storageAccessible });
|
|
137
190
|
if (storedMemoryId) {
|
|
@@ -223,10 +276,19 @@ export async function importPiHistory(deps, filters) {
|
|
|
223
276
|
skipReasons: {},
|
|
224
277
|
projects: [],
|
|
225
278
|
units: [],
|
|
279
|
+
unitsHeldBack: 0,
|
|
280
|
+
unitsUntimed: 0,
|
|
226
281
|
};
|
|
282
|
+
const selected = filters.selectionKeys ? new Set(filters.selectionKeys) : null;
|
|
227
283
|
const projectAggregates = new Map();
|
|
228
284
|
const candidates = [];
|
|
229
285
|
for (const discovered of discovery.sessions) {
|
|
286
|
+
if (deps.signal?.aborted)
|
|
287
|
+
break;
|
|
288
|
+
if (selected && !selected.has(discovered.key)) {
|
|
289
|
+
report.sessionsFilteredOut++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
230
292
|
if (filters.session) {
|
|
231
293
|
const wanted = filters.session;
|
|
232
294
|
if (discovered.sessionId !== wanted && discovered.file !== wanted) {
|
|
@@ -245,7 +307,16 @@ export async function importPiHistory(deps, filters) {
|
|
|
245
307
|
});
|
|
246
308
|
continue;
|
|
247
309
|
}
|
|
248
|
-
|
|
310
|
+
// The header ID names the session in the page's list; a file that loads as
|
|
311
|
+
// another session would be imported under keys the user never saw.
|
|
312
|
+
if (discovered.sessionId && discovered.sessionId !== session.sessionId) {
|
|
313
|
+
report.loadErrors.push({
|
|
314
|
+
file: discovered.file,
|
|
315
|
+
error: "Loaded session ID does not match the file header",
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const directory = resolveImportProject(session.cwd, pathMaps).directory;
|
|
249
320
|
if (!directory) {
|
|
250
321
|
report.unresolvableSessions.push({ file: session.sourceFile, cwd: session.cwd });
|
|
251
322
|
continue;
|
|
@@ -269,10 +340,10 @@ export async function importPiHistory(deps, filters) {
|
|
|
269
340
|
projectAggregates.set(tags.project.tag, aggregate);
|
|
270
341
|
}
|
|
271
342
|
aggregate.sessions.add(session.sessionId);
|
|
272
|
-
const windows = extractPiConversationWindows(session.branch);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
343
|
+
const windows = selectImportWindows(extractPiConversationWindows(session.branch), filters);
|
|
344
|
+
report.unitsHeldBack += windows.heldBack;
|
|
345
|
+
report.unitsUntimed += windows.untimed;
|
|
346
|
+
for (const window of windows.kept) {
|
|
276
347
|
const key = buildImportKey(session.sessionId, window);
|
|
277
348
|
if (!key)
|
|
278
349
|
continue;
|
|
@@ -311,10 +382,11 @@ export async function importPiHistory(deps, filters) {
|
|
|
311
382
|
sessions: aggregate.sessions.size,
|
|
312
383
|
units: aggregate.units,
|
|
313
384
|
}));
|
|
314
|
-
if (deps.profile) {
|
|
385
|
+
if (deps.profile && !deps.signal?.aborted) {
|
|
315
386
|
const { importProfileFromHistory } = await import("./profile-import.js");
|
|
316
387
|
report.profile = await importProfileFromHistory(sourceSessions, {
|
|
317
388
|
host: "pi",
|
|
389
|
+
signal: deps.signal,
|
|
318
390
|
dryRun,
|
|
319
391
|
model: deps.profile.model,
|
|
320
392
|
batchSize: deps.profile.batchSize,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BackfillHost } from "./backfill-model.js";
|
|
2
|
+
/** Keep an automatic run from starting during a manual session import. */
|
|
3
|
+
export declare function beginManualImport(host: BackfillHost): () => void;
|
|
4
|
+
export declare function isManualImportRunning(host: BackfillHost): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const running = new Set();
|
|
2
|
+
/** Keep an automatic run from starting during a manual session import. */
|
|
3
|
+
export function beginManualImport(host) {
|
|
4
|
+
running.add(host);
|
|
5
|
+
return () => running.delete(host);
|
|
6
|
+
}
|
|
7
|
+
export function isManualImportRunning(host) {
|
|
8
|
+
return running.has(host);
|
|
9
|
+
}
|
|
@@ -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>;
|