codemem 0.30.0-alpha.3 → 0.30.0-alpha.5

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