codemem 0.30.0-alpha.2 → 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 +428 -182
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3118,11 +3118,22 @@ cmd$1.action((inputFile, opts) => {
|
|
|
3118
3118
|
`Prompts: ${payload.user_prompts.length.toLocaleString()}`
|
|
3119
3119
|
].join("\n"));
|
|
3120
3120
|
}
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3121
|
+
let result;
|
|
3122
|
+
try {
|
|
3123
|
+
result = importMemories(payload, {
|
|
3124
|
+
dbPath: resolveDbPath(resolveDbOpt(opts)),
|
|
3125
|
+
remapProject: opts.remapProject,
|
|
3126
|
+
dryRun: opts.dryRun
|
|
3127
|
+
});
|
|
3128
|
+
} catch (error) {
|
|
3129
|
+
const message = error instanceof Error ? error.message : "Import failed";
|
|
3130
|
+
if (opts.json) emitJsonError("import_failed", message);
|
|
3131
|
+
else {
|
|
3132
|
+
p.log.error(message);
|
|
3133
|
+
process.exitCode = 1;
|
|
3134
|
+
}
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3126
3137
|
if (opts.json) {
|
|
3127
3138
|
console.log(JSON.stringify({
|
|
3128
3139
|
sessions: result.sessions,
|
|
@@ -3145,11 +3156,193 @@ cmd$1.action((inputFile, opts) => {
|
|
|
3145
3156
|
});
|
|
3146
3157
|
var importMemoriesCommand = cmd$1;
|
|
3147
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
|
|
3148
3338
|
//#region src/commands/maintenance.ts
|
|
3149
3339
|
var maintenanceCmd = new Command("maintenance").configureHelp(helpStyle).description("Inspect background maintenance / backfill jobs");
|
|
3150
3340
|
var statusCmd$1 = new Command("status").configureHelp(helpStyle).description("Print current status of all maintenance jobs");
|
|
3151
3341
|
addDbOption(statusCmd$1);
|
|
3152
3342
|
addJsonOption(statusCmd$1);
|
|
3343
|
+
var workerCmd = new Command("worker").configureHelp(helpStyle).description("Run maintenance worker plumbing");
|
|
3344
|
+
addDbOption(workerCmd);
|
|
3345
|
+
addConfigOption(workerCmd);
|
|
3153
3346
|
statusCmd$1.action((opts) => {
|
|
3154
3347
|
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
3155
3348
|
try {
|
|
@@ -3163,7 +3356,48 @@ statusCmd$1.action((opts) => {
|
|
|
3163
3356
|
store.close();
|
|
3164
3357
|
}
|
|
3165
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
|
+
});
|
|
3166
3399
|
maintenanceCmd.addCommand(statusCmd$1);
|
|
3400
|
+
maintenanceCmd.addCommand(workerCmd, { hidden: true });
|
|
3167
3401
|
var maintenanceCommand = maintenanceCmd;
|
|
3168
3402
|
function printJobsTable(jobs) {
|
|
3169
3403
|
p.intro("codemem maintenance");
|
|
@@ -3343,6 +3577,14 @@ function forgetMemoryAction(idStr, opts) {
|
|
|
3343
3577
|
}
|
|
3344
3578
|
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
3345
3579
|
try {
|
|
3580
|
+
if (!store.get(memoryId)) {
|
|
3581
|
+
if (opts.json) emitJsonError("not_found", `Memory ${memoryId} not found`);
|
|
3582
|
+
else {
|
|
3583
|
+
p.log.error(`Memory ${memoryId} not found`);
|
|
3584
|
+
process.exitCode = 1;
|
|
3585
|
+
}
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3346
3588
|
store.forget(memoryId);
|
|
3347
3589
|
if (opts.json) console.log(JSON.stringify({
|
|
3348
3590
|
id: memoryId,
|
|
@@ -3353,6 +3595,19 @@ function forgetMemoryAction(idStr, opts) {
|
|
|
3353
3595
|
store.close();
|
|
3354
3596
|
}
|
|
3355
3597
|
}
|
|
3598
|
+
function rollbackManualMemory(store, sessionId, memoryId) {
|
|
3599
|
+
store.db.transaction(() => {
|
|
3600
|
+
const row = store.db.prepare("SELECT import_key FROM memory_items WHERE id = ?").get(memoryId);
|
|
3601
|
+
store.db.prepare("DELETE FROM memory_vectors WHERE memory_id = ?").run(memoryId);
|
|
3602
|
+
store.db.prepare("DELETE FROM memory_file_refs WHERE memory_id = ?").run(memoryId);
|
|
3603
|
+
store.db.prepare("DELETE FROM memory_concept_refs WHERE memory_id = ?").run(memoryId);
|
|
3604
|
+
store.db.prepare("DELETE FROM replication_ops WHERE entity_type = 'memory_item' AND (entity_id = ? OR entity_id = ?)").run(row?.import_key ?? "", String(memoryId));
|
|
3605
|
+
store.db.prepare("DELETE FROM memory_items WHERE id = ?").run(memoryId);
|
|
3606
|
+
store.db.prepare(`DELETE FROM sessions
|
|
3607
|
+
WHERE id = ?
|
|
3608
|
+
AND NOT EXISTS (SELECT 1 FROM memory_items WHERE session_id = ?)`).run(sessionId, sessionId);
|
|
3609
|
+
})();
|
|
3610
|
+
}
|
|
3356
3611
|
async function rememberMemoryAction(opts) {
|
|
3357
3612
|
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
3358
3613
|
let sessionId = null;
|
|
@@ -3366,8 +3621,14 @@ async function rememberMemoryAction(opts) {
|
|
|
3366
3621
|
metadata: { manual: true }
|
|
3367
3622
|
});
|
|
3368
3623
|
const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, .5, opts.tags);
|
|
3369
|
-
|
|
3624
|
+
if (!store.get(memId)) {
|
|
3625
|
+
await store.flushPendingVectorWrites();
|
|
3626
|
+
rollbackManualMemory(store, sessionId, memId);
|
|
3627
|
+
sessionId = null;
|
|
3628
|
+
throw new Error("unauthorized_scope");
|
|
3629
|
+
}
|
|
3370
3630
|
store.endSession(sessionId, { manual: true });
|
|
3631
|
+
await store.flushPendingVectorWrites();
|
|
3371
3632
|
if (opts.json) console.log(JSON.stringify({ id: memId }));
|
|
3372
3633
|
else p.log.success(`Stored memory ${memId}`);
|
|
3373
3634
|
} catch (err) {
|
|
@@ -4149,7 +4410,7 @@ function warnIfViewerExposed(host, port) {
|
|
|
4149
4410
|
function isLikelyViewerCommand(command) {
|
|
4150
4411
|
const lowered = command.toLowerCase();
|
|
4151
4412
|
if (!/\bserve\s+start\b/.test(lowered)) return false;
|
|
4152
|
-
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");
|
|
4153
4414
|
}
|
|
4154
4415
|
function prepareViewerDatabase(dbPath) {
|
|
4155
4416
|
return initDatabase(dbPath ?? void 0).path;
|
|
@@ -4199,6 +4460,44 @@ function isTrustedViewerPid(pid, target, listenerPid) {
|
|
|
4199
4460
|
function pidFilePath(dbPath) {
|
|
4200
4461
|
return join(dirname(dbPath), "viewer.pid");
|
|
4201
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
|
+
}
|
|
4202
4501
|
function readViewerPidRecord(dbPath) {
|
|
4203
4502
|
const pidPath = pidFilePath(dbPath);
|
|
4204
4503
|
if (!existsSync(pidPath)) return null;
|
|
@@ -4289,6 +4588,44 @@ async function waitForProcessExit(pid, timeoutMs = 3e4) {
|
|
|
4289
4588
|
}
|
|
4290
4589
|
return !isProcessRunning(pid);
|
|
4291
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
|
+
}
|
|
4292
4629
|
async function waitForPortRelease(host, port, timeoutMs = 1e4) {
|
|
4293
4630
|
const deadline = Date.now() + timeoutMs;
|
|
4294
4631
|
while (Date.now() < deadline) {
|
|
@@ -4298,21 +4635,13 @@ async function waitForPortRelease(host, port, timeoutMs = 1e4) {
|
|
|
4298
4635
|
return false;
|
|
4299
4636
|
}
|
|
4300
4637
|
async function stopExistingViewer(dbPath, target) {
|
|
4301
|
-
const terminatePid = async (pid) => {
|
|
4302
|
-
try {
|
|
4303
|
-
process.kill(pid, "SIGTERM");
|
|
4304
|
-
return await waitForProcessExit(pid);
|
|
4305
|
-
} catch {
|
|
4306
|
-
return true;
|
|
4307
|
-
}
|
|
4308
|
-
};
|
|
4309
4638
|
const pidPath = pidFilePath(dbPath);
|
|
4310
4639
|
const record = readViewerPidRecord(dbPath);
|
|
4311
4640
|
const viewerPidFromStats = await lookupViewerPidFromStats(target.host, target.port);
|
|
4312
4641
|
const listenerPid = lookupListeningPid(target.host, target.port);
|
|
4313
4642
|
const viewerPid = pickViewerPidCandidate(viewerPidFromStats, listenerPid);
|
|
4314
4643
|
if (viewerPid && isTrustedViewerPid(viewerPid, target, listenerPid)) {
|
|
4315
|
-
if (!await
|
|
4644
|
+
if (!await terminateTrustedViewerPid(viewerPid)) return {
|
|
4316
4645
|
stopped: false,
|
|
4317
4646
|
pid: viewerPid
|
|
4318
4647
|
};
|
|
@@ -4328,12 +4657,19 @@ async function stopExistingViewer(dbPath, target) {
|
|
|
4328
4657
|
stopped: false,
|
|
4329
4658
|
pid: null
|
|
4330
4659
|
};
|
|
4331
|
-
|
|
4332
|
-
|
|
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 {
|
|
4333
4666
|
stopped: false,
|
|
4334
4667
|
pid: record.pid
|
|
4335
4668
|
};
|
|
4336
|
-
}
|
|
4669
|
+
} else return {
|
|
4670
|
+
stopped: false,
|
|
4671
|
+
pid: null
|
|
4672
|
+
};
|
|
4337
4673
|
try {
|
|
4338
4674
|
rmSync(pidPath);
|
|
4339
4675
|
} catch {}
|
|
@@ -4357,6 +4693,56 @@ function buildForegroundRunnerArgs(scriptPath, invocation, execArgv = process.ex
|
|
|
4357
4693
|
if (invocation.dbPath) args.push("--db-path", invocation.dbPath);
|
|
4358
4694
|
return args;
|
|
4359
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
|
+
}
|
|
4360
4746
|
function isSqliteVecLoadFailure(error) {
|
|
4361
4747
|
if (!(error instanceof Error)) return false;
|
|
4362
4748
|
const text = error.message.toLowerCase();
|
|
@@ -4404,88 +4790,6 @@ async function startBackgroundViewer(invocation) {
|
|
|
4404
4790
|
p.intro("codemem viewer");
|
|
4405
4791
|
p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
|
|
4406
4792
|
}
|
|
4407
|
-
function createSequentialBackfillCoordinator(store, jobPlans, signal) {
|
|
4408
|
-
const pollIntervalMs = 1e3;
|
|
4409
|
-
let activeRunner = null;
|
|
4410
|
-
let activePlan = null;
|
|
4411
|
-
let activePollTimer = null;
|
|
4412
|
-
let nextJobIndex = 0;
|
|
4413
|
-
let stopped = false;
|
|
4414
|
-
const clearPollTimer = () => {
|
|
4415
|
-
if (!activePollTimer) return;
|
|
4416
|
-
clearTimeout(activePollTimer);
|
|
4417
|
-
activePollTimer = null;
|
|
4418
|
-
};
|
|
4419
|
-
const schedulePoll = (fn) => {
|
|
4420
|
-
clearPollTimer();
|
|
4421
|
-
activePollTimer = setTimeout(fn, pollIntervalMs);
|
|
4422
|
-
if (typeof activePollTimer === "object" && "unref" in activePollTimer) activePollTimer.unref();
|
|
4423
|
-
};
|
|
4424
|
-
const waitForCurrentJob = () => {
|
|
4425
|
-
if (stopped || signal?.aborted || !activePlan || !activeRunner) return;
|
|
4426
|
-
const job = getMaintenanceJob(store.db, activePlan.kind);
|
|
4427
|
-
if (!activePlan.isPending(store.db)) {
|
|
4428
|
-
const finishedPlan = activePlan;
|
|
4429
|
-
const finishedRunner = activeRunner;
|
|
4430
|
-
activePlan = null;
|
|
4431
|
-
activeRunner = null;
|
|
4432
|
-
finishedRunner.stop().finally(() => {
|
|
4433
|
-
if (!stopped && !signal?.aborted) {
|
|
4434
|
-
p.log.step(`${finishedPlan.name} backfill complete`);
|
|
4435
|
-
startNextJob();
|
|
4436
|
-
}
|
|
4437
|
-
});
|
|
4438
|
-
return;
|
|
4439
|
-
}
|
|
4440
|
-
if (job?.status === "failed") {
|
|
4441
|
-
const failedPlan = activePlan;
|
|
4442
|
-
const failedRunner = activeRunner;
|
|
4443
|
-
activePlan = null;
|
|
4444
|
-
activeRunner = null;
|
|
4445
|
-
failedRunner.stop().finally(() => {
|
|
4446
|
-
if (!stopped && !signal?.aborted) {
|
|
4447
|
-
p.log.warn(`${failedPlan.name} backfill failed and will be retried on a later startup`);
|
|
4448
|
-
startNextJob();
|
|
4449
|
-
}
|
|
4450
|
-
});
|
|
4451
|
-
return;
|
|
4452
|
-
}
|
|
4453
|
-
schedulePoll(waitForCurrentJob);
|
|
4454
|
-
};
|
|
4455
|
-
const startNextJob = () => {
|
|
4456
|
-
clearPollTimer();
|
|
4457
|
-
if (stopped || signal?.aborted) return;
|
|
4458
|
-
while (nextJobIndex < jobPlans.length) {
|
|
4459
|
-
const plan = jobPlans[nextJobIndex++];
|
|
4460
|
-
if (!plan) continue;
|
|
4461
|
-
if (!plan.isPending(store.db)) continue;
|
|
4462
|
-
activePlan = plan;
|
|
4463
|
-
activeRunner = plan.createRunner();
|
|
4464
|
-
p.log.step(`${plan.name} backfill started`);
|
|
4465
|
-
activeRunner.start();
|
|
4466
|
-
schedulePoll(waitForCurrentJob);
|
|
4467
|
-
return;
|
|
4468
|
-
}
|
|
4469
|
-
p.log.step("All backfill jobs complete");
|
|
4470
|
-
};
|
|
4471
|
-
return {
|
|
4472
|
-
start: () => {
|
|
4473
|
-
if (stopped || signal?.aborted) return;
|
|
4474
|
-
const pendingCount = jobPlans.filter((plan) => plan.isPending(store.db)).length;
|
|
4475
|
-
if (pendingCount === 0) return;
|
|
4476
|
-
p.log.step(`${pendingCount} backfill job(s) pending — starting sequential runners`);
|
|
4477
|
-
startNextJob();
|
|
4478
|
-
},
|
|
4479
|
-
stop: async () => {
|
|
4480
|
-
stopped = true;
|
|
4481
|
-
clearPollTimer();
|
|
4482
|
-
const runner = activeRunner;
|
|
4483
|
-
activeRunner = null;
|
|
4484
|
-
activePlan = null;
|
|
4485
|
-
if (runner) await runner.stop();
|
|
4486
|
-
}
|
|
4487
|
-
};
|
|
4488
|
-
}
|
|
4489
4793
|
async function startForegroundViewer(invocation) {
|
|
4490
4794
|
const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
|
|
4491
4795
|
const { serve } = await import("@hono/node-server");
|
|
@@ -4514,67 +4818,13 @@ async function startForegroundViewer(invocation) {
|
|
|
4514
4818
|
const sweeper = new RawEventSweeper(store, { observer });
|
|
4515
4819
|
sweeper.start();
|
|
4516
4820
|
const syncAbort = new AbortController();
|
|
4517
|
-
const retentionAbort = new AbortController();
|
|
4518
|
-
const backfillAbort = new AbortController();
|
|
4519
4821
|
const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
|
|
4520
4822
|
const syncEnabled = syncConfig.syncEnabled;
|
|
4521
4823
|
const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
});
|
|
4526
|
-
const vectorMigrationRunner = new VectorModelMigrationRunner({
|
|
4527
|
-
dbPath,
|
|
4528
|
-
signal: backfillAbort.signal
|
|
4529
|
-
});
|
|
4530
|
-
const backfillCoordinator = createSequentialBackfillCoordinator(store, [
|
|
4531
|
-
{
|
|
4532
|
-
name: "Sharing-domain",
|
|
4533
|
-
kind: SCOPE_BACKFILL_JOB,
|
|
4534
|
-
isPending: hasPendingScopeBackfill,
|
|
4535
|
-
createRunner: () => new ScopeBackfillRunner({
|
|
4536
|
-
dbPath,
|
|
4537
|
-
signal: backfillAbort.signal
|
|
4538
|
-
})
|
|
4539
|
-
},
|
|
4540
|
-
{
|
|
4541
|
-
name: "Dedup-key",
|
|
4542
|
-
kind: DEDUP_KEY_BACKFILL_JOB,
|
|
4543
|
-
isPending: hasPendingDedupKeyBackfill,
|
|
4544
|
-
createRunner: () => new DedupKeyBackfillRunner({
|
|
4545
|
-
dbPath,
|
|
4546
|
-
signal: backfillAbort.signal
|
|
4547
|
-
})
|
|
4548
|
-
},
|
|
4549
|
-
{
|
|
4550
|
-
name: "Session-context",
|
|
4551
|
-
kind: SESSION_CONTEXT_BACKFILL_JOB,
|
|
4552
|
-
isPending: hasPendingSessionContextBackfill,
|
|
4553
|
-
createRunner: () => new SessionContextBackfillRunner({
|
|
4554
|
-
dbPath,
|
|
4555
|
-
signal: backfillAbort.signal
|
|
4556
|
-
})
|
|
4557
|
-
},
|
|
4558
|
-
{
|
|
4559
|
-
name: "Ref",
|
|
4560
|
-
kind: REF_BACKFILL_JOB,
|
|
4561
|
-
isPending: hasPendingRefBackfill,
|
|
4562
|
-
createRunner: () => new RefBackfillRunner({
|
|
4563
|
-
dbPath,
|
|
4564
|
-
signal: backfillAbort.signal
|
|
4565
|
-
})
|
|
4566
|
-
},
|
|
4567
|
-
{
|
|
4568
|
-
name: "Session-summary dedup",
|
|
4569
|
-
kind: SUMMARY_DEDUP_BACKFILL_JOB,
|
|
4570
|
-
isPending: hasPendingSummaryDedupBackfill,
|
|
4571
|
-
createRunner: () => new SummaryDedupBackfillRunner({
|
|
4572
|
-
dbPath,
|
|
4573
|
-
deviceId: store.deviceId,
|
|
4574
|
-
signal: backfillAbort.signal
|
|
4575
|
-
})
|
|
4576
|
-
}
|
|
4577
|
-
], 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");
|
|
4578
4828
|
const syncRuntimeStatus = {
|
|
4579
4829
|
phase: syncEnabled ? "starting" : "disabled",
|
|
4580
4830
|
detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
|
|
@@ -4587,6 +4837,7 @@ async function startForegroundViewer(invocation) {
|
|
|
4587
4837
|
};
|
|
4588
4838
|
const app = createApp(appOpts);
|
|
4589
4839
|
const pidPath = pidFilePath(dbPath);
|
|
4840
|
+
let maintenanceWorker = null;
|
|
4590
4841
|
let syncServer = null;
|
|
4591
4842
|
let syncListenerReady = false;
|
|
4592
4843
|
if (syncEnabled) {
|
|
@@ -4617,15 +4868,7 @@ async function startForegroundViewer(invocation) {
|
|
|
4617
4868
|
p.log.success(`Listening on http://${info.address}:${info.port}`);
|
|
4618
4869
|
p.log.info(`Database: ${preparedDb}`);
|
|
4619
4870
|
p.log.step("Raw event sweeper started");
|
|
4620
|
-
|
|
4621
|
-
vectorMigrationRunner.start();
|
|
4622
|
-
p.log.step("Vector maintenance runner started");
|
|
4623
|
-
}
|
|
4624
|
-
backfillCoordinator.start();
|
|
4625
|
-
if (syncConfig.syncRetentionEnabled) {
|
|
4626
|
-
retentionRunner.start();
|
|
4627
|
-
p.log.step("Retention maintenance runner started");
|
|
4628
|
-
}
|
|
4871
|
+
maintenanceWorker = startMaintenanceWorkerProcess(invocation);
|
|
4629
4872
|
if (syncEnabled) {
|
|
4630
4873
|
const syncStartDelayMs = 3e3;
|
|
4631
4874
|
p.log.step(`Sync daemon will start in background (${syncStartDelayMs / 1e3}s delay)`);
|
|
@@ -4667,24 +4910,11 @@ async function startForegroundViewer(invocation) {
|
|
|
4667
4910
|
else p.log.error(err.message);
|
|
4668
4911
|
process.exit(1);
|
|
4669
4912
|
});
|
|
4670
|
-
const walCheckpointTimer = setInterval(() => {
|
|
4671
|
-
try {
|
|
4672
|
-
store.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
4673
|
-
} catch (err) {
|
|
4674
|
-
p.log.warn(`WAL checkpoint failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4675
|
-
}
|
|
4676
|
-
}, 300 * 1e3);
|
|
4677
|
-
walCheckpointTimer.unref();
|
|
4678
4913
|
const shutdown = async () => {
|
|
4679
4914
|
p.outro("shutting down");
|
|
4680
|
-
clearInterval(walCheckpointTimer);
|
|
4681
4915
|
syncAbort.abort();
|
|
4682
|
-
|
|
4683
|
-
backfillAbort.abort();
|
|
4916
|
+
await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
|
|
4684
4917
|
await sweeper.stop();
|
|
4685
|
-
await vectorMigrationRunner.stop();
|
|
4686
|
-
await retentionRunner.stop();
|
|
4687
|
-
await backfillCoordinator.stop();
|
|
4688
4918
|
await new Promise((resolve) => {
|
|
4689
4919
|
let remaining = syncServer ? 2 : 1;
|
|
4690
4920
|
const done = () => {
|
|
@@ -4701,9 +4931,15 @@ async function startForegroundViewer(invocation) {
|
|
|
4701
4931
|
};
|
|
4702
4932
|
const forceShutdown = () => {
|
|
4703
4933
|
setTimeout(() => {
|
|
4934
|
+
if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
|
|
4935
|
+
process.kill(maintenanceWorker.pid, "SIGKILL");
|
|
4936
|
+
} catch {}
|
|
4704
4937
|
try {
|
|
4705
4938
|
rmSync(pidPath);
|
|
4706
4939
|
} catch {}
|
|
4940
|
+
try {
|
|
4941
|
+
rmSync(maintenanceWorkerPidFilePath(dbPath));
|
|
4942
|
+
} catch {}
|
|
4707
4943
|
closeStore();
|
|
4708
4944
|
process.exit(1);
|
|
4709
4945
|
}, 3e4).unref();
|
|
@@ -4736,8 +4972,13 @@ async function runServeInvocation(invocation) {
|
|
|
4736
4972
|
port: invocation.port
|
|
4737
4973
|
});
|
|
4738
4974
|
if (result.stopped) {
|
|
4975
|
+
const workerStopped = await terminateTrustedMaintenanceWorker(dbPath, {
|
|
4976
|
+
gracefulMs: 5e3,
|
|
4977
|
+
forceMs: 5e3
|
|
4978
|
+
});
|
|
4739
4979
|
p.intro("codemem viewer");
|
|
4740
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");
|
|
4741
4982
|
if (invocation.mode === "stop") {
|
|
4742
4983
|
p.outro("done");
|
|
4743
4984
|
return;
|
|
@@ -4749,7 +4990,12 @@ async function runServeInvocation(invocation) {
|
|
|
4749
4990
|
process.exitCode = 1;
|
|
4750
4991
|
return;
|
|
4751
4992
|
} else if (invocation.mode === "stop") {
|
|
4993
|
+
const workerStopped = await terminateTrustedMaintenanceWorker(dbPath, {
|
|
4994
|
+
gracefulMs: 5e3,
|
|
4995
|
+
forceMs: 5e3
|
|
4996
|
+
});
|
|
4752
4997
|
p.intro("codemem viewer");
|
|
4998
|
+
if (!workerStopped) p.log.warn("Maintenance worker pidfile exists but did not match trusted worker command");
|
|
4753
4999
|
p.outro("No background viewer found");
|
|
4754
5000
|
return;
|
|
4755
5001
|
}
|