@indigoai-us/hq-cloud 6.18.93 → 6.18.95

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.
Files changed (89) hide show
  1. package/dist/bin/sync-runner.d.ts +20 -1
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +25 -0
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/cli/reindex.d.ts +24 -0
  6. package/dist/cli/reindex.d.ts.map +1 -1
  7. package/dist/cli/reindex.js +26 -1
  8. package/dist/cli/reindex.js.map +1 -1
  9. package/dist/cli/rescue-core.d.ts +18 -3
  10. package/dist/cli/rescue-core.d.ts.map +1 -1
  11. package/dist/cli/rescue-core.js +129 -22
  12. package/dist/cli/rescue-core.js.map +1 -1
  13. package/dist/cli/share.d.ts +28 -5
  14. package/dist/cli/share.d.ts.map +1 -1
  15. package/dist/cli/share.js +189 -22
  16. package/dist/cli/share.js.map +1 -1
  17. package/dist/cli/sync.d.ts +11 -1
  18. package/dist/cli/sync.d.ts.map +1 -1
  19. package/dist/cli/sync.js +77 -8
  20. package/dist/cli/sync.js.map +1 -1
  21. package/dist/cloud-placeholder.d.ts +23 -1
  22. package/dist/cloud-placeholder.d.ts.map +1 -1
  23. package/dist/cloud-placeholder.js +57 -0
  24. package/dist/cloud-placeholder.js.map +1 -1
  25. package/dist/hq-root-recovery.d.ts +37 -6
  26. package/dist/hq-root-recovery.d.ts.map +1 -1
  27. package/dist/hq-root-recovery.js +266 -22
  28. package/dist/hq-root-recovery.js.map +1 -1
  29. package/dist/journal.d.ts +11 -1
  30. package/dist/journal.d.ts.map +1 -1
  31. package/dist/journal.js +23 -2
  32. package/dist/journal.js.map +1 -1
  33. package/dist/lib/cloud-authoritative.d.ts +10 -0
  34. package/dist/lib/cloud-authoritative.d.ts.map +1 -1
  35. package/dist/lib/cloud-authoritative.js +23 -0
  36. package/dist/lib/cloud-authoritative.js.map +1 -1
  37. package/dist/manifest-reconcile.d.ts +14 -0
  38. package/dist/manifest-reconcile.d.ts.map +1 -1
  39. package/dist/manifest-reconcile.js +20 -2
  40. package/dist/manifest-reconcile.js.map +1 -1
  41. package/dist/outposts/client.d.ts +6 -1
  42. package/dist/outposts/client.d.ts.map +1 -1
  43. package/dist/outposts/client.js +13 -1
  44. package/dist/outposts/client.js.map +1 -1
  45. package/dist/outposts/command.d.ts +1 -1
  46. package/dist/outposts/command.d.ts.map +1 -1
  47. package/dist/outposts/command.js +58 -2
  48. package/dist/outposts/command.js.map +1 -1
  49. package/dist/outposts/company.d.ts +15 -1
  50. package/dist/outposts/company.d.ts.map +1 -1
  51. package/dist/outposts/company.js +36 -1
  52. package/dist/outposts/company.js.map +1 -1
  53. package/dist/s3.d.ts +22 -0
  54. package/dist/s3.d.ts.map +1 -1
  55. package/dist/s3.js +44 -0
  56. package/dist/s3.js.map +1 -1
  57. package/dist/session-log-capture.d.ts.map +1 -1
  58. package/dist/session-log-capture.js +5 -2
  59. package/dist/session-log-capture.js.map +1 -1
  60. package/dist/sync/area-ledger.d.ts.map +1 -1
  61. package/dist/sync/area-ledger.js +3 -3
  62. package/dist/sync/area-ledger.js.map +1 -1
  63. package/dist/sync/durable-apply.js +2 -2
  64. package/dist/sync/durable-apply.js.map +1 -1
  65. package/dist/sync/per-area-maintenance.js +2 -2
  66. package/dist/sync/per-area-maintenance.js.map +1 -1
  67. package/dist/sync/reconcile-cursor.js +2 -2
  68. package/dist/sync/reconcile-cursor.js.map +1 -1
  69. package/dist/sync/secret-path-policy.d.ts +37 -11
  70. package/dist/sync/secret-path-policy.d.ts.map +1 -1
  71. package/dist/sync/secret-path-policy.js +68 -34
  72. package/dist/sync/secret-path-policy.js.map +1 -1
  73. package/dist/sync/snapshot-client.js +2 -2
  74. package/dist/sync/snapshot-client.js.map +1 -1
  75. package/dist/sync/state-store.d.ts +19 -0
  76. package/dist/sync/state-store.d.ts.map +1 -1
  77. package/dist/sync/state-store.js +184 -4
  78. package/dist/sync/state-store.js.map +1 -1
  79. package/dist/sync/uploader-finalization.d.ts.map +1 -1
  80. package/dist/sync/uploader-finalization.js +2 -2
  81. package/dist/sync/uploader-finalization.js.map +1 -1
  82. package/dist/test-fixtures/unknown-journal-generation.d.ts +9 -0
  83. package/dist/test-fixtures/unknown-journal-generation.d.ts.map +1 -0
  84. package/dist/test-fixtures/unknown-journal-generation.js +37 -0
  85. package/dist/test-fixtures/unknown-journal-generation.js.map +1 -0
  86. package/dist/watcher.d.ts.map +1 -1
  87. package/dist/watcher.js +3 -3
  88. package/dist/watcher.js.map +1 -1
  89. package/package.json +1 -1
