run402 4.55.0 → 4.56.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/git-remote-run402.mjs +6 -0
- package/gitvault-surface.json +1 -1
- package/lib/repos.mjs +12 -0
- package/package.json +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +24 -0
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +31 -0
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-keystore.d.ts +11 -1
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-keystore.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +33 -0
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +209 -11
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
|
@@ -40,6 +40,7 @@ import { LocalError, isRun402Error } from "../errors.js";
|
|
|
40
40
|
import { fetchGitvaultObjectBytes } from "./gitvault-edge-fetch.js";
|
|
41
41
|
import { GITVAULT_FORMAT, GITVAULT_GENESIS_EPOCH, GITVAULT_GENESIS_GENERATION, GITVAULT_HEX16_RE, GITVAULT_OID40_RE, GITVAULT_SUITE, attemptKeyCommitment, bytesToHex, checkFreshEpochKeyAgainstPriorKeys, checkHPartition, computeRotationId, computeTargetPartitionDigest, deriveDigestKey, deriveObjectKey, ekFingerprint, epochRotationKeyCommitment, formatGitvaultTimestamp, fromBase64url, hexToBytes, jcs, keyEnvelopeLedgerId, keyedCommitment, newGitvaultId, newHex32, nextEpoch, objectsetContent, openBindingPreimage, openEpochRotationForRecipient, openFrame, parseGitvaultStrict, parseRotateEpochPayload, pinManifestLedgerId, randomBytes, sealFrame, sealKeyEnvelope, sha256Hex, signGitvaultObject, storedBytes, storedBytesSha256, toBase64url, verifyGitvaultObject, } from "../namespaces/gitvault.crypto.js";
|
|
42
42
|
import { GITVAULT_ZERO_SHA256_SENTINEL } from "../namespaces/gitvault.types.js";
|
|
43
|
+
import { gitvaultCheckpointStaleness } from "../namespaces/gitvault.js";
|
|
43
44
|
import { crossProfileGitvaultHint } from "./gitvault-profile-scan.js";
|
|
44
45
|
import { GITVAULT_DEPLOY_REF, hardenedGit, hasObject, isAncestor } from "./gitvault-snapshot.js";
|
|
45
46
|
// ─── Constants (constants.json) ──────────────────────────────────────────────
|
|
@@ -1189,6 +1190,22 @@ export class GitvaultVault {
|
|
|
1189
1190
|
repoDir;
|
|
1190
1191
|
now;
|
|
1191
1192
|
budget;
|
|
1193
|
+
/**
|
|
1194
|
+
* gitvault-clone-scaling (bench P2): the CURRENT listing page's batched
|
|
1195
|
+
* bytes — head bytes and carrier frames, keyed by storage path — filled
|
|
1196
|
+
* by `verifyToNewest`'s per-page prefetch and consulted by
|
|
1197
|
+
* {@link readCachedHeadBytes} / {@link openMaterializeCarriers} before
|
|
1198
|
+
* they pay a network read. Transient (REPLACED each page, so memory is
|
|
1199
|
+
* bounded by one listing page) and UNTRUSTED: every consumer sha-checks
|
|
1200
|
+
* an entry against the exact value it would check network bytes against
|
|
1201
|
+
* (the listing's `stored_bytes_sha256`, a receipt's `ciphertext_sha256`),
|
|
1202
|
+
* the same discipline as the keystore object cache — a wrong or stale
|
|
1203
|
+
* entry is a MISS, never a verification bypass. Deliberately NOT the
|
|
1204
|
+
* keystore cache: that cache's eviction window is a handful of newest
|
|
1205
|
+
* generations by design, so routing a whole page through it would evict
|
|
1206
|
+
* the very bytes the ordered walk is about to read.
|
|
1207
|
+
*/
|
|
1208
|
+
walkPrefetch = null;
|
|
1192
1209
|
retries;
|
|
1193
1210
|
servicePublicKey;
|
|
1194
1211
|
genesisCache = null;
|
|
@@ -1228,6 +1245,17 @@ export class GitvaultVault {
|
|
|
1228
1245
|
}
|
|
1229
1246
|
kRepo() { return hexToBytes(this.repoFile().k_repo_hex); }
|
|
1230
1247
|
epoch() { return this.repoFile().epoch; }
|
|
1248
|
+
/**
|
|
1249
|
+
* gitvault-clone-scaling (P3): staleness of the newest checkpoint coverage
|
|
1250
|
+
* this checkout has locally learned, measured at `newestGeneration`. Reads
|
|
1251
|
+
* the keystore AFTER the caller's own persist (a checkpoint-form push has
|
|
1252
|
+
* already recorded its fresh coverage by the time its result is built), so
|
|
1253
|
+
* a compacting push reports itself current. Pure + never-throwing by way of
|
|
1254
|
+
* the helper; unknown coverage reads as `{0, advised: false}` — silent.
|
|
1255
|
+
*/
|
|
1256
|
+
checkpointStalenessNow(newestGeneration) {
|
|
1257
|
+
return gitvaultCheckpointStaleness({ newest_generation: newestGeneration, covers_through_generation: this.repoFile().checkpoint_covers_through ?? null });
|
|
1258
|
+
}
|
|
1231
1259
|
git() {
|
|
1232
1260
|
if (!this.repoDir)
|
|
1233
1261
|
fail("GITVAULT_REPO_DIR_REQUIRED", "this operation needs the local git repository (repo_dir)", "gitvault publication");
|
|
@@ -1441,6 +1469,12 @@ export class GitvaultVault {
|
|
|
1441
1469
|
let request = { after_generation: anchor, limit: String(GITVAULT_MAX_HEADS_PER_LISTING_PAGE) };
|
|
1442
1470
|
let verified = 0;
|
|
1443
1471
|
const rotations = [];
|
|
1472
|
+
// gitvault-clone-scaling (P3): coverage this walk LEARNS. A checkpoint
|
|
1473
|
+
// head names it outright; a walk anchored at GENESIS that completes
|
|
1474
|
+
// without seeing one proves coverage = genesis. A partial (non-genesis)
|
|
1475
|
+
// walk that sees none proves nothing and persists nothing.
|
|
1476
|
+
let walkCheckpointCoverage = null;
|
|
1477
|
+
const walkedFromGenesis = anchor === GITVAULT_GENESIS_GENERATION;
|
|
1444
1478
|
// Consumed by (at most) the FIRST for(;;) iteration below — a listHeads
|
|
1445
1479
|
// page this call never had to ask the network for, because the state
|
|
1446
1480
|
// read above already proved it (0 entries: pin already current; 1 entry:
|
|
@@ -1543,6 +1577,82 @@ export class GitvaultVault {
|
|
|
1543
1577
|
: await this.transport.listHeads({ repo_id: this.repoId, ...request });
|
|
1544
1578
|
syntheticEntries = null;
|
|
1545
1579
|
progress = verifyHeadsListingPage(page, request, progress, this.repoId);
|
|
1580
|
+
// gitvault-clone-scaling (bench P2): the page just verified names every
|
|
1581
|
+
// entry's generation + stored_bytes_sha256, and the BYTES reads are
|
|
1582
|
+
// independent — verification (and decryption) order is a LOCAL
|
|
1583
|
+
// obligation. Two batched getObjects per page (the same D2 machinery
|
|
1584
|
+
// the restore pack fetches ride), bounded by the remaining
|
|
1585
|
+
// verification budget, replace this page's per-head serial reads:
|
|
1586
|
+
// first the cache-missing HEAD bytes, then — the same split one level
|
|
1587
|
+
// deeper — the ref_state/retention_roots FRAMES each decrypt-validated
|
|
1588
|
+
// head will open (their paths + expected ciphertext hashes parse out
|
|
1589
|
+
// of the head bytes just fetched; parsing is local CPU). Results land
|
|
1590
|
+
// in the transient `walkPrefetch` map (NOT the keystore cache — its
|
|
1591
|
+
// eviction window is smaller than a page; see the field's doc).
|
|
1592
|
+
// Failure fidelity: only a hash-matching result is kept — an absent,
|
|
1593
|
+
// mismatched, or unparseable entry (or a batch that fails outright)
|
|
1594
|
+
// leaves its slot empty, and the ordered loop's own per-head reads
|
|
1595
|
+
// reproduce the exact unbatched envelopes. Single-miss sets skip the
|
|
1596
|
+
// batch (one direct read costs the same).
|
|
1597
|
+
{
|
|
1598
|
+
const prefetch = new Map();
|
|
1599
|
+
this.walkPrefetch = prefetch;
|
|
1600
|
+
try {
|
|
1601
|
+
const wanted = page.heads.slice(0, Math.max(0, this.budget - verified));
|
|
1602
|
+
const cachedHeadIfMatching = (e) => {
|
|
1603
|
+
const cached = this.keystore.readCachedHead(this.repoId, e.generation);
|
|
1604
|
+
return cached && sha256Hex(cached.bytes) === e.stored_bytes_sha256 ? cached.bytes : null;
|
|
1605
|
+
};
|
|
1606
|
+
const missingHeads = wanted.filter((e) => cachedHeadIfMatching(e) === null);
|
|
1607
|
+
if (missingHeads.length > 1) {
|
|
1608
|
+
const fetched = await this.transport.getObjects({ repo_id: this.repoId, paths: missingHeads.map((e) => gitvaultPaths.head(e.generation)) });
|
|
1609
|
+
for (let i = 0; i < missingHeads.length; i++) {
|
|
1610
|
+
const e = missingHeads[i];
|
|
1611
|
+
const bytes = fetched[i] ?? null;
|
|
1612
|
+
if (bytes && sha256Hex(bytes) === e.stored_bytes_sha256)
|
|
1613
|
+
prefetch.set(gitvaultPaths.head(e.generation), bytes);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (decryptValidate && wanted.length > 1) {
|
|
1617
|
+
const carriers = [];
|
|
1618
|
+
for (const e of wanted) {
|
|
1619
|
+
const headBytes = prefetch.get(gitvaultPaths.head(e.generation)) ?? cachedHeadIfMatching(e);
|
|
1620
|
+
if (!headBytes)
|
|
1621
|
+
continue;
|
|
1622
|
+
let parsed;
|
|
1623
|
+
try {
|
|
1624
|
+
parsed = parseGitvaultStrict(new TextDecoder().decode(headBytes));
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
continue; // the ordered loop's checkChainLink owns rejecting it
|
|
1628
|
+
}
|
|
1629
|
+
for (const w of [
|
|
1630
|
+
{ receipt: parsed.ref_state, path: gitvaultPaths.refState(parsed.ref_state.object_id) },
|
|
1631
|
+
{ receipt: parsed.retention_roots, path: gitvaultPaths.retentionRoots(parsed.retention_roots.object_id) },
|
|
1632
|
+
]) {
|
|
1633
|
+
const cached = this.keystore.readCachedCarrier(this.repoId, w.receipt.object_id);
|
|
1634
|
+
if (cached && sha256Hex(cached.bytes) === w.receipt.ciphertext_sha256)
|
|
1635
|
+
continue;
|
|
1636
|
+
carriers.push({ path: w.path, sha: w.receipt.ciphertext_sha256 });
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (carriers.length > 1) {
|
|
1640
|
+
const fetched = await this.transport.getObjects({ repo_id: this.repoId, paths: carriers.map((c) => c.path) });
|
|
1641
|
+
for (let i = 0; i < carriers.length; i++) {
|
|
1642
|
+
const bytes = fetched[i] ?? null;
|
|
1643
|
+
if (bytes && sha256Hex(bytes) === carriers[i].sha)
|
|
1644
|
+
prefetch.set(carriers[i].path, bytes);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
catch {
|
|
1650
|
+
// A batch that fails OUTRIGHT (network, not a per-slot null) must
|
|
1651
|
+
// not introduce a failure mode the unbatched walk never had — the
|
|
1652
|
+
// ordered loop below re-reads what it needs itself and fails (or
|
|
1653
|
+
// succeeds, if the fault was transient) with its own envelopes.
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1546
1656
|
for (const entry of page.heads) {
|
|
1547
1657
|
if (verified >= this.budget) {
|
|
1548
1658
|
if (persist)
|
|
@@ -1581,6 +1691,8 @@ export class GitvaultVault {
|
|
|
1581
1691
|
prevEpoch = head.epoch;
|
|
1582
1692
|
pin = { generation: head.generation, head_sha256: entry.stored_bytes_sha256, pinned_at: formatGitvaultTimestamp(this.now()) };
|
|
1583
1693
|
lastHead = head;
|
|
1694
|
+
if (head.checkpoint)
|
|
1695
|
+
walkCheckpointCoverage = head.checkpoint.covers_through_generation;
|
|
1584
1696
|
verified += 1;
|
|
1585
1697
|
// The chain walk ALWAYS continues below regardless of decrypt
|
|
1586
1698
|
// outcome (`tryDecrypt` never throws) — `!decryptFailure` just stops
|
|
@@ -1599,6 +1711,15 @@ export class GitvaultVault {
|
|
|
1599
1711
|
break;
|
|
1600
1712
|
request = next;
|
|
1601
1713
|
}
|
|
1714
|
+
// P3: reaching here means the walk COMPLETED (a budget pause throws
|
|
1715
|
+
// above) — persist whatever coverage it proved. A genesis-anchored walk
|
|
1716
|
+
// is authoritative for its whole history, so no checkpoint seen means
|
|
1717
|
+
// coverage = genesis, honestly.
|
|
1718
|
+
if (persist) {
|
|
1719
|
+
const learned = walkCheckpointCoverage ?? (walkedFromGenesis ? GITVAULT_GENESIS_GENERATION : null);
|
|
1720
|
+
if (learned !== null)
|
|
1721
|
+
this.keystore.updateRepo(this.repoId, { checkpoint_covers_through: learned });
|
|
1722
|
+
}
|
|
1602
1723
|
// Catch-up: a call with NOTHING new to walk (this repo's chain-verified
|
|
1603
1724
|
// pin was already at `pin`/`lastHead` — e.g. an EARLIER, decrypt-blind
|
|
1604
1725
|
// `verifyToNewest({})` call already advanced `head_pin` to the newest,
|
|
@@ -1700,7 +1821,16 @@ export class GitvaultVault {
|
|
|
1700
1821
|
const cached = this.keystore.readCachedHead(this.repoId, generation);
|
|
1701
1822
|
if (cached && sha256Hex(cached.bytes) === expectedSha256)
|
|
1702
1823
|
return cached.bytes;
|
|
1703
|
-
|
|
1824
|
+
// gitvault-clone-scaling (P2): a page-prefetched head serves exactly as
|
|
1825
|
+
// a network fetch would — sha-checked here against the SAME expected
|
|
1826
|
+
// value, then cache-warmed. A miss/mismatch falls through to the read.
|
|
1827
|
+
const path = gitvaultPaths.head(generation);
|
|
1828
|
+
const prefetched = this.walkPrefetch?.get(path);
|
|
1829
|
+
if (prefetched && sha256Hex(prefetched) === expectedSha256) {
|
|
1830
|
+
this.keystore.writeCachedHead(this.repoId, generation, expectedSha256, prefetched);
|
|
1831
|
+
return prefetched;
|
|
1832
|
+
}
|
|
1833
|
+
const bytes = await this.transport.getObject({ repo_id: this.repoId, path });
|
|
1704
1834
|
if (bytes && sha256Hex(bytes) === expectedSha256)
|
|
1705
1835
|
this.keystore.writeCachedHead(this.repoId, generation, expectedSha256, bytes);
|
|
1706
1836
|
return bytes;
|
|
@@ -1795,15 +1925,22 @@ export class GitvaultVault {
|
|
|
1795
1925
|
const cachedRoots = this.keystore.readCachedCarrier(this.repoId, rootsReceipt.object_id);
|
|
1796
1926
|
const refStateHit = Boolean(cachedRefState && sha256Hex(cachedRefState.bytes) === refStateReceipt.ciphertext_sha256);
|
|
1797
1927
|
const rootsHit = Boolean(cachedRoots && sha256Hex(cachedRoots.bytes) === rootsReceipt.ciphertext_sha256);
|
|
1928
|
+
// gitvault-clone-scaling (P2): a page-prefetched frame serves exactly as
|
|
1929
|
+
// a fetched one — sha-checked against the receipt's ciphertext hash; a
|
|
1930
|
+
// miss/mismatch falls through to the batched network read below.
|
|
1931
|
+
const preRefState = !refStateHit ? (this.walkPrefetch?.get(refStatePath) ?? null) : null;
|
|
1932
|
+
const refStatePre = preRefState && sha256Hex(preRefState) === refStateReceipt.ciphertext_sha256 ? preRefState : null;
|
|
1933
|
+
const preRoots = !rootsHit ? (this.walkPrefetch?.get(rootsPath) ?? null) : null;
|
|
1934
|
+
const rootsPre = preRoots && sha256Hex(preRoots) === rootsReceipt.ciphertext_sha256 ? preRoots : null;
|
|
1798
1935
|
const missingPaths = [];
|
|
1799
|
-
if (!refStateHit)
|
|
1936
|
+
if (!refStateHit && !refStatePre)
|
|
1800
1937
|
missingPaths.push(refStatePath);
|
|
1801
|
-
if (!rootsHit)
|
|
1938
|
+
if (!rootsHit && !rootsPre)
|
|
1802
1939
|
missingPaths.push(rootsPath);
|
|
1803
1940
|
const fetched = missingPaths.length > 0 ? await this.transport.getObjects({ repo_id: this.repoId, paths: missingPaths }) : [];
|
|
1804
1941
|
let next = 0;
|
|
1805
|
-
const refStateFrame = refStateHit ? cachedRefState.bytes : (fetched[next++] ?? null);
|
|
1806
|
-
const rootsFrame = rootsHit ? cachedRoots.bytes : (fetched[next++] ?? null);
|
|
1942
|
+
const refStateFrame = refStateHit ? cachedRefState.bytes : (refStatePre ?? fetched[next++] ?? null);
|
|
1943
|
+
const rootsFrame = rootsHit ? cachedRoots.bytes : (rootsPre ?? fetched[next++] ?? null);
|
|
1807
1944
|
const refState = this.decodeCarrierFrame("ref_state", refStateReceipt, refStateFrame, writerKey, keyOverride);
|
|
1808
1945
|
const roots = this.decodeCarrierFrame("retention_roots", rootsReceipt, rootsFrame, writerKey, keyOverride);
|
|
1809
1946
|
if (!refStateHit && refStateFrame)
|
|
@@ -2427,7 +2564,7 @@ export class GitvaultVault {
|
|
|
2427
2564
|
if (published.outcome === "dry_run")
|
|
2428
2565
|
fail("GIT_COMMAND_FAILED", "internal: push() received a dry-run result it never requested", "publishing gitvault head");
|
|
2429
2566
|
this.keystore.updateRepo(this.repoId, { last_ref_transaction: { generation: published.generation, transaction: options.transaction, at: formatGitvaultTimestamp(this.now()) } });
|
|
2430
|
-
return { generation: published.generation, head_sha256: published.head_sha256, head: published.head, admission_record_sha256: published.admission_record_sha256, capture_receipt: published.capture_receipt, form: published.form, conflicts_retried: conflicts, refs: published.refs };
|
|
2567
|
+
return { generation: published.generation, head_sha256: published.head_sha256, head: published.head, admission_record_sha256: published.admission_record_sha256, capture_receipt: published.capture_receipt, form: published.form, conflicts_retried: conflicts, refs: published.refs, checkpoint_staleness: this.checkpointStalenessNow(published.generation) };
|
|
2431
2568
|
}
|
|
2432
2569
|
}
|
|
2433
2570
|
/**
|
|
@@ -2517,7 +2654,7 @@ export class GitvaultVault {
|
|
|
2517
2654
|
// unreachable here — narrows `published` to `"admitted"` below.
|
|
2518
2655
|
if (published.outcome === "dry_run")
|
|
2519
2656
|
fail("GIT_COMMAND_FAILED", "internal: publishCheckpoint() received a dry-run result it never requested", "publishing gitvault checkpoint");
|
|
2520
|
-
return { generation: published.generation, head_sha256: published.head_sha256, head: published.head, admission_record_sha256: published.admission_record_sha256, capture_receipt: published.capture_receipt, form: published.form, conflicts_retried: conflicts, refs: published.refs };
|
|
2657
|
+
return { generation: published.generation, head_sha256: published.head_sha256, head: published.head, admission_record_sha256: published.admission_record_sha256, capture_receipt: published.capture_receipt, form: published.form, conflicts_retried: conflicts, refs: published.refs, checkpoint_staleness: this.checkpointStalenessNow(published.generation) };
|
|
2521
2658
|
}
|
|
2522
2659
|
}
|
|
2523
2660
|
/** Request a `retention_cutoff` ticket and check it binds THIS base head (and the service key, when one is pinned). */
|
|
@@ -2580,7 +2717,14 @@ export class GitvaultVault {
|
|
|
2580
2717
|
// generation's head entirely.
|
|
2581
2718
|
this.keystore.writeCachedHead(this.repoId, head.generation, hash, back);
|
|
2582
2719
|
const pin = { generation: head.generation, head_sha256: hash, pinned_at: formatGitvaultTimestamp(this.now()) };
|
|
2583
|
-
this.keystore.updateRepo(this.repoId, {
|
|
2720
|
+
this.keystore.updateRepo(this.repoId, {
|
|
2721
|
+
head_pin: pin,
|
|
2722
|
+
materialized_pin: pin,
|
|
2723
|
+
verified_prefix: null,
|
|
2724
|
+
// gitvault-clone-scaling (P3): a checkpoint-form head IS fresh
|
|
2725
|
+
// coverage this checkout just learned first-hand.
|
|
2726
|
+
...(head.checkpoint ? { checkpoint_covers_through: head.checkpoint.covers_through_generation } : {}),
|
|
2727
|
+
});
|
|
2584
2728
|
return { outcome: "admitted", head_sha256: hash, admission_record_sha256: result.admission_record_sha256, capture_receipt: result.capture_receipt };
|
|
2585
2729
|
}
|
|
2586
2730
|
// ── epoch rotation (D193-D203, rev 42, change gitvault-human-envelopes) ──
|
|
@@ -2748,7 +2892,7 @@ export class GitvaultVault {
|
|
|
2748
2892
|
// predecessor manifest back to compute confirmed() (D196) — resolves
|
|
2749
2893
|
// it locally instead of a network object-reads round trip.
|
|
2750
2894
|
this.keystore.updateRepo(this.repoId, { known_pin_manifest: { pin_manifest_version: nextVersion, stored_bytes_sha256: manifestSha, pins } });
|
|
2751
|
-
return { generation, head_sha256: admitted.head_sha256, head, admission_record_sha256: admitted.admission_record_sha256, capture_receipt: admitted.capture_receipt, form: "wal", conflicts_retried: conflicts, refs: base.refs };
|
|
2895
|
+
return { generation, head_sha256: admitted.head_sha256, head, admission_record_sha256: admitted.admission_record_sha256, capture_receipt: admitted.capture_receipt, form: "wal", conflicts_retried: conflicts, refs: base.refs, checkpoint_staleness: this.checkpointStalenessNow(generation) };
|
|
2752
2896
|
}
|
|
2753
2897
|
}
|
|
2754
2898
|
/**
|
|
@@ -3160,7 +3304,7 @@ export class GitvaultVault {
|
|
|
3160
3304
|
const admitted = await this.admit(head);
|
|
3161
3305
|
if (admitted.outcome === "conflict")
|
|
3162
3306
|
fail("HEAD_CAS_CONFLICT", "a different head was admitted while the repair was being prepared", "publishing repair head", { winner: admitted.winner }, [{ action: "verify the attached winner from storage, rebase, retry" }]);
|
|
3163
|
-
return { generation: repairGen, head_sha256: admitted.head_sha256, head, admission_record_sha256: admitted.admission_record_sha256, capture_receipt: null, form: "checkpoint", conflicts_retried: 0, refs: repairedRefs };
|
|
3307
|
+
return { generation: repairGen, head_sha256: admitted.head_sha256, head, admission_record_sha256: admitted.admission_record_sha256, capture_receipt: null, form: "checkpoint", conflicts_retried: 0, refs: repairedRefs, checkpoint_staleness: this.checkpointStalenessNow(repairGen) };
|
|
3164
3308
|
}
|
|
3165
3309
|
/** Heads `base..newest` (already chain-verified by `verifyToNewest`) re-read + hash-checked from storage. */
|
|
3166
3310
|
async chainFrom(baseGeneration, newest) {
|
|
@@ -3230,6 +3374,57 @@ export class GitvaultVault {
|
|
|
3230
3374
|
const heads = [];
|
|
3231
3375
|
let cur = newest.head;
|
|
3232
3376
|
let incremental = marker !== null;
|
|
3377
|
+
// gitvault-clone-scaling (bench P2): the backward walk's head PATHS are
|
|
3378
|
+
// all derivable up front (generation N−1, N−2, …) — only each head's
|
|
3379
|
+
// expected hash arrives chain-sequentially. Same fetch-concurrent /
|
|
3380
|
+
// verify-ordered split as the forward walk: page-sized windows of
|
|
3381
|
+
// predecessor head bytes batch into the transient `walkPrefetch` map
|
|
3382
|
+
// (use-time sha-checked by `readCachedHeadBytes`; a miss, mismatch, or
|
|
3383
|
+
// failed batch falls back to that read's own single fetch), refilled as
|
|
3384
|
+
// the walk descends past the window floor. The floor ESTIMATE — the
|
|
3385
|
+
// marker on the incremental path, else locally learned checkpoint
|
|
3386
|
+
// coverage, else genesis — only bounds over-fetch; the walk's own stop
|
|
3387
|
+
// conditions are unchanged, and a wrong estimate costs at most one
|
|
3388
|
+
// window of concurrent GETs, never correctness.
|
|
3389
|
+
let prefetchFloor = null;
|
|
3390
|
+
const prefetchBackwardWindow = async (hi) => {
|
|
3391
|
+
let lo = 1n;
|
|
3392
|
+
if (incremental && marker)
|
|
3393
|
+
lo = generationToBigInt(marker.generation) + 1n;
|
|
3394
|
+
else {
|
|
3395
|
+
const known = this.repoFile().checkpoint_covers_through;
|
|
3396
|
+
if (known && /^[0-9a-f]{16}$/.test(known)) {
|
|
3397
|
+
const k = generationToBigInt(known);
|
|
3398
|
+
if (k > lo)
|
|
3399
|
+
lo = k;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
const capLo = hi - BigInt(GITVAULT_MAX_HEADS_PER_LISTING_PAGE) + 1n;
|
|
3403
|
+
if (capLo > lo)
|
|
3404
|
+
lo = capLo;
|
|
3405
|
+
if (lo < 1n)
|
|
3406
|
+
lo = 1n;
|
|
3407
|
+
prefetchFloor = lo;
|
|
3408
|
+
if (hi - lo < 1n)
|
|
3409
|
+
return; // 0 or 1 path — a single read costs the same
|
|
3410
|
+
const gens = [];
|
|
3411
|
+
for (let g = hi; g >= lo; g--)
|
|
3412
|
+
gens.push(bigIntToGeneration(g));
|
|
3413
|
+
try {
|
|
3414
|
+
const fetched = await this.transport.getObjects({ repo_id: this.repoId, paths: gens.map((g) => gitvaultPaths.head(g)) });
|
|
3415
|
+
const map = new Map();
|
|
3416
|
+
for (let i = 0; i < gens.length; i++) {
|
|
3417
|
+
const b = fetched[i];
|
|
3418
|
+
if (b)
|
|
3419
|
+
map.set(gitvaultPaths.head(gens[i]), b);
|
|
3420
|
+
}
|
|
3421
|
+
this.walkPrefetch = map;
|
|
3422
|
+
}
|
|
3423
|
+
catch {
|
|
3424
|
+
// Fidelity: an outright batch failure leaves the map alone — the
|
|
3425
|
+
// walk's own per-head reads take over with their own envelopes.
|
|
3426
|
+
}
|
|
3427
|
+
};
|
|
3233
3428
|
while (cur) {
|
|
3234
3429
|
// `cur` disqualifies incremental (checked BEFORE including it): abort
|
|
3235
3430
|
// and restart as the wholesale walk. Re-visiting heads already fetched
|
|
@@ -3248,11 +3443,14 @@ export class GitvaultVault {
|
|
|
3248
3443
|
// Before fetching `cur`'s predecessor, check whether the marker
|
|
3249
3444
|
// already names it — a pure LOCAL comparison against bytes this
|
|
3250
3445
|
// client already applied last time, no network round trip.
|
|
3251
|
-
const
|
|
3446
|
+
const prevBig = generationToBigInt(cur.generation) - 1n;
|
|
3447
|
+
const prevGen = bigIntToGeneration(prevBig);
|
|
3252
3448
|
if (incremental && cur.prev_sha256 === marker.head_sha256 && prevGen === marker.generation) {
|
|
3253
3449
|
cur = null; // the predecessor is the marker's own head — already applied; `heads` already holds everything above it
|
|
3254
3450
|
break;
|
|
3255
3451
|
}
|
|
3452
|
+
if (prefetchFloor === null || prevBig < prefetchFloor)
|
|
3453
|
+
await prefetchBackwardWindow(prevBig);
|
|
3256
3454
|
const bytes = await this.readCachedHeadBytes(prevGen, cur.prev_sha256);
|
|
3257
3455
|
if (!bytes || sha256Hex(bytes) !== cur.prev_sha256)
|
|
3258
3456
|
fail("CHAIN_BROKEN", `head ${prevGen} does not match the chain during restore`, "restoring gitvault objects");
|