codemem 0.30.0-alpha.3 → 0.30.0-alpha.4
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/index.js +384 -176
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3156,11 +3156,193 @@ cmd$1.action((inputFile, opts) => {
|
|
|
3156
3156
|
});
|
|
3157
3157
|
var importMemoriesCommand = cmd$1;
|
|
3158
3158
|
//#endregion
|
|
3159
|
+
//#region src/maintenance-worker-runtime.ts
|
|
3160
|
+
var defaultLogger = {
|
|
3161
|
+
step: (message) => console.error(message),
|
|
3162
|
+
warn: (message) => console.error(`Warning: ${message}`),
|
|
3163
|
+
error: (message) => console.error(`Error: ${message}`)
|
|
3164
|
+
};
|
|
3165
|
+
function readWorkerSyncConfig(configPath) {
|
|
3166
|
+
return readCoordinatorSyncConfig(configPath ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile());
|
|
3167
|
+
}
|
|
3168
|
+
function createSequentialBackfillCoordinator(store, jobPlans, options) {
|
|
3169
|
+
const pollIntervalMs = 1e3;
|
|
3170
|
+
let activeRunner = null;
|
|
3171
|
+
let activePlan = null;
|
|
3172
|
+
let activePollTimer = null;
|
|
3173
|
+
let nextJobIndex = 0;
|
|
3174
|
+
let stopped = false;
|
|
3175
|
+
const clearPollTimer = () => {
|
|
3176
|
+
if (!activePollTimer) return;
|
|
3177
|
+
clearTimeout(activePollTimer);
|
|
3178
|
+
activePollTimer = null;
|
|
3179
|
+
};
|
|
3180
|
+
const schedulePoll = (fn) => {
|
|
3181
|
+
clearPollTimer();
|
|
3182
|
+
activePollTimer = setTimeout(fn, pollIntervalMs);
|
|
3183
|
+
if (typeof activePollTimer === "object" && "unref" in activePollTimer) activePollTimer.unref();
|
|
3184
|
+
};
|
|
3185
|
+
const startNextJob = () => {
|
|
3186
|
+
clearPollTimer();
|
|
3187
|
+
if (stopped || options.signal?.aborted) return;
|
|
3188
|
+
while (nextJobIndex < jobPlans.length) {
|
|
3189
|
+
const plan = jobPlans[nextJobIndex++];
|
|
3190
|
+
if (!plan) continue;
|
|
3191
|
+
if (!plan.isPending(store.db)) continue;
|
|
3192
|
+
activePlan = plan;
|
|
3193
|
+
activeRunner = plan.createRunner();
|
|
3194
|
+
options.logger.step(`${plan.name} backfill started`);
|
|
3195
|
+
activeRunner.start();
|
|
3196
|
+
schedulePoll(waitForCurrentJob);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
options.logger.step("All backfill jobs complete");
|
|
3200
|
+
};
|
|
3201
|
+
const waitForCurrentJob = () => {
|
|
3202
|
+
if (stopped || options.signal?.aborted || !activePlan || !activeRunner) return;
|
|
3203
|
+
const job = getMaintenanceJob(store.db, activePlan.kind);
|
|
3204
|
+
if (!activePlan.isPending(store.db)) {
|
|
3205
|
+
const finishedPlan = activePlan;
|
|
3206
|
+
const finishedRunner = activeRunner;
|
|
3207
|
+
activePlan = null;
|
|
3208
|
+
activeRunner = null;
|
|
3209
|
+
finishedRunner.stop().finally(() => {
|
|
3210
|
+
if (!stopped && !options.signal?.aborted) {
|
|
3211
|
+
options.logger.step(`${finishedPlan.name} backfill complete`);
|
|
3212
|
+
startNextJob();
|
|
3213
|
+
}
|
|
3214
|
+
});
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
if (job?.status === "failed") {
|
|
3218
|
+
const failedPlan = activePlan;
|
|
3219
|
+
const failedRunner = activeRunner;
|
|
3220
|
+
activePlan = null;
|
|
3221
|
+
activeRunner = null;
|
|
3222
|
+
failedRunner.stop().finally(() => {
|
|
3223
|
+
if (!stopped && !options.signal?.aborted) {
|
|
3224
|
+
options.logger.warn(`${failedPlan.name} backfill failed and will be retried on a later startup`);
|
|
3225
|
+
startNextJob();
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
schedulePoll(waitForCurrentJob);
|
|
3231
|
+
};
|
|
3232
|
+
return {
|
|
3233
|
+
start: () => {
|
|
3234
|
+
if (stopped || options.signal?.aborted) return;
|
|
3235
|
+
const pendingCount = jobPlans.filter((plan) => plan.isPending(store.db)).length;
|
|
3236
|
+
if (pendingCount === 0) return;
|
|
3237
|
+
options.logger.step(`${pendingCount} backfill job(s) pending — starting sequential runners`);
|
|
3238
|
+
startNextJob();
|
|
3239
|
+
},
|
|
3240
|
+
stop: async () => {
|
|
3241
|
+
stopped = true;
|
|
3242
|
+
clearPollTimer();
|
|
3243
|
+
const runner = activeRunner;
|
|
3244
|
+
activeRunner = null;
|
|
3245
|
+
activePlan = null;
|
|
3246
|
+
if (runner) await runner.stop();
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
function startMaintenanceWorkerRuntime(options = {}) {
|
|
3251
|
+
const logger = options.logger ?? defaultLogger;
|
|
3252
|
+
const dbPath = resolveDbPath(options.dbPath ?? void 0);
|
|
3253
|
+
const store = new MemoryStore(dbPath);
|
|
3254
|
+
const syncConfig = readWorkerSyncConfig(options.configPath);
|
|
3255
|
+
const runners = [];
|
|
3256
|
+
const walCheckpointTimer = setInterval(() => {
|
|
3257
|
+
try {
|
|
3258
|
+
store.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
logger.warn(`WAL checkpoint failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3261
|
+
}
|
|
3262
|
+
}, 300 * 1e3);
|
|
3263
|
+
walCheckpointTimer.unref();
|
|
3264
|
+
if (!isEmbeddingDisabled()) runners.push(new VectorModelMigrationRunner({
|
|
3265
|
+
dbPath,
|
|
3266
|
+
signal: options.signal,
|
|
3267
|
+
idleIntervalMs: 5e3
|
|
3268
|
+
}));
|
|
3269
|
+
runners.push(createSequentialBackfillCoordinator(store, [
|
|
3270
|
+
{
|
|
3271
|
+
name: "Sharing-domain",
|
|
3272
|
+
kind: SCOPE_BACKFILL_JOB,
|
|
3273
|
+
isPending: hasPendingScopeBackfill,
|
|
3274
|
+
createRunner: () => new ScopeBackfillRunner({
|
|
3275
|
+
dbPath,
|
|
3276
|
+
signal: options.signal
|
|
3277
|
+
})
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
name: "Dedup-key",
|
|
3281
|
+
kind: DEDUP_KEY_BACKFILL_JOB,
|
|
3282
|
+
isPending: hasPendingDedupKeyBackfill,
|
|
3283
|
+
createRunner: () => new DedupKeyBackfillRunner({
|
|
3284
|
+
dbPath,
|
|
3285
|
+
signal: options.signal
|
|
3286
|
+
})
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
name: "Session-context",
|
|
3290
|
+
kind: SESSION_CONTEXT_BACKFILL_JOB,
|
|
3291
|
+
isPending: hasPendingSessionContextBackfill,
|
|
3292
|
+
createRunner: () => new SessionContextBackfillRunner({
|
|
3293
|
+
dbPath,
|
|
3294
|
+
signal: options.signal
|
|
3295
|
+
})
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
name: "Ref",
|
|
3299
|
+
kind: REF_BACKFILL_JOB,
|
|
3300
|
+
isPending: hasPendingRefBackfill,
|
|
3301
|
+
createRunner: () => new RefBackfillRunner({
|
|
3302
|
+
dbPath,
|
|
3303
|
+
signal: options.signal
|
|
3304
|
+
})
|
|
3305
|
+
},
|
|
3306
|
+
{
|
|
3307
|
+
name: "Session-summary dedup",
|
|
3308
|
+
kind: SUMMARY_DEDUP_BACKFILL_JOB,
|
|
3309
|
+
isPending: hasPendingSummaryDedupBackfill,
|
|
3310
|
+
createRunner: () => new SummaryDedupBackfillRunner({
|
|
3311
|
+
dbPath,
|
|
3312
|
+
deviceId: store.deviceId,
|
|
3313
|
+
signal: options.signal
|
|
3314
|
+
})
|
|
3315
|
+
}
|
|
3316
|
+
], {
|
|
3317
|
+
signal: options.signal,
|
|
3318
|
+
logger
|
|
3319
|
+
}));
|
|
3320
|
+
if (syncConfig.syncRetentionEnabled) runners.push(new SyncRetentionRunner({
|
|
3321
|
+
dbPath,
|
|
3322
|
+
signal: options.signal
|
|
3323
|
+
}));
|
|
3324
|
+
for (const runner of runners) runner.start();
|
|
3325
|
+
logger.step("Maintenance worker started");
|
|
3326
|
+
return {
|
|
3327
|
+
start: () => {
|
|
3328
|
+
for (const runner of runners) runner.start();
|
|
3329
|
+
},
|
|
3330
|
+
stop: async () => {
|
|
3331
|
+
clearInterval(walCheckpointTimer);
|
|
3332
|
+
for (const runner of [...runners].reverse()) await runner.stop();
|
|
3333
|
+
store.close();
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
//#endregion
|
|
3159
3338
|
//#region src/commands/maintenance.ts
|
|
3160
3339
|
var maintenanceCmd = new Command("maintenance").configureHelp(helpStyle).description("Inspect background maintenance / backfill jobs");
|
|
3161
3340
|
var statusCmd$1 = new Command("status").configureHelp(helpStyle).description("Print current status of all maintenance jobs");
|
|
3162
3341
|
addDbOption(statusCmd$1);
|
|
3163
3342
|
addJsonOption(statusCmd$1);
|
|
3343
|
+
var workerCmd = new Command("worker").configureHelp(helpStyle).description("Run maintenance worker plumbing");
|
|
3344
|
+
addDbOption(workerCmd);
|
|
3345
|
+
addConfigOption(workerCmd);
|
|
3164
3346
|
statusCmd$1.action((opts) => {
|
|
3165
3347
|
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
3166
3348
|
try {
|
|
@@ -3174,7 +3356,48 @@ statusCmd$1.action((opts) => {
|
|
|
3174
3356
|
store.close();
|
|
3175
3357
|
}
|
|
3176
3358
|
});
|
|
3359
|
+
async function runMaintenanceWorkerCommand(opts) {
|
|
3360
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
3361
|
+
if (opts.config) process.env.CODEMEM_CONFIG = opts.config;
|
|
3362
|
+
process.env.CODEMEM_DB = dbPath;
|
|
3363
|
+
const abort = new AbortController();
|
|
3364
|
+
const runtime = startMaintenanceWorkerRuntime({
|
|
3365
|
+
dbPath,
|
|
3366
|
+
configPath: opts.config ?? null,
|
|
3367
|
+
signal: abort.signal
|
|
3368
|
+
});
|
|
3369
|
+
const keepAlive = setInterval(() => void 0, 6e4);
|
|
3370
|
+
let stopping = false;
|
|
3371
|
+
const shutdown = async () => {
|
|
3372
|
+
if (stopping) return;
|
|
3373
|
+
stopping = true;
|
|
3374
|
+
abort.abort();
|
|
3375
|
+
clearInterval(keepAlive);
|
|
3376
|
+
const forceExit = setTimeout(() => {
|
|
3377
|
+
process.exit(1);
|
|
3378
|
+
}, 3e4);
|
|
3379
|
+
try {
|
|
3380
|
+
await runtime.stop();
|
|
3381
|
+
process.exit(0);
|
|
3382
|
+
} finally {
|
|
3383
|
+
clearTimeout(forceExit);
|
|
3384
|
+
}
|
|
3385
|
+
};
|
|
3386
|
+
await new Promise((resolve, reject) => {
|
|
3387
|
+
process.once("SIGINT", () => void shutdown().then(resolve, reject));
|
|
3388
|
+
process.once("SIGTERM", () => void shutdown().then(resolve, reject));
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
workerCmd.action(async (opts) => {
|
|
3392
|
+
try {
|
|
3393
|
+
await runMaintenanceWorkerCommand(opts);
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3396
|
+
process.exitCode = 1;
|
|
3397
|
+
}
|
|
3398
|
+
});
|
|
3177
3399
|
maintenanceCmd.addCommand(statusCmd$1);
|
|
3400
|
+
maintenanceCmd.addCommand(workerCmd, { hidden: true });
|
|
3178
3401
|
var maintenanceCommand = maintenanceCmd;
|
|
3179
3402
|
function printJobsTable(jobs) {
|
|
3180
3403
|
p.intro("codemem maintenance");
|
|
@@ -4187,7 +4410,7 @@ function warnIfViewerExposed(host, port) {
|
|
|
4187
4410
|
function isLikelyViewerCommand(command) {
|
|
4188
4411
|
const lowered = command.toLowerCase();
|
|
4189
4412
|
if (!/\bserve\s+start\b/.test(lowered)) return false;
|
|
4190
|
-
return lowered.includes("codemem") || lowered.includes("packages/cli/dist/index.js") || lowered.includes("/cli/dist/index.js");
|
|
4413
|
+
return lowered.includes("codemem") || lowered.includes("packages/cli/dist/index.js") || lowered.includes("/cli/dist/index.js") || lowered.includes("packages/cli/src/index.ts");
|
|
4191
4414
|
}
|
|
4192
4415
|
function prepareViewerDatabase(dbPath) {
|
|
4193
4416
|
return initDatabase(dbPath ?? void 0).path;
|
|
@@ -4237,6 +4460,44 @@ function isTrustedViewerPid(pid, target, listenerPid) {
|
|
|
4237
4460
|
function pidFilePath(dbPath) {
|
|
4238
4461
|
return join(dirname(dbPath), "viewer.pid");
|
|
4239
4462
|
}
|
|
4463
|
+
function maintenanceWorkerPidFilePath(dbPath) {
|
|
4464
|
+
return join(dirname(dbPath), "maintenance-worker.pid");
|
|
4465
|
+
}
|
|
4466
|
+
function readMaintenanceWorkerPidRecord(dbPath) {
|
|
4467
|
+
const pidPath = maintenanceWorkerPidFilePath(dbPath);
|
|
4468
|
+
if (!existsSync(pidPath)) return null;
|
|
4469
|
+
const raw = readFileSync(pidPath, "utf-8").trim();
|
|
4470
|
+
try {
|
|
4471
|
+
const parsed = JSON.parse(raw);
|
|
4472
|
+
if (typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0) return {
|
|
4473
|
+
pid: Math.trunc(parsed),
|
|
4474
|
+
dbPath: null
|
|
4475
|
+
};
|
|
4476
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.pid === "number" && Number.isFinite(parsed.pid) && parsed.pid > 0) return {
|
|
4477
|
+
pid: Math.trunc(parsed.pid),
|
|
4478
|
+
dbPath: typeof parsed.dbPath === "string" ? resolveDbPath(parsed.dbPath) : null
|
|
4479
|
+
};
|
|
4480
|
+
} catch {
|
|
4481
|
+
const parsed = Number.parseInt(raw, 10);
|
|
4482
|
+
if (Number.isFinite(parsed) && parsed > 0) return {
|
|
4483
|
+
pid: parsed,
|
|
4484
|
+
dbPath: null
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
return null;
|
|
4488
|
+
}
|
|
4489
|
+
function isLikelyMaintenanceWorkerCommand(command) {
|
|
4490
|
+
const lowered = command.toLowerCase();
|
|
4491
|
+
if (!/\bmaintenance\s+worker\b/.test(lowered)) return false;
|
|
4492
|
+
return lowered.includes("codemem") || lowered.includes("packages/cli/dist/index.js") || lowered.includes("/cli/dist/index.js") || lowered.includes("packages/cli/src/index.ts");
|
|
4493
|
+
}
|
|
4494
|
+
function escapeRegExp(value) {
|
|
4495
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4496
|
+
}
|
|
4497
|
+
function commandHasExactDbPath(command, dbPath) {
|
|
4498
|
+
const escapedPath = escapeRegExp(resolveDbPath(dbPath));
|
|
4499
|
+
return new RegExp(`(?:^|\\s)--db-path(?:=|\\s+)${escapedPath}(?:\\s|$)`).test(command);
|
|
4500
|
+
}
|
|
4240
4501
|
function readViewerPidRecord(dbPath) {
|
|
4241
4502
|
const pidPath = pidFilePath(dbPath);
|
|
4242
4503
|
if (!existsSync(pidPath)) return null;
|
|
@@ -4327,6 +4588,44 @@ async function waitForProcessExit(pid, timeoutMs = 3e4) {
|
|
|
4327
4588
|
}
|
|
4328
4589
|
return !isProcessRunning(pid);
|
|
4329
4590
|
}
|
|
4591
|
+
async function terminateProcessPid(pid, timeouts = {}) {
|
|
4592
|
+
try {
|
|
4593
|
+
process.kill(pid, "SIGTERM");
|
|
4594
|
+
} catch {
|
|
4595
|
+
return true;
|
|
4596
|
+
}
|
|
4597
|
+
if (await waitForProcessExit(pid, timeouts.gracefulMs ?? 3e4)) return true;
|
|
4598
|
+
try {
|
|
4599
|
+
process.kill(pid, "SIGKILL");
|
|
4600
|
+
} catch {
|
|
4601
|
+
return true;
|
|
4602
|
+
}
|
|
4603
|
+
return waitForProcessExit(pid, timeouts.forceMs ?? 5e3);
|
|
4604
|
+
}
|
|
4605
|
+
async function terminateTrustedViewerPid(pid, timeouts = {}) {
|
|
4606
|
+
return terminateProcessPid(pid, timeouts);
|
|
4607
|
+
}
|
|
4608
|
+
async function terminateTrustedMaintenanceWorker(dbPath, timeouts = {}) {
|
|
4609
|
+
const pidPath = maintenanceWorkerPidFilePath(dbPath);
|
|
4610
|
+
const record = readMaintenanceWorkerPidRecord(dbPath);
|
|
4611
|
+
if (!record) return true;
|
|
4612
|
+
const expectedDbPath = resolveDbPath(dbPath);
|
|
4613
|
+
if (record.dbPath && record.dbPath !== expectedDbPath) return false;
|
|
4614
|
+
if (!isProcessRunning(record.pid)) {
|
|
4615
|
+
try {
|
|
4616
|
+
rmSync(pidPath);
|
|
4617
|
+
} catch {}
|
|
4618
|
+
return true;
|
|
4619
|
+
}
|
|
4620
|
+
const command = readProcessCommand(record.pid);
|
|
4621
|
+
if (!command || !isLikelyMaintenanceWorkerCommand(command)) return false;
|
|
4622
|
+
if (record.dbPath !== expectedDbPath || !commandHasExactDbPath(command, expectedDbPath)) return false;
|
|
4623
|
+
const stopped = await terminateProcessPid(record.pid, timeouts);
|
|
4624
|
+
if (stopped) try {
|
|
4625
|
+
rmSync(pidPath);
|
|
4626
|
+
} catch {}
|
|
4627
|
+
return stopped;
|
|
4628
|
+
}
|
|
4330
4629
|
async function waitForPortRelease(host, port, timeoutMs = 1e4) {
|
|
4331
4630
|
const deadline = Date.now() + timeoutMs;
|
|
4332
4631
|
while (Date.now() < deadline) {
|
|
@@ -4336,21 +4635,13 @@ async function waitForPortRelease(host, port, timeoutMs = 1e4) {
|
|
|
4336
4635
|
return false;
|
|
4337
4636
|
}
|
|
4338
4637
|
async function stopExistingViewer(dbPath, target) {
|
|
4339
|
-
const terminatePid = async (pid) => {
|
|
4340
|
-
try {
|
|
4341
|
-
process.kill(pid, "SIGTERM");
|
|
4342
|
-
return await waitForProcessExit(pid);
|
|
4343
|
-
} catch {
|
|
4344
|
-
return true;
|
|
4345
|
-
}
|
|
4346
|
-
};
|
|
4347
4638
|
const pidPath = pidFilePath(dbPath);
|
|
4348
4639
|
const record = readViewerPidRecord(dbPath);
|
|
4349
4640
|
const viewerPidFromStats = await lookupViewerPidFromStats(target.host, target.port);
|
|
4350
4641
|
const listenerPid = lookupListeningPid(target.host, target.port);
|
|
4351
4642
|
const viewerPid = pickViewerPidCandidate(viewerPidFromStats, listenerPid);
|
|
4352
4643
|
if (viewerPid && isTrustedViewerPid(viewerPid, target, listenerPid)) {
|
|
4353
|
-
if (!await
|
|
4644
|
+
if (!await terminateTrustedViewerPid(viewerPid)) return {
|
|
4354
4645
|
stopped: false,
|
|
4355
4646
|
pid: viewerPid
|
|
4356
4647
|
};
|
|
@@ -4366,12 +4657,19 @@ async function stopExistingViewer(dbPath, target) {
|
|
|
4366
4657
|
stopped: false,
|
|
4367
4658
|
pid: null
|
|
4368
4659
|
};
|
|
4369
|
-
|
|
4370
|
-
|
|
4660
|
+
const recordListenerPid = lookupListeningPid(record.host, record.port);
|
|
4661
|
+
if (await respondsLikeCodememViewer(record) && isTrustedViewerPid(record.pid, {
|
|
4662
|
+
host: record.host,
|
|
4663
|
+
port: record.port
|
|
4664
|
+
}, recordListenerPid)) {
|
|
4665
|
+
if (!await terminateTrustedViewerPid(record.pid)) return {
|
|
4371
4666
|
stopped: false,
|
|
4372
4667
|
pid: record.pid
|
|
4373
4668
|
};
|
|
4374
|
-
}
|
|
4669
|
+
} else return {
|
|
4670
|
+
stopped: false,
|
|
4671
|
+
pid: null
|
|
4672
|
+
};
|
|
4375
4673
|
try {
|
|
4376
4674
|
rmSync(pidPath);
|
|
4377
4675
|
} catch {}
|
|
@@ -4395,6 +4693,56 @@ function buildForegroundRunnerArgs(scriptPath, invocation, execArgv = process.ex
|
|
|
4395
4693
|
if (invocation.dbPath) args.push("--db-path", invocation.dbPath);
|
|
4396
4694
|
return args;
|
|
4397
4695
|
}
|
|
4696
|
+
function buildMaintenanceWorkerArgs(scriptPath, invocation, execArgv = process.execArgv) {
|
|
4697
|
+
const args = [
|
|
4698
|
+
...execArgv,
|
|
4699
|
+
scriptPath,
|
|
4700
|
+
"maintenance",
|
|
4701
|
+
"worker"
|
|
4702
|
+
];
|
|
4703
|
+
if (invocation.dbPath) args.push("--db-path", invocation.dbPath);
|
|
4704
|
+
if (invocation.configPath) args.push("--config", invocation.configPath);
|
|
4705
|
+
return args;
|
|
4706
|
+
}
|
|
4707
|
+
function startMaintenanceWorkerProcess(invocation) {
|
|
4708
|
+
const scriptPath = process.argv[1];
|
|
4709
|
+
if (!scriptPath) {
|
|
4710
|
+
p.log.warn("Unable to resolve CLI entrypoint for maintenance worker launch");
|
|
4711
|
+
return null;
|
|
4712
|
+
}
|
|
4713
|
+
const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
|
|
4714
|
+
const child = spawn(process.execPath, buildMaintenanceWorkerArgs(scriptPath, invocation), {
|
|
4715
|
+
cwd: process.cwd(),
|
|
4716
|
+
stdio: "ignore",
|
|
4717
|
+
env: {
|
|
4718
|
+
...process.env,
|
|
4719
|
+
CODEMEM_DB: dbPath,
|
|
4720
|
+
...invocation.configPath ? { CODEMEM_CONFIG: invocation.configPath } : {}
|
|
4721
|
+
}
|
|
4722
|
+
});
|
|
4723
|
+
child.unref();
|
|
4724
|
+
if (child.pid) {
|
|
4725
|
+
writeFileSync(maintenanceWorkerPidFilePath(dbPath), JSON.stringify({
|
|
4726
|
+
pid: child.pid,
|
|
4727
|
+
dbPath
|
|
4728
|
+
}), "utf-8");
|
|
4729
|
+
p.log.step(`Maintenance worker started (pid ${child.pid})`);
|
|
4730
|
+
}
|
|
4731
|
+
return child;
|
|
4732
|
+
}
|
|
4733
|
+
async function stopMaintenanceWorkerProcess(child, dbPath) {
|
|
4734
|
+
if (child?.pid && isProcessRunning(child.pid)) await terminateProcessPid(child.pid, {
|
|
4735
|
+
gracefulMs: 5e3,
|
|
4736
|
+
forceMs: 5e3
|
|
4737
|
+
});
|
|
4738
|
+
else await terminateTrustedMaintenanceWorker(dbPath, {
|
|
4739
|
+
gracefulMs: 5e3,
|
|
4740
|
+
forceMs: 5e3
|
|
4741
|
+
});
|
|
4742
|
+
try {
|
|
4743
|
+
rmSync(maintenanceWorkerPidFilePath(dbPath));
|
|
4744
|
+
} catch {}
|
|
4745
|
+
}
|
|
4398
4746
|
function isSqliteVecLoadFailure(error) {
|
|
4399
4747
|
if (!(error instanceof Error)) return false;
|
|
4400
4748
|
const text = error.message.toLowerCase();
|
|
@@ -4442,88 +4790,6 @@ async function startBackgroundViewer(invocation) {
|
|
|
4442
4790
|
p.intro("codemem viewer");
|
|
4443
4791
|
p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
|
|
4444
4792
|
}
|
|
4445
|
-
function createSequentialBackfillCoordinator(store, jobPlans, signal) {
|
|
4446
|
-
const pollIntervalMs = 1e3;
|
|
4447
|
-
let activeRunner = null;
|
|
4448
|
-
let activePlan = null;
|
|
4449
|
-
let activePollTimer = null;
|
|
4450
|
-
let nextJobIndex = 0;
|
|
4451
|
-
let stopped = false;
|
|
4452
|
-
const clearPollTimer = () => {
|
|
4453
|
-
if (!activePollTimer) return;
|
|
4454
|
-
clearTimeout(activePollTimer);
|
|
4455
|
-
activePollTimer = null;
|
|
4456
|
-
};
|
|
4457
|
-
const schedulePoll = (fn) => {
|
|
4458
|
-
clearPollTimer();
|
|
4459
|
-
activePollTimer = setTimeout(fn, pollIntervalMs);
|
|
4460
|
-
if (typeof activePollTimer === "object" && "unref" in activePollTimer) activePollTimer.unref();
|
|
4461
|
-
};
|
|
4462
|
-
const waitForCurrentJob = () => {
|
|
4463
|
-
if (stopped || signal?.aborted || !activePlan || !activeRunner) return;
|
|
4464
|
-
const job = getMaintenanceJob(store.db, activePlan.kind);
|
|
4465
|
-
if (!activePlan.isPending(store.db)) {
|
|
4466
|
-
const finishedPlan = activePlan;
|
|
4467
|
-
const finishedRunner = activeRunner;
|
|
4468
|
-
activePlan = null;
|
|
4469
|
-
activeRunner = null;
|
|
4470
|
-
finishedRunner.stop().finally(() => {
|
|
4471
|
-
if (!stopped && !signal?.aborted) {
|
|
4472
|
-
p.log.step(`${finishedPlan.name} backfill complete`);
|
|
4473
|
-
startNextJob();
|
|
4474
|
-
}
|
|
4475
|
-
});
|
|
4476
|
-
return;
|
|
4477
|
-
}
|
|
4478
|
-
if (job?.status === "failed") {
|
|
4479
|
-
const failedPlan = activePlan;
|
|
4480
|
-
const failedRunner = activeRunner;
|
|
4481
|
-
activePlan = null;
|
|
4482
|
-
activeRunner = null;
|
|
4483
|
-
failedRunner.stop().finally(() => {
|
|
4484
|
-
if (!stopped && !signal?.aborted) {
|
|
4485
|
-
p.log.warn(`${failedPlan.name} backfill failed and will be retried on a later startup`);
|
|
4486
|
-
startNextJob();
|
|
4487
|
-
}
|
|
4488
|
-
});
|
|
4489
|
-
return;
|
|
4490
|
-
}
|
|
4491
|
-
schedulePoll(waitForCurrentJob);
|
|
4492
|
-
};
|
|
4493
|
-
const startNextJob = () => {
|
|
4494
|
-
clearPollTimer();
|
|
4495
|
-
if (stopped || signal?.aborted) return;
|
|
4496
|
-
while (nextJobIndex < jobPlans.length) {
|
|
4497
|
-
const plan = jobPlans[nextJobIndex++];
|
|
4498
|
-
if (!plan) continue;
|
|
4499
|
-
if (!plan.isPending(store.db)) continue;
|
|
4500
|
-
activePlan = plan;
|
|
4501
|
-
activeRunner = plan.createRunner();
|
|
4502
|
-
p.log.step(`${plan.name} backfill started`);
|
|
4503
|
-
activeRunner.start();
|
|
4504
|
-
schedulePoll(waitForCurrentJob);
|
|
4505
|
-
return;
|
|
4506
|
-
}
|
|
4507
|
-
p.log.step("All backfill jobs complete");
|
|
4508
|
-
};
|
|
4509
|
-
return {
|
|
4510
|
-
start: () => {
|
|
4511
|
-
if (stopped || signal?.aborted) return;
|
|
4512
|
-
const pendingCount = jobPlans.filter((plan) => plan.isPending(store.db)).length;
|
|
4513
|
-
if (pendingCount === 0) return;
|
|
4514
|
-
p.log.step(`${pendingCount} backfill job(s) pending — starting sequential runners`);
|
|
4515
|
-
startNextJob();
|
|
4516
|
-
},
|
|
4517
|
-
stop: async () => {
|
|
4518
|
-
stopped = true;
|
|
4519
|
-
clearPollTimer();
|
|
4520
|
-
const runner = activeRunner;
|
|
4521
|
-
activeRunner = null;
|
|
4522
|
-
activePlan = null;
|
|
4523
|
-
if (runner) await runner.stop();
|
|
4524
|
-
}
|
|
4525
|
-
};
|
|
4526
|
-
}
|
|
4527
4793
|
async function startForegroundViewer(invocation) {
|
|
4528
4794
|
const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
|
|
4529
4795
|
const { serve } = await import("@hono/node-server");
|
|
@@ -4552,67 +4818,13 @@ async function startForegroundViewer(invocation) {
|
|
|
4552
4818
|
const sweeper = new RawEventSweeper(store, { observer });
|
|
4553
4819
|
sweeper.start();
|
|
4554
4820
|
const syncAbort = new AbortController();
|
|
4555
|
-
const retentionAbort = new AbortController();
|
|
4556
|
-
const backfillAbort = new AbortController();
|
|
4557
4821
|
const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
|
|
4558
4822
|
const syncEnabled = syncConfig.syncEnabled;
|
|
4559
4823
|
const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
});
|
|
4564
|
-
const vectorMigrationRunner = new VectorModelMigrationRunner({
|
|
4565
|
-
dbPath,
|
|
4566
|
-
signal: backfillAbort.signal
|
|
4567
|
-
});
|
|
4568
|
-
const backfillCoordinator = createSequentialBackfillCoordinator(store, [
|
|
4569
|
-
{
|
|
4570
|
-
name: "Sharing-domain",
|
|
4571
|
-
kind: SCOPE_BACKFILL_JOB,
|
|
4572
|
-
isPending: hasPendingScopeBackfill,
|
|
4573
|
-
createRunner: () => new ScopeBackfillRunner({
|
|
4574
|
-
dbPath,
|
|
4575
|
-
signal: backfillAbort.signal
|
|
4576
|
-
})
|
|
4577
|
-
},
|
|
4578
|
-
{
|
|
4579
|
-
name: "Dedup-key",
|
|
4580
|
-
kind: DEDUP_KEY_BACKFILL_JOB,
|
|
4581
|
-
isPending: hasPendingDedupKeyBackfill,
|
|
4582
|
-
createRunner: () => new DedupKeyBackfillRunner({
|
|
4583
|
-
dbPath,
|
|
4584
|
-
signal: backfillAbort.signal
|
|
4585
|
-
})
|
|
4586
|
-
},
|
|
4587
|
-
{
|
|
4588
|
-
name: "Session-context",
|
|
4589
|
-
kind: SESSION_CONTEXT_BACKFILL_JOB,
|
|
4590
|
-
isPending: hasPendingSessionContextBackfill,
|
|
4591
|
-
createRunner: () => new SessionContextBackfillRunner({
|
|
4592
|
-
dbPath,
|
|
4593
|
-
signal: backfillAbort.signal
|
|
4594
|
-
})
|
|
4595
|
-
},
|
|
4596
|
-
{
|
|
4597
|
-
name: "Ref",
|
|
4598
|
-
kind: REF_BACKFILL_JOB,
|
|
4599
|
-
isPending: hasPendingRefBackfill,
|
|
4600
|
-
createRunner: () => new RefBackfillRunner({
|
|
4601
|
-
dbPath,
|
|
4602
|
-
signal: backfillAbort.signal
|
|
4603
|
-
})
|
|
4604
|
-
},
|
|
4605
|
-
{
|
|
4606
|
-
name: "Session-summary dedup",
|
|
4607
|
-
kind: SUMMARY_DEDUP_BACKFILL_JOB,
|
|
4608
|
-
isPending: hasPendingSummaryDedupBackfill,
|
|
4609
|
-
createRunner: () => new SummaryDedupBackfillRunner({
|
|
4610
|
-
dbPath,
|
|
4611
|
-
deviceId: store.deviceId,
|
|
4612
|
-
signal: backfillAbort.signal
|
|
4613
|
-
})
|
|
4614
|
-
}
|
|
4615
|
-
], backfillAbort.signal);
|
|
4824
|
+
if (!await terminateTrustedMaintenanceWorker(dbPath, {
|
|
4825
|
+
gracefulMs: 1e3,
|
|
4826
|
+
forceMs: 5e3
|
|
4827
|
+
})) p.log.warn("Existing maintenance worker is not trusted or did not stop; starting viewer anyway");
|
|
4616
4828
|
const syncRuntimeStatus = {
|
|
4617
4829
|
phase: syncEnabled ? "starting" : "disabled",
|
|
4618
4830
|
detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
|
|
@@ -4625,6 +4837,7 @@ async function startForegroundViewer(invocation) {
|
|
|
4625
4837
|
};
|
|
4626
4838
|
const app = createApp(appOpts);
|
|
4627
4839
|
const pidPath = pidFilePath(dbPath);
|
|
4840
|
+
let maintenanceWorker = null;
|
|
4628
4841
|
let syncServer = null;
|
|
4629
4842
|
let syncListenerReady = false;
|
|
4630
4843
|
if (syncEnabled) {
|
|
@@ -4655,15 +4868,7 @@ async function startForegroundViewer(invocation) {
|
|
|
4655
4868
|
p.log.success(`Listening on http://${info.address}:${info.port}`);
|
|
4656
4869
|
p.log.info(`Database: ${preparedDb}`);
|
|
4657
4870
|
p.log.step("Raw event sweeper started");
|
|
4658
|
-
|
|
4659
|
-
vectorMigrationRunner.start();
|
|
4660
|
-
p.log.step("Vector maintenance runner started");
|
|
4661
|
-
}
|
|
4662
|
-
backfillCoordinator.start();
|
|
4663
|
-
if (syncConfig.syncRetentionEnabled) {
|
|
4664
|
-
retentionRunner.start();
|
|
4665
|
-
p.log.step("Retention maintenance runner started");
|
|
4666
|
-
}
|
|
4871
|
+
maintenanceWorker = startMaintenanceWorkerProcess(invocation);
|
|
4667
4872
|
if (syncEnabled) {
|
|
4668
4873
|
const syncStartDelayMs = 3e3;
|
|
4669
4874
|
p.log.step(`Sync daemon will start in background (${syncStartDelayMs / 1e3}s delay)`);
|
|
@@ -4705,24 +4910,11 @@ async function startForegroundViewer(invocation) {
|
|
|
4705
4910
|
else p.log.error(err.message);
|
|
4706
4911
|
process.exit(1);
|
|
4707
4912
|
});
|
|
4708
|
-
const walCheckpointTimer = setInterval(() => {
|
|
4709
|
-
try {
|
|
4710
|
-
store.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
4711
|
-
} catch (err) {
|
|
4712
|
-
p.log.warn(`WAL checkpoint failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4713
|
-
}
|
|
4714
|
-
}, 300 * 1e3);
|
|
4715
|
-
walCheckpointTimer.unref();
|
|
4716
4913
|
const shutdown = async () => {
|
|
4717
4914
|
p.outro("shutting down");
|
|
4718
|
-
clearInterval(walCheckpointTimer);
|
|
4719
4915
|
syncAbort.abort();
|
|
4720
|
-
|
|
4721
|
-
backfillAbort.abort();
|
|
4916
|
+
await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
|
|
4722
4917
|
await sweeper.stop();
|
|
4723
|
-
await vectorMigrationRunner.stop();
|
|
4724
|
-
await retentionRunner.stop();
|
|
4725
|
-
await backfillCoordinator.stop();
|
|
4726
4918
|
await new Promise((resolve) => {
|
|
4727
4919
|
let remaining = syncServer ? 2 : 1;
|
|
4728
4920
|
const done = () => {
|
|
@@ -4739,9 +4931,15 @@ async function startForegroundViewer(invocation) {
|
|
|
4739
4931
|
};
|
|
4740
4932
|
const forceShutdown = () => {
|
|
4741
4933
|
setTimeout(() => {
|
|
4934
|
+
if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
|
|
4935
|
+
process.kill(maintenanceWorker.pid, "SIGKILL");
|
|
4936
|
+
} catch {}
|
|
4742
4937
|
try {
|
|
4743
4938
|
rmSync(pidPath);
|
|
4744
4939
|
} catch {}
|
|
4940
|
+
try {
|
|
4941
|
+
rmSync(maintenanceWorkerPidFilePath(dbPath));
|
|
4942
|
+
} catch {}
|
|
4745
4943
|
closeStore();
|
|
4746
4944
|
process.exit(1);
|
|
4747
4945
|
}, 3e4).unref();
|
|
@@ -4774,8 +4972,13 @@ async function runServeInvocation(invocation) {
|
|
|
4774
4972
|
port: invocation.port
|
|
4775
4973
|
});
|
|
4776
4974
|
if (result.stopped) {
|
|
4975
|
+
const workerStopped = await terminateTrustedMaintenanceWorker(dbPath, {
|
|
4976
|
+
gracefulMs: 5e3,
|
|
4977
|
+
forceMs: 5e3
|
|
4978
|
+
});
|
|
4777
4979
|
p.intro("codemem viewer");
|
|
4778
4980
|
p.log.success(`Stopped viewer${result.pid ? ` (pid ${result.pid})` : ""}`);
|
|
4981
|
+
if (!workerStopped) p.log.warn("Maintenance worker pidfile exists but did not match trusted worker command");
|
|
4779
4982
|
if (invocation.mode === "stop") {
|
|
4780
4983
|
p.outro("done");
|
|
4781
4984
|
return;
|
|
@@ -4787,7 +4990,12 @@ async function runServeInvocation(invocation) {
|
|
|
4787
4990
|
process.exitCode = 1;
|
|
4788
4991
|
return;
|
|
4789
4992
|
} else if (invocation.mode === "stop") {
|
|
4993
|
+
const workerStopped = await terminateTrustedMaintenanceWorker(dbPath, {
|
|
4994
|
+
gracefulMs: 5e3,
|
|
4995
|
+
forceMs: 5e3
|
|
4996
|
+
});
|
|
4790
4997
|
p.intro("codemem viewer");
|
|
4998
|
+
if (!workerStopped) p.log.warn("Maintenance worker pidfile exists but did not match trusted worker command");
|
|
4791
4999
|
p.outro("No background viewer found");
|
|
4792
5000
|
return;
|
|
4793
5001
|
}
|