@indigoai-us/hq-cloud 6.15.78 → 6.15.80

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 (95) hide show
  1. package/dist/bin/area-ledger-migration.d.ts +13 -0
  2. package/dist/bin/area-ledger-migration.d.ts.map +1 -0
  3. package/dist/bin/area-ledger-migration.js +64 -0
  4. package/dist/bin/area-ledger-migration.js.map +1 -0
  5. package/dist/bin/area-ledger-migration.test.d.ts +2 -0
  6. package/dist/bin/area-ledger-migration.test.d.ts.map +1 -0
  7. package/dist/bin/area-ledger-migration.test.js +55 -0
  8. package/dist/bin/area-ledger-migration.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +6 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +6 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.js +47 -0
  14. package/dist/bin/sync-runner-company.test.js.map +1 -1
  15. package/dist/cli/share.d.ts +8 -0
  16. package/dist/cli/share.d.ts.map +1 -1
  17. package/dist/cli/share.js +184 -10
  18. package/dist/cli/share.js.map +1 -1
  19. package/dist/cli/share.test.js +330 -7
  20. package/dist/cli/share.test.js.map +1 -1
  21. package/dist/cli/sync.d.ts +7 -0
  22. package/dist/cli/sync.d.ts.map +1 -1
  23. package/dist/cli/sync.js +11 -1
  24. package/dist/cli/sync.js.map +1 -1
  25. package/dist/cli/sync.test.js +25 -0
  26. package/dist/cli/sync.test.js.map +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.test.js +11 -1
  32. package/dist/index.test.js.map +1 -1
  33. package/dist/journal-store.test.js +42 -1
  34. package/dist/journal-store.test.js.map +1 -1
  35. package/dist/journal.d.ts +13 -1
  36. package/dist/journal.d.ts.map +1 -1
  37. package/dist/journal.js +104 -3
  38. package/dist/journal.js.map +1 -1
  39. package/dist/sync/area-ledger-migration.d.ts +116 -0
  40. package/dist/sync/area-ledger-migration.d.ts.map +1 -0
  41. package/dist/sync/area-ledger-migration.js +505 -0
  42. package/dist/sync/area-ledger-migration.js.map +1 -0
  43. package/dist/sync/area-ledger-migration.test.d.ts +2 -0
  44. package/dist/sync/area-ledger-migration.test.d.ts.map +1 -0
  45. package/dist/sync/area-ledger-migration.test.js +274 -0
  46. package/dist/sync/area-ledger-migration.test.js.map +1 -0
  47. package/dist/sync/area-ledger.d.ts +93 -0
  48. package/dist/sync/area-ledger.d.ts.map +1 -0
  49. package/dist/sync/area-ledger.js +460 -0
  50. package/dist/sync/area-ledger.js.map +1 -0
  51. package/dist/sync/area-ledger.test.d.ts +2 -0
  52. package/dist/sync/area-ledger.test.d.ts.map +1 -0
  53. package/dist/sync/area-ledger.test.js +199 -0
  54. package/dist/sync/area-ledger.test.js.map +1 -0
  55. package/dist/sync/event-sync.d.ts +14 -4
  56. package/dist/sync/event-sync.d.ts.map +1 -1
  57. package/dist/sync/event-sync.js +177 -61
  58. package/dist/sync/event-sync.js.map +1 -1
  59. package/dist/sync/event-sync.test.js +140 -10
  60. package/dist/sync/event-sync.test.js.map +1 -1
  61. package/dist/sync/index.d.ts +2 -0
  62. package/dist/sync/index.d.ts.map +1 -1
  63. package/dist/sync/index.js +1 -0
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
  66. package/dist/sync/parallel-sync-coordinator.js +22 -4
  67. package/dist/sync/parallel-sync-coordinator.js.map +1 -1
  68. package/dist/sync/parallel-sync-coordinator.test.js +45 -0
  69. package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
  70. package/dist/sync/reconcile-cursor.d.ts +97 -0
  71. package/dist/sync/reconcile-cursor.d.ts.map +1 -0
  72. package/dist/sync/reconcile-cursor.js +215 -0
  73. package/dist/sync/reconcile-cursor.js.map +1 -0
  74. package/dist/sync/reconcile-cursor.test.d.ts +2 -0
  75. package/dist/sync/reconcile-cursor.test.d.ts.map +1 -0
  76. package/dist/sync/reconcile-cursor.test.js +139 -0
  77. package/dist/sync/reconcile-cursor.test.js.map +1 -0
  78. package/dist/sync/state-store.d.ts +76 -0
  79. package/dist/sync/state-store.d.ts.map +1 -1
  80. package/dist/sync/state-store.js +317 -3
  81. package/dist/sync/state-store.js.map +1 -1
  82. package/dist/sync/state-store.test.js +39 -1
  83. package/dist/sync/state-store.test.js.map +1 -1
  84. package/dist/sync/sync-work.d.ts +2 -1
  85. package/dist/sync/sync-work.d.ts.map +1 -1
  86. package/dist/sync/sync-work.js.map +1 -1
  87. package/dist/types.d.ts +6 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vault-client.d.ts +9 -2
  90. package/dist/vault-client.d.ts.map +1 -1
  91. package/dist/vault-client.js +3 -2
  92. package/dist/vault-client.js.map +1 -1
  93. package/dist/vault-client.test.js +26 -0
  94. package/dist/vault-client.test.js.map +1 -1
  95. package/package.json +3 -2
package/dist/cli/share.js CHANGED
@@ -26,6 +26,7 @@ import { isCloudAuthoritative } from "../lib/cloud-authoritative.js";
26
26
  import { VaultAuthError, VaultClient, } from "../vault-client.js";
27
27
  import { describeError } from "../lib/describe-error.js";
28
28
  import { readlinkOrNull } from "../lib/readlink-safe.js";
29
+ import { checkpointReconcileApply } from "../sync/reconcile-cursor.js";
29
30
  /**
30
31
  * Push-side fresh-collision convergence probe.
31
32
  *
@@ -686,6 +687,20 @@ export async function share(options) {
686
687
  await executeDeletes(run, plans.deletePlan, plans.decommissionPlan, counters, filesRefusedStalePaths, pathResults, !uploadResult.hadUploadFailure && uploadResult.workerErrors.length === 0);
687
688
  finalizeShareJournal(run);
688
689
  throwUploadWorkerErrors(uploadResult.workerErrors);
690
+ // A reconcile cursor is an all-or-nothing boundary for this bounded apply
691
+ // slice. Upload and delete errors are intentionally reported per-path so an
692
+ // ordinary runner can continue, but none of those retryable outcomes may
693
+ // move the cursor beyond the failed mutation.
694
+ if (!uploadResult.hadUploadFailure &&
695
+ uploadResult.workerErrors.length === 0 &&
696
+ !pathResults.some((result) => result.status === "refused")) {
697
+ checkpointReconcileApply(options.reconcileApply, [
698
+ ...plans.pushPlan.items.map((item) => item.relativePath),
699
+ ...plans.deletePlan.toDelete.map((item) => item.key),
700
+ ...plans.deletePlan.toTombstone,
701
+ ...plans.decommissionPlan,
702
+ ]);
703
+ }
689
704
  return buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths, pathResults, false);
690
705
  }
691
706
  /**
@@ -899,7 +914,11 @@ function skillRegistrarForRun(run) {
899
914
  if (!run.vaultConfig)
900
915
  return undefined;
901
916
  const client = new VaultClient(run.vaultConfig);
902
- return ({ companySlug, path: skillPath, content }) => client.registerCompanySkill(companySlug, { path: skillPath, content });
917
+ return ({ companySlug, path: skillPath, content, landed }) => client.registerCompanySkill(companySlug, {
918
+ path: skillPath,
919
+ content,
920
+ ...(landed === true ? { landed: true } : {}),
921
+ });
903
922
  }
904
923
  function assertValidSkillRegistration(result, expectedPath) {
905
924
  const stampedUid = parseRegisteredCompanySkillUid(result.content);
@@ -943,18 +962,106 @@ async function registerOneCompanySkill(run, entry, registrar) {
943
962
  async function preflightCompanySkillIdentities(run, entries) {
944
963
  if (run.options.personalMode === true || run.ctx.uid.startsWith("prs_"))
945
964
  return;
946
- const unstamped = entries.filter((entry) => entry.kind === "file" &&
947
- CANONICAL_COMPANY_SKILL_RE.test(entry.relativePath) &&
948
- !parseRegisteredCompanySkillUid(fs.readFileSync(entry.absolutePath, "utf8")));
949
- if (unstamped.length === 0)
965
+ const canonical = entries.filter((entry) => entry.kind === "file" &&
966
+ CANONICAL_COMPANY_SKILL_RE.test(entry.relativePath));
967
+ const unstamped = canonical.filter((entry) => !parseRegisteredCompanySkillUid(fs.readFileSync(entry.absolutePath, "utf8")));
968
+ const pending = canonical.filter((entry) => {
969
+ const journalEntry = run.journal.files[entry.relativePath];
970
+ return (journalEntry?.skillMetadataPending === true &&
971
+ journalEntry.hash === hashFile(entry.absolutePath));
972
+ });
973
+ if (unstamped.length === 0 && pending.length === 0)
950
974
  return;
951
975
  const registrar = skillRegistrarForRun(run);
952
976
  if (!registrar) {
953
- throw new UnregisteredCompanySkillError(unstamped.map((entry) => entry.relativePath));
977
+ if (unstamped.length > 0) {
978
+ throw new UnregisteredCompanySkillError(unstamped.map((entry) => entry.relativePath));
979
+ }
980
+ return;
954
981
  }
955
982
  for (const entry of unstamped) {
956
983
  await registerOneCompanySkill(run, entry, registrar);
957
984
  }
985
+ for (const entry of pending) {
986
+ const journalEntry = run.journal.files[entry.relativePath];
987
+ run.options.onHeartbeat?.();
988
+ const remote = await headRemoteFile(run.ctx, entry.relativePath);
989
+ if (!remote) {
990
+ // The upload this marker referred to no longer exists. Drop the stale
991
+ // baseline so the normal planner cannot skip the local file as unchanged
992
+ // and will upload it again on this pass.
993
+ delete run.journal.files[entry.relativePath];
994
+ flushShareJournal(run);
995
+ continue;
996
+ }
997
+ if (!journalEntry?.remoteEtag ||
998
+ normalizeEtag(remote.etag) !== normalizeEtag(journalEntry.remoteEtag)) {
999
+ // The successful PUT this marker describes is no longer the live remote
1000
+ // object. Reconcile the current remote bytes instead of stranding the old
1001
+ // marker behind skipUnchanged; the backend independently revalidates this
1002
+ // exact content against S3 before accepting its metadata.
1003
+ // Invalidate the stale baseline before any network work. If recovery
1004
+ // fails, the next pass must still see a real divergence rather than
1005
+ // classifying the local file as unchanged forever.
1006
+ delete run.journal.files[entry.relativePath];
1007
+ flushShareJournal(run);
1008
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-skill-metadata-"));
1009
+ const tempFile = path.join(tempDir, "SKILL.md");
1010
+ try {
1011
+ await downloadFile(run.ctx, entry.relativePath, tempFile);
1012
+ await reconcileLandedCompanySkill(run, entry.relativePath, fs.readFileSync(tempFile, "utf8"));
1013
+ }
1014
+ finally {
1015
+ fs.rmSync(tempDir, { recursive: true, force: true });
1016
+ }
1017
+ continue;
1018
+ }
1019
+ const landedBytes = fs.readFileSync(entry.absolutePath);
1020
+ const landedHash = crypto
1021
+ .createHash("sha256")
1022
+ .update(landedBytes)
1023
+ .digest("hex");
1024
+ if (landedHash !== journalEntry.hash) {
1025
+ // The editor saved after this entry was classified but before the
1026
+ // remote proof returned. Reconcile only the single local snapshot that
1027
+ // produced the journaled upload; the newer edit belongs to the planner.
1028
+ continue;
1029
+ }
1030
+ await reconcileLandedCompanySkill(run, entry.relativePath, landedBytes.toString("utf8"));
1031
+ delete run.journal.files[entry.relativePath]?.skillMetadataPending;
1032
+ checkpointShareJournal(run);
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Reconcile metadata only after the canonical bytes have landed. Identity
1037
+ * reservation happens before upload; tags and other source-authored metadata
1038
+ * must not become authoritative until the S3 write succeeds.
1039
+ */
1040
+ async function reconcileLandedCompanySkill(run, relativePath, content) {
1041
+ if (run.options.personalMode === true ||
1042
+ run.ctx.uid.startsWith("prs_") ||
1043
+ !CANONICAL_COMPANY_SKILL_RE.test(relativePath)) {
1044
+ return;
1045
+ }
1046
+ const registrar = skillRegistrarForRun(run);
1047
+ if (!registrar) {
1048
+ return;
1049
+ }
1050
+ const localUid = parseRegisteredCompanySkillUid(content);
1051
+ if (!localUid) {
1052
+ throw new UnregisteredCompanySkillError([relativePath]);
1053
+ }
1054
+ run.options.onHeartbeat?.();
1055
+ const result = await registrar({
1056
+ companySlug: run.ctx.slug,
1057
+ path: relativePath,
1058
+ content,
1059
+ landed: true,
1060
+ });
1061
+ assertValidSkillRegistration(result, relativePath);
1062
+ if (result.skillUid !== localUid) {
1063
+ throw new Error(`Skill registration returned identity '${result.skillUid}' for '${relativePath}', but the landed file is stamped '${localUid}'.`);
1064
+ }
958
1065
  }
959
1066
  async function buildSharePlans(run) {
960
1067
  const collected = collectFiles(run.paths, run.hqRoot, run.syncRoot, run.shouldSync, {
@@ -1324,12 +1431,41 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
1324
1431
  }
1325
1432
  }
1326
1433
  if (freshContentConverged) {
1434
+ // The equality probe is an await boundary. Confirm that the local body
1435
+ // still matches the bytes we proved are already remote before recording
1436
+ // convergence or reconciling source-authored metadata.
1437
+ const afterProbe = refreshUploadItem();
1438
+ if (afterProbe === null)
1439
+ return;
1440
+ if (!uploadBodyMatchesPlan(item, afterProbe)) {
1441
+ await restartForChangedBody(afterProbe);
1442
+ return;
1443
+ }
1444
+ const shouldReconcileSkill = item.kind === "file" &&
1445
+ CANONICAL_COMPANY_SKILL_RE.test(relativePath) &&
1446
+ skillRegistrarForRun(run) !== undefined;
1447
+ const skillBytes = shouldReconcileSkill
1448
+ ? fs.readFileSync(absolutePath)
1449
+ : undefined;
1450
+ if (skillBytes !== undefined &&
1451
+ crypto.createHash("sha256").update(skillBytes).digest("hex") !== localHash) {
1452
+ // The editor saved after the equality proof. Re-run the normal
1453
+ // remote decision for those new bytes rather than claiming they have
1454
+ // already landed and letting their metadata get ahead of S3.
1455
+ const afterSkillRead = refreshUploadItem();
1456
+ if (afterSkillRead === null)
1457
+ return;
1458
+ await restartForChangedBody(afterSkillRead);
1459
+ return;
1460
+ }
1461
+ const skillContent = skillBytes?.toString("utf8");
1327
1462
  const lstat = fs.lstatSync(absolutePath);
1328
1463
  updateEntry(run.journal, relativePath, localHash, lstat.size, "up", absolutePath, {
1329
1464
  remoteEtag: remoteMeta.etag,
1330
1465
  mtimeMs: lstat.mtimeMs,
1331
1466
  ctimeMs: ctimeToStamp(item.kind === "file" ? item.statAtHash : undefined, lstat),
1332
1467
  kind: item.kind,
1468
+ skillMetadataPending: skillContent !== undefined,
1333
1469
  });
1334
1470
  // Same checkpoint contract as an actual upload. After repeated
1335
1471
  // interrupted passes most of a large vault is already present remotely
@@ -1337,6 +1473,14 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
1337
1473
  // through performUpload. Without this, that pass could reconcile
1338
1474
  // hundreds of thousands of entries purely in memory, be interrupted,
1339
1475
  // and persist none of them — the exact loop this change exists to end.
1476
+ if (skillContent !== undefined) {
1477
+ // This is the crash-recovery path for a PUT that completed before its
1478
+ // journal row was persisted. Make the retry marker durable before the
1479
+ // registry call so another interruption remains recoverable.
1480
+ flushShareJournal(run);
1481
+ await reconcileLandedCompanySkill(run, relativePath, skillContent);
1482
+ delete run.journal.files[relativePath]?.skillMetadataPending;
1483
+ }
1340
1484
  checkpointShareJournal(run);
1341
1485
  run.emit({ type: "reconciled", path: relativePath, direction: "push" });
1342
1486
  counters.filesSkipped++;
@@ -1437,10 +1581,24 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
1437
1581
  const size = isSymlinkUpload ? 0 : lstat.size;
1438
1582
  const mtimeMs = lstat.mtimeMs;
1439
1583
  const ctimeMs = ctimeToStamp(item.kind === "file" ? item.statAtHash : undefined, lstat);
1584
+ // Capture the candidate metadata bytes immediately before uploadFile's
1585
+ // own synchronous read. If an editor saves while the network request is
1586
+ // in flight, the comparison below suppresses reconciliation; that newer
1587
+ // version will upload and reconcile on the next pass.
1588
+ const skillContentAtPut = !isSymlinkUpload && CANONICAL_COMPANY_SKILL_RE.test(relativePath)
1589
+ ? fs.readFileSync(absolutePath, "utf8")
1590
+ : undefined;
1440
1591
  const { etag } = isSymlinkUpload
1441
1592
  ? await uploadSymlink(run.ctx, item.target, relativePath, run.options.author, pc)
1442
1593
  : await uploadFile(run.ctx, absolutePath, relativePath, run.options.author, pc);
1443
- updateEntry(run.journal, relativePath, localHash, size, "up", absolutePath, { remoteEtag: etag, mtimeMs, ctimeMs, kind: item.kind });
1594
+ updateEntry(run.journal, relativePath, localHash, size, "up", absolutePath, {
1595
+ remoteEtag: etag,
1596
+ mtimeMs,
1597
+ ctimeMs,
1598
+ kind: item.kind,
1599
+ skillMetadataPending: skillContentAtPut !== undefined &&
1600
+ skillRegistrarForRun(run) !== undefined,
1601
+ });
1444
1602
  if (run.message) {
1445
1603
  run.journal.files[relativePath] = {
1446
1604
  ...run.journal.files[relativePath],
@@ -1449,9 +1607,25 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
1449
1607
  }
1450
1608
  counters.filesUploaded++;
1451
1609
  counters.bytesUploaded += size;
1452
- // Strictly after the PUT returned and the entry was applied, so an
1453
- // interrupted pass can only ever re-upload a file, never skip one.
1454
- checkpointShareJournal(run);
1610
+ // A landed company skill needs a durable retry marker before the registry
1611
+ // call. The ordinary cadence is intentionally sparse for large vaults,
1612
+ // but losing this row can strand tags after a crash between PUT and
1613
+ // reconciliation.
1614
+ if (run.journal.files[relativePath]?.skillMetadataPending === true) {
1615
+ flushShareJournal(run);
1616
+ }
1617
+ else {
1618
+ // Strictly after the PUT returned and the entry was applied, so an
1619
+ // interrupted pass can only ever re-upload a file, never skip one.
1620
+ checkpointShareJournal(run);
1621
+ }
1622
+ if (skillContentAtPut !== undefined &&
1623
+ skillRegistrarForRun(run) !== undefined &&
1624
+ fs.readFileSync(absolutePath, "utf8") === skillContentAtPut) {
1625
+ await reconcileLandedCompanySkill(run, relativePath, skillContentAtPut);
1626
+ delete run.journal.files[relativePath]?.skillMetadataPending;
1627
+ checkpointShareJournal(run);
1628
+ }
1455
1629
  run.emit({
1456
1630
  type: "progress",
1457
1631
  path: relativePath,