codemem 0.30.0-alpha.3 → 0.30.0-alpha.4

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