om-memory-system 3.3.0-next.17 → 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/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.js +3 -0
- 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 +50 -0
- package/dist/adapters/pi/import-command.js +3 -0
- 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 +13 -1
- package/dist/config.js +28 -0
- 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/importer.js +14 -0
- package/dist/importer/manual-import-guard.d.ts +4 -0
- package/dist/importer/manual-import-guard.js +9 -0
- package/dist/importer/settings-health.d.ts +1 -2
- package/dist/importer/settings-health.js +2 -11
- package/dist/index.js +30 -0
- package/dist/services/backfill-state.d.ts +25 -0
- package/dist/services/backfill-state.js +68 -0
- package/dist/services/global-config-writer.js +5 -1
- package/dist/services/live-captured-entries.d.ts +3 -0
- package/dist/services/live-captured-entries.js +34 -0
- package/dist/services/safe-health-error.d.ts +2 -0
- package/dist/services/safe-health-error.js +11 -0
- package/dist/services/settings-snapshot.js +14 -2
- package/dist/services/web-autostart.d.ts +29 -0
- package/dist/services/web-autostart.js +208 -0
- package/dist/services/web-server.js +11 -0
- package/dist/web/assets/index-B-_gMSn9.js +124 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-BMkPht7g.js +0 -124
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { CONFIG, refreshConfigIfChanged } from "../config.js";
|
|
2
|
+
import { log } from "../services/logger.js";
|
|
3
|
+
import { tryAcquireBackfillLock } from "./backfill-lock.js";
|
|
4
|
+
import { getBackfillCutoff, updateBackfillStatus, } from "../services/backfill-state.js";
|
|
5
|
+
import { runHistoryImport, } from "./run-import.js";
|
|
6
|
+
import { isManualImportRunning } from "./manual-import-guard.js";
|
|
7
|
+
const zero = () => ({ imported: 0, skipped: 0, failed: 0, pending: 0, unresolved: 0 });
|
|
8
|
+
function counts(report) {
|
|
9
|
+
return {
|
|
10
|
+
imported: report.unitsImported,
|
|
11
|
+
skipped: report.unitsSkipped,
|
|
12
|
+
failed: report.unitsFailed,
|
|
13
|
+
pending: report.unitsWouldImport,
|
|
14
|
+
unresolved: (report.unresolvedProjects?.length ?? 0) + report.unresolvableSessions.length,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const delay = (signal) => new Promise((resolve) => {
|
|
18
|
+
if (signal?.aborted)
|
|
19
|
+
return resolve();
|
|
20
|
+
const done = () => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
signal?.removeEventListener("abort", done);
|
|
23
|
+
resolve();
|
|
24
|
+
};
|
|
25
|
+
const timer = setTimeout(done, 30_000);
|
|
26
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
27
|
+
});
|
|
28
|
+
/** Schedule one host's history import after startup; callers do not await it. */
|
|
29
|
+
export async function scheduleAutoBackfill(options) {
|
|
30
|
+
await (options.wait ?? (() => delay(options.signal)))();
|
|
31
|
+
if (options.signal?.aborted)
|
|
32
|
+
return;
|
|
33
|
+
if (!(options.enabled ??
|
|
34
|
+
(() => CONFIG.autoBackfill && process.env.OMMS_DISABLE_AUTO_BACKFILL !== "1"))())
|
|
35
|
+
return;
|
|
36
|
+
if (isManualImportRunning(options.host))
|
|
37
|
+
return;
|
|
38
|
+
const release = await tryAcquireBackfillLock(options.host);
|
|
39
|
+
if (!release)
|
|
40
|
+
return;
|
|
41
|
+
const clock = options.now ?? Date.now;
|
|
42
|
+
const run = options.run ?? runHistoryImport;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const onAbort = () => controller.abort();
|
|
45
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
if (options.signal?.aborted)
|
|
47
|
+
controller.abort();
|
|
48
|
+
let state = "failed";
|
|
49
|
+
let model = null;
|
|
50
|
+
let current = zero();
|
|
51
|
+
let lastError;
|
|
52
|
+
try {
|
|
53
|
+
if (controller.signal.aborted) {
|
|
54
|
+
state = "stopped";
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const cutoff = await getBackfillCutoff(options.host, clock());
|
|
58
|
+
const args = {
|
|
59
|
+
help: false,
|
|
60
|
+
dryRun: true,
|
|
61
|
+
force: false,
|
|
62
|
+
skipMemories: false,
|
|
63
|
+
skipProfile: false,
|
|
64
|
+
scope: "all-projects",
|
|
65
|
+
pathMaps: [],
|
|
66
|
+
until: cutoff,
|
|
67
|
+
errors: [],
|
|
68
|
+
};
|
|
69
|
+
const dry = await run(options.host, args, {
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
models: {},
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
});
|
|
74
|
+
current = counts(dry);
|
|
75
|
+
if (controller.signal.aborted) {
|
|
76
|
+
state = "stopped";
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!current.pending) {
|
|
80
|
+
state = "done";
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const resolved = await options.resolveModels();
|
|
84
|
+
if (controller.signal.aborted) {
|
|
85
|
+
state = "stopped";
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
model = resolved.model;
|
|
89
|
+
options.notify(`${options.host} automatic import: ${current.pending} exchanges with ${model}. Set autoBackfill to false to stop it.`);
|
|
90
|
+
state = "running";
|
|
91
|
+
await updateBackfillStatus(options.host, { state, model, counts: current });
|
|
92
|
+
if (controller.signal.aborted) {
|
|
93
|
+
state = "stopped";
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let failures = 0;
|
|
97
|
+
const capture = resolved.models.capture;
|
|
98
|
+
const models = capture
|
|
99
|
+
? {
|
|
100
|
+
...resolved.models,
|
|
101
|
+
capture: {
|
|
102
|
+
summarize: async (request) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await capture.summarize(request);
|
|
105
|
+
failures = 0;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
lastError = error;
|
|
110
|
+
if (++failures >= 5)
|
|
111
|
+
controller.abort();
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: resolved.models;
|
|
118
|
+
let checkedAt = clock();
|
|
119
|
+
let processed = 0;
|
|
120
|
+
let pendingWrite = Promise.resolve();
|
|
121
|
+
const real = await run(options.host, { ...args, dryRun: false }, {
|
|
122
|
+
cwd: options.cwd,
|
|
123
|
+
models,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
onProgress: (done, total) => {
|
|
126
|
+
processed = done;
|
|
127
|
+
const now = clock();
|
|
128
|
+
if (now - checkedAt < 5_000)
|
|
129
|
+
return;
|
|
130
|
+
checkedAt = now;
|
|
131
|
+
refreshConfigIfChanged(options.cwd);
|
|
132
|
+
if (!CONFIG.autoBackfill)
|
|
133
|
+
controller.abort();
|
|
134
|
+
current = { ...current, pending: Math.max(0, total - done) };
|
|
135
|
+
pendingWrite = pendingWrite
|
|
136
|
+
.then(() => updateBackfillStatus(options.host, { state: "running", model, counts: current }))
|
|
137
|
+
.catch((error) => {
|
|
138
|
+
log("Backfill status write failed", {
|
|
139
|
+
host: options.host,
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
await pendingWrite;
|
|
146
|
+
current = { ...counts(real), pending: Math.max(0, dry.unitsWouldImport - processed) };
|
|
147
|
+
state = controller.signal.aborted ? "stopped" : real.unitsFailed ? "failed" : "done";
|
|
148
|
+
if (!options.signal?.aborted) {
|
|
149
|
+
options.notify(`${options.host} automatic import ${state}: ${current.imported} imported, ${current.failed} failed. autoBackfill controls the next run.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
lastError = error;
|
|
154
|
+
state = controller.signal.aborted ? "stopped" : "failed";
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
try {
|
|
158
|
+
await updateBackfillStatus(options.host, { state, model, counts: current, error: lastError });
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
162
|
+
await release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
import { tursoConnectionManager } from "../services/turso/connection-manager.js";
|
|
5
|
+
function live(pid) {
|
|
6
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
7
|
+
return false;
|
|
8
|
+
try {
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return error.code === "EPERM";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Claim one host's import without holding a database transaction during the run. */
|
|
17
|
+
export async function tryAcquireBackfillLock(host, storagePath = CONFIG.storagePath) {
|
|
18
|
+
const db = await tursoConnectionManager.getConnection(join(storagePath, "import-ledger.db"));
|
|
19
|
+
await db.run(`CREATE TABLE IF NOT EXISTS backfill_locks (
|
|
20
|
+
host TEXT PRIMARY KEY, pid INTEGER NOT NULL, token TEXT NOT NULL
|
|
21
|
+
)`);
|
|
22
|
+
const state = { pid: process.pid, token: randomUUID() };
|
|
23
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
24
|
+
const inserted = await db.run("INSERT OR IGNORE INTO backfill_locks (host, pid, token) VALUES (?, ?, ?)", [host, state.pid, state.token]);
|
|
25
|
+
if (!inserted) {
|
|
26
|
+
const holder = await db.get("SELECT pid, token FROM backfill_locks WHERE host = ?", [host]);
|
|
27
|
+
if (!holder)
|
|
28
|
+
continue;
|
|
29
|
+
if (live(holder.pid))
|
|
30
|
+
return null;
|
|
31
|
+
const replaced = await db.run("UPDATE backfill_locks SET pid = ?, token = ? WHERE host = ? AND pid = ? AND token = ?", [state.pid, state.token, host, holder.pid, holder.token]);
|
|
32
|
+
if (!replaced)
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let released = false;
|
|
36
|
+
return async () => {
|
|
37
|
+
if (released)
|
|
38
|
+
return;
|
|
39
|
+
released = true;
|
|
40
|
+
await db.run("DELETE FROM backfill_locks WHERE host = ? AND token = ?", [host, state.token]);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type BackfillHost = "pi" | "opencode";
|
|
2
|
+
export type BackfillModel = "inherit" | {
|
|
3
|
+
provider: string;
|
|
4
|
+
model: string;
|
|
5
|
+
};
|
|
6
|
+
/** Parse the global backfill model without importing the runtime config. */
|
|
7
|
+
export declare function parseBackfillModel(config: {
|
|
8
|
+
piBackfillModel?: unknown;
|
|
9
|
+
opencodeBackfillModel?: unknown;
|
|
10
|
+
}, host: BackfillHost): BackfillModel;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Parse the global backfill model without importing the runtime config. */
|
|
2
|
+
export function parseBackfillModel(config, host) {
|
|
3
|
+
const value = host === "pi" ? config.piBackfillModel : config.opencodeBackfillModel;
|
|
4
|
+
if (value === undefined || value === "inherit")
|
|
5
|
+
return "inherit";
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
throw new Error(`Invalid ${host}BackfillModel config`);
|
|
8
|
+
const slash = value.indexOf("/");
|
|
9
|
+
if (slash <= 0 || slash === value.length - 1) {
|
|
10
|
+
throw new Error(`Invalid ${host}BackfillModel config`);
|
|
11
|
+
}
|
|
12
|
+
return { provider: value.slice(0, slash), model: value.slice(slash + 1) };
|
|
13
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
@@ -171,6 +172,19 @@ export async function importHistorySource(source, host, deps, filters, report) {
|
|
|
171
172
|
report.units.push(unit);
|
|
172
173
|
continue;
|
|
173
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
|
+
}
|
|
174
188
|
// Crash reconciliation: the memory landed but the ledger never committed.
|
|
175
189
|
const storedMemoryId = await lookupExistingMemory(hash, key, { storageAccessible });
|
|
176
190
|
if (storedMemoryId) {
|
|
@@ -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,10 +1,9 @@
|
|
|
1
|
+
export { safeHealthError } from "../services/safe-health-error.js";
|
|
1
2
|
export type HealthRow = {
|
|
2
3
|
check: string;
|
|
3
4
|
status: "pass" | "warn" | "fail";
|
|
4
5
|
reason: string;
|
|
5
6
|
};
|
|
6
|
-
/** Remove configured credentials before returning a model error to the browser. */
|
|
7
|
-
export declare function safeHealthError(error: unknown, secrets: Array<string | undefined>): string;
|
|
8
7
|
export declare function captureFailureHealth(byModel: Array<{
|
|
9
8
|
total: number;
|
|
10
9
|
failed: number;
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { CONFIG } from "../config.js";
|
|
2
2
|
import { getAutoCaptureProviderStatus, resolvePiLiveModel, } from "../services/ai/live-model-choice.js";
|
|
3
3
|
import { isLoopbackHost } from "../services/web-api-auth.js";
|
|
4
|
+
import { safeHealthError } from "../services/safe-health-error.js";
|
|
5
|
+
export { safeHealthError } from "../services/safe-health-error.js";
|
|
4
6
|
const PROBE = "Reply with a short acknowledgement.";
|
|
5
|
-
/** Remove configured credentials before returning a model error to the browser. */
|
|
6
|
-
export function safeHealthError(error, secrets) {
|
|
7
|
-
let message = error instanceof Error ? error.message : String(error);
|
|
8
|
-
for (const secret of secrets) {
|
|
9
|
-
if (secret && secret.length > 2)
|
|
10
|
-
message = message.replaceAll(secret, "[redacted]");
|
|
11
|
-
}
|
|
12
|
-
return message
|
|
13
|
-
.replace(/(Bearer\s+)[^\s]+/gi, "$1[redacted]")
|
|
14
|
-
.replace(/(api[_-]?key[=:]\s*)[^\s&]+/gi, "$1[redacted]");
|
|
15
|
-
}
|
|
16
7
|
export function captureFailureHealth(byModel, byReason) {
|
|
17
8
|
const total = byModel.reduce((count, row) => count + row.total, 0);
|
|
18
9
|
const failed = byModel.reduce((count, row) => count + row.failed, 0);
|
package/dist/index.js
CHANGED
|
@@ -183,6 +183,13 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
183
183
|
initConfigWithLegacyMigration(directory);
|
|
184
184
|
// Runs even with tracing off, so turning it off does not leave old traces behind.
|
|
185
185
|
pruneTraces(CONFIG);
|
|
186
|
+
if (CONFIG.webServerAutoStart !== undefined && process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
|
|
187
|
+
void import("./services/web-autostart.js")
|
|
188
|
+
.then(({ reconcileWebAutostart }) => reconcileWebAutostart(CONFIG))
|
|
189
|
+
.catch((error) => log("OpenCode login item reconciliation failed", {
|
|
190
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
186
193
|
logAutoCaptureProviderStatus();
|
|
187
194
|
const tags = getTags(directory);
|
|
188
195
|
const autoCaptureHost = createOpenCodeAutoCaptureHost(ctx);
|
|
@@ -203,9 +210,11 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
203
210
|
}
|
|
204
211
|
await configureOpencodeHostTransport(ctx);
|
|
205
212
|
(async () => {
|
|
213
|
+
let connected = [];
|
|
206
214
|
try {
|
|
207
215
|
const providerResult = await ctx.client.provider.list();
|
|
208
216
|
if (providerResult.data?.connected) {
|
|
217
|
+
connected = providerResult.data.connected;
|
|
209
218
|
const { setConnectedProviders } = await loadOpencodeProvider();
|
|
210
219
|
setConnectedProviders(providerResult.data.connected);
|
|
211
220
|
log("opencode providers connected", {
|
|
@@ -222,6 +231,27 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
222
231
|
catch (error) {
|
|
223
232
|
log("Failed to initialize opencode provider state", { error: String(error) });
|
|
224
233
|
}
|
|
234
|
+
if (CONFIG.autoBackfill && process.env.OMMS_DISABLE_AUTO_BACKFILL !== "1" && isConfigured()) {
|
|
235
|
+
void import("./adapters/opencode/backfill-startup.js")
|
|
236
|
+
.then(({ startOpencodeBackfill }) => startOpencodeBackfill({
|
|
237
|
+
connected,
|
|
238
|
+
directory,
|
|
239
|
+
configModel: async () => {
|
|
240
|
+
const response = await ctx.client.config.get();
|
|
241
|
+
return unwrapSdkData(response)?.model ?? null;
|
|
242
|
+
},
|
|
243
|
+
notify: (message) => {
|
|
244
|
+
void ctx.client.tui
|
|
245
|
+
?.showToast({
|
|
246
|
+
body: { title: "Automatic import", message, variant: "info", duration: 5000 },
|
|
247
|
+
})
|
|
248
|
+
.catch(() => { });
|
|
249
|
+
},
|
|
250
|
+
}))
|
|
251
|
+
.catch((error) => log("OpenCode backfill failed", {
|
|
252
|
+
error: error instanceof Error ? error.message : String(error),
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
225
255
|
})();
|
|
226
256
|
let tursoReadyForWeb = !isConfigured();
|
|
227
257
|
if (CONFIG.webServerEnabled && isConfigured()) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type BackfillHost = "pi" | "opencode";
|
|
2
|
+
export type BackfillStatus = {
|
|
3
|
+
host: BackfillHost;
|
|
4
|
+
cutoff: number;
|
|
5
|
+
state: "not-started" | "running" | "stopped" | "done" | "failed";
|
|
6
|
+
model: string | null;
|
|
7
|
+
counts: {
|
|
8
|
+
imported: number;
|
|
9
|
+
skipped: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
pending: number;
|
|
12
|
+
unresolved: number;
|
|
13
|
+
};
|
|
14
|
+
error: string | null;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
};
|
|
17
|
+
/** Record a fixed per-host cutoff before the first import. */
|
|
18
|
+
export declare function getBackfillCutoff(host: BackfillHost, now?: number): Promise<number>;
|
|
19
|
+
/** Read progress without creating a store or a ledger table. */
|
|
20
|
+
export declare function readBackfillStatus(host: BackfillHost): Promise<BackfillStatus | null>;
|
|
21
|
+
/** Save only numeric counts and a redacted error, never report units or prompts. */
|
|
22
|
+
export declare function updateBackfillStatus(host: BackfillHost, update: Pick<BackfillStatus, "state" | "model" | "counts"> & {
|
|
23
|
+
error?: unknown;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
import { tursoConnectionManager } from "./turso/connection-manager.js";
|
|
5
|
+
import { safeHealthError } from "./safe-health-error.js";
|
|
6
|
+
const importLedgerDbPath = () => join(CONFIG.storagePath, "import-ledger.db");
|
|
7
|
+
const emptyCounts = () => ({ imported: 0, skipped: 0, failed: 0, pending: 0, unresolved: 0 });
|
|
8
|
+
async function table() {
|
|
9
|
+
const db = await tursoConnectionManager.getConnection(importLedgerDbPath());
|
|
10
|
+
await db.run(`CREATE TABLE IF NOT EXISTS backfill_state (
|
|
11
|
+
host TEXT PRIMARY KEY, cutoff INTEGER NOT NULL, state TEXT,
|
|
12
|
+
model TEXT, counts TEXT, error TEXT, updated_at INTEGER
|
|
13
|
+
)`);
|
|
14
|
+
return db;
|
|
15
|
+
}
|
|
16
|
+
/** Record a fixed per-host cutoff before the first import. */
|
|
17
|
+
export async function getBackfillCutoff(host, now = Date.now()) {
|
|
18
|
+
const db = await table();
|
|
19
|
+
await db.run(`INSERT OR IGNORE INTO backfill_state (host, cutoff, state, counts, updated_at)
|
|
20
|
+
VALUES (?, ?, 'not-started', ?, ?)`, [host, now, JSON.stringify(emptyCounts()), now]);
|
|
21
|
+
const row = await db.get("SELECT cutoff FROM backfill_state WHERE host = ?", [
|
|
22
|
+
host,
|
|
23
|
+
]);
|
|
24
|
+
return Number(row.cutoff);
|
|
25
|
+
}
|
|
26
|
+
/** Read progress without creating a store or a ledger table. */
|
|
27
|
+
export async function readBackfillStatus(host) {
|
|
28
|
+
if (!existsSync(importLedgerDbPath()))
|
|
29
|
+
return null;
|
|
30
|
+
const db = await tursoConnectionManager.getConnection(importLedgerDbPath());
|
|
31
|
+
const exists = await db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'backfill_state'");
|
|
32
|
+
if (!exists)
|
|
33
|
+
return null;
|
|
34
|
+
const row = await db.get("SELECT * FROM backfill_state WHERE host = ?", [
|
|
35
|
+
host,
|
|
36
|
+
]);
|
|
37
|
+
if (!row)
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
host,
|
|
41
|
+
cutoff: Number(row.cutoff),
|
|
42
|
+
state: row.state,
|
|
43
|
+
model: row.model === null ? null : String(row.model),
|
|
44
|
+
counts: row.counts
|
|
45
|
+
? JSON.parse(String(row.counts))
|
|
46
|
+
: emptyCounts(),
|
|
47
|
+
error: row.error === null ? null : String(row.error),
|
|
48
|
+
updatedAt: Number(row.updated_at),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Save only numeric counts and a redacted error, never report units or prompts. */
|
|
52
|
+
export async function updateBackfillStatus(host, update) {
|
|
53
|
+
const counts = Object.fromEntries(Object.keys(emptyCounts()).map((key) => [
|
|
54
|
+
key,
|
|
55
|
+
Number.isSafeInteger(update.counts[key]) && update.counts[key] >= 0 ? update.counts[key] : 0,
|
|
56
|
+
]));
|
|
57
|
+
const db = await table();
|
|
58
|
+
await db.run(`UPDATE backfill_state SET state = ?, model = ?, counts = ?, error = ?, updated_at = ? WHERE host = ?`, [
|
|
59
|
+
update.state,
|
|
60
|
+
update.model,
|
|
61
|
+
JSON.stringify(counts),
|
|
62
|
+
update.error == null
|
|
63
|
+
? null
|
|
64
|
+
: safeHealthError(update.error, [CONFIG.memoryApiKey]).slice(0, 500),
|
|
65
|
+
Date.now(),
|
|
66
|
+
host,
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
@@ -11,6 +11,10 @@ const keys = new Set([
|
|
|
11
11
|
"captureTrace",
|
|
12
12
|
"captureTraceRetentionDays",
|
|
13
13
|
"captureAttemptRetentionDays",
|
|
14
|
+
"autoBackfill",
|
|
15
|
+
"opencodeBackfillModel",
|
|
16
|
+
"piBackfillModel",
|
|
17
|
+
"webServerAutoStart",
|
|
14
18
|
]);
|
|
15
19
|
export class ConfigConflictError extends Error {
|
|
16
20
|
status = 409;
|
|
@@ -46,7 +50,7 @@ export async function writeGlobalConfigKeys(edits, expectedRevision) {
|
|
|
46
50
|
for (const [key, value] of Object.entries(edits)) {
|
|
47
51
|
if (!keys.has(key))
|
|
48
52
|
throw new Error(`Setting ${key} cannot be edited here`);
|
|
49
|
-
if (key === "captureTrace"
|
|
53
|
+
if (key === "captureTrace" || key === "autoBackfill" || key === "webServerAutoStart"
|
|
50
54
|
? typeof value !== "boolean"
|
|
51
55
|
: key.endsWith("RetentionDays")
|
|
52
56
|
? !Number.isSafeInteger(value) || value < 1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
import { tursoConnectionManager } from "./turso/connection-manager.js";
|
|
5
|
+
import { tursoShardManager } from "./turso/shard-manager.js";
|
|
6
|
+
/** Read user and assistant IDs saved by live capture in one project session. */
|
|
7
|
+
export async function findLiveCapturedEntryIds(projectHash, host, sessionId) {
|
|
8
|
+
const entries = new Set();
|
|
9
|
+
if (!existsSync(join(CONFIG.storagePath, "metadata.db")))
|
|
10
|
+
return entries;
|
|
11
|
+
const shards = await tursoShardManager.getAllShards("project", projectHash);
|
|
12
|
+
for (const shard of shards) {
|
|
13
|
+
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
14
|
+
const rows = await db.all(`SELECT metadata FROM memories WHERE json_valid(metadata)
|
|
15
|
+
AND json_extract(metadata, '$.host') = ?
|
|
16
|
+
AND json_extract(metadata, '$.hostSessionId') = ?
|
|
17
|
+
AND json_extract(metadata, '$.sourceType') != 'history-import'`, [host, sessionId]);
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const metadata = JSON.parse(row.metadata);
|
|
20
|
+
if (!metadata || typeof metadata !== "object")
|
|
21
|
+
continue;
|
|
22
|
+
const provenance = metadata;
|
|
23
|
+
if (typeof provenance.promptId === "string")
|
|
24
|
+
entries.add(provenance.promptId);
|
|
25
|
+
const ids = provenance.sourceEntryIds;
|
|
26
|
+
if (Array.isArray(ids)) {
|
|
27
|
+
for (const id of ids)
|
|
28
|
+
if (typeof id === "string")
|
|
29
|
+
entries.add(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return entries;
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Remove configured credentials before returning a model error to the browser. */
|
|
2
|
+
export function safeHealthError(error, secrets) {
|
|
3
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
4
|
+
for (const secret of secrets) {
|
|
5
|
+
if (secret && secret.length > 2)
|
|
6
|
+
message = message.replaceAll(secret, "[redacted]");
|
|
7
|
+
}
|
|
8
|
+
return message
|
|
9
|
+
.replace(/(Bearer\s+)[^\s]+/gi, "$1[redacted]")
|
|
10
|
+
.replace(/(api[_-]?key[=:]\s*)[^\s&]+/gi, "$1[redacted]");
|
|
11
|
+
}
|
|
@@ -12,6 +12,10 @@ const editable = [
|
|
|
12
12
|
"captureTrace",
|
|
13
13
|
"captureTraceRetentionDays",
|
|
14
14
|
"captureAttemptRetentionDays",
|
|
15
|
+
"autoBackfill",
|
|
16
|
+
"opencodeBackfillModel",
|
|
17
|
+
"piBackfillModel",
|
|
18
|
+
"webServerAutoStart",
|
|
15
19
|
];
|
|
16
20
|
function readSettingsFile(path) {
|
|
17
21
|
if (!path)
|
|
@@ -33,13 +37,21 @@ export function getSettingsSnapshot(directory) {
|
|
|
33
37
|
const project = readSettingsFile(projectFile(directory));
|
|
34
38
|
const settings = Object.fromEntries(editable.map((key) => {
|
|
35
39
|
const projectOverrides = Object.hasOwn(project, key) &&
|
|
36
|
-
(key === "captureTrace"
|
|
40
|
+
(key === "captureTrace"
|
|
41
|
+
? project[key] === false
|
|
42
|
+
: !key.endsWith("RetentionDays") &&
|
|
43
|
+
![
|
|
44
|
+
"autoBackfill",
|
|
45
|
+
"opencodeBackfillModel",
|
|
46
|
+
"piBackfillModel",
|
|
47
|
+
"webServerAutoStart",
|
|
48
|
+
].includes(key));
|
|
37
49
|
const source = projectOverrides
|
|
38
50
|
? "project"
|
|
39
51
|
: Object.hasOwn(global, key)
|
|
40
52
|
? "global"
|
|
41
53
|
: "default";
|
|
42
|
-
const value = projectOverrides ? project[key] : (
|
|
54
|
+
const value = projectOverrides ? project[key] : (global[key] ?? CONFIG[key]);
|
|
43
55
|
return [key, { value, source, globalValue: global[key] ?? CONFIG[key] }];
|
|
44
56
|
}));
|
|
45
57
|
const secrets = Object.fromEntries(["memoryApiKey", "embeddingApiKey", "webServerApiToken", "webServerAuthPassword"].map((key) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CONFIG } from "../config.js";
|
|
2
|
+
type State = "installed" | "not-installed" | "unsupported" | "no-runtime" | "no-package";
|
|
3
|
+
export type WebAutostartStatus = {
|
|
4
|
+
state: State;
|
|
5
|
+
path?: string;
|
|
6
|
+
runtime?: string;
|
|
7
|
+
packagePath?: string;
|
|
8
|
+
command?: string;
|
|
9
|
+
};
|
|
10
|
+
export interface WebAutostartOptions {
|
|
11
|
+
home?: string;
|
|
12
|
+
platform?: string;
|
|
13
|
+
runtime?: string | null;
|
|
14
|
+
packageRoot?: string | null;
|
|
15
|
+
systemctlAvailable?: boolean;
|
|
16
|
+
run?: (command: string, args: string[]) => void;
|
|
17
|
+
start?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** OpenCode's executable is not a JavaScript runtime. */
|
|
20
|
+
export declare function resolveWebRuntime(execPath?: string, platform?: NodeJS.Platform): string | null;
|
|
21
|
+
/** Read the login item's state without changing any system files. */
|
|
22
|
+
export declare function webAutostartStatus(options?: WebAutostartOptions): WebAutostartStatus;
|
|
23
|
+
/** Install or update only the fixed-name OMMS item. `start` launches it now. */
|
|
24
|
+
export declare function installWebAutostart(options?: WebAutostartOptions): WebAutostartStatus;
|
|
25
|
+
/** Remove only an item carrying the OMMS marker. */
|
|
26
|
+
export declare function removeWebAutostart(options?: WebAutostartOptions): WebAutostartStatus;
|
|
27
|
+
/** Host start checks the global settings. A failed item cannot block a session. */
|
|
28
|
+
export declare function reconcileWebAutostart(config?: Pick<typeof CONFIG, "webServerAutoStart" | "webServerEnabled">, options?: WebAutostartOptions): void;
|
|
29
|
+
export {};
|