run402 4.55.0 → 4.56.1
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 +47 -0
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +227 -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,23 @@ export class GitvaultVault {
|
|
|
1189
1190
|
repoDir;
|
|
1190
1191
|
now;
|
|
1191
1192
|
budget;
|
|
1193
|
+
/**
|
|
1194
|
+
* gitvault-clone-scaling (bench P2): the CURRENT walk window's prefetched
|
|
1195
|
+
* bytes — head bytes (bounded-concurrent direct reads) and carrier frames
|
|
1196
|
+
* (one batched getObjects), keyed by storage path — filled by
|
|
1197
|
+
* `verifyToNewest`'s per-page prefetch and `restoreObjectsInto`'s
|
|
1198
|
+
* backward-window prefetch, consulted by {@link readCachedHeadBytes} /
|
|
1199
|
+
* {@link openMaterializeCarriers} before they pay a network read. Transient (REPLACED each page, so memory is
|
|
1200
|
+
* bounded by one listing page) and UNTRUSTED: every consumer sha-checks
|
|
1201
|
+
* an entry against the exact value it would check network bytes against
|
|
1202
|
+
* (the listing's `stored_bytes_sha256`, a receipt's `ciphertext_sha256`),
|
|
1203
|
+
* the same discipline as the keystore object cache — a wrong or stale
|
|
1204
|
+
* entry is a MISS, never a verification bypass. Deliberately NOT the
|
|
1205
|
+
* keystore cache: that cache's eviction window is a handful of newest
|
|
1206
|
+
* generations by design, so routing a whole page through it would evict
|
|
1207
|
+
* the very bytes the ordered walk is about to read.
|
|
1208
|
+
*/
|
|
1209
|
+
walkPrefetch = null;
|
|
1192
1210
|
retries;
|
|
1193
1211
|
servicePublicKey;
|
|
1194
1212
|
genesisCache = null;
|
|
@@ -1228,6 +1246,39 @@ export class GitvaultVault {
|
|
|
1228
1246
|
}
|
|
1229
1247
|
kRepo() { return hexToBytes(this.repoFile().k_repo_hex); }
|
|
1230
1248
|
epoch() { return this.repoFile().epoch; }
|
|
1249
|
+
/**
|
|
1250
|
+
* gitvault-clone-scaling (P3): staleness of the newest checkpoint coverage
|
|
1251
|
+
* this checkout has locally learned, measured at `newestGeneration`. Reads
|
|
1252
|
+
* the keystore AFTER the caller's own persist (a checkpoint-form push has
|
|
1253
|
+
* already recorded its fresh coverage by the time its result is built), so
|
|
1254
|
+
* a compacting push reports itself current. Pure + never-throwing by way of
|
|
1255
|
+
* the helper; unknown coverage reads as `{0, advised: false}` — silent.
|
|
1256
|
+
*/
|
|
1257
|
+
checkpointStalenessNow(newestGeneration) {
|
|
1258
|
+
return gitvaultCheckpointStaleness({ newest_generation: newestGeneration, covers_through_generation: this.repoFile().checkpoint_covers_through ?? null });
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* gitvault-clone-scaling (bench P2): bounded-concurrent direct reads of
|
|
1262
|
+
* many generation-addressed head paths. Heads deliberately do NOT ride
|
|
1263
|
+
* `getObjects` — the `object-reads` presign batch is CARRIER-ONLY by wire
|
|
1264
|
+
* design (see `getObjectsBytes`'s fail-closed doc comment; the live probe
|
|
1265
|
+
* that caught this recorded `getObjects paths=67 FAILED 1ms` followed by
|
|
1266
|
+
* 25 serial singles) — so head bytes parallelize as the SAME direct GETs
|
|
1267
|
+
* the ordered walk itself would issue, just early and overlapped. A
|
|
1268
|
+
* per-path failure resolves `null` (the walk's own read owns that
|
|
1269
|
+
* failure and its envelope); results are raw and UNTRUSTED — callers
|
|
1270
|
+
* sha-check before use, per `walkPrefetch`'s contract.
|
|
1271
|
+
*/
|
|
1272
|
+
async prefetchHeadsConcurrent(paths) {
|
|
1273
|
+
const map = new Map();
|
|
1274
|
+
const fetched = await mapBounded(paths, GITVAULT_TRANSPORT_CONCURRENCY, (path) => this.transport.getObject({ repo_id: this.repoId, path }).catch(() => null));
|
|
1275
|
+
for (let i = 0; i < paths.length; i++) {
|
|
1276
|
+
const b = fetched[i];
|
|
1277
|
+
if (b)
|
|
1278
|
+
map.set(paths[i], b);
|
|
1279
|
+
}
|
|
1280
|
+
return map;
|
|
1281
|
+
}
|
|
1231
1282
|
git() {
|
|
1232
1283
|
if (!this.repoDir)
|
|
1233
1284
|
fail("GITVAULT_REPO_DIR_REQUIRED", "this operation needs the local git repository (repo_dir)", "gitvault publication");
|
|
@@ -1441,6 +1492,12 @@ export class GitvaultVault {
|
|
|
1441
1492
|
let request = { after_generation: anchor, limit: String(GITVAULT_MAX_HEADS_PER_LISTING_PAGE) };
|
|
1442
1493
|
let verified = 0;
|
|
1443
1494
|
const rotations = [];
|
|
1495
|
+
// gitvault-clone-scaling (P3): coverage this walk LEARNS. A checkpoint
|
|
1496
|
+
// head names it outright; a walk anchored at GENESIS that completes
|
|
1497
|
+
// without seeing one proves coverage = genesis. A partial (non-genesis)
|
|
1498
|
+
// walk that sees none proves nothing and persists nothing.
|
|
1499
|
+
let walkCheckpointCoverage = null;
|
|
1500
|
+
const walkedFromGenesis = anchor === GITVAULT_GENESIS_GENERATION;
|
|
1444
1501
|
// Consumed by (at most) the FIRST for(;;) iteration below — a listHeads
|
|
1445
1502
|
// page this call never had to ask the network for, because the state
|
|
1446
1503
|
// read above already proved it (0 entries: pin already current; 1 entry:
|
|
@@ -1543,6 +1600,84 @@ export class GitvaultVault {
|
|
|
1543
1600
|
: await this.transport.listHeads({ repo_id: this.repoId, ...request });
|
|
1544
1601
|
syntheticEntries = null;
|
|
1545
1602
|
progress = verifyHeadsListingPage(page, request, progress, this.repoId);
|
|
1603
|
+
// gitvault-clone-scaling (bench P2): the page just verified names every
|
|
1604
|
+
// entry's generation + stored_bytes_sha256, and the BYTES reads are
|
|
1605
|
+
// independent — verification (and decryption) order is a LOCAL
|
|
1606
|
+
// obligation. Bounded by the remaining verification budget, this
|
|
1607
|
+
// page's per-head SERIAL reads are replaced by: the cache-missing
|
|
1608
|
+
// HEAD bytes as bounded-CONCURRENT direct reads (heads cannot ride
|
|
1609
|
+
// the object-reads presign batch — it is carrier-only by wire design;
|
|
1610
|
+
// see prefetchHeadsConcurrent), then — the same split one level
|
|
1611
|
+
// deeper — ONE batched getObjects for the ref_state/retention_roots
|
|
1612
|
+
// FRAMES each decrypt-validated head will open (their paths +
|
|
1613
|
+
// expected ciphertext hashes parse out of the head bytes just
|
|
1614
|
+
// fetched; parsing is local CPU). Results land in the transient
|
|
1615
|
+
// `walkPrefetch` map (NOT the keystore cache — its eviction window is
|
|
1616
|
+
// smaller than a page; see the field's doc). Failure fidelity: only a
|
|
1617
|
+
// hash-matching result is kept — an absent, mismatched, or
|
|
1618
|
+
// unparseable entry (or a prefetch that fails outright) leaves its
|
|
1619
|
+
// slot empty, and the ordered loop's own per-head reads reproduce the
|
|
1620
|
+
// exact unbatched envelopes. Single-miss sets skip their prefetch
|
|
1621
|
+
// (one direct read costs the same).
|
|
1622
|
+
{
|
|
1623
|
+
const prefetch = new Map();
|
|
1624
|
+
this.walkPrefetch = prefetch;
|
|
1625
|
+
try {
|
|
1626
|
+
const wanted = page.heads.slice(0, Math.max(0, this.budget - verified));
|
|
1627
|
+
const cachedHeadIfMatching = (e) => {
|
|
1628
|
+
const cached = this.keystore.readCachedHead(this.repoId, e.generation);
|
|
1629
|
+
return cached && sha256Hex(cached.bytes) === e.stored_bytes_sha256 ? cached.bytes : null;
|
|
1630
|
+
};
|
|
1631
|
+
const missingHeads = wanted.filter((e) => cachedHeadIfMatching(e) === null);
|
|
1632
|
+
if (missingHeads.length > 1) {
|
|
1633
|
+
const fetched = await this.prefetchHeadsConcurrent(missingHeads.map((e) => gitvaultPaths.head(e.generation)));
|
|
1634
|
+
for (const e of missingHeads) {
|
|
1635
|
+
const path = gitvaultPaths.head(e.generation);
|
|
1636
|
+
const bytes = fetched.get(path) ?? null;
|
|
1637
|
+
if (bytes && sha256Hex(bytes) === e.stored_bytes_sha256)
|
|
1638
|
+
prefetch.set(path, bytes);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (decryptValidate && wanted.length > 1) {
|
|
1642
|
+
const carriers = [];
|
|
1643
|
+
for (const e of wanted) {
|
|
1644
|
+
const headBytes = prefetch.get(gitvaultPaths.head(e.generation)) ?? cachedHeadIfMatching(e);
|
|
1645
|
+
if (!headBytes)
|
|
1646
|
+
continue;
|
|
1647
|
+
let parsed;
|
|
1648
|
+
try {
|
|
1649
|
+
parsed = parseGitvaultStrict(new TextDecoder().decode(headBytes));
|
|
1650
|
+
}
|
|
1651
|
+
catch {
|
|
1652
|
+
continue; // the ordered loop's checkChainLink owns rejecting it
|
|
1653
|
+
}
|
|
1654
|
+
for (const w of [
|
|
1655
|
+
{ receipt: parsed.ref_state, path: gitvaultPaths.refState(parsed.ref_state.object_id) },
|
|
1656
|
+
{ receipt: parsed.retention_roots, path: gitvaultPaths.retentionRoots(parsed.retention_roots.object_id) },
|
|
1657
|
+
]) {
|
|
1658
|
+
const cached = this.keystore.readCachedCarrier(this.repoId, w.receipt.object_id);
|
|
1659
|
+
if (cached && sha256Hex(cached.bytes) === w.receipt.ciphertext_sha256)
|
|
1660
|
+
continue;
|
|
1661
|
+
carriers.push({ path: w.path, sha: w.receipt.ciphertext_sha256 });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (carriers.length > 1) {
|
|
1665
|
+
const fetched = await this.transport.getObjects({ repo_id: this.repoId, paths: carriers.map((c) => c.path) });
|
|
1666
|
+
for (let i = 0; i < carriers.length; i++) {
|
|
1667
|
+
const bytes = fetched[i] ?? null;
|
|
1668
|
+
if (bytes && sha256Hex(bytes) === carriers[i].sha)
|
|
1669
|
+
prefetch.set(carriers[i].path, bytes);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
catch {
|
|
1675
|
+
// A batch that fails OUTRIGHT (network, not a per-slot null) must
|
|
1676
|
+
// not introduce a failure mode the unbatched walk never had — the
|
|
1677
|
+
// ordered loop below re-reads what it needs itself and fails (or
|
|
1678
|
+
// succeeds, if the fault was transient) with its own envelopes.
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1546
1681
|
for (const entry of page.heads) {
|
|
1547
1682
|
if (verified >= this.budget) {
|
|
1548
1683
|
if (persist)
|
|
@@ -1581,6 +1716,8 @@ export class GitvaultVault {
|
|
|
1581
1716
|
prevEpoch = head.epoch;
|
|
1582
1717
|
pin = { generation: head.generation, head_sha256: entry.stored_bytes_sha256, pinned_at: formatGitvaultTimestamp(this.now()) };
|
|
1583
1718
|
lastHead = head;
|
|
1719
|
+
if (head.checkpoint)
|
|
1720
|
+
walkCheckpointCoverage = head.checkpoint.covers_through_generation;
|
|
1584
1721
|
verified += 1;
|
|
1585
1722
|
// The chain walk ALWAYS continues below regardless of decrypt
|
|
1586
1723
|
// outcome (`tryDecrypt` never throws) — `!decryptFailure` just stops
|
|
@@ -1599,6 +1736,15 @@ export class GitvaultVault {
|
|
|
1599
1736
|
break;
|
|
1600
1737
|
request = next;
|
|
1601
1738
|
}
|
|
1739
|
+
// P3: reaching here means the walk COMPLETED (a budget pause throws
|
|
1740
|
+
// above) — persist whatever coverage it proved. A genesis-anchored walk
|
|
1741
|
+
// is authoritative for its whole history, so no checkpoint seen means
|
|
1742
|
+
// coverage = genesis, honestly.
|
|
1743
|
+
if (persist) {
|
|
1744
|
+
const learned = walkCheckpointCoverage ?? (walkedFromGenesis ? GITVAULT_GENESIS_GENERATION : null);
|
|
1745
|
+
if (learned !== null)
|
|
1746
|
+
this.keystore.updateRepo(this.repoId, { checkpoint_covers_through: learned });
|
|
1747
|
+
}
|
|
1602
1748
|
// Catch-up: a call with NOTHING new to walk (this repo's chain-verified
|
|
1603
1749
|
// pin was already at `pin`/`lastHead` — e.g. an EARLIER, decrypt-blind
|
|
1604
1750
|
// `verifyToNewest({})` call already advanced `head_pin` to the newest,
|
|
@@ -1700,7 +1846,16 @@ export class GitvaultVault {
|
|
|
1700
1846
|
const cached = this.keystore.readCachedHead(this.repoId, generation);
|
|
1701
1847
|
if (cached && sha256Hex(cached.bytes) === expectedSha256)
|
|
1702
1848
|
return cached.bytes;
|
|
1703
|
-
|
|
1849
|
+
// gitvault-clone-scaling (P2): a page-prefetched head serves exactly as
|
|
1850
|
+
// a network fetch would — sha-checked here against the SAME expected
|
|
1851
|
+
// value, then cache-warmed. A miss/mismatch falls through to the read.
|
|
1852
|
+
const path = gitvaultPaths.head(generation);
|
|
1853
|
+
const prefetched = this.walkPrefetch?.get(path);
|
|
1854
|
+
if (prefetched && sha256Hex(prefetched) === expectedSha256) {
|
|
1855
|
+
this.keystore.writeCachedHead(this.repoId, generation, expectedSha256, prefetched);
|
|
1856
|
+
return prefetched;
|
|
1857
|
+
}
|
|
1858
|
+
const bytes = await this.transport.getObject({ repo_id: this.repoId, path });
|
|
1704
1859
|
if (bytes && sha256Hex(bytes) === expectedSha256)
|
|
1705
1860
|
this.keystore.writeCachedHead(this.repoId, generation, expectedSha256, bytes);
|
|
1706
1861
|
return bytes;
|
|
@@ -1795,15 +1950,22 @@ export class GitvaultVault {
|
|
|
1795
1950
|
const cachedRoots = this.keystore.readCachedCarrier(this.repoId, rootsReceipt.object_id);
|
|
1796
1951
|
const refStateHit = Boolean(cachedRefState && sha256Hex(cachedRefState.bytes) === refStateReceipt.ciphertext_sha256);
|
|
1797
1952
|
const rootsHit = Boolean(cachedRoots && sha256Hex(cachedRoots.bytes) === rootsReceipt.ciphertext_sha256);
|
|
1953
|
+
// gitvault-clone-scaling (P2): a page-prefetched frame serves exactly as
|
|
1954
|
+
// a fetched one — sha-checked against the receipt's ciphertext hash; a
|
|
1955
|
+
// miss/mismatch falls through to the batched network read below.
|
|
1956
|
+
const preRefState = !refStateHit ? (this.walkPrefetch?.get(refStatePath) ?? null) : null;
|
|
1957
|
+
const refStatePre = preRefState && sha256Hex(preRefState) === refStateReceipt.ciphertext_sha256 ? preRefState : null;
|
|
1958
|
+
const preRoots = !rootsHit ? (this.walkPrefetch?.get(rootsPath) ?? null) : null;
|
|
1959
|
+
const rootsPre = preRoots && sha256Hex(preRoots) === rootsReceipt.ciphertext_sha256 ? preRoots : null;
|
|
1798
1960
|
const missingPaths = [];
|
|
1799
|
-
if (!refStateHit)
|
|
1961
|
+
if (!refStateHit && !refStatePre)
|
|
1800
1962
|
missingPaths.push(refStatePath);
|
|
1801
|
-
if (!rootsHit)
|
|
1963
|
+
if (!rootsHit && !rootsPre)
|
|
1802
1964
|
missingPaths.push(rootsPath);
|
|
1803
1965
|
const fetched = missingPaths.length > 0 ? await this.transport.getObjects({ repo_id: this.repoId, paths: missingPaths }) : [];
|
|
1804
1966
|
let next = 0;
|
|
1805
|
-
const refStateFrame = refStateHit ? cachedRefState.bytes : (fetched[next++] ?? null);
|
|
1806
|
-
const rootsFrame = rootsHit ? cachedRoots.bytes : (fetched[next++] ?? null);
|
|
1967
|
+
const refStateFrame = refStateHit ? cachedRefState.bytes : (refStatePre ?? fetched[next++] ?? null);
|
|
1968
|
+
const rootsFrame = rootsHit ? cachedRoots.bytes : (rootsPre ?? fetched[next++] ?? null);
|
|
1807
1969
|
const refState = this.decodeCarrierFrame("ref_state", refStateReceipt, refStateFrame, writerKey, keyOverride);
|
|
1808
1970
|
const roots = this.decodeCarrierFrame("retention_roots", rootsReceipt, rootsFrame, writerKey, keyOverride);
|
|
1809
1971
|
if (!refStateHit && refStateFrame)
|
|
@@ -2427,7 +2589,7 @@ export class GitvaultVault {
|
|
|
2427
2589
|
if (published.outcome === "dry_run")
|
|
2428
2590
|
fail("GIT_COMMAND_FAILED", "internal: push() received a dry-run result it never requested", "publishing gitvault head");
|
|
2429
2591
|
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 };
|
|
2592
|
+
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
2593
|
}
|
|
2432
2594
|
}
|
|
2433
2595
|
/**
|
|
@@ -2517,7 +2679,7 @@ export class GitvaultVault {
|
|
|
2517
2679
|
// unreachable here — narrows `published` to `"admitted"` below.
|
|
2518
2680
|
if (published.outcome === "dry_run")
|
|
2519
2681
|
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 };
|
|
2682
|
+
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
2683
|
}
|
|
2522
2684
|
}
|
|
2523
2685
|
/** Request a `retention_cutoff` ticket and check it binds THIS base head (and the service key, when one is pinned). */
|
|
@@ -2580,7 +2742,14 @@ export class GitvaultVault {
|
|
|
2580
2742
|
// generation's head entirely.
|
|
2581
2743
|
this.keystore.writeCachedHead(this.repoId, head.generation, hash, back);
|
|
2582
2744
|
const pin = { generation: head.generation, head_sha256: hash, pinned_at: formatGitvaultTimestamp(this.now()) };
|
|
2583
|
-
this.keystore.updateRepo(this.repoId, {
|
|
2745
|
+
this.keystore.updateRepo(this.repoId, {
|
|
2746
|
+
head_pin: pin,
|
|
2747
|
+
materialized_pin: pin,
|
|
2748
|
+
verified_prefix: null,
|
|
2749
|
+
// gitvault-clone-scaling (P3): a checkpoint-form head IS fresh
|
|
2750
|
+
// coverage this checkout just learned first-hand.
|
|
2751
|
+
...(head.checkpoint ? { checkpoint_covers_through: head.checkpoint.covers_through_generation } : {}),
|
|
2752
|
+
});
|
|
2584
2753
|
return { outcome: "admitted", head_sha256: hash, admission_record_sha256: result.admission_record_sha256, capture_receipt: result.capture_receipt };
|
|
2585
2754
|
}
|
|
2586
2755
|
// ── epoch rotation (D193-D203, rev 42, change gitvault-human-envelopes) ──
|
|
@@ -2748,7 +2917,7 @@ export class GitvaultVault {
|
|
|
2748
2917
|
// predecessor manifest back to compute confirmed() (D196) — resolves
|
|
2749
2918
|
// it locally instead of a network object-reads round trip.
|
|
2750
2919
|
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 };
|
|
2920
|
+
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
2921
|
}
|
|
2753
2922
|
}
|
|
2754
2923
|
/**
|
|
@@ -3160,7 +3329,7 @@ export class GitvaultVault {
|
|
|
3160
3329
|
const admitted = await this.admit(head);
|
|
3161
3330
|
if (admitted.outcome === "conflict")
|
|
3162
3331
|
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 };
|
|
3332
|
+
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
3333
|
}
|
|
3165
3334
|
/** Heads `base..newest` (already chain-verified by `verifyToNewest`) re-read + hash-checked from storage. */
|
|
3166
3335
|
async chainFrom(baseGeneration, newest) {
|
|
@@ -3230,6 +3399,50 @@ export class GitvaultVault {
|
|
|
3230
3399
|
const heads = [];
|
|
3231
3400
|
let cur = newest.head;
|
|
3232
3401
|
let incremental = marker !== null;
|
|
3402
|
+
// gitvault-clone-scaling (bench P2): the backward walk's head PATHS are
|
|
3403
|
+
// all derivable up front (generation N−1, N−2, …) — only each head's
|
|
3404
|
+
// expected hash arrives chain-sequentially. Same fetch-concurrent /
|
|
3405
|
+
// verify-ordered split as the forward walk: page-sized windows of
|
|
3406
|
+
// predecessor head bytes batch into the transient `walkPrefetch` map
|
|
3407
|
+
// (use-time sha-checked by `readCachedHeadBytes`; a miss, mismatch, or
|
|
3408
|
+
// failed batch falls back to that read's own single fetch), refilled as
|
|
3409
|
+
// the walk descends past the window floor. The floor ESTIMATE — the
|
|
3410
|
+
// marker on the incremental path, else locally learned checkpoint
|
|
3411
|
+
// coverage, else genesis — only bounds over-fetch; the walk's own stop
|
|
3412
|
+
// conditions are unchanged, and a wrong estimate costs at most one
|
|
3413
|
+
// window of concurrent GETs, never correctness.
|
|
3414
|
+
let prefetchFloor = null;
|
|
3415
|
+
const prefetchBackwardWindow = async (hi) => {
|
|
3416
|
+
let lo = 1n;
|
|
3417
|
+
if (incremental && marker)
|
|
3418
|
+
lo = generationToBigInt(marker.generation) + 1n;
|
|
3419
|
+
else {
|
|
3420
|
+
const known = this.repoFile().checkpoint_covers_through;
|
|
3421
|
+
if (known && /^[0-9a-f]{16}$/.test(known)) {
|
|
3422
|
+
const k = generationToBigInt(known);
|
|
3423
|
+
if (k > lo)
|
|
3424
|
+
lo = k;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
const capLo = hi - BigInt(GITVAULT_MAX_HEADS_PER_LISTING_PAGE) + 1n;
|
|
3428
|
+
if (capLo > lo)
|
|
3429
|
+
lo = capLo;
|
|
3430
|
+
if (lo < 1n)
|
|
3431
|
+
lo = 1n;
|
|
3432
|
+
prefetchFloor = lo;
|
|
3433
|
+
if (hi - lo < 1n)
|
|
3434
|
+
return; // 0 or 1 path — a single read costs the same
|
|
3435
|
+
const gens = [];
|
|
3436
|
+
for (let g = hi; g >= lo; g--)
|
|
3437
|
+
gens.push(bigIntToGeneration(g));
|
|
3438
|
+
try {
|
|
3439
|
+
this.walkPrefetch = await this.prefetchHeadsConcurrent(gens.map((g) => gitvaultPaths.head(g)));
|
|
3440
|
+
}
|
|
3441
|
+
catch {
|
|
3442
|
+
// Fidelity: an outright prefetch failure leaves the map alone — the
|
|
3443
|
+
// walk's own per-head reads take over with their own envelopes.
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3233
3446
|
while (cur) {
|
|
3234
3447
|
// `cur` disqualifies incremental (checked BEFORE including it): abort
|
|
3235
3448
|
// and restart as the wholesale walk. Re-visiting heads already fetched
|
|
@@ -3248,11 +3461,14 @@ export class GitvaultVault {
|
|
|
3248
3461
|
// Before fetching `cur`'s predecessor, check whether the marker
|
|
3249
3462
|
// already names it — a pure LOCAL comparison against bytes this
|
|
3250
3463
|
// client already applied last time, no network round trip.
|
|
3251
|
-
const
|
|
3464
|
+
const prevBig = generationToBigInt(cur.generation) - 1n;
|
|
3465
|
+
const prevGen = bigIntToGeneration(prevBig);
|
|
3252
3466
|
if (incremental && cur.prev_sha256 === marker.head_sha256 && prevGen === marker.generation) {
|
|
3253
3467
|
cur = null; // the predecessor is the marker's own head — already applied; `heads` already holds everything above it
|
|
3254
3468
|
break;
|
|
3255
3469
|
}
|
|
3470
|
+
if (prefetchFloor === null || prevBig < prefetchFloor)
|
|
3471
|
+
await prefetchBackwardWindow(prevBig);
|
|
3256
3472
|
const bytes = await this.readCachedHeadBytes(prevGen, cur.prev_sha256);
|
|
3257
3473
|
if (!bytes || sha256Hex(bytes) !== cur.prev_sha256)
|
|
3258
3474
|
fail("CHAIN_BROKEN", `head ${prevGen} does not match the chain during restore`, "restoring gitvault objects");
|