@@ -79,6 +79,15 @@ const MALFORMED_LOCK_GRACE_MS = 250;
79
79
  const SYNC_LOCK_TIMEOUT_MS = 5_000;
80
80
  const ASYNC_LOCK_TIMEOUT_MS = 60_000;
81
81
  const OPTIMISTIC_RECOVERY_MAX_RESAMPLES = 3;
82
+ /**
83
+ * Off-lock recovery passes before falling back to one read under append.lock.
84
+ * Each failed pass means a concurrent writer moved the generation or WAL
85
+ * between the read and its locked validation; without a ceiling a busy writer
86
+ * kept a one-shot sync spinning at full CPU for hours (feedback_b7c00a62).
87
+ */
88
+ const OPTIMISTIC_RECOVERY_MAX_ATTEMPTS = 8;
89
+ /** Same ceiling for repairIfNeeded's plan loop, which then fails loudly. */
90
+ const MAINTENANCE_PLAN_MAX_ATTEMPTS = 8;
82
91
  /** Base wall-clock budget for one contended fold. Scaled by WAL/record pressure. */
83
92
  const COMPACTION_RETRY_BASE_BUDGET_MS = 50;
84
93
  /** Append must not block indefinitely waiting to fold. */
@@ -333,6 +342,54 @@ export class StateStoreCorruptionError extends Error {
333
342
  this.rejection = options.rejection;
334
343
  }
335
344
  }
