om-memory-system 4.26.2 → 4.28.0
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/dist/importer/import-sessions.d.ts +24 -0
- package/dist/importer/import-sessions.js +47 -5
- package/dist/importer/ledger.d.ts +11 -0
- package/dist/importer/ledger.js +54 -0
- package/dist/importer/web-import-group.d.ts +6 -0
- package/dist/importer/web-import-group.js +9 -1
- package/dist/web/assets/index-CLK-rpXr.js +126 -0
- package/dist/web/assets/index-ClsmAqHW.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Cq_D7prc.js +0 -126
- package/dist/web/assets/index-DH6qnFfk.css +0 -2
|
@@ -8,6 +8,11 @@ import type { ImportPathMap } from "./importer.js";
|
|
|
8
8
|
* imported set always use the same discovery and project rules.
|
|
9
9
|
*/
|
|
10
10
|
export declare const MAX_SELECTED_KEYS = 1000;
|
|
11
|
+
/** The outputs a run produces; a session is handled only when every ticked one is finished. */
|
|
12
|
+
export interface ImportOutputs {
|
|
13
|
+
memories: boolean;
|
|
14
|
+
profile: boolean;
|
|
15
|
+
}
|
|
11
16
|
export interface ImportSessionRow {
|
|
12
17
|
/**
|
|
13
18
|
* Selection key: the OpenCode session ID, or the Pi or Claude Code file path
|
|
@@ -30,6 +35,10 @@ export interface ImportMatchOptions {
|
|
|
30
35
|
/** Grouped imports can retain a valid source with no matching sessions as no-work. */
|
|
31
36
|
allowEmpty?: boolean;
|
|
32
37
|
signal?: AbortSignal;
|
|
38
|
+
/** Outputs the job writes; decides which sessions an "all" selection drops as handled. */
|
|
39
|
+
outputs?: ImportOutputs;
|
|
40
|
+
/** A forced run re-analyses handled history, so it keeps handled sessions. */
|
|
41
|
+
force?: boolean;
|
|
33
42
|
}
|
|
34
43
|
export declare class StaleSelectionError extends Error {
|
|
35
44
|
readonly status = 409;
|
|
@@ -44,17 +53,27 @@ interface Matched {
|
|
|
44
53
|
revision: string;
|
|
45
54
|
}
|
|
46
55
|
export declare function matchImportSessions(identity: ImportSourceIdentity, options: ImportMatchOptions, snapshotMode: "fresh" | "reuse" | "any"): Promise<Matched>;
|
|
56
|
+
/**
|
|
57
|
+
* Sessions the ledger shows as finished for every ticked output. The check uses
|
|
58
|
+
* each row's `sessionId`, since Pi and Claude Code keys are file paths. A row
|
|
59
|
+
* without a session id is never handled. The ledger is read, never created.
|
|
60
|
+
*/
|
|
61
|
+
export declare function handledKeys(rows: ImportSessionRow[], outputs: ImportOutputs): Promise<Set<string>>;
|
|
47
62
|
export interface ImportSessionPage {
|
|
48
63
|
source: {
|
|
49
64
|
kind: ImportSourceIdentity["kind"];
|
|
50
65
|
displayPath: string;
|
|
51
66
|
sourceToken: string;
|
|
52
67
|
};
|
|
68
|
+
/** Rows shown: handled sessions are left out unless the request keeps them. */
|
|
53
69
|
total: number;
|
|
54
70
|
offset: number;
|
|
55
71
|
rows: Array<ImportSessionRow & {
|
|
56
72
|
selectable: boolean;
|
|
73
|
+
handled: boolean;
|
|
57
74
|
}>;
|
|
75
|
+
/** Handled sessions left out of the list. */
|
|
76
|
+
hiddenHandled: number;
|
|
58
77
|
/** Sessions whose recorded directory no longer exists. */
|
|
59
78
|
unresolvedCount: number;
|
|
60
79
|
revision: string;
|
|
@@ -65,6 +84,8 @@ export interface SessionListRequest {
|
|
|
65
84
|
offset?: number;
|
|
66
85
|
limit?: number;
|
|
67
86
|
refresh?: boolean;
|
|
87
|
+
/** True when the page shows handled sessions; "Re-analyse handled history" implies it. */
|
|
88
|
+
includeHandled?: boolean;
|
|
68
89
|
match: Omit<ImportMatchOptions, "cwd">;
|
|
69
90
|
}
|
|
70
91
|
/** Check a session-list request body from the page. */
|
|
@@ -75,6 +96,7 @@ export declare function listImportSessions(request: {
|
|
|
75
96
|
offset?: number;
|
|
76
97
|
limit?: number;
|
|
77
98
|
refresh?: boolean;
|
|
99
|
+
includeHandled?: boolean;
|
|
78
100
|
}, options: ImportMatchOptions): Promise<ImportSessionPage>;
|
|
79
101
|
export type ImportSelection = {
|
|
80
102
|
mode: "ids";
|
|
@@ -88,6 +110,8 @@ export type ImportSelection = {
|
|
|
88
110
|
excludedKeys: string[];
|
|
89
111
|
revision: string;
|
|
90
112
|
listedAt: number;
|
|
113
|
+
/** False or absent: the server drops sessions already handled for the job's outputs. */
|
|
114
|
+
includeHandled?: boolean;
|
|
91
115
|
};
|
|
92
116
|
export declare function validateImportSelection(value: unknown): ImportSelection;
|
|
93
117
|
export interface ResolvedImportSelection {
|
|
@@ -10,6 +10,7 @@ import { assertImportSourceUnchanged, importSourceKey, readImportSourceToken, va
|
|
|
10
10
|
*/
|
|
11
11
|
export const MAX_SELECTED_KEYS = 1000;
|
|
12
12
|
const MAX_PAGE = 200;
|
|
13
|
+
const ALL_OUTPUTS = { memories: true, profile: true };
|
|
13
14
|
export class StaleSelectionError extends Error {
|
|
14
15
|
status = 409;
|
|
15
16
|
constructor(message = "The session list is out of date. Refresh the list and try again.") {
|
|
@@ -97,6 +98,19 @@ export async function matchImportSessions(identity, options, snapshotMode) {
|
|
|
97
98
|
revision: revisionOf(identity, merged, projectTag, matching.map((row) => row.key)),
|
|
98
99
|
};
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Sessions the ledger shows as finished for every ticked output. The check uses
|
|
103
|
+
* each row's `sessionId`, since Pi and Claude Code keys are file paths. A row
|
|
104
|
+
* without a session id is never handled. The ledger is read, never created.
|
|
105
|
+
*/
|
|
106
|
+
export async function handledKeys(rows, outputs) {
|
|
107
|
+
const ids = rows.flatMap((row) => (row.sessionId ? [row.sessionId] : []));
|
|
108
|
+
if (ids.length === 0)
|
|
109
|
+
return new Set();
|
|
110
|
+
const { ImportLedger } = await import("./ledger.js");
|
|
111
|
+
const handled = await new ImportLedger().handledSessionIds(ids, outputs);
|
|
112
|
+
return new Set(rows.filter((row) => row.sessionId && handled.has(row.sessionId)).map((r) => r.key));
|
|
113
|
+
}
|
|
100
114
|
/** Check a session-list request body from the page. */
|
|
101
115
|
export function validateSessionListRequest(value) {
|
|
102
116
|
const input = value;
|
|
@@ -124,16 +138,25 @@ export function validateSessionListRequest(value) {
|
|
|
124
138
|
throw new Error(`Invalid ${key}`);
|
|
125
139
|
}
|
|
126
140
|
}
|
|
141
|
+
for (const key of ["skipMemories", "skipProfile", "force", "includeHandled"]) {
|
|
142
|
+
if (input[key] !== undefined && typeof input[key] !== "boolean") {
|
|
143
|
+
throw new Error(`Invalid ${key}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const force = input.force === true;
|
|
127
147
|
return {
|
|
128
148
|
...(input.source !== undefined ? { sourceToken: input.source } : {}),
|
|
129
149
|
...(input.offset !== undefined ? { offset: input.offset } : {}),
|
|
130
150
|
...(input.limit !== undefined ? { limit: input.limit } : {}),
|
|
131
151
|
refresh: input.refresh === true,
|
|
152
|
+
includeHandled: input.includeHandled === true || force,
|
|
132
153
|
match: {
|
|
133
154
|
host: input.host,
|
|
134
155
|
scope,
|
|
135
156
|
...(project ? { project } : {}),
|
|
136
157
|
pathMaps: maps,
|
|
158
|
+
outputs: { memories: input.skipMemories !== true, profile: input.skipProfile !== true },
|
|
159
|
+
...(force ? { force } : {}),
|
|
137
160
|
},
|
|
138
161
|
};
|
|
139
162
|
}
|
|
@@ -152,17 +175,21 @@ export async function listImportSessions(request, options) {
|
|
|
152
175
|
// Feed the Directory maps list; a failure here must not break the listing.
|
|
153
176
|
const { recordUnresolvedDirectories, summarizeUnresolvedDirectories } = await import("../services/backfill-state.js");
|
|
154
177
|
await recordUnresolvedDirectories(options.host, summarizeUnresolvedDirectories(matched.unresolved.map((row) => ({ directory: row.recordedDirectory })))).catch(() => { });
|
|
155
|
-
const
|
|
156
|
-
|
|
178
|
+
const handled = await handledKeys(matched.matching, options.outputs ?? ALL_OUTPUTS);
|
|
179
|
+
const listed = [
|
|
180
|
+
...matched.matching.map((row) => ({ ...row, selectable: true, handled: handled.has(row.key) })),
|
|
157
181
|
...(options.scope === "all-projects"
|
|
158
|
-
? matched.unresolved.map((row) => ({ ...row, selectable: false }))
|
|
182
|
+
? matched.unresolved.map((row) => ({ ...row, selectable: false, handled: false }))
|
|
159
183
|
: []),
|
|
160
184
|
];
|
|
185
|
+
const keep = request.includeHandled === true || options.force === true;
|
|
186
|
+
const rows = keep ? listed : listed.filter((row) => !row.handled);
|
|
161
187
|
return {
|
|
162
188
|
source: { kind: identity.kind, displayPath: identity.realPath, sourceToken: token },
|
|
163
189
|
total: rows.length,
|
|
164
190
|
offset,
|
|
165
191
|
rows: rows.slice(offset, offset + limit),
|
|
192
|
+
hiddenHandled: listed.length - rows.length,
|
|
166
193
|
unresolvedCount: matched.unresolved.length,
|
|
167
194
|
revision: matched.revision,
|
|
168
195
|
listedAt,
|
|
@@ -201,7 +228,16 @@ export function validateImportSelection(value) {
|
|
|
201
228
|
}
|
|
202
229
|
if (typeof input.revision !== "string")
|
|
203
230
|
throw new Error("Invalid session list revision");
|
|
204
|
-
|
|
231
|
+
if (input.includeHandled !== undefined && typeof input.includeHandled !== "boolean") {
|
|
232
|
+
throw new Error("Invalid includeHandled");
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
mode: "all",
|
|
236
|
+
excludedKeys: excluded,
|
|
237
|
+
revision: input.revision,
|
|
238
|
+
listedAt,
|
|
239
|
+
...(input.includeHandled === true ? { includeHandled: true } : {}),
|
|
240
|
+
};
|
|
205
241
|
}
|
|
206
242
|
throw new Error("Choose sessions to import");
|
|
207
243
|
}
|
|
@@ -223,7 +259,13 @@ export async function resolveImportSelection(sourceToken, selection, options) {
|
|
|
223
259
|
if (matched.revision !== selection.revision)
|
|
224
260
|
throw new StaleSelectionError();
|
|
225
261
|
const excluded = new Set(selection.excludedKeys);
|
|
226
|
-
|
|
262
|
+
const keep = selection.includeHandled === true || options.force === true;
|
|
263
|
+
const handled = keep
|
|
264
|
+
? new Set()
|
|
265
|
+
: await handledKeys(matched.matching, options.outputs ?? ALL_OUTPUTS);
|
|
266
|
+
keys = matched.matching
|
|
267
|
+
.map((row) => row.key)
|
|
268
|
+
.filter((key) => !excluded.has(key) && !handled.has(key));
|
|
227
269
|
}
|
|
228
270
|
else {
|
|
229
271
|
const current = new Map(matched.matching.map((row) => [row.key, row.directory]));
|
|
@@ -32,6 +32,17 @@ export declare class ImportLedger {
|
|
|
32
32
|
* A ledger file without the table reads as empty.
|
|
33
33
|
*/
|
|
34
34
|
peek(key: string): Promise<ImportLedgerRow | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Sessions whose ledger rows are all finished for every ticked output.
|
|
37
|
+
* Memory rows and profile rows (`project_hash = 'profile'`) are judged apart.
|
|
38
|
+
* A ticked output passes when the session has at least one row of its kind
|
|
39
|
+
* and every such row is imported or skipped. Read-only like `peek`: a missing
|
|
40
|
+
* file or table gives an empty set and nothing is created.
|
|
41
|
+
*/
|
|
42
|
+
handledSessionIds(sessionIds: readonly string[], outputs: {
|
|
43
|
+
memories: boolean;
|
|
44
|
+
profile: boolean;
|
|
45
|
+
}): Promise<Set<string>>;
|
|
35
46
|
countByStatus(): Promise<Record<string, number>>;
|
|
36
47
|
/** Upsert an in-progress marker before processing a unit. */
|
|
37
48
|
begin(input: {
|
package/dist/importer/ledger.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { CONFIG } from "../config.js";
|
|
3
4
|
import { tursoConnectionManager } from "../services/turso/connection-manager.js";
|
|
@@ -87,6 +88,59 @@ export class ImportLedger {
|
|
|
87
88
|
const row = await db.get(`SELECT * FROM import_ledger WHERE key = ?`, [key]);
|
|
88
89
|
return row ? rowToEntry(row) : null;
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Sessions whose ledger rows are all finished for every ticked output.
|
|
93
|
+
* Memory rows and profile rows (`project_hash = 'profile'`) are judged apart.
|
|
94
|
+
* A ticked output passes when the session has at least one row of its kind
|
|
95
|
+
* and every such row is imported or skipped. Read-only like `peek`: a missing
|
|
96
|
+
* file or table gives an empty set and nothing is created.
|
|
97
|
+
*/
|
|
98
|
+
async handledSessionIds(sessionIds, outputs) {
|
|
99
|
+
const handled = new Set();
|
|
100
|
+
const ids = [...new Set(sessionIds.filter((id) => id !== ""))];
|
|
101
|
+
if ((!outputs.memories && !outputs.profile) || ids.length === 0)
|
|
102
|
+
return handled;
|
|
103
|
+
// Opening a connection creates the file, so a missing ledger is read as empty first.
|
|
104
|
+
if (!existsSync(importLedgerDbPath()))
|
|
105
|
+
return handled;
|
|
106
|
+
const db = await tursoConnectionManager.getConnection(importLedgerDbPath());
|
|
107
|
+
const table = await db.get(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'import_ledger'`);
|
|
108
|
+
if (!table)
|
|
109
|
+
return handled;
|
|
110
|
+
const kinds = new Map();
|
|
111
|
+
for (let start = 0; start < ids.length; start += 500) {
|
|
112
|
+
const chunk = ids.slice(start, start + 500);
|
|
113
|
+
const rows = await db.all(`
|
|
114
|
+
SELECT session_id,
|
|
115
|
+
CASE WHEN project_hash = 'profile' THEN 'profile' ELSE 'memory' END AS kind,
|
|
116
|
+
COUNT(*) AS total,
|
|
117
|
+
SUM(CASE WHEN status IN ('imported', 'skipped') THEN 1 ELSE 0 END) AS done
|
|
118
|
+
FROM import_ledger
|
|
119
|
+
WHERE session_id IN (${chunk.map(() => "?").join(", ")})
|
|
120
|
+
GROUP BY session_id, kind
|
|
121
|
+
`, chunk);
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
const entry = kinds.get(String(row.session_id)) ?? {
|
|
124
|
+
memory: [0, 0],
|
|
125
|
+
profile: [0, 0],
|
|
126
|
+
};
|
|
127
|
+
entry[row.kind === "profile" ? "profile" : "memory"] = [
|
|
128
|
+
Number(row.total),
|
|
129
|
+
Number(row.done),
|
|
130
|
+
];
|
|
131
|
+
kinds.set(String(row.session_id), entry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const passes = ([total, done]) => total > 0 && total === done;
|
|
135
|
+
for (const [sessionId, entry] of kinds) {
|
|
136
|
+
if (outputs.memories && !passes(entry.memory))
|
|
137
|
+
continue;
|
|
138
|
+
if (outputs.profile && !passes(entry.profile))
|
|
139
|
+
continue;
|
|
140
|
+
handled.add(sessionId);
|
|
141
|
+
}
|
|
142
|
+
return handled;
|
|
143
|
+
}
|
|
90
144
|
async countByStatus() {
|
|
91
145
|
const db = await this.db();
|
|
92
146
|
const rows = await db.all(`SELECT status, COUNT(*) AS count FROM import_ledger GROUP BY status`);
|
|
@@ -52,6 +52,12 @@ type Deps = GroupImportDeps & {
|
|
|
52
52
|
resolveSelection: typeof resolveImportSelection;
|
|
53
53
|
readiness: () => Promise<ImportReadiness>;
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Fixed sentences with no number, so the page can translate them by exact text.
|
|
57
|
+
* The failed count travels apart in the child's summary.
|
|
58
|
+
*/
|
|
59
|
+
export declare const MEMORY_FAILED_MESSAGE = "Some memory units failed. Completed work is saved. A new preview and import retries only the failed units.";
|
|
60
|
+
export declare const PROFILE_FAILED_MESSAGE = "Profile learning failed. Completed memory work is saved. Refresh the preview and retry.";
|
|
55
61
|
/** Preflight every child before launching a bounded sequential group in the existing slot. */
|
|
56
62
|
export declare function startImportGroup(input: GroupImportRequest, directory: string, deps: Deps, publish: (job: GroupImportJob, controller: AbortController) => void): Promise<GroupImportJob>;
|
|
57
63
|
export {};
|
|
@@ -38,6 +38,12 @@ async function discardSelection(identity) {
|
|
|
38
38
|
const { opencodeSnapshots } = await import("./opencode-snapshot.js");
|
|
39
39
|
opencodeSnapshots.discard(importSourceKey(identity));
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Fixed sentences with no number, so the page can translate them by exact text.
|
|
43
|
+
* The failed count travels apart in the child's summary.
|
|
44
|
+
*/
|
|
45
|
+
export const MEMORY_FAILED_MESSAGE = "Some memory units failed. Completed work is saved. A new preview and import retries only the failed units.";
|
|
46
|
+
export const PROFILE_FAILED_MESSAGE = "Profile learning failed. Completed memory work is saved. Refresh the preview and retry.";
|
|
41
47
|
function matchOptions(child, directory, signal) {
|
|
42
48
|
return {
|
|
43
49
|
host: child.input.host,
|
|
@@ -47,6 +53,8 @@ function matchOptions(child, directory, signal) {
|
|
|
47
53
|
cwd: directory,
|
|
48
54
|
allowEmpty: true,
|
|
49
55
|
signal,
|
|
56
|
+
outputs: { memories: !child.args.skipMemories, profile: !child.args.skipProfile },
|
|
57
|
+
...(child.args.force ? { force: true } : {}),
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
60
|
function hostError(host, error) {
|
|
@@ -326,7 +334,7 @@ async function executeGroup(children, job, signal, directory, deps, waiting) {
|
|
|
326
334
|
else if (historyImportFailed(report)) {
|
|
327
335
|
child.row.state = "failed";
|
|
328
336
|
failed = true;
|
|
329
|
-
child.row.error = `${hostLabel(host)}: ${report.unitsFailed ?
|
|
337
|
+
child.row.error = `${hostLabel(host)}: ${report.unitsFailed ? MEMORY_FAILED_MESSAGE : PROFILE_FAILED_MESSAGE}`;
|
|
330
338
|
job.error = child.row.error;
|
|
331
339
|
}
|
|
332
340
|
else {
|