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 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
- const result = importMemories(payload, {
3122
- dbPath: resolveDbPath(resolveDbOpt(opts)),
3123
- remapProject: opts.remapProject,
3124
- dryRun: opts.dryRun
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
- await store.flushPendingVectorWrites();
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 terminatePid(viewerPid)) return {
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
- if (await respondsLikeCodememViewer(record)) {
4332
- if (!await terminatePid(record.pid)) return {
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
- const retentionRunner = new SyncRetentionRunner({
4523
- dbPath,
4524
- signal: retentionAbort.signal
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
- if (!isEmbeddingDisabled()) {
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
- retentionAbort.abort();
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
  }