345
+ export function readStateStoreRecoveryMarker(rootDir, scopeId) {
346
+ const markerPath = recoveryMarkerPath(stateDirectory(rootDir, digestScope(scopeId)));
347
+ try {
348
+ const parsed = JSON.parse(fs.readFileSync(markerPath, "utf8"));
349
+ if (!parsed || typeof parsed !== "object")
350
+ throw new Error(`state-store: invalid recovery marker at ${markerPath}`);
351
+ const marker = parsed;
352
+ if (marker.kind !== "rebuilt_from_prefix" || typeof marker.createdAt !== "string" || typeof marker.quarantinePath !== "string") {
353
+ throw new Error(`state-store: invalid recovery marker at ${markerPath}`);
354
+ }
355
+ return marker;
356
+ }
357
+ catch (error) {
358
+ if (isMissing(error))
359
+ return undefined;
360
+ throw new Error(`state-store: cannot read recovery marker at ${markerPath}`, { cause: error });
361
+ }
362
+ }
363
+ export function clearStateStoreRecoveryMarker(rootDir, scopeId) {
364
+ const dir = stateDirectory(rootDir, digestScope(scopeId));
365
+ const markerPath = recoveryMarkerPath(dir);
366
+ try {
367
+ fs.unlinkSync(markerPath);
368
+ fsyncDirectory(dir);
369
+ }
370
+ catch (error) {
371
+ if (isMissing(error))
372
+ return;
373
+ throw new Error(`state-store: cannot clear recovery marker at ${markerPath}`, { cause: error });
374
+ }
375
+ }
376
+ export class StateStoreIncompatibleGenerationError extends Error {
377
+ generation;
378
+ constructor(generation, options) {
379
+ super(`state-store: incompatible generation ${String(generation)}: a newer hq-cloud wrote it and this client must be upgraded before retrying. Generation files were left unchanged.`, options);
380
+ this.name = "StateStoreIncompatibleGenerationError";
381
+ this.generation = generation;
382
+ }
383
+ }
384
+ /** A reducer recognized the frame but does not recognize its WAL record type. */
385
+ export class StateStoreUnsupportedRecordTypeError extends Error {
386
+ recordType;
387
+ constructor(recordType, reducer) {
388
+ super(`${reducer}: unsupported WAL record type ${String(recordType)}`);
389
+ this.name = "StateStoreUnsupportedRecordTypeError";
390
+ this.recordType = recordType;
391
+ }
392
+ }
336
393
  /**
337
394
  * A snapshot whose 8-byte magic this reader does not understand.
338
395
  *
@@ -421,7 +478,15 @@ export class StateStore {
421
478
  const dir = stateDirectory(normalized.rootDir, scopeDigest);
422
479
  fs.mkdirSync(dir, { recursive: true });
423
480
  while (true) {
424
- const recovered = readNewestGenerationOptimistically(dir, scopeDigest, normalized.reduce, normalized.snapshotDecoder, normalized.snapshotMagics);
481
+ let recovered;
482
+ try {
483
+ recovered = readNewestGenerationOptimistically(dir, scopeDigest, normalized.reduce, normalized.snapshotDecoder, normalized.snapshotMagics);
484
+ }
485
+ catch (error) {
486
+ if (!normalized.recoverRejectedDelta || !isReducerRejectedGeneration(error))
487
+ throw error;
488
+ recovered = recoverRejectedDeltaGeneration(dir, scopeDigest, normalized);
489
+ }
425
490
  if (recovered) {
426
491
  const store = new StateStore(normalized, recovered, false);
427
492
  // A process can die after its durable append but before append() gets
@@ -469,7 +534,15 @@ export class StateStore {
469
534
  const dir = stateDirectory(normalized.rootDir, scopeDigest);
470
535
  await fs.promises.mkdir(dir, { recursive: true });
471
536
  while (true) {
472
- const recovered = await readNewestGenerationOptimisticallyAsync(dir, scopeDigest, normalized.reduce, normalized.snapshotDecoder, normalized.snapshotMagics);
537
+ let recovered;
538
+ try {
539
+ recovered = await readNewestGenerationOptimisticallyAsync(dir, scopeDigest, normalized.reduce, normalized.snapshotDecoder, normalized.snapshotMagics);
540
+ }
541
+ catch (error) {
542
+ if (!normalized.recoverRejectedDelta || !isReducerRejectedGeneration(error))
543
+ throw error;
544
+ recovered = recoverRejectedDeltaGeneration(dir, scopeDigest, normalized);
545
+ }
473
546
  if (recovered) {
474
547
  const store = new StateStore(normalized, recovered, false);
475
548
  // Match open(): a recovered over-budget WAL is compacted before this
@@ -589,7 +662,11 @@ export class StateStore {
589
662
  if (!hasSnapshotGeneration(dir))
590
663
  return { status: "missing" };
591
664
  let plan;
665
+ let planAttempts = 0;
592
666
  while (!plan) {
667
+ if (planAttempts++ >= MAINTENANCE_PLAN_MAX_ATTEMPTS) {
668
+ throw new StateStoreLockError("state-store maintenance did not converge: writers kept moving the store");
669
+ }
593
670
  const preflight = withScopeLock(dir, () => {
594
671
  cleanupUnpublishedInitialGeneration(dir);
595
672
  const newestGeneration = highestCompleteGenerationOnDisk(dir);
@@ -1301,6 +1378,79 @@ export class StateStore {
1301
1378
  this.observedHighestSnapshotGeneration = generation.observedHighestSnapshotGeneration ?? generation.generation;
1302
1379
  }
1303
1380
  }
1381
+ function isReducerRejectedGeneration(error) {
1382
+ if (!(error instanceof StateStoreCorruptionError))
1383
+ return false;
1384
+ const reducerError = error.cause;
1385
+ return reducerError instanceof StateStoreReducerError &&
1386
+ !(reducerError.cause instanceof StateStoreUnsupportedRecordTypeError);
1387
+ }
1388
+ function recoveryMarkerPath(dir) {
1389
+ return path.join(dir, "recovery-marker.json");
1390
+ }
1391
+ function recoverRejectedDeltaGeneration(dir, scopeDigest, options) {
1392
+ return withScopeLock(dir, () => {
1393
+ const generations = fs.readdirSync(dir)
1394
+ .map((name) => /^snapshot-(\d+)\.bin$/.exec(name)?.[1])
1395
+ .filter((value) => value !== undefined)
1396
+ .map(Number).filter(Number.isSafeInteger).sort((a, b) => b - a);
1397
+ const generation = highestCompleteGenerationOnDisk(dir);
1398
+ if (generation === undefined)
1399
+ throw new StateStoreCorruptionError("no complete generation available for rejected-delta recovery");
1400
+ const snapshot = readSnapshot(snapshotPath(dir, generation), scopeDigest, generation, options.snapshotDecoder, options.snapshotMagics);
1401
+ const sourceWalPath = walPath(dir, generation);
1402
+ const parsed = readWal(sourceWalPath, scopeDigest, generation, snapshot.payload.lastRecordSequence, snapshot.payload.state, options.reduce, 0, undefined, undefined, false, true);
1403
+ if (parsed.rejectedSuffixOffset === undefined) {
1404
+ return {
1405
+ generation,
1406
+ state: parsed.state,
1407
+ lastRecordSequence: parsed.lastRecordSequence,
1408
+ recordCount: parsed.recordCount,
1409
+ walBytes: parsed.walBytes,
1410
+ maxReadBytes: parsed.maxReadBytes,
1411
+ observedWalBytes: parsed.observedWalBytes,
1412
+ observedHighestSnapshotGeneration: generation,
1413
+ };
1414
+ }
1415
+ const rejectedSequence = parsed.lastRecordSequence + 1;
1416
+ // The first replay may have partially mutated its state before rejection.
1417
+ // Re-decode the snapshot so the accepted-prefix replay starts from pristine bytes.
1418
+ const prefixSnapshot = readSnapshot(snapshotPath(dir, generation), scopeDigest, generation, options.snapshotDecoder, options.snapshotMagics);
1419
+ const prefix = readWal(sourceWalPath, scopeDigest, generation, prefixSnapshot.payload.lastRecordSequence, prefixSnapshot.payload.state, (state, record) => {
1420
+ if (record.sequence === rejectedSequence)
1421
+ throw new Error("stop before reducer-rejected record");
1422
+ return options.reduce(state, record);
1423
+ }, 0, undefined, undefined, false, true);
1424
+ if (prefix.rejectedSuffixOffset !== parsed.rejectedSuffixOffset) {
1425
+ throw new StateStoreCorruptionError(`generation ${generation} changed during rejected-delta recovery`);
1426
+ }
1427
+ const suffix = fs.readFileSync(sourceWalPath).subarray(parsed.rejectedSuffixOffset);
1428
+ const quarantinePath = path.join(dir, `wal-${generation}.rejected-${Date.now()}-${crypto.randomBytes(5).toString("hex")}.bin`);
1429
+ try {
1430
+ writeNewDurably(quarantinePath, suffix);
1431
+ const marker = {
1432
+ kind: "rebuilt_from_prefix",
1433
+ createdAt: new Date().toISOString(),
1434
+ quarantinePath,
1435
+ };
1436
+ writeDurableTempThenRename(dir, recoveryMarkerPath(dir), [Buffer.from(JSON.stringify(marker), "utf8")]);
1437
+ const next = {
1438
+ generation: Math.max(...generations) + 1,
1439
+ state: prefix.state,
1440
+ lastRecordSequence: prefix.lastRecordSequence,
1441
+ recordCount: 0,
1442
+ walBytes: WAL_HEADER_BYTES,
1443
+ maxReadBytes: parsed.maxReadBytes,
1444
+ };
1445
+ writeGeneration(dir, scopeDigest, next, options.snapshotEncoder);
1446
+ writeCurrentPointer(dir, next.generation);
1447
+ return next;
1448
+ }
1449
+ catch (error) {
1450
+ throw new Error(`state-store: rejected-delta recovery failed; inspect state path ${dir} and preserve its WAL files`, { cause: error });
1451
+ }
1452
+ });
1453
+ }
1304
1454
  function readNewestGeneration(dir, scopeDigest, reduce, snapshotDecoder, snapshotMagics, onProgress, repairTornTail = true) {
1305
1455
  const names = fs.readdirSync(dir);
1306
1456
  const generations = names
@@ -1320,6 +1470,13 @@ function readNewestGeneration(dir, scopeDigest, reduce, snapshotDecoder, snapsho
1320
1470
  }
1321
1471
  catch (error) {
1322
1472
  if (error instanceof StateStoreReducerError) {
1473
+ if (error.cause instanceof StateStoreUnsupportedRecordTypeError) {
1474
+ // The frame passed its integrity checks, and this reducer explicitly
1475
+ // identified a record type it does not know. Do not label that as
1476
+ // corruption: callers may quarantine corruption errors. Never fall
1477
+ // back to an older generation, which would lose newer writes.
1478
+ throw new StateStoreIncompatibleGenerationError(generation, { cause: error });
1479
+ }
1323
1480
  // A CRC-valid record which the owning reducer cannot interpret is
1324
1481
  // corrupt for that projection. Surface the public corruption contract
1325
1482
  // so callers can quarantine/fail-close without depending on this
@@ -1378,7 +1535,16 @@ function readGeneration(dir, scopeDigest, generation, reduce, snapshotDecoder, s
1378
1535
  */
1379
1536
  function readNewestGenerationOptimistically(dir, scopeDigest, reduce, snapshotDecoder, snapshotMagics, onProgress) {
1380
1537
  let resamples = 0;
1538
+ let attempts = 0;
1381
1539
  while (true) {
1540
+ if (attempts++ >= OPTIMISTIC_RECOVERY_MAX_ATTEMPTS) {
1541
+ // Writers kept moving the store under every optimistic pass. One read
1542
+ // under the lock always makes progress (and repairs a torn tail).
1543
+ return withScopeLock(dir, () => {
1544
+ cleanupUnpublishedInitialGeneration(dir);
1545
+ return readNewestGeneration(dir, scopeDigest, reduce, snapshotDecoder, snapshotMagics, onProgress, true);
1546
+ });
1547
+ }
1382
1548
  const candidate = withScopeLock(dir, () => {
1383
1549
  cleanupUnpublishedInitialGeneration(dir);
1384
1550
  return highestCompleteGenerationOnDisk(dir);
@@ -1409,7 +1575,16 @@ function readNewestGenerationOptimistically(dir, scopeDigest, reduce, snapshotDe
1409
1575
  }
1410
1576
  async function readNewestGenerationOptimisticallyAsync(dir, scopeDigest, reduce, snapshotDecoder, snapshotMagics, onProgress) {
1411
1577
  let resamples = 0;
1578
+ let attempts = 0;
1412
1579
  while (true) {
1580
+ if (attempts++ >= OPTIMISTIC_RECOVERY_MAX_ATTEMPTS) {
1581
+ // Writers kept moving the store under every optimistic pass. One read
1582
+ // under the lock always makes progress (and repairs a torn tail).
1583
+ return await withScopeLockAsync(dir, () => {
1584
+ cleanupUnpublishedInitialGeneration(dir);
1585
+ return readNewestGeneration(dir, scopeDigest, reduce, snapshotDecoder, snapshotMagics, onProgress, true);
1586
+ });
1587
+ }
1413
1588
  const candidate = await withScopeLockAsync(dir, () => {
1414
1589
  cleanupUnpublishedInitialGeneration(dir);
1415
1590
  return highestCompleteGenerationOnDisk(dir);
@@ -1844,7 +2019,7 @@ function readSnapshot(filePath, scopeDigest, generation, snapshotDecoder, snapsh
1844
2019
  fs.closeSync(fd);
1845
2020
  }
1846
2021
  }
1847
- function readWal(filePath, scopeDigest, generation, snapshotSequence, initialState, reduce, recoveryBytesBeforeWal = 0, recoveryTotalBytes, onProgress, repairTornTail = true) {
2022
+ function readWal(filePath, scopeDigest, generation, snapshotSequence, initialState, reduce, recoveryBytesBeforeWal = 0, recoveryTotalBytes, onProgress, repairTornTail = true, stopOnReducerRejection = false) {
1848
2023
  let fd;
1849
2024
  try {
1850
2025
  fd = fs.openSync(filePath, "r");
@@ -1856,6 +2031,7 @@ function readWal(filePath, scopeDigest, generation, snapshotSequence, initialSta
1856
2031
  throw new Error(`state-store: cannot read WAL generation ${generation}`, { cause: error });
1857
2032
  }
1858
2033
  let truncateBoundary;
2034
+ let rejectedSuffixOffset;
1859
2035
  try {
1860
2036
  const walBytes = fs.fstatSync(fd).size;
1861
2037
  walReadHookForTest?.();
@@ -1916,7 +2092,10 @@ function readWal(filePath, scopeDigest, generation, snapshotSequence, initialSta
1916
2092
  state = reduce(state, parsed.record);
1917
2093
  }
1918
2094
  catch (error) {
1919
- throw new StateStoreReducerError(parsed.record, error);
2095
+ if (!stopOnReducerRejection)
2096
+ throw new StateStoreReducerError(parsed.record, error);
2097
+ rejectedSuffixOffset = offset;
2098
+ break;
1920
2099
  }
1921
2100
  walRecordReadHookForTest?.(parsed.record);
1922
2101
  sequence = parsed.record.sequence;
@@ -1938,6 +2117,7 @@ function readWal(filePath, scopeDigest, generation, snapshotSequence, initialSta
1938
2117
  walBytes: truncateBoundary ?? walBytes,
1939
2118
  observedWalBytes: walBytes,
1940
2119
  tornWalTailBoundary: truncateBoundary,
2120
+ rejectedSuffixOffset,
1941
2121
  maxReadBytes,
1942
2122
  };
1943
2123
  }