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