@indigoai-us/hq-cloud 6.18.53 → 6.18.54

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 (110) hide show
  1. package/dist/bin/sync-runner-audit.d.ts +26 -0
  2. package/dist/bin/sync-runner-audit.d.ts.map +1 -0
  3. package/dist/bin/sync-runner-audit.js +51 -0
  4. package/dist/bin/sync-runner-audit.js.map +1 -0
  5. package/dist/bin/sync-runner-company.d.ts +18 -0
  6. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-company.js +186 -9
  8. package/dist/bin/sync-runner-company.js.map +1 -1
  9. package/dist/bin/sync-runner-watch-loop.d.ts +107 -5
  10. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-watch-loop.js +1382 -183
  12. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  13. package/dist/bin/sync-runner.d.ts +63 -11
  14. package/dist/bin/sync-runner.d.ts.map +1 -1
  15. package/dist/bin/sync-runner.js +350 -95
  16. package/dist/bin/sync-runner.js.map +1 -1
  17. package/dist/bin/watcher-catchup-skip-store.d.ts +60 -0
  18. package/dist/bin/watcher-catchup-skip-store.d.ts.map +1 -0
  19. package/dist/bin/watcher-catchup-skip-store.js +282 -0
  20. package/dist/bin/watcher-catchup-skip-store.js.map +1 -0
  21. package/dist/cli/conflict.d.ts +6 -0
  22. package/dist/cli/conflict.d.ts.map +1 -1
  23. package/dist/cli/conflict.js +3 -1
  24. package/dist/cli/conflict.js.map +1 -1
  25. package/dist/cli/reindex.d.ts.map +1 -1
  26. package/dist/cli/reindex.js +95 -5
  27. package/dist/cli/reindex.js.map +1 -1
  28. package/dist/cli/rescue-core.d.ts +25 -1
  29. package/dist/cli/rescue-core.d.ts.map +1 -1
  30. package/dist/cli/rescue-core.js +69 -4
  31. package/dist/cli/rescue-core.js.map +1 -1
  32. package/dist/cli/share.d.ts +27 -0
  33. package/dist/cli/share.d.ts.map +1 -1
  34. package/dist/cli/share.js +140 -23
  35. package/dist/cli/share.js.map +1 -1
  36. package/dist/cli/sync.d.ts +18 -2
  37. package/dist/cli/sync.d.ts.map +1 -1
  38. package/dist/cli/sync.js +90 -25
  39. package/dist/cli/sync.js.map +1 -1
  40. package/dist/ignore.d.ts +1 -1
  41. package/dist/ignore.d.ts.map +1 -1
  42. package/dist/ignore.js +2 -2
  43. package/dist/ignore.js.map +1 -1
  44. package/dist/journal-row-store.d.ts.map +1 -1
  45. package/dist/journal-row-store.js +6 -0
  46. package/dist/journal-row-store.js.map +1 -1
  47. package/dist/journal-snapshot-v4.d.ts +8 -0
  48. package/dist/journal-snapshot-v4.d.ts.map +1 -1
  49. package/dist/journal-snapshot-v4.js +208 -17
  50. package/dist/journal-snapshot-v4.js.map +1 -1
  51. package/dist/journal.d.ts +18 -2
  52. package/dist/journal.d.ts.map +1 -1
  53. package/dist/journal.js +248 -17
  54. package/dist/journal.js.map +1 -1
  55. package/dist/lib/cloud-authoritative.d.ts +9 -2
  56. package/dist/lib/cloud-authoritative.d.ts.map +1 -1
  57. package/dist/lib/cloud-authoritative.js +34 -2
  58. package/dist/lib/cloud-authoritative.js.map +1 -1
  59. package/dist/lib/exit-codes.d.ts +2 -0
  60. package/dist/lib/exit-codes.d.ts.map +1 -1
  61. package/dist/lib/exit-codes.js +2 -0
  62. package/dist/lib/exit-codes.js.map +1 -1
  63. package/dist/personal-vault-exclusions.d.ts +2 -2
  64. package/dist/personal-vault-exclusions.d.ts.map +1 -1
  65. package/dist/personal-vault-exclusions.js +5 -3
  66. package/dist/personal-vault-exclusions.js.map +1 -1
  67. package/dist/personal-vault.d.ts +7 -0
  68. package/dist/personal-vault.d.ts.map +1 -1
  69. package/dist/personal-vault.js +10 -0
  70. package/dist/personal-vault.js.map +1 -1
  71. package/dist/qmd-runner-admission.d.ts +22 -0
  72. package/dist/qmd-runner-admission.d.ts.map +1 -0
  73. package/dist/qmd-runner-admission.js +107 -0
  74. package/dist/qmd-runner-admission.js.map +1 -0
  75. package/dist/symlink-target-policy.d.ts +11 -0
  76. package/dist/symlink-target-policy.d.ts.map +1 -1
  77. package/dist/symlink-target-policy.js +110 -0
  78. package/dist/symlink-target-policy.js.map +1 -1
  79. package/dist/sync/area-ledger-migration.d.ts +3 -1
  80. package/dist/sync/area-ledger-migration.d.ts.map +1 -1
  81. package/dist/sync/area-ledger-migration.js +3 -0
  82. package/dist/sync/area-ledger-migration.js.map +1 -1
  83. package/dist/sync/area-ledger.d.ts +3 -0
  84. package/dist/sync/area-ledger.d.ts.map +1 -1
  85. package/dist/sync/area-ledger.js +29 -6
  86. package/dist/sync/area-ledger.js.map +1 -1
  87. package/dist/sync/mutation-session.d.ts.map +1 -1
  88. package/dist/sync/mutation-session.js +4 -0
  89. package/dist/sync/mutation-session.js.map +1 -1
  90. package/dist/sync/parallel-sync-coordinator.d.ts +29 -0
  91. package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
  92. package/dist/sync/parallel-sync-coordinator.js +229 -16
  93. package/dist/sync/parallel-sync-coordinator.js.map +1 -1
  94. package/dist/sync/push-receiver.d.ts +5 -0
  95. package/dist/sync/push-receiver.d.ts.map +1 -1
  96. package/dist/sync/push-receiver.js +32 -10
  97. package/dist/sync/push-receiver.js.map +1 -1
  98. package/dist/sync/state-store.d.ts +7 -0
  99. package/dist/sync/state-store.d.ts.map +1 -1
  100. package/dist/sync/state-store.js +113 -6
  101. package/dist/sync/state-store.js.map +1 -1
  102. package/dist/watch-owner-lease.d.ts +4 -0
  103. package/dist/watch-owner-lease.d.ts.map +1 -1
  104. package/dist/watch-owner-lease.js +40 -6
  105. package/dist/watch-owner-lease.js.map +1 -1
  106. package/dist/watcher.d.ts +16 -3
  107. package/dist/watcher.d.ts.map +1 -1
  108. package/dist/watcher.js +114 -21
  109. package/dist/watcher.js.map +1 -1
  110. package/package.json +4 -3
package/dist/journal.js CHANGED
@@ -20,11 +20,12 @@ import { spawn } from "node:child_process";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { JournalRowOverlay, JournalRowStore, journalEntryFingerprint, } from "./journal-row-store.js";
22
22
  import { createJournalRecordProxy, journalRecordRows } from "./journal-record-proxy.js";
23
+ import { fsyncWithCompatibility } from "./sync/fsync-compat.js";
23
24
  import { decodeJournalSnapshot, decodedJournalRows } from "./journal-snapshot-decoder.js";
24
25
  import { forgetSharedAreaJournalRows, } from "./journal-snapshot-rows.js";
25
26
  export { JOURNAL_CACHE_MAX_ROWS, JOURNAL_DECODE_WINDOW, dropJournalDecodeWindow, emitJournalCacheMetric, journalCacheMetric, journalFingerprintMatchesStat, resetJournalDecodeWindowForTest, } from "./journal-decode-window.js";
26
27
  import { journalCacheMetric } from "./journal-decode-window.js";
27
- import { encodeV4JournalSnapshotChunks } from "./journal-snapshot-v4.js";
28
+ import { encodeV4JournalSnapshotChunks, encodeV4JournalSnapshotChunksAsync, } from "./journal-snapshot-v4.js";
28
29
  import { toPosixKey } from "./posix-key.js";
29
30
  import { isUnderAnyRoot } from "./path-ancestors.js";
30
31
  export { isUnderAnyRoot } from "./path-ancestors.js";
@@ -60,6 +61,16 @@ const journalBaselines = new WeakMap();
60
61
  const storeBackedJournals = new WeakMap();
61
62
  /** Full-pass journals must not retain the post-cutover v3 rollback mirror between checkpoints. */
62
63
  const fullPassAreaJournals = new WeakSet();
64
+ // Large asynchronous snapshot capture is enabled only by the long-lived watch
65
+ // runner. One-shot journal writers keep the synchronous writer's lower
66
+ // transient-memory profile.
67
+ let asyncJournalCompactionEnabled = false;
68
+ export function setAsyncJournalCompactionForWatchRunner(watchMode) {
69
+ asyncJournalCompactionEnabled = watchMode;
70
+ }
71
+ export function enableAsyncJournalCompactionForWatchRunner() {
72
+ setAsyncJournalCompactionForWatchRunner(true);
73
+ }
63
74
  /**
64
75
  * Record-shaped journals are a public compatibility boundary. Internal callers
65
76
  * use this adapter so packed rows stay lazy through reads, scans and writes.
@@ -506,6 +517,42 @@ function sanitizeSlug(slug) {
506
517
  export function getJournalPath(slug) {
507
518
  return path.join(getStateDir(), `${JOURNAL_FILE_PREFIX}${sanitizeSlug(slug)}${JOURNAL_FILE_SUFFIX}`);
508
519
  }
520
+ /**
521
+ * Paths retained in the durable quarantine sidecars for this journal scope.
522
+ * Ordinary quarantined delta paths remain blocked until repaired. An area row
523
+ * marked `freshCompare` has no trustworthy baseline, so sync may compare it as
524
+ * an unsynced path while still retaining its quarantine evidence.
525
+ */
526
+ export function readQuarantinedJournalPaths(slug) {
527
+ const scope = crypto.createHash("sha256").update(journalScopeId(slug)).digest("hex");
528
+ const directory = path.join(getStateDir(), "journal-quarantine", scope);
529
+ let names;
530
+ try {
531
+ names = fs.readdirSync(directory);
532
+ }
533
+ catch (error) {
534
+ if (error.code === "ENOENT")
535
+ return new Set();
536
+ throw error;
537
+ }
538
+ const paths = new Set();
539
+ for (const name of names) {
540
+ if (!name.endsWith(".json"))
541
+ continue;
542
+ const sidecar = JSON.parse(fs.readFileSync(path.join(directory, name), "utf8"));
543
+ if (sidecar.format !== "hq-journal-quarantine@1" || !Array.isArray(sidecar.invalidUpserts)) {
544
+ throw new Error("journal: invalid quarantine sidecar");
545
+ }
546
+ if (sidecar.freshCompare === true)
547
+ continue;
548
+ for (const invalid of sidecar.invalidUpserts) {
549
+ if (typeof invalid.key !== "string")
550
+ throw new Error("journal: invalid quarantined path");
551
+ paths.add(invalid.key);
552
+ }
553
+ }
554
+ return paths;
555
+ }
509
556
  /**
510
557
  * Reserved journal slug for the personal-vault fanout slot in the `--companies`
511
558
  * runner. The vault slot uploads the whole HQ overlay (`.claude/`, `core/`,
@@ -571,7 +618,7 @@ export function migratePersonalVaultJournal() {
571
618
  * When the file doesn't exist, we return a fresh v2 journal directly — new
572
619
  * installs never pass through v1 on disk.
573
620
  */
574
- export function readJournal(slug) {
621
+ export function readJournal(slug, companyUid) {
575
622
  const areaMigration = openAreaMigrationIfAuthoritative(slug);
576
623
  if (areaMigration) {
577
624
  // Area rows stay in their shared packed store. This record-compatible
@@ -583,7 +630,7 @@ export function readJournal(slug) {
583
630
  // same immutable packed rows. Each owns only its own overlay, so an
584
631
  // uncommitted full pass is never visible to a scoped reader (and a scoped
585
632
  // delta cannot dirty the full pass).
586
- return readStoreBackedJournal(readJournalAggregateWith(slug, areaMigration, true), { privateOverlay: true });
633
+ return readStoreBackedJournal(readJournalAggregateWith(slug, areaMigration, true, companyUid), { privateOverlay: true });
587
634
  }
588
635
  /**
589
636
  * Read the mutable outer shell required by a full pull without detaching every
@@ -599,10 +646,10 @@ export function readJournal(slug) {
599
646
  *
600
647
  * @internal Full-pull boundary; not a public journal record API.
601
648
  */
602
- export function readJournalForPass(slug) {
649
+ export function readJournalForPass(slug, companyUid) {
603
650
  const areaMigration = openAreaMigrationIfAuthoritative(slug);
604
651
  if (!areaMigration)
605
- return readJournal(slug);
652
+ return readJournal(slug, companyUid);
606
653
  // The overlay owns only keys this pass changes; it borrows all untouched
607
654
  // packed rows from the cached aggregate and keeps the public record facade.
608
655
  const journal = readStoreBackedJournal(areaMigration.readAreaJournalCached().journal, { privateOverlay: true });
@@ -631,12 +678,12 @@ export function readJournalForPass(slug) {
631
678
  function readJournalAggregate(slug) {
632
679
  return readJournalAggregateWith(slug, openAreaMigrationIfAuthoritative(slug));
633
680
  }
634
- function readJournalAggregateWith(slug, areaMigration, retainScopedProjection = false) {
681
+ function readJournalAggregateWith(slug, areaMigration, retainScopedProjection = false, companyUid) {
635
682
  if (areaMigration)
636
683
  return areaMigration.readAreaJournal();
637
684
  const store = StateStore.exists(getStateDir(), journalScopeId(slug))
638
- ? openJournalStore(slug, emptyJournal())
639
- : openJournalStore(slug, readLegacyJournal(slug));
685
+ ? openJournalStore(slug, emptyJournal(), companyUid)
686
+ : openJournalStore(slug, readLegacyJournal(slug), companyUid);
640
687
  if (retainScopedProjection) {
641
688
  // A full pass has already paid to recover this packed snapshot. Retain its
642
689
  // projection for the next scoped pass instead of making that pass decode
@@ -805,7 +852,7 @@ export function setJournalSnapshotDecoderForTest(decoder) {
805
852
  export function setJournalSnapshotEncoderForTest(encoder) {
806
853
  journalSnapshotEncoderForTest = encoder;
807
854
  }
808
- function journalStoreOptions(slug, initialJournal) {
855
+ function journalStoreOptions(slug, initialJournal, companyUid) {
809
856
  return {
810
857
  rootDir: getStateDir(),
811
858
  scopeId: journalScopeId(slug),
@@ -816,6 +863,9 @@ function journalStoreOptions(slug, initialJournal) {
816
863
  snapshotEncoder: journalSnapshotEncoderForTest === undefined
817
864
  ? (generation, scopeDigest, payload) => encodeV4JournalSnapshotChunks(generation, scopeDigest, payload, (diagnostic) => journalSnapshotQuarantineListeners.get(slug)?.(diagnostic))
818
865
  : journalSnapshotEncoderForTest ?? undefined,
866
+ snapshotEncoderAsync: asyncJournalCompactionEnabled && journalSnapshotEncoderForTest === undefined
867
+ ? (generation, scopeDigest, payload) => encodeV4JournalSnapshotChunksAsync(generation, scopeDigest, payload, (diagnostic) => journalSnapshotQuarantineListeners.get(slug)?.(diagnostic))
868
+ : undefined,
819
869
  snapshotMagics: [SNAPSHOT_V4_MAGIC],
820
870
  validateRecord: validateJournalRecord,
821
871
  reduce: (state, record) => {
@@ -828,6 +878,7 @@ function journalStoreOptions(slug, initialJournal) {
828
878
  }
829
879
  if (record.type === JOURNAL_DELTA_RECORD_TYPE) {
830
880
  const delta = record.payload;
881
+ const replayDelta = quarantineIncompleteJournalUpserts(slug, companyUid, record, delta);
831
882
  const existingRows = journalRecordRows(state.journal.files) ?? decodedJournalRows(state.journal);
832
883
  if (existingRows) {
833
884
  // StateStore's aggregate is also the immutable source for any
@@ -835,14 +886,14 @@ function journalStoreOptions(slug, initialJournal) {
835
886
  // observed, put durable changes in a separate overlay rather than
836
887
  // mutating that shared packed row store.
837
888
  const rows = existingRows instanceof JournalRowOverlay
838
- ? existingRows.withCommittedDelta(delta.upserts, delta.deletes)
839
- : new JournalRowOverlay(existingRows).withCommittedDelta(delta.upserts, delta.deletes);
889
+ ? existingRows.withCommittedDelta(replayDelta.upserts, replayDelta.deletes)
890
+ : new JournalRowOverlay(existingRows).withCommittedDelta(replayDelta.upserts, replayDelta.deletes);
840
891
  state.journal.files = createJournalRecordProxy(rows);
841
892
  }
842
893
  else {
843
- for (const key of delta.deletes)
894
+ for (const key of replayDelta.deletes)
844
895
  delete state.journal.files[key];
845
- Object.assign(state.journal.files, delta.upserts);
896
+ Object.assign(state.journal.files, replayDelta.upserts);
846
897
  }
847
898
  state.journal.version = delta.version;
848
899
  state.journal.lastSync = delta.lastSync ?? state.journal.lastSync;
@@ -861,12 +912,14 @@ function validateJournalRecord(record) {
861
912
  }
862
913
  return;
863
914
  }
864
- if (record.type === JOURNAL_DELTA_RECORD_TYPE && isJournalDelta(record.payload))
915
+ if (record.type === JOURNAL_DELTA_RECORD_TYPE && isJournalDelta(record.payload)) {
916
+ assertCompleteJournalUpserts(record.payload);
865
917
  return;
918
+ }
866
919
  throw new Error(`journal: unexpected v3 WAL record type ${record.type}`);
867
920
  }
868
- function openJournalStore(slug, initialJournal) {
869
- return StateStore.open(journalStoreOptions(slug, initialJournal));
921
+ function openJournalStore(slug, initialJournal, companyUid) {
922
+ return StateStore.open(journalStoreOptions(slug, initialJournal, companyUid));
870
923
  }
871
924
  /**
872
925
  * A committed area manifest is an authority decision, not a hint. If its
@@ -913,6 +966,7 @@ function openAreaMigrationIfAuthoritative(slug) {
913
966
  rootDir: stateDir,
914
967
  journalSlug: slug,
915
968
  resolver,
969
+ onIncompleteJournalRow: (key, entry) => quarantineIncompleteAreaJournalRow(slug, key, entry),
916
970
  // Area authority is already committed. Open the full reverse-shadow v3
917
971
  // projection only during a mirror mutation instead of pinning it next to
918
972
  // the authoritative area ledger for the runner's whole lifetime.
@@ -1245,6 +1299,7 @@ export class JournalStore {
1245
1299
  upserts: structuredClone(delta.upserts ?? {}),
1246
1300
  deletes: [...(delta.deletes ?? [])],
1247
1301
  };
1302
+ assertCompleteJournalUpserts(payload);
1248
1303
  if (delta.metadata?.lastSync !== undefined)
1249
1304
  payload.lastSync = delta.metadata.lastSync;
1250
1305
  if (delta.metadata?.pulls !== undefined)
@@ -1275,6 +1330,7 @@ export class JournalStore {
1275
1330
  payload.lastSync = delta.metadata.lastSync;
1276
1331
  if (delta.metadata?.pulls !== undefined)
1277
1332
  payload.pulls = structuredClone(delta.metadata.pulls);
1333
+ assertCompleteJournalUpserts(payload);
1278
1334
  return payload;
1279
1335
  };
1280
1336
  if (this.areaMigration) {
@@ -1312,6 +1368,7 @@ export class JournalStore {
1312
1368
  upserts: structuredClone(delta.upserts ?? {}),
1313
1369
  deletes: [...(delta.deletes ?? [])],
1314
1370
  };
1371
+ assertCompleteJournalUpserts(payload);
1315
1372
  if (delta.metadata?.lastSync !== undefined)
1316
1373
  payload.lastSync = delta.metadata.lastSync;
1317
1374
  if (delta.metadata?.pulls !== undefined)
@@ -1591,6 +1648,180 @@ function isJournalDelta(value) {
1591
1648
  Array.isArray(delta.deletes) && delta.deletes.every((key) => typeof key === "string") &&
1592
1649
  (delta.pulls === undefined || Array.isArray(delta.pulls));
1593
1650
  }
1651
+ export class JournalIncompleteUpsertError extends Error {
1652
+ missingFields;
1653
+ errorClass = "JournalIncompleteUpsertError";
1654
+ constructor(missingFields) {
1655
+ super(`journal delta upsert is incomplete (${missingFields.join(",")})`);
1656
+ this.missingFields = missingFields;
1657
+ this.name = "JournalIncompleteUpsertError";
1658
+ }
1659
+ }
1660
+ function incompleteJournalEntryFields(value) {
1661
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1662
+ return ["hash", "size", "syncedAt", "direction"];
1663
+ }
1664
+ const entry = value;
1665
+ const missing = [];
1666
+ if (typeof entry.hash !== "string")
1667
+ missing.push("hash");
1668
+ if (typeof entry.size !== "number" || !Number.isFinite(entry.size))
1669
+ missing.push("size");
1670
+ if (typeof entry.syncedAt !== "string")
1671
+ missing.push("syncedAt");
1672
+ if (entry.direction !== "up" && entry.direction !== "down")
1673
+ missing.push("direction");
1674
+ return missing;
1675
+ }
1676
+ function assertCompleteJournalUpserts(delta) {
1677
+ for (const entry of Object.values(delta.upserts)) {
1678
+ const missing = incompleteJournalEntryFields(entry);
1679
+ if (missing.length > 0)
1680
+ throw new JournalIncompleteUpsertError(missing);
1681
+ }
1682
+ }
1683
+ const loggedQuarantinedJournalRecords = new Set();
1684
+ function quarantineIncompleteJournalUpserts(slug, companyUid, record, delta, freshCompare = false) {
1685
+ const invalid = Object.entries(delta.upserts)
1686
+ .map(([key, value]) => ({ key, value, missingFields: incompleteJournalEntryFields(value) }))
1687
+ .filter((entry) => entry.missingFields.length > 0);
1688
+ if (invalid.length === 0)
1689
+ return delta;
1690
+ const content = JSON.stringify({
1691
+ format: "hq-journal-quarantine@1",
1692
+ slug,
1693
+ companyUid,
1694
+ sequence: record.sequence,
1695
+ recordType: record.type,
1696
+ ...(freshCompare ? { freshCompare: true } : {}),
1697
+ invalidUpserts: invalid,
1698
+ });
1699
+ const digest = crypto.createHash("sha256").update(content).digest("hex");
1700
+ const quarantineDirectory = path.join(getStateDir(), "journal-quarantine", crypto.createHash("sha256").update(journalScopeId(slug)).digest("hex"));
1701
+ const finalPath = path.join(quarantineDirectory, `${record.sequence}-${digest}.json`);
1702
+ let linkAttempted = false;
1703
+ try {
1704
+ fs.mkdirSync(quarantineDirectory, { recursive: true, mode: 0o700 });
1705
+ const tempPath = path.join(quarantineDirectory, `${record.sequence}-${digest}-${crypto.randomBytes(8).toString("hex")}.json.tmp`);
1706
+ let descriptor;
1707
+ let operationError;
1708
+ let operationFailed = false;
1709
+ let cleanupError;
1710
+ let cleanupFailed = false;
1711
+ try {
1712
+ descriptor = fs.openSync(tempPath, "wx", 0o600);
1713
+ fs.writeFileSync(descriptor, `${content}\n`);
1714
+ fs.fsyncSync(descriptor);
1715
+ fs.closeSync(descriptor);
1716
+ descriptor = undefined;
1717
+ // A hard link publishes the fully synced inode atomically and fails if a
1718
+ // previous replay already installed this immutable record. Unlike rename,
1719
+ // link never overwrites an existing complete quarantine sidecar.
1720
+ linkAttempted = true;
1721
+ fs.linkSync(tempPath, finalPath);
1722
+ }
1723
+ catch (error) {
1724
+ operationError = error;
1725
+ operationFailed = true;
1726
+ }
1727
+ finally {
1728
+ if (descriptor !== undefined) {
1729
+ try {
1730
+ fs.closeSync(descriptor);
1731
+ }
1732
+ catch (error) {
1733
+ cleanupError = error;
1734
+ cleanupFailed = true;
1735
+ }
1736
+ }
1737
+ try {
1738
+ fs.unlinkSync(tempPath);
1739
+ }
1740
+ catch (error) {
1741
+ if (error.code !== "ENOENT" && !cleanupFailed) {
1742
+ cleanupError = error;
1743
+ cleanupFailed = true;
1744
+ }
1745
+ }
1746
+ }
1747
+ // Keep the operation's failure authoritative if cleanup also fails. The
1748
+ // cleanup error is surfaced only when publishing itself succeeded.
1749
+ if (operationFailed)
1750
+ throw operationError;
1751
+ if (cleanupFailed)
1752
+ throw cleanupError;
1753
+ const directoryDescriptor = fs.openSync(quarantineDirectory, "r");
1754
+ try {
1755
+ fsyncWithCompatibility(directoryDescriptor, {
1756
+ site: "journal quarantine directory",
1757
+ handleKind: "directory",
1758
+ });
1759
+ }
1760
+ finally {
1761
+ fs.closeSync(directoryDescriptor);
1762
+ }
1763
+ }
1764
+ catch (error) {
1765
+ // Replay may proceed without a write only when the same immutable sidecar
1766
+ // was already published. Other persistence failures must stop replay so a
1767
+ // later compaction cannot discard the only copy of this malformed record.
1768
+ if (linkAttempted &&
1769
+ isAlreadyExistsError(error) &&
1770
+ quarantineSidecarMatches(finalPath, `${content}\n`)) {
1771
+ // An earlier replay installed this exact record.
1772
+ }
1773
+ else {
1774
+ writeJournalRecoveryEvent("journal_quarantine_write_failed", "JournalQuarantineWriteError", companyUid);
1775
+ throw error;
1776
+ }
1777
+ }
1778
+ const eventKey = `${slug}:${record.sequence}:${digest}:${companyUid ?? ""}`;
1779
+ if (!loggedQuarantinedJournalRecords.has(eventKey)) {
1780
+ loggedQuarantinedJournalRecords.add(eventKey);
1781
+ writeJournalRecoveryEvent("journal_record_quarantined", "JournalIncompleteUpsertError", companyUid);
1782
+ }
1783
+ const invalidPaths = new Set(invalid.map((entry) => entry.key));
1784
+ const upserts = Object.fromEntries(Object.entries(delta.upserts).filter(([key]) => !invalidPaths.has(key)));
1785
+ // A malformed upsert never authorizes deletion. Preserve any base row for
1786
+ // that path, even if the same WAL record also listed it in `deletes`.
1787
+ const deletes = delta.deletes.filter((key) => !invalidPaths.has(key));
1788
+ return { ...delta, upserts, deletes };
1789
+ }
1790
+ function quarantineIncompleteAreaJournalRow(slug, key, entry) {
1791
+ const delta = {
1792
+ version: JOURNAL_VERSION_CURRENT,
1793
+ upserts: { [key]: entry },
1794
+ deletes: [],
1795
+ };
1796
+ quarantineIncompleteJournalUpserts(slug, undefined, { sequence: 0, type: JOURNAL_DELTA_RECORD_TYPE, payload: { source: "area-ledger-row" } }, delta, true);
1797
+ }
1798
+ function isAlreadyExistsError(error) {
1799
+ return typeof error === "object" && error !== null && "code" in error &&
1800
+ error.code === "EEXIST";
1801
+ }
1802
+ function quarantineSidecarMatches(filePath, expectedContent) {
1803
+ try {
1804
+ return fs.readFileSync(filePath, "utf8") === expectedContent;
1805
+ }
1806
+ catch {
1807
+ return false;
1808
+ }
1809
+ }
1810
+ function writeJournalRecoveryEvent(event, errorClass, companyUid) {
1811
+ try {
1812
+ process.stderr.write(`${JSON.stringify({
1813
+ type: "error",
1814
+ diagnostic: true,
1815
+ event,
1816
+ errorClass,
1817
+ ...(companyUid === undefined ? {} : { companyUid }),
1818
+ })}\n`);
1819
+ }
1820
+ catch {
1821
+ // A diagnostic event must not break recovery or sync.
1822
+ process.emitWarning(`journal recovery diagnostic could not be emitted (${errorClass})`);
1823
+ }
1824
+ }
1594
1825
  function isJournal(value) {
1595
1826
  return typeof value === "object" && value !== null &&
1596
1827
  (value.version === "1" || value.version === "2") &&
@@ -2237,7 +2468,7 @@ function ensureJournalLocator(slug) {
2237
2468
  */
2238
2469
  export function repairJournalStateIfNeeded(slug, options = {}) {
2239
2470
  const safeSlug = sanitizeSlug(slug);
2240
- const storeOptions = journalStoreOptions(safeSlug, emptyJournal());
2471
+ const storeOptions = journalStoreOptions(safeSlug, emptyJournal(), options.companyUid);
2241
2472
  if (options.maxWalBytesBeforeCompaction !== undefined) {
2242
2473
  storeOptions.maxWalBytesBeforeCompaction = options.maxWalBytesBeforeCompaction;
2243
2474
  }