@tickernelz/wardmote 2026.1111.0 → 2026.1112.0

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
@@ -1039,7 +1039,7 @@ var init_constants = __esm({
1039
1039
  "pipelines:write",
1040
1040
  "joins:approve"
1041
1041
  ];
1042
- TOOL_APPLICATION_TYPES = ["rest_api", "mcp_http", "mcp_stdio", "paperclip_plugin", "a2a", "chat"];
1042
+ TOOL_APPLICATION_TYPES = ["rest_api", "mcp_http", "mcp_stdio", "wardmote_plugin", "a2a", "chat"];
1043
1043
  TOOL_APPLICATION_STATUSES = ["draft", "active", "disabled", "archived"];
1044
1044
  TOOL_CONNECTION_KINDS = ["managed"];
1045
1045
  TOOL_CONNECTION_HEALTH_STATUSES = [
@@ -16605,17 +16605,20 @@ var init_smoke_lab = __esm({
16605
16605
  });
16606
16606
 
16607
16607
  // ../packages/shared/src/types/instance.ts
16608
- var DAILY_RETENTION_PRESETS, WEEKLY_RETENTION_PRESETS, MONTHLY_RETENTION_PRESETS, DEFAULT_BACKUP_RETENTION, SPEECH_TO_TEXT_DEFAULTS, ATTACHMENT_MAX_MEGABYTES_MIN, ATTACHMENT_MAX_MEGABYTES_MAX, ATTACHMENT_ORPHAN_AFTER_DAYS_MIN, ATTACHMENT_ORPHAN_AFTER_DAYS_MAX, ATTACHMENT_CLOSED_TASK_AFTER_DAYS_MIN, ATTACHMENT_CLOSED_TASK_AFTER_DAYS_MAX, DEFAULT_ATTACHMENT_RETENTION, DEFAULT_LOCAL_RUN_LIMITS_SETTINGS, DEFAULT_MESSAGE_DELIVERY;
16608
+ var DAILY_RETENTION_PRESETS, WEEKLY_RETENTION_PRESETS, MONTHLY_RETENTION_PRESETS, BACKUP_MAX_FILES_MIN, BACKUP_MAX_FILES_MAX, DEFAULT_BACKUP_RETENTION, SPEECH_TO_TEXT_DEFAULTS, ATTACHMENT_MAX_MEGABYTES_MIN, ATTACHMENT_MAX_MEGABYTES_MAX, ATTACHMENT_ORPHAN_AFTER_DAYS_MIN, ATTACHMENT_ORPHAN_AFTER_DAYS_MAX, ATTACHMENT_CLOSED_TASK_AFTER_DAYS_MIN, ATTACHMENT_CLOSED_TASK_AFTER_DAYS_MAX, DEFAULT_ATTACHMENT_RETENTION, DEFAULT_LOCAL_RUN_LIMITS_SETTINGS, DEFAULT_MESSAGE_DELIVERY;
16609
16609
  var init_instance = __esm({
16610
16610
  "../packages/shared/src/types/instance.ts"() {
16611
16611
  "use strict";
16612
16612
  DAILY_RETENTION_PRESETS = [3, 7, 14];
16613
16613
  WEEKLY_RETENTION_PRESETS = [1, 2, 4];
16614
16614
  MONTHLY_RETENTION_PRESETS = [1, 3, 6];
16615
+ BACKUP_MAX_FILES_MIN = 1;
16616
+ BACKUP_MAX_FILES_MAX = 48;
16615
16617
  DEFAULT_BACKUP_RETENTION = {
16616
16618
  dailyDays: 7,
16617
16619
  weeklyWeeks: 4,
16618
- monthlyMonths: 1
16620
+ monthlyMonths: 1,
16621
+ maxFiles: 2
16619
16622
  };
16620
16623
  SPEECH_TO_TEXT_DEFAULTS = {
16621
16624
  enabled: false,
@@ -17214,7 +17217,8 @@ var init_instance2 = __esm({
17214
17217
  backupRetentionPolicySchema = z40.object({
17215
17218
  dailyDays: presetSchema(DAILY_RETENTION_PRESETS, "dailyDays").default(DEFAULT_BACKUP_RETENTION.dailyDays),
17216
17219
  weeklyWeeks: presetSchema(WEEKLY_RETENTION_PRESETS, "weeklyWeeks").default(DEFAULT_BACKUP_RETENTION.weeklyWeeks),
17217
- monthlyMonths: presetSchema(MONTHLY_RETENTION_PRESETS, "monthlyMonths").default(DEFAULT_BACKUP_RETENTION.monthlyMonths)
17220
+ monthlyMonths: presetSchema(MONTHLY_RETENTION_PRESETS, "monthlyMonths").default(DEFAULT_BACKUP_RETENTION.monthlyMonths),
17221
+ maxFiles: z40.number().int().min(BACKUP_MAX_FILES_MIN).max(BACKUP_MAX_FILES_MAX).nullable().default(DEFAULT_BACKUP_RETENTION.maxFiles)
17218
17222
  });
17219
17223
  speechToTextSettingsSchema = z40.object({
17220
17224
  enabled: z40.boolean().default(SPEECH_TO_TEXT_DEFAULTS.enabled),
@@ -20431,7 +20435,7 @@ function findWardmoteConfigKeyWarnings(config) {
20431
20435
  collectConfigKeyWarnings(config, wardmoteConfigSchema, "", warnings);
20432
20436
  return warnings;
20433
20437
  }
20434
- var configMetaSchema, llmConfigSchema, databaseBackupConfigSchema, databaseConfigSchema, loggingConfigSchema, serverConfigSchema, authConfigSchema, storageLocalDiskConfigSchema, storageS3ConfigSchema, storageConfigSchema, secretsLocalEncryptedConfigSchema, secretsConfigSchema, telemetryConfigSchema, updatesConfigSchema, runLogRetentionConfigSchema, runLogsConfigSchema, wardmoteConfigSchema;
20438
+ var configMetaSchema, llmConfigSchema, databaseBackupConfigSchema, databaseConfigSchema, loggingConfigSchema, serverConfigSchema, authConfigSchema, storageLocalDiskConfigSchema, storageS3ConfigSchema, storageConfigSchema, secretsLocalEncryptedConfigSchema, secretsConfigSchema, telemetryConfigSchema, updatesConfigSchema, runLogRetentionConfigSchema, diskGuardConfigSchema, runLogsConfigSchema, wardmoteConfigSchema;
20435
20439
  var init_config_schema = __esm({
20436
20440
  "../packages/shared/src/config-schema.ts"() {
20437
20441
  "use strict";
@@ -20450,7 +20454,11 @@ var init_config_schema = __esm({
20450
20454
  enabled: z70.boolean().default(true),
20451
20455
  intervalMinutes: z70.number().int().min(1).max(7 * 24 * 60).default(60),
20452
20456
  retentionDays: z70.number().int().min(1).max(3650).default(7),
20453
- dir: z70.string().default("~/.wardmote/instances/default/data/backups")
20457
+ dir: z70.string().default("~/.wardmote/instances/default/data/backups"),
20458
+ maxFiles: z70.number().int().min(1).max(48).optional(),
20459
+ reservePercent: z70.number().min(0).max(50).optional(),
20460
+ reserveGb: z70.number().min(0).optional(),
20461
+ pinnedMaxAgeDays: z70.number().int().min(0).max(3650).optional()
20454
20462
  }).passthrough();
20455
20463
  databaseConfigSchema = z70.object({
20456
20464
  mode: z70.enum(["embedded-postgres", "postgres"]).default("embedded-postgres"),
@@ -20530,6 +20538,19 @@ var init_config_schema = __esm({
20530
20538
  recentTailBytes: z70.number().int().min(0).default(5 * 1024 * 1024),
20531
20539
  oldTailBytes: z70.number().int().min(0).default(256 * 1024)
20532
20540
  }).passthrough().prefault({});
20541
+ diskGuardConfigSchema = z70.object({
20542
+ enabled: z70.boolean().default(true),
20543
+ intervalMinutes: z70.number().int().min(1).max(24 * 60).default(5),
20544
+ warnFreePercent: z70.number().min(0).max(100).default(20),
20545
+ warnFreeGb: z70.number().min(0).default(100),
20546
+ actionFreePercent: z70.number().min(0).max(100).default(10),
20547
+ emergencyFreePercent: z70.number().min(0).max(100).default(5),
20548
+ ompSessionMaxIdleDays: z70.number().int().min(5).max(3650).default(7),
20549
+ ompSessionDir: z70.string().optional(),
20550
+ ompBlobDir: z70.string().optional(),
20551
+ ompAgentDir: z70.string().optional(),
20552
+ ompSessionSweep: z70.boolean().optional()
20553
+ }).passthrough().prefault({});
20533
20554
  runLogsConfigSchema = z70.object({
20534
20555
  retention: runLogRetentionConfigSchema
20535
20556
  }).passthrough().prefault({});
@@ -20564,7 +20585,8 @@ var init_config_schema = __esm({
20564
20585
  keyFilePath: "~/.wardmote/instances/default/secrets/master.key"
20565
20586
  }
20566
20587
  }),
20567
- runLogs: runLogsConfigSchema.optional()
20588
+ runLogs: runLogsConfigSchema.optional(),
20589
+ diskGuard: diskGuardConfigSchema.optional()
20568
20590
  }).passthrough().superRefine((value, ctx) => {
20569
20591
  if (value.server.deploymentMode === "local_trusted" && value.server.exposure !== "private") {
20570
20592
  ctx.addIssue({
@@ -20671,7 +20693,8 @@ var init_notifications = __esm({
20671
20693
  "assignment",
20672
20694
  "review",
20673
20695
  "run_failed",
20674
- "join_request"
20696
+ "join_request",
20697
+ "disk_space"
20675
20698
  ];
20676
20699
  }
20677
20700
  });
@@ -47801,11 +47824,12 @@ var init_test_embedded_postgres = __esm({
47801
47824
  });
47802
47825
 
47803
47826
  // ../packages/db/src/backup-lib.ts
47804
- import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync } from "node:fs";
47805
- import { basename, resolve as resolve2 } from "node:path";
47827
+ import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statfsSync, statSync, unlinkSync, writeFileSync } from "node:fs";
47828
+ import { basename, join as join2, resolve as resolve2 } from "node:path";
47806
47829
  import { createInterface as createInterface2 } from "node:readline";
47807
47830
  import { spawn as spawn4 } from "node:child_process";
47808
47831
  import { open as openFile } from "node:fs/promises";
47832
+ import { Writable } from "node:stream";
47809
47833
  import { pipeline } from "node:stream/promises";
47810
47834
  import { createGunzip, createGzip } from "node:zlib";
47811
47835
  import postgres3 from "postgres";
@@ -47840,9 +47864,169 @@ function monthlyRetentionCutoff(nowMs, monthlyMonths) {
47840
47864
  const now = new Date(nowMs);
47841
47865
  return Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - months, 1);
47842
47866
  }
47867
+ function readBackupDiskSpace(dir) {
47868
+ const stats = statfsSync(dir);
47869
+ return { freeBytes: stats.bavail * stats.bsize, totalBytes: stats.blocks * stats.bsize };
47870
+ }
47871
+ function pinnedBackupDir(backupDir) {
47872
+ return join2(backupDir, PINNED_BACKUP_DIRNAME);
47873
+ }
47874
+ function backupIdentity(file2) {
47875
+ const stats = statSync(file2);
47876
+ return `${stats.size}:${stats.mtimeMs}`;
47877
+ }
47878
+ async function verifyGzipBackup(file2) {
47879
+ if (statSync(file2).size === 0) throw new Error(`Backup ${basename(file2)} is empty`);
47880
+ await pipeline(
47881
+ createReadStream(file2, { highWaterMark: 1024 * 1024 }),
47882
+ createGunzip({ chunkSize: 1024 * 1024 }),
47883
+ new Writable({ write(_chunk, _encoding, callback) {
47884
+ callback();
47885
+ } })
47886
+ );
47887
+ }
47888
+ function isArchiveCorruption(error) {
47889
+ const code = error?.code;
47890
+ return typeof code === "string" && code.startsWith("Z_");
47891
+ }
47892
+ async function checkBackup(file2) {
47893
+ const stats = statSync(file2, { throwIfNoEntry: false });
47894
+ if (!stats || stats.size === 0) return "invalid";
47895
+ if (!file2.endsWith(".gz")) return "valid";
47896
+ if (verifiedBackups.get(file2) === `${stats.size}:${stats.mtimeMs}`) return "valid";
47897
+ try {
47898
+ await verifyGzipBackup(file2);
47899
+ } catch (error) {
47900
+ return isArchiveCorruption(error) ? "invalid" : "unreadable";
47901
+ }
47902
+ verifiedBackups.set(file2, backupIdentity(file2));
47903
+ return "valid";
47904
+ }
47905
+ function listRotationBackups(backupDir, filenamePrefix) {
47906
+ if (!existsSync(backupDir)) return [];
47907
+ const entries2 = [];
47908
+ for (const name of readdirSync(backupDir)) {
47909
+ if (!name.startsWith(`${filenamePrefix}-`) || !isBackupFilename(name)) continue;
47910
+ const fullPath = resolve2(backupDir, name);
47911
+ const stats = statSync(fullPath, { throwIfNoEntry: false });
47912
+ if (!stats?.isFile()) continue;
47913
+ entries2.push({ fullPath, sizeBytes: stats.size, mtimeMs: stats.mtimeMs });
47914
+ }
47915
+ return entries2.sort((a, b) => b.mtimeMs - a.mtimeMs);
47916
+ }
47917
+ function removeBackupFile(file2) {
47918
+ rmSync(file2, { force: true });
47919
+ verifiedBackups.delete(file2);
47920
+ }
47921
+ async function pruneToMaxFiles(backupDir, filenamePrefix, maxFiles, newestFile) {
47922
+ let kept = 1;
47923
+ let removed = 0;
47924
+ const unreadable = [];
47925
+ for (const entry of listRotationBackups(backupDir, filenamePrefix)) {
47926
+ if (entry.fullPath === newestFile) continue;
47927
+ const check46 = await checkBackup(entry.fullPath);
47928
+ if (check46 === "unreadable") {
47929
+ unreadable.push(entry.fullPath);
47930
+ continue;
47931
+ }
47932
+ if (kept < maxFiles && check46 === "valid") {
47933
+ kept += 1;
47934
+ continue;
47935
+ }
47936
+ removeBackupFile(entry.fullPath);
47937
+ removed += 1;
47938
+ }
47939
+ return { removed, unreadable };
47940
+ }
47941
+ function prunePinnedBackups(backupDir, maxAgeDays, nowMs = Date.now()) {
47942
+ if (!maxAgeDays || maxAgeDays <= 0) return 0;
47943
+ const dir = pinnedBackupDir(backupDir);
47944
+ if (!existsSync(dir)) return 0;
47945
+ let removed = 0;
47946
+ for (const name of readdirSync(dir)) {
47947
+ if (!isBackupFilename(name)) continue;
47948
+ const fullPath = resolve2(dir, name);
47949
+ const stats = statSync(fullPath, { throwIfNoEntry: false });
47950
+ if (!stats?.isFile() || nowMs - stats.mtimeMs < maxAgeDays * DAY_MS) continue;
47951
+ removeBackupFile(fullPath);
47952
+ removed += 1;
47953
+ }
47954
+ return removed;
47955
+ }
47956
+ function backupReserveBytes(guard, totalBytes) {
47957
+ return Math.max(guard.reserveBytes, Math.ceil(totalBytes * guard.reservePercent / 100));
47958
+ }
47959
+ async function ensureBackupSpace(input2) {
47960
+ const { guard } = input2;
47961
+ const statfs = guard.statfs ?? readBackupDiskSpace;
47962
+ const existing = listRotationBackups(input2.backupDir, input2.filenamePrefix);
47963
+ const lastSize = existing.find((entry) => entry.sizeBytes > 0)?.sizeBytes ?? 0;
47964
+ const space = statfs(input2.targetDir);
47965
+ const requiredBytes = Math.ceil(lastSize * (guard.headroomFactor ?? DEFAULT_BACKUP_HEADROOM_FACTOR)) + backupReserveBytes(guard, space.totalBytes);
47966
+ if (space.freeBytes >= requiredBytes) return 0;
47967
+ if (input2.rotate) {
47968
+ const invalid = [];
47969
+ const valid = [];
47970
+ for (const entry of existing) {
47971
+ const check46 = await checkBackup(entry.fullPath);
47972
+ if (check46 === "valid") valid.push(entry);
47973
+ else if (check46 === "invalid") invalid.push(entry);
47974
+ }
47975
+ const chosen = [];
47976
+ let reclaimed = 0;
47977
+ for (const entry of [...invalid, ...valid.slice(1).reverse()]) {
47978
+ if (space.freeBytes + reclaimed >= requiredBytes) break;
47979
+ chosen.push(entry);
47980
+ reclaimed += entry.sizeBytes;
47981
+ }
47982
+ if (space.freeBytes + reclaimed >= requiredBytes) {
47983
+ for (const entry of chosen) removeBackupFile(entry.fullPath);
47984
+ return chosen.length;
47985
+ }
47986
+ writeFileSync(
47987
+ join2(input2.backupDir, DATABASE_BACKUP_SKIPPED_MARKER),
47988
+ JSON.stringify({
47989
+ skippedAt: (/* @__PURE__ */ new Date()).toISOString(),
47990
+ freeBytes: space.freeBytes,
47991
+ requiredBytes,
47992
+ totalBytes: space.totalBytes
47993
+ })
47994
+ );
47995
+ }
47996
+ throw new DatabaseBackupSkippedError(input2.backupDir, space.freeBytes, requiredBytes);
47997
+ }
47843
47998
  function isBackupFilename(name) {
47844
47999
  return name.endsWith(".sql") || name.endsWith(".sql.gz");
47845
48000
  }
48001
+ function partialBackupPath(finalPath) {
48002
+ return `${finalPath}.${process.pid}.partial`;
48003
+ }
48004
+ function isProcessAlive(pid) {
48005
+ try {
48006
+ process.kill(pid, 0);
48007
+ return true;
48008
+ } catch (error) {
48009
+ return error.code === "EPERM";
48010
+ }
48011
+ }
48012
+ function removeAbandonedPartialBackups(backupDir, nowMs = Date.now()) {
48013
+ if (!existsSync(backupDir)) return 0;
48014
+ let removed = 0;
48015
+ for (const name of readdirSync(backupDir)) {
48016
+ const match = PARTIAL_BACKUP_PATTERN.exec(name);
48017
+ if (!match || !isBackupFilename(name.slice(0, match.index))) continue;
48018
+ const fullPath = resolve2(backupDir, name);
48019
+ const pid = Number(match[1]);
48020
+ const ownerGone = pid === process.pid ? !activePartialBackupFiles.has(fullPath) : !isProcessAlive(pid);
48021
+ if (!ownerGone) {
48022
+ const mtimeMs = statSync(fullPath, { throwIfNoEntry: false })?.mtimeMs;
48023
+ if (mtimeMs === void 0 || nowMs - mtimeMs < ABANDONED_PARTIAL_BACKUP_AGE_MS) continue;
48024
+ }
48025
+ rmSync(fullPath, { force: true });
48026
+ removed += 1;
48027
+ }
48028
+ return removed;
48029
+ }
47846
48030
  function adoptRetiredBackupFilenames(backupDir) {
47847
48031
  if (!existsSync(backupDir)) return 0;
47848
48032
  let renamed = 0;
@@ -48203,12 +48387,50 @@ async function runDatabaseBackup(opts) {
48203
48387
  sqlClosed = true;
48204
48388
  await sql72.end();
48205
48389
  };
48206
- mkdirSync(opts.backupDir, { recursive: true });
48390
+ const targetDir = opts.pinned ? pinnedBackupDir(opts.backupDir) : opts.backupDir;
48391
+ mkdirSync(targetDir, { recursive: true });
48207
48392
  if (filenamePrefix === DATABASE_BACKUP_FILENAME_PREFIX) adoptRetiredBackupFilenames(opts.backupDir);
48208
48393
  const checkpointSuffix = opts.verifyBeforePrune ? `-${randomUUID3()}` : "";
48209
- const sqlFile = resolve2(opts.backupDir, `${filenamePrefix}-${timestamp171()}${checkpointSuffix}.sql`);
48210
- const backupFile = `${sqlFile}.gz`;
48394
+ removeAbandonedPartialBackups(targetDir);
48395
+ let freedForSpace = 0;
48396
+ if (opts.spaceGuard) {
48397
+ try {
48398
+ freedForSpace = await ensureBackupSpace({
48399
+ backupDir: opts.backupDir,
48400
+ targetDir,
48401
+ filenamePrefix,
48402
+ guard: opts.spaceGuard,
48403
+ rotate: !opts.pinned
48404
+ });
48405
+ } catch (error) {
48406
+ await closeSql();
48407
+ throw error;
48408
+ }
48409
+ }
48410
+ const finalSqlFile = resolve2(targetDir, `${filenamePrefix}-${timestamp171()}${checkpointSuffix}.sql`);
48411
+ const backupFile = `${finalSqlFile}.gz`;
48412
+ const sqlFile = partialBackupPath(finalSqlFile);
48413
+ const partialBackupFile = partialBackupPath(backupFile);
48414
+ activePartialBackupFiles.add(sqlFile);
48415
+ activePartialBackupFiles.add(partialBackupFile);
48211
48416
  const writer = createBufferedTextFileWriter(sqlFile);
48417
+ const finalizeBackup = async () => {
48418
+ await verifyGzipBackup(partialBackupFile);
48419
+ renameSync(partialBackupFile, backupFile);
48420
+ verifiedBackups.set(backupFile, backupIdentity(backupFile));
48421
+ const sizeBytes = statSync(backupFile).size;
48422
+ await opts.verifyBeforePrune?.(backupFile);
48423
+ if (opts.pinned) return { backupFile, sizeBytes, prunedCount: freedForSpace };
48424
+ const rotated = retention.maxFiles && retention.maxFiles > 0 ? await pruneToMaxFiles(opts.backupDir, filenamePrefix, retention.maxFiles, backupFile) : { removed: pruneOldBackups(opts.backupDir, retention, filenamePrefix), unreadable: [] };
48425
+ const prunedCount = freedForSpace + rotated.removed + prunePinnedBackups(opts.backupDir, opts.pinnedMaxAgeDays);
48426
+ rmSync(join2(opts.backupDir, DATABASE_BACKUP_SKIPPED_MARKER), { force: true });
48427
+ return {
48428
+ backupFile,
48429
+ sizeBytes,
48430
+ prunedCount,
48431
+ ...rotated.unreadable.length > 0 ? { unreadableBackups: rotated.unreadable } : {}
48432
+ };
48433
+ };
48212
48434
  try {
48213
48435
  if (backupEngine === "pg_dump" || backupEngine === "auto" && canUsePgDump) {
48214
48436
  await sql72`SELECT 1`;
@@ -48217,14 +48439,14 @@ async function runDatabaseBackup(opts) {
48217
48439
  await closeSql();
48218
48440
  await runPgDumpBackup({
48219
48441
  connectionString: opts.connectionString,
48220
- backupFile,
48442
+ backupFile: partialBackupFile,
48221
48443
  connectTimeout
48222
48444
  });
48223
48445
  dumped = true;
48224
48446
  } catch (error) {
48225
- if (existsSync(backupFile)) {
48447
+ if (existsSync(partialBackupFile)) {
48226
48448
  try {
48227
- unlinkSync(backupFile);
48449
+ unlinkSync(partialBackupFile);
48228
48450
  } catch {
48229
48451
  }
48230
48452
  }
@@ -48237,10 +48459,7 @@ async function runDatabaseBackup(opts) {
48237
48459
  }
48238
48460
  if (dumped) {
48239
48461
  await writer.abort();
48240
- const sizeBytes2 = statSync(backupFile).size;
48241
- await opts.verifyBeforePrune?.(backupFile);
48242
- const prunedCount2 = pruneOldBackups(opts.backupDir, retention, filenamePrefix);
48243
- return { backupFile, sizeBytes: sizeBytes2, prunedCount: prunedCount2 };
48462
+ return await finalizeBackup();
48244
48463
  }
48245
48464
  }
48246
48465
  await sql72`SELECT 1`;
@@ -48620,33 +48839,24 @@ async function runDatabaseBackup(opts) {
48620
48839
  emit2("");
48621
48840
  await writer.close();
48622
48841
  const sqlReadStream = createReadStream(sqlFile);
48623
- const gzWriteStream = createWriteStream(backupFile);
48842
+ const gzWriteStream = createWriteStream(partialBackupFile);
48624
48843
  await pipeline(sqlReadStream, createGzip(), gzWriteStream);
48625
48844
  unlinkSync(sqlFile);
48626
- const sizeBytes = statSync(backupFile).size;
48627
- await opts.verifyBeforePrune?.(backupFile);
48628
- const prunedCount = pruneOldBackups(opts.backupDir, retention, filenamePrefix);
48629
- return {
48630
- backupFile,
48631
- sizeBytes,
48632
- prunedCount
48633
- };
48845
+ return await finalizeBackup();
48634
48846
  } catch (error) {
48635
48847
  await writer.abort();
48636
- if (existsSync(backupFile)) {
48637
- try {
48638
- unlinkSync(backupFile);
48639
- } catch {
48640
- }
48641
- }
48642
- if (existsSync(sqlFile)) {
48643
- try {
48644
- unlinkSync(sqlFile);
48645
- } catch {
48848
+ for (const file2 of [backupFile, partialBackupFile, sqlFile]) {
48849
+ if (existsSync(file2)) {
48850
+ try {
48851
+ unlinkSync(file2);
48852
+ } catch {
48853
+ }
48646
48854
  }
48647
48855
  }
48648
48856
  throw error;
48649
48857
  } finally {
48858
+ activePartialBackupFiles.delete(sqlFile);
48859
+ activePartialBackupFiles.delete(partialBackupFile);
48650
48860
  await closeSql();
48651
48861
  }
48652
48862
  }
@@ -48683,9 +48893,10 @@ async function runDatabaseRestore(opts) {
48683
48893
  function formatDatabaseBackupResult(result) {
48684
48894
  const size = formatBackupSize(result.sizeBytes);
48685
48895
  const pruned = result.prunedCount > 0 ? `; pruned ${result.prunedCount} old backup(s)` : "";
48686
- return `${result.backupFile} (${size}${pruned})`;
48896
+ const unreadable = result.unreadableBackups?.length ? `; kept ${result.unreadableBackups.length} unreadable backup(s)` : "";
48897
+ return `${result.backupFile} (${size}${pruned}${unreadable})`;
48687
48898
  }
48688
- var DEFAULT_BACKUP_WRITE_BUFFER_BYTES, BACKUP_DATA_CURSOR_ROWS, BACKUP_CLI_STDERR_BYTES, BACKUP_BREAKPOINT_DETECT_BYTES, STATEMENT_BREAKPOINT, DATABASE_BACKUP_FILENAME_PREFIX, RETIRED_BACKUP_FILENAME_PREFIX;
48899
+ var DEFAULT_BACKUP_WRITE_BUFFER_BYTES, BACKUP_DATA_CURSOR_ROWS, BACKUP_CLI_STDERR_BYTES, BACKUP_BREAKPOINT_DETECT_BYTES, STATEMENT_BREAKPOINT, DATABASE_BACKUP_FILENAME_PREFIX, PINNED_BACKUP_DIRNAME, DATABASE_BACKUP_SKIPPED_MARKER, DEFAULT_BACKUP_HEADROOM_FACTOR, DAY_MS, DatabaseBackupSkippedError, verifiedBackups, RETIRED_BACKUP_FILENAME_PREFIX, PARTIAL_BACKUP_PATTERN, ABANDONED_PARTIAL_BACKUP_AGE_MS, activePartialBackupFiles;
48689
48900
  var init_backup_lib = __esm({
48690
48901
  "../packages/db/src/backup-lib.ts"() {
48691
48902
  "use strict";
@@ -48695,7 +48906,30 @@ var init_backup_lib = __esm({
48695
48906
  BACKUP_BREAKPOINT_DETECT_BYTES = 64 * 1024;
48696
48907
  STATEMENT_BREAKPOINT = "-- paperclip statement breakpoint 69f6f3f1-42fd-46a6-bf17-d1d85f8f3900";
48697
48908
  DATABASE_BACKUP_FILENAME_PREFIX = "wardmote";
48909
+ PINNED_BACKUP_DIRNAME = "pinned";
48910
+ DATABASE_BACKUP_SKIPPED_MARKER = "backup-skipped.json";
48911
+ DEFAULT_BACKUP_HEADROOM_FACTOR = 1.5;
48912
+ DAY_MS = 24 * 60 * 60 * 1e3;
48913
+ DatabaseBackupSkippedError = class extends Error {
48914
+ constructor(backupDir, freeBytes, requiredBytes) {
48915
+ super(
48916
+ `Database backup skipped: ${formatBackupSize(freeBytes)} free on the backup volume, ${formatBackupSize(requiredBytes)} required (last backup x${DEFAULT_BACKUP_HEADROOM_FACTOR} plus reserve).`
48917
+ );
48918
+ this.backupDir = backupDir;
48919
+ this.freeBytes = freeBytes;
48920
+ this.requiredBytes = requiredBytes;
48921
+ this.name = "DatabaseBackupSkippedError";
48922
+ }
48923
+ backupDir;
48924
+ freeBytes;
48925
+ requiredBytes;
48926
+ code = "database_backup_skipped_low_disk";
48927
+ };
48928
+ verifiedBackups = /* @__PURE__ */ new Map();
48698
48929
  RETIRED_BACKUP_FILENAME_PREFIX = "paperclip";
48930
+ PARTIAL_BACKUP_PATTERN = /\.(\d+)\.partial$/;
48931
+ ABANDONED_PARTIAL_BACKUP_AGE_MS = 24 * 60 * 60 * 1e3;
48932
+ activePartialBackupFiles = /* @__PURE__ */ new Set();
48699
48933
  }
48700
48934
  });
48701
48935
 
@@ -48746,10 +48980,13 @@ var src_exports = {};
48746
48980
  __export(src_exports, {
48747
48981
  ADAPTER_AUTH_SESSION_ACTIVE_STATES: () => ADAPTER_AUTH_SESSION_ACTIVE_STATES,
48748
48982
  DATABASE_BACKUP_FILENAME_PREFIX: () => DATABASE_BACKUP_FILENAME_PREFIX,
48983
+ DATABASE_BACKUP_SKIPPED_MARKER: () => DATABASE_BACKUP_SKIPPED_MARKER,
48984
+ DatabaseBackupSkippedError: () => DatabaseBackupSkippedError,
48749
48985
  EMBEDDED_POSTGRES_DATABASE: () => EMBEDDED_POSTGRES_DATABASE,
48750
48986
  EMBEDDED_POSTGRES_PASSWORD: () => EMBEDDED_POSTGRES_PASSWORD,
48751
48987
  EMBEDDED_POSTGRES_TEST_TIMEOUT_MS: () => EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
48752
48988
  EMBEDDED_POSTGRES_USER: () => EMBEDDED_POSTGRES_USER,
48989
+ PINNED_BACKUP_DIRNAME: () => PINNED_BACKUP_DIRNAME,
48753
48990
  accountingRuntimeBaselines: () => accountingRuntimeBaselines,
48754
48991
  activityLog: () => activityLog,
48755
48992
  adapterAuthSessions: () => adapterAuthSessions,
@@ -48977,6 +49214,7 @@ __export(src_exports, {
48977
49214
  modelCooldowns: () => modelCooldowns,
48978
49215
  nativeRunFinalizations: () => nativeRunFinalizations,
48979
49216
  nativeRunResults: () => nativeRunResults,
49217
+ pinnedBackupDir: () => pinnedBackupDir,
48980
49218
  pipelineAutomationExecutions: () => pipelineAutomationExecutions,
48981
49219
  pipelineCaseBlockers: () => pipelineCaseBlockers,
48982
49220
  pipelineCaseDocuments: () => pipelineCaseDocuments,
@@ -49010,6 +49248,7 @@ __export(src_exports, {
49010
49248
  projects: () => projects,
49011
49249
  providerBillingSnapshots: () => providerBillingSnapshots,
49012
49250
  providerTraceRecords: () => providerTraceRecords,
49251
+ readBackupDiskSpace: () => readBackupDiskSpace,
49013
49252
  reconcileDatabaseWork: () => reconcileDatabaseWork,
49014
49253
  reconcilePendingMigrationHistory: () => reconcilePendingMigrationHistory,
49015
49254
  remoteAgentProfiles: () => remoteAgentProfiles,
@@ -49067,6 +49306,7 @@ __export(src_exports, {
49067
49306
  userSecretDeclarations: () => userSecretDeclarations,
49068
49307
  userSecretDefinitions: () => userSecretDefinitions,
49069
49308
  userSidebarPreferences: () => userSidebarPreferences,
49309
+ verifyGzipBackup: () => verifyGzipBackup,
49070
49310
  webPushSubscriptions: () => webPushSubscriptions,
49071
49311
  withDedicatedDbConnection: () => withDedicatedDbConnection,
49072
49312
  workAssessments: () => workAssessments,
@@ -49651,7 +49891,7 @@ var init_client2 = __esm({
49651
49891
 
49652
49892
  // ../packages/shared/src/telemetry/state.ts
49653
49893
  import { randomUUID as randomUUID4, randomBytes as randomBytes5 } from "node:crypto";
49654
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
49894
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
49655
49895
  import path15 from "node:path";
49656
49896
  function loadOrCreateState(stateDir, version) {
49657
49897
  const filePath = path15.join(stateDir, "state.json");
@@ -49672,7 +49912,7 @@ function loadOrCreateState(stateDir, version) {
49672
49912
  firstSeenVersion: version
49673
49913
  };
49674
49914
  mkdirSync2(stateDir, { recursive: true });
49675
- writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
49915
+ writeFileSync2(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
49676
49916
  return state;
49677
49917
  }
49678
49918
  var init_state = __esm({
@@ -60932,6 +61172,13 @@ function normalizeRetentionDays(value, fallback) {
60932
61172
  }
60933
61173
  return candidate;
60934
61174
  }
61175
+ function normalizeMaxFiles(value, fallback) {
61176
+ const candidate = value ?? fallback;
61177
+ if (!Number.isInteger(candidate) || candidate < 0 || candidate > 48) {
61178
+ throw new Error(`Invalid max files '${String(candidate)}'. Use an integer from 0 to 48.`);
61179
+ }
61180
+ return candidate;
61181
+ }
60935
61182
  function resolveBackupDir(raw) {
60936
61183
  return path29.resolve(expandHomePrefix(raw.trim()));
60937
61184
  }
@@ -60948,19 +61195,28 @@ async function dbBackupCommand(opts) {
60948
61195
  opts.retentionDays,
60949
61196
  config?.database.backup.retentionDays ?? 30
60950
61197
  );
61198
+ const maxFiles = normalizeMaxFiles(opts.maxFiles, config?.database.backup.maxFiles ?? 2);
60951
61199
  const filenamePrefix = opts.filenamePrefix?.trim() || DATABASE_BACKUP_FILENAME_PREFIX;
61200
+ const pinned = opts.pin === true;
61201
+ const retentionLabel = pinned ? "pinned (outside rotation)" : maxFiles > 0 ? `newest ${maxFiles} verified backup(s)` : `${retentionDays} day(s), tiered`;
60952
61202
  p10.log.message(pc7.dim(`Config: ${configPath}`));
60953
61203
  p10.log.message(pc7.dim(`Connection source: ${connection.source}`));
60954
- p10.log.message(pc7.dim(`Backup dir: ${backupDir}`));
60955
- p10.log.message(pc7.dim(`Retention: ${retentionDays} day(s)`));
61204
+ p10.log.message(pc7.dim(`Backup dir: ${pinned ? pinnedBackupDir(backupDir) : backupDir}`));
61205
+ p10.log.message(pc7.dim(`Retention: ${retentionLabel}`));
60956
61206
  const spinner4 = p10.spinner();
60957
61207
  spinner4.start("Creating database backup...");
60958
61208
  try {
60959
61209
  const result = await runDatabaseBackup({
60960
61210
  connectionString: connection.value,
60961
61211
  backupDir,
60962
- retention: { dailyDays: retentionDays, weeklyWeeks: 4, monthlyMonths: 1 },
60963
- filenamePrefix
61212
+ retention: { dailyDays: retentionDays, weeklyWeeks: 4, monthlyMonths: 1, maxFiles: maxFiles > 0 ? maxFiles : null },
61213
+ filenamePrefix,
61214
+ pinned,
61215
+ pinnedMaxAgeDays: config?.database.backup.pinnedMaxAgeDays ?? 30,
61216
+ spaceGuard: {
61217
+ reservePercent: config?.database.backup.reservePercent ?? 5,
61218
+ reserveBytes: Math.round((config?.database.backup.reserveGb ?? 10) * 1024 ** 3)
61219
+ }
60964
61220
  });
60965
61221
  spinner4.stop(`Backup saved: ${formatDatabaseBackupResult(result)}`);
60966
61222
  if (opts.json) {
@@ -60972,6 +61228,8 @@ async function dbBackupCommand(opts) {
60972
61228
  prunedCount: result.prunedCount,
60973
61229
  backupDir,
60974
61230
  retentionDays,
61231
+ maxFiles,
61232
+ pinned,
60975
61233
  connectionSource: connection.source
60976
61234
  },
60977
61235
  null,
@@ -60996,7 +61254,7 @@ var init_db_backup = __esm({
60996
61254
  });
60997
61255
 
60998
61256
  // src/services/instance-drain.ts
60999
- function isProcessAlive(pid) {
61257
+ function isProcessAlive2(pid) {
61000
61258
  try {
61001
61259
  process.kill(pid, 0);
61002
61260
  return true;
@@ -61011,7 +61269,7 @@ function resolveConfiguredInstanceApiBase(instanceId) {
61011
61269
  }
61012
61270
  function resolveInstanceEndpoint(instanceId, deps = {}) {
61013
61271
  const runtime = (deps.readRuntime ?? readRuntimeInfo)(instanceId);
61014
- const alive = deps.isProcessAlive ?? isProcessAlive;
61272
+ const alive = deps.isProcessAlive ?? isProcessAlive2;
61015
61273
  if (runtime && alive(runtime.pid)) {
61016
61274
  return { apiBase: buildLocalAppUrl(runtime.host, runtime.port), source: "runtime-info", pid: runtime.pid };
61017
61275
  }
@@ -61224,7 +61482,7 @@ async function withHotRestartLock(instanceId, callback, options = {}) {
61224
61482
  const token = `${process.pid}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
61225
61483
  const deadline = Date.now() + (options.timeoutMs ?? 12e4);
61226
61484
  const pollMs = options.pollMs ?? 100;
61227
- const isProcessAlive3 = options.isProcessAlive ?? ((pid) => {
61485
+ const isProcessAlive4 = options.isProcessAlive ?? ((pid) => {
61228
61486
  try {
61229
61487
  process.kill(pid, 0);
61230
61488
  return true;
@@ -61243,7 +61501,7 @@ async function withHotRestartLock(instanceId, callback, options = {}) {
61243
61501
  try {
61244
61502
  const existingToken = (await fs28.readFile(lockPath, "utf8")).trim();
61245
61503
  const ownerPid = Number.parseInt(existingToken.split(":", 1)[0] ?? "", 10);
61246
- if (Number.isInteger(ownerPid) && ownerPid > 0 && !isProcessAlive3(ownerPid)) {
61504
+ if (Number.isInteger(ownerPid) && ownerPid > 0 && !isProcessAlive4(ownerPid)) {
61247
61505
  if ((await fs28.readFile(lockPath, "utf8")).trim() === existingToken) {
61248
61506
  await fs28.rm(lockPath, { force: true });
61249
61507
  continue;
@@ -61326,7 +61584,7 @@ async function startManagedService(input2 = {}) {
61326
61584
  return { status: await detection.manager.status(), health };
61327
61585
  }
61328
61586
  async function resolveInstanceLiveness(input2) {
61329
- const alive = input2.isProcessAlive ?? isProcessAlive;
61587
+ const alive = input2.isProcessAlive ?? isProcessAlive2;
61330
61588
  const runtime = (input2.readRuntime ?? readRuntimeInfo)(input2.instanceId);
61331
61589
  if (runtime && alive(runtime.pid)) {
61332
61590
  return { running: true, detail: `server pid ${runtime.pid} is listening on ${runtime.host}:${runtime.port}` };
@@ -61672,7 +61930,7 @@ async function updateCommand(options, overrides = {}) {
61672
61930
  const confirmed = await (overrides.confirm ?? defaultConfirm)(`Update from ${manifest.repo}@${manifest.ref} and execute build scripts from commit ${targetSha.slice(0, 12)}?`);
61673
61931
  if (!confirmed) throw new Error("Git update cancelled. Re-run with --yes to confirm executing build scripts from the updated commit.");
61674
61932
  }
61675
- if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})), overrides.hasInstanceData);
61933
+ if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({ pin: true })), overrides.hasInstanceData);
61676
61934
  const installed2 = await withInstallStoreLock(async () => {
61677
61935
  assertManagedShimWritable(paths);
61678
61936
  const payload = await installGitPayload(manifest.repo, targetSha, runCommand2, paths);
@@ -61755,7 +62013,7 @@ async function updateCommand(options, overrides = {}) {
61755
62013
  return;
61756
62014
  }
61757
62015
  await assertReleaseSetPublished(targetVersion, runCommand2);
61758
- if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})), overrides.hasInstanceData);
62016
+ if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({ pin: true })), overrides.hasInstanceData);
61759
62017
  const installed = await withInstallStoreLock(async () => {
61760
62018
  assertManagedShimWritable(paths);
61761
62019
  const payload = await installNpmPayload(targetVersion, runCommand2, paths);
@@ -62219,7 +62477,7 @@ function writeRegistryLockOwner(lockPath, owner) {
62219
62477
  fs31.renameSync(temporaryPath, ownerPath);
62220
62478
  }
62221
62479
  }
62222
- function isProcessAlive2(pid) {
62480
+ function isProcessAlive3(pid) {
62223
62481
  try {
62224
62482
  process.kill(pid, 0);
62225
62483
  return true;
@@ -62283,7 +62541,7 @@ function removeStaleRegistryLock(lockPath) {
62283
62541
  const ageMs = Date.now() - fs31.statSync(lockPath).mtimeMs;
62284
62542
  if (ageMs <= WORKTREE_PORT_REGISTRY_LOCK_STALE_MS) return false;
62285
62543
  const owner = readRegistryLockOwner(lockPath);
62286
- if (owner && isProcessAlive2(owner.pid)) {
62544
+ if (owner && isProcessAlive3(owner.pid)) {
62287
62545
  if (probeRegistryLockOwner(owner)) return false;
62288
62546
  const currentIdentity = readProcessIdentity(owner.pid);
62289
62547
  if (owner.processIdentity === currentIdentity) return false;
@@ -63244,10 +63502,10 @@ import {
63244
63502
  readFileSync as readFileSync4,
63245
63503
  readlinkSync,
63246
63504
  renameSync as renameSync2,
63247
- rmSync,
63505
+ rmSync as rmSync2,
63248
63506
  statSync as statSync3,
63249
63507
  symlinkSync,
63250
- writeFileSync as writeFileSync2
63508
+ writeFileSync as writeFileSync3
63251
63509
  } from "node:fs";
63252
63510
  import os10 from "node:os";
63253
63511
  import path37 from "node:path";
@@ -63637,7 +63895,7 @@ function copyDirectoryContents(sourceDir, targetDir) {
63637
63895
  continue;
63638
63896
  }
63639
63897
  if (entry.isSymbolicLink()) {
63640
- rmSync(targetPath, { recursive: true, force: true });
63898
+ rmSync2(targetPath, { recursive: true, force: true });
63641
63899
  symlinkSync(readlinkSync(sourcePath), targetPath);
63642
63900
  copied = true;
63643
63901
  continue;
@@ -63897,7 +64155,7 @@ function copySeededSecretsKey(input2) {
63897
64155
  const allowProcessEnvFallback = isCurrentSourceConfigPath(input2.sourceConfigPath);
63898
64156
  const sourceInlineMasterKey = nonEmpty2(input2.sourceEnvEntries.WARDMOTE_SECRETS_MASTER_KEY) ?? (allowProcessEnvFallback ? nonEmpty2(process.env.WARDMOTE_SECRETS_MASTER_KEY) : null);
63899
64157
  if (sourceInlineMasterKey) {
63900
- writeFileSync2(input2.targetKeyFilePath, sourceInlineMasterKey, {
64158
+ writeFileSync3(input2.targetKeyFilePath, sourceInlineMasterKey, {
63901
64159
  encoding: "utf8",
63902
64160
  mode: 384
63903
64161
  });
@@ -63973,7 +64231,7 @@ async function ensureEmbeddedPostgres(dataDir, preferredPort, options = {}) {
63973
64231
  }
63974
64232
  }
63975
64233
  if (existsSync4(postmasterPidFile)) {
63976
- rmSync(postmasterPidFile, { force: true });
64234
+ rmSync2(postmasterPidFile, { force: true });
63977
64235
  }
63978
64236
  try {
63979
64237
  await instance.start();
@@ -64466,7 +64724,7 @@ function resolveSeedInstanceId(configPath) {
64466
64724
  function writeWorktreeSeedManifest(filePath, manifest) {
64467
64725
  mkdirSync3(path37.dirname(filePath), { recursive: true });
64468
64726
  const temporaryPath = `${filePath}.${process.pid}.${randomUUID6()}.tmp`;
64469
- writeFileSync2(temporaryPath, `${JSON.stringify(manifest, null, 2)}
64727
+ writeFileSync3(temporaryPath, `${JSON.stringify(manifest, null, 2)}
64470
64728
  `, { mode: 384 });
64471
64729
  renameSync2(temporaryPath, filePath);
64472
64730
  }
@@ -64514,8 +64772,8 @@ function markWorktreeSeedPending(input2) {
64514
64772
  ...input2.diagnosticMessage ? { message: input2.diagnosticMessage.slice(0, WORKTREE_SEED_DIAGNOSTIC_MESSAGE_LIMIT) } : {}
64515
64773
  }]
64516
64774
  });
64517
- rmSync(markers.complete, { force: true });
64518
- rmSync(markers.pending, { force: true });
64775
+ rmSync2(markers.complete, { force: true });
64776
+ rmSync2(markers.pending, { force: true });
64519
64777
  }
64520
64778
  function updateWorktreeSeedManifest(input2) {
64521
64779
  const markers = resolveWorktreeSeedMarkerPaths(input2.configPath);
@@ -64899,14 +65157,14 @@ async function runWorktreeInit(opts) {
64899
65157
  );
64900
65158
  }
64901
65159
  if (opts.force) {
64902
- rmSync(paths.configPath, { force: true });
64903
- rmSync(paths.envPath, { force: true });
65160
+ rmSync2(paths.configPath, { force: true });
65161
+ rmSync2(paths.envPath, { force: true });
64904
65162
  const seedMarkers = resolveWorktreeSeedMarkerPaths(paths.configPath);
64905
- rmSync(seedMarkers.manifest, { force: true });
64906
- rmSync(seedMarkers.pending, { force: true });
64907
- rmSync(seedMarkers.complete, { force: true });
64908
- rmSync(seedMarkers.empty, { force: true });
64909
- rmSync(paths.instanceRoot, { recursive: true, force: true });
65163
+ rmSync2(seedMarkers.manifest, { force: true });
65164
+ rmSync2(seedMarkers.pending, { force: true });
65165
+ rmSync2(seedMarkers.complete, { force: true });
65166
+ rmSync2(seedMarkers.empty, { force: true });
65167
+ rmSync2(paths.instanceRoot, { recursive: true, force: true });
64910
65168
  }
64911
65169
  const { serverPort, databasePort, targetConfig } = await withWorktreePortRegistryLock(
64912
65170
  paths.homeDir,
@@ -64934,7 +65192,7 @@ async function runWorktreeInit(opts) {
64934
65192
  try {
64935
65193
  if (opts.empty) {
64936
65194
  mkdirSync3(paths.repoConfigDir, { recursive: true });
64937
- writeFileSync2(resolveWorktreeSeedMarkerPaths(paths.configPath).empty, "Explicitly empty instance; automatic database copying is disabled.\n", { mode: 384 });
65195
+ writeFileSync3(resolveWorktreeSeedMarkerPaths(paths.configPath).empty, "Explicitly empty instance; automatic database copying is disabled.\n", { mode: 384 });
64938
65196
  }
64939
65197
  writeConfig(selectedConfig, paths.configPath);
64940
65198
  writeWorktreePortRegistry(paths.homeDir, [
@@ -64942,8 +65200,8 @@ async function runWorktreeInit(opts) {
64942
65200
  paths.configPath
64943
65201
  ]);
64944
65202
  } catch (error) {
64945
- rmSync(paths.configPath, { force: true });
64946
- if (opts.empty) rmSync(resolveWorktreeSeedMarkerPaths(paths.configPath).empty, { force: true });
65203
+ rmSync2(paths.configPath, { force: true });
65204
+ if (opts.empty) rmSync2(resolveWorktreeSeedMarkerPaths(paths.configPath).empty, { force: true });
64947
65205
  throw error;
64948
65206
  }
64949
65207
  return {
@@ -65345,7 +65603,7 @@ async function worktreeCleanupCommand(nameArg, opts) {
65345
65603
  if (existsSync4(targetPath)) {
65346
65604
  const spinner4 = p14.spinner();
65347
65605
  spinner4.start(`Removing worktree directory ${targetPath}...`);
65348
- rmSync(targetPath, { recursive: true, force: true });
65606
+ rmSync2(targetPath, { recursive: true, force: true });
65349
65607
  spinner4.stop(`Removed worktree directory ${targetPath}.`);
65350
65608
  }
65351
65609
  if (localBranchExists(sourceCwd, name)) {
@@ -65366,7 +65624,7 @@ async function worktreeCleanupCommand(nameArg, opts) {
65366
65624
  if (existsSync4(instanceRoot)) {
65367
65625
  const spinner4 = p14.spinner();
65368
65626
  spinner4.start(`Removing instance data at ${instanceRoot}...`);
65369
- rmSync(instanceRoot, { recursive: true, force: true });
65627
+ rmSync2(instanceRoot, { recursive: true, force: true });
65370
65628
  spinner4.stop(`Removed instance data at ${instanceRoot}.`);
65371
65629
  }
65372
65630
  p14.outro(pc10.green("Cleanup complete."));
@@ -66382,7 +66640,7 @@ async function runWorktreeReseed(opts) {
66382
66640
  expectedCompanyId: nonEmpty2(process.env.WARDMOTE_SEED_EXPECTED_COMPANY_ID) ?? void 0,
66383
66641
  seedDatabase: seedWorktreeDatabase
66384
66642
  });
66385
- rmSync(markers.empty, { force: true });
66643
+ rmSync2(markers.empty, { force: true });
66386
66644
  spinner4.stop(`Reseeded ${targetEndpoint.label} (${seedMode}).`);
66387
66645
  p14.log.message(pc10.dim(`Source: ${source.configPath}`));
66388
66646
  p14.log.message(pc10.dim(`Target: ${targetEndpoint.configPath}`));
@@ -79356,9 +79614,9 @@ function registerAccessCommands(program2) {
79356
79614
  }
79357
79615
  })
79358
79616
  );
79359
- const join2 = program2.command("join").description("Join request operations");
79617
+ const join3 = program2.command("join").description("Join request operations");
79360
79618
  addCommonClientOptions(
79361
- join2.command("list").description("List join requests").option("-C, --company-id <id>", "Company ID").option("--status <status>", "Filter by status (pending_approval, approved, rejected; pending alias accepted)").option("--request-type <type>", "Filter by request type").action(async (opts) => {
79619
+ join3.command("list").description("List join requests").option("-C, --company-id <id>", "Company ID").option("--status <status>", "Filter by status (pending_approval, approved, rejected; pending alias accepted)").option("--request-type <type>", "Filter by request type").action(async (opts) => {
79362
79620
  try {
79363
79621
  const ctx = resolveCommandContext(opts, { requireCompany: true });
79364
79622
  const params = new URLSearchParams();
@@ -79373,10 +79631,10 @@ function registerAccessCommands(program2) {
79373
79631
  }),
79374
79632
  { includeCompany: false }
79375
79633
  );
79376
- addJoinAction(join2, "approve");
79377
- addJoinAction(join2, "reject");
79634
+ addJoinAction(join3, "approve");
79635
+ addJoinAction(join3, "reject");
79378
79636
  addCommonClientOptions(
79379
- join2.command("claim-key").description("Claim an agent API key for an approved join request").argument("<requestId>", "Join request ID").requiredOption("--claim-secret <secret>", "Claim secret").action(async (requestId, opts) => {
79637
+ join3.command("claim-key").description("Claim an agent API key for an approved join request").argument("<requestId>", "Join request ID").requiredOption("--claim-secret <secret>", "Claim secret").action(async (requestId, opts) => {
79380
79638
  try {
79381
79639
  const ctx = resolveCommandContext(opts);
79382
79640
  printOutput(await ctx.api.post(apiPath`/api/join-requests/${requestId}/claim-api-key`, { claimSecret: opts.claimSecret }), { json: ctx.json });
@@ -80564,6 +80822,20 @@ function readIfExists(filePath) {
80564
80822
  throw error;
80565
80823
  }
80566
80824
  }
80825
+ function rewriteLegacyStoreMarker(markerPath) {
80826
+ let contents;
80827
+ try {
80828
+ const stat3 = fs37.lstatSync(markerPath);
80829
+ const getuid = process.getuid;
80830
+ if (!stat3.isFile() || stat3.nlink > 1 || typeof getuid === "function" && stat3.uid !== getuid()) return false;
80831
+ contents = fs37.readFileSync(markerPath, "utf8");
80832
+ } catch {
80833
+ return false;
80834
+ }
80835
+ if (contents !== LEGACY_STORE_MARKER) return false;
80836
+ writeFileAtomic(markerPath, MANAGED_STORE_MARKER, 384);
80837
+ return true;
80838
+ }
80567
80839
  function isLegacyShim(file2) {
80568
80840
  const contents = readIfExists(file2);
80569
80841
  return contents !== null && isManagedShimContents(contents, LEGACY_SHIM_MARKER);
@@ -80607,9 +80879,13 @@ async function migrateHomeCommand(options = {}, overrides = {}) {
80607
80879
  const unrecognizedShims = [legacyShim, shim].filter((file2) => !legacyShims.includes(file2) && readIfExists(file2)?.includes(`# ${LEGACY_SHIM_MARKER}
80608
80880
  `));
80609
80881
  const rcFiles = [".bashrc", ".zshrc"].map((name) => path50.join(deps.userHomeDir, name)).filter((file2) => readIfExists(file2)?.includes(LEGACY_PATH_BLOCK_START));
80610
- const storeHome = home === "absent" ? source : target;
80882
+ const storeHome = target;
80611
80883
  const markerPath = path50.join(home === "move" ? source : storeHome, "cli", ".managed-install");
80612
- const legacyStore = readIfExists(markerPath) === LEGACY_STORE_MARKER;
80884
+ const storeMarker = readIfExists(markerPath);
80885
+ const legacyStore = storeMarker === LEGACY_STORE_MARKER;
80886
+ if (storeMarker !== null && !legacyStore && storeMarker !== MANAGED_STORE_MARKER) {
80887
+ throw new Error(`Refusing to migrate: unrecognized install store marker ${markerPath}.`);
80888
+ }
80613
80889
  const steps = [
80614
80890
  home === "move" ? `move ${source} to ${target} and link ${source} -> ${target}` : null,
80615
80891
  ...definitions.map((definition) => `replace ${path50.basename(definition.from)}${fs37.existsSync(`${definition.from}.d`) ? " and its drop-in directory" : ""} with ${path50.basename(definition.to)}`),
@@ -80682,6 +80958,9 @@ async function migrateHomeCommand(options = {}, overrides = {}) {
80682
80958
  if (definitions.length > 0) deps.log("Start the service with `wardmote service start`.");
80683
80959
  }
80684
80960
 
80961
+ // src/index.ts
80962
+ init_install_store();
80963
+
80685
80964
  // src/commands/migrate-names.ts
80686
80965
  init_src2();
80687
80966
  init_db_backup();
@@ -81058,6 +81337,9 @@ program.hook("preAction", async (_thisCommand, actionCommand) => {
81058
81337
  assertTestDriveDatabaseIsolation(options.config);
81059
81338
  }
81060
81339
  initTelemetryFromConfigFile(options.config);
81340
+ if (["install", "update", "uninstall"].includes(actionCommand.name())) {
81341
+ rewriteLegacyStoreMarker(resolveInstallStorePaths().markerPath);
81342
+ }
81061
81343
  });
81062
81344
  registerTestDriveCommand(program);
81063
81345
  registerRuntimeCommands(program);
@@ -81070,7 +81352,7 @@ program.command("channels").description("Show the release channels and which one
81070
81352
  await channelsCommand(opts);
81071
81353
  });
81072
81354
  program.command("configure").description("Update configuration sections").option("-c, --config <path>", "Path to config file").option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP).option("-s, --section <section>", "Section to configure (llm, database, logging, server, storage, secrets)").action(configure);
81073
- program.command("db:backup").description("Create a one-off database backup using current config").option("-c, --config <path>", "Path to config file").option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP).option("--dir <path>", "Backup output directory (overrides config)").option("--retention-days <days>", "Retention window used for pruning", (value) => Number(value)).option("--filename-prefix <prefix>", "Backup filename prefix", "paperclip").option("--json", "Print backup metadata as JSON").action(async (opts) => {
81355
+ program.command("db:backup").description("Create a one-off database backup using current config").option("-c, --config <path>", "Path to config file").option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP).option("--dir <path>", "Backup output directory (overrides config)").option("--max-files <count>", "Keep this many newest verified backups (0 = tiered retention by --retention-days)", (value) => Number(value)).option("--retention-days <days>", "Daily window for tiered retention when --max-files is 0", (value) => Number(value)).option("--pin", "Write a pinned backup under <dir>/pinned, outside rotation (release and rollback dumps)").option("--filename-prefix <prefix>", "Backup filename prefix (default: wardmote)").option("--json", "Print backup metadata as JSON").action(async (opts) => {
81074
81356
  await dbBackupCommand(opts);
81075
81357
  });
81076
81358
  program.command("allowed-hostname").description("Allow a hostname for authenticated/private mode access").argument("<host>", "Hostname to allow (for example dotta-macbook-pro)").option("-c, --config <path>", "Path to config file").option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP).action(addAllowedHostname);