@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.
- package/dist/bin/area-ledger-migration.d.ts +13 -0
- package/dist/bin/area-ledger-migration.d.ts.map +1 -0
- package/dist/bin/area-ledger-migration.js +64 -0
- package/dist/bin/area-ledger-migration.js.map +1 -0
- package/dist/bin/area-ledger-migration.test.d.ts +2 -0
- package/dist/bin/area-ledger-migration.test.d.ts.map +1 -0
- package/dist/bin/area-ledger-migration.test.js +55 -0
- package/dist/bin/area-ledger-migration.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +6 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +6 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +47 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/cli/share.d.ts +8 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +184 -10
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +330 -7
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +11 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +25 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +11 -1
- package/dist/index.test.js.map +1 -1
- package/dist/journal-store.test.js +42 -1
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +13 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +104 -3
- package/dist/journal.js.map +1 -1
- package/dist/sync/area-ledger-migration.d.ts +116 -0
- package/dist/sync/area-ledger-migration.d.ts.map +1 -0
- package/dist/sync/area-ledger-migration.js +505 -0
- package/dist/sync/area-ledger-migration.js.map +1 -0
- package/dist/sync/area-ledger-migration.test.d.ts +2 -0
- package/dist/sync/area-ledger-migration.test.d.ts.map +1 -0
- package/dist/sync/area-ledger-migration.test.js +274 -0
- package/dist/sync/area-ledger-migration.test.js.map +1 -0
- package/dist/sync/area-ledger.d.ts +93 -0
- package/dist/sync/area-ledger.d.ts.map +1 -0
- package/dist/sync/area-ledger.js +460 -0
- package/dist/sync/area-ledger.js.map +1 -0
- package/dist/sync/area-ledger.test.d.ts +2 -0
- package/dist/sync/area-ledger.test.d.ts.map +1 -0
- package/dist/sync/area-ledger.test.js +199 -0
- package/dist/sync/area-ledger.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +14 -4
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +177 -61
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +140 -10
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
- package/dist/sync/parallel-sync-coordinator.js +22 -4
- package/dist/sync/parallel-sync-coordinator.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +45 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/reconcile-cursor.d.ts +97 -0
- package/dist/sync/reconcile-cursor.d.ts.map +1 -0
- package/dist/sync/reconcile-cursor.js +215 -0
- package/dist/sync/reconcile-cursor.js.map +1 -0
- package/dist/sync/reconcile-cursor.test.d.ts +2 -0
- package/dist/sync/reconcile-cursor.test.d.ts.map +1 -0
- package/dist/sync/reconcile-cursor.test.js +139 -0
- package/dist/sync/reconcile-cursor.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +76 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +317 -3
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +39 -1
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync/sync-work.d.ts +2 -1
- package/dist/sync/sync-work.d.ts.map +1 -1
- package/dist/sync/sync-work.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vault-client.d.ts +9 -2
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +3 -2
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +26 -0
- package/dist/vault-client.test.js.map +1 -1
- 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, {
|
|
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
|
|
947
|
-
CANONICAL_COMPANY_SKILL_RE.test(entry.relativePath)
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
|
|
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,
|