@run402/sdk 4.61.1 → 4.62.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.
@@ -1101,6 +1101,11 @@ export function createGitvaultHttpTransport(client, options = {}) {
1101
1101
  },
1102
1102
  getVaultRecord: ({ repo_id }) => client.request(base(repo_id), { context: "reading the gitvault record" }),
1103
1103
  getState: ({ repo_id, since }) => getVaultStateOut(repo_id, since),
1104
+ openCompactionGrant: ({ repo_id }) => client.request(`${base(repo_id)}/compaction-grant`, { method: "POST", context: "opening the gitvault compaction headroom grant" }),
1105
+ closeCompactionGrant: async ({ repo_id }) => {
1106
+ const r = await client.request(`${base(repo_id)}/compaction-grant`, { method: "DELETE", context: "closing the gitvault compaction headroom grant" });
1107
+ return { closed: r.closed === true };
1108
+ },
1104
1109
  findVaultByProject: ({ project_id }) => client.request(`/gitvault/v1/vaults?project_id=${encodeURIComponent(project_id)}`, { context: "resolving the project's gitvault" }),
1105
1110
  findVaultByRepo: ({ org_slug, repo_name }) => client.request(`/gitvault/v1/vaults?repo=${encodeURIComponent(`${org_slug}/${repo_name}`)}`, { context: "resolving the gitvault by repo address" }),
1106
1111
  listOrgEncryptionKeys: ({ org_id }) => client.request(`/orgs/v1/${encodeURIComponent(org_id)}/encryption-keys`, { context: "reading the org encryption-key directory" }),
@@ -1165,8 +1170,33 @@ export function createGitvaultHttpTransport(client, options = {}) {
1165
1170
  parsed = null;
1166
1171
  }
1167
1172
  if (!r.ok) {
1173
+ // kychee-com/run402#578 fix 1: the gateway's error envelope is FLAT
1174
+ // (docs/style.md §Errors — `buildErrorEnvelope` in the gateway) —
1175
+ // `code`, `message`, `details`, `trace_id`, and `next_actions` all
1176
+ // ride at the TOP level, never nested under an `error` object (that
1177
+ // key is a human-readable STRING alias for `message`, kept only for
1178
+ // legacy consumers). Reading `envelope.error.code` here — as if the
1179
+ // envelope were `{error: {code, message, details}}` — always missed,
1180
+ // so EVERY refusal (receipt-id reuse, the retention floor,
1181
+ // GC_EPOCH_STALE, …) collapsed to the same opaque
1182
+ // `GITVAULT_PRUNE_SUBMIT_FAILED {details: null}`. Read the real
1183
+ // shape so the gateway's own refusal rides through verbatim; this
1184
+ // wrapper only adds the HTTP status, never replaces content.
1168
1185
  const envelope = parsed;
1169
- fail(envelope?.error?.code ?? "GITVAULT_PRUNE_SUBMIT_FAILED", envelope?.error?.message ?? `prune intent submission failed (HTTP ${r.status})`, "submitting the gitvault prune intent", { status: r.status, details: envelope?.error?.details ?? null });
1186
+ // Spread the gateway's own `details` verbatim (its keys survive
1187
+ // untouched — `ineligible`, `expires_at`, whatever the refusal
1188
+ // carries) and add `http_status`/`trace_id` alongside as this
1189
+ // wrapper's own context, never replacing what the gateway sent.
1190
+ const gatewayDetails = envelope?.details;
1191
+ const details = gatewayDetails && typeof gatewayDetails === "object" && !Array.isArray(gatewayDetails)
1192
+ ? { ...gatewayDetails }
1193
+ : gatewayDetails !== undefined
1194
+ ? { gateway_details: gatewayDetails }
1195
+ : {};
1196
+ details.http_status = r.status;
1197
+ if (envelope?.trace_id)
1198
+ details.trace_id = envelope.trace_id;
1199
+ fail(envelope?.code ?? "GITVAULT_PRUNE_SUBMIT_FAILED", envelope?.message ?? (typeof envelope?.error === "string" ? envelope.error : undefined) ?? `prune intent submission failed (HTTP ${r.status})`, "submitting the gitvault prune intent", details, Array.isArray(envelope?.next_actions) ? envelope.next_actions : undefined);
1170
1200
  }
1171
1201
  const body = (parsed ?? {});
1172
1202
  return { ...body, stored: body.stored === true };
@@ -1318,6 +1348,35 @@ async function writeGitvaultRestoreMarker(targetRepoDir, generation, headSha256)
1318
1348
  await hardenedGit(targetRepoDir, ["config", "--local", GITVAULT_RESTORE_MARKER_GENERATION_KEY, generation]);
1319
1349
  await hardenedGit(targetRepoDir, ["config", "--local", GITVAULT_RESTORE_MARKER_SHA256_KEY, headSha256]);
1320
1350
  }
1351
+ // ─── auto-gc cadence threshold (gitvault-checkpoint-cadence design D1) ───────
1352
+ //
1353
+ // `auto_gc_generations` rides the SAME local-git-config mechanism as the
1354
+ // restore marker above — a per-CHECKOUT knob, exactly like git's own
1355
+ // `gc.auto` (`git config gc.auto`), not a server-side vault policy. Read
1356
+ // fresh every push (a cheap local read, never network); `0` disables;
1357
+ // absent reads as the default.
1358
+ const GITVAULT_AUTO_GC_GENERATIONS_KEY = "r402.autoGcGenerations";
1359
+ /** Default `auto_gc_generations` — see the change proposal's rationale (≈2-3s over the fresh-checkpoint floor at this backlog, a busy repo compacts roughly once per few dozen pushes). */
1360
+ export const GITVAULT_AUTO_GC_GENERATIONS_DEFAULT = 32;
1361
+ /**
1362
+ * Read this checkout's auto-gc threshold, or the default when unset or
1363
+ * unparseable. Never throws — a corrupt local config value degrades to the
1364
+ * default rather than blocking a push's own auto-gc check.
1365
+ */
1366
+ export async function readGitvaultAutoGcThreshold(targetRepoDir) {
1367
+ const raw = await readLocalGitConfigValue(targetRepoDir, GITVAULT_AUTO_GC_GENERATIONS_KEY);
1368
+ if (raw === null)
1369
+ return GITVAULT_AUTO_GC_GENERATIONS_DEFAULT;
1370
+ const trimmed = raw.trim();
1371
+ if (!/^\d+$/.test(trimmed))
1372
+ return GITVAULT_AUTO_GC_GENERATIONS_DEFAULT;
1373
+ const n = Number.parseInt(trimmed, 10);
1374
+ return Number.isSafeInteger(n) && n >= 0 ? n : GITVAULT_AUTO_GC_GENERATIONS_DEFAULT;
1375
+ }
1376
+ /** Set this checkout's auto-gc threshold. `0` disables auto-gc entirely. */
1377
+ export async function writeGitvaultAutoGcThreshold(targetRepoDir, generations) {
1378
+ await hardenedGit(targetRepoDir, ["config", "--local", GITVAULT_AUTO_GC_GENERATIONS_KEY, String(generations)]);
1379
+ }
1321
1380
  /** A transport-agnostic view of git ops the publication needs (the local repository). */
1322
1381
  export class GitvaultVault {
1323
1382
  keystore;
@@ -3578,13 +3637,28 @@ export class GitvaultVault {
3578
3637
  * verification and retained-refs reconciliation run UNCHANGED on both
3579
3638
  * paths; the marker only advances after they both succeed.
3580
3639
  */
3581
- async restoreObjectsInto(targetRepoDir) {
3640
+ async restoreObjectsInto(targetRepoDir, reuse) {
3582
3641
  // gitvault-delta-fetch: the marker is read BEFORE materialize so the
3583
3642
  // state read can carry THIS git dir's applied position as `since` —
3584
3643
  // see tryStateFastPath's own comment on why the pin is the wrong
3585
3644
  // position for a standing clone.
3645
+ //
3646
+ // gitvault-session-state-reuse: `reuse` lets a caller that ALREADY
3647
+ // materialized state for this same target directory — using the SAME
3648
+ // marker `since` this method would itself read — hand that response
3649
+ // in directly, skipping this method's own state read entirely (the
3650
+ // remote-helper session's `list` phase is the caller: see `runFetch`
3651
+ // in `cli/lib/remote-helper-session.mjs`). The marker below is read
3652
+ // regardless (a cheap local `git config` read, never a network call)
3653
+ // and compared against `reuse.marker`: only an EXACT match (both
3654
+ // `null`, or both present with equal `generation`/`head_sha256`) is
3655
+ // trusted — any mismatch (a different push admitted in this same
3656
+ // session, a stale handoff) falls through to this method's own
3657
+ // `materialize()` exactly as if `reuse` had never been passed. A wrong
3658
+ // reuse therefore costs one extra read, never a wrong result.
3586
3659
  const marker = await readGitvaultRestoreMarker(targetRepoDir);
3587
- const newest = await this.materialize({ ...(marker ? { deltaSince: marker.generation } : {}) });
3660
+ const markerMatches = (a, b) => a === null ? b === null : b !== null && a.generation === b.generation && a.head_sha256 === b.head_sha256;
3661
+ const newest = reuse && markerMatches(reuse.marker, marker) ? reuse.state : await this.materialize({ ...(marker ? { deltaSince: marker.generation } : {}) });
3588
3662
  if (!newest.head) {
3589
3663
  const retained_refs = await reconcileRetainedTipRefs(targetRepoDir, { refs: {}, roots: [], head_target: newest.head_target });
3590
3664
  return { refs: {}, head_target: newest.head_target, generation: newest.generation, retained_refs };
@@ -3844,6 +3918,15 @@ export class GitvaultVault {
3844
3918
  await writeGitvaultRestoreMarker(targetRepoDir, newest.generation, newest.head_sha256);
3845
3919
  return { refs: newest.refs, head_target: newest.head_target, generation: newest.generation, retained_refs };
3846
3920
  }
3921
+ // ── compaction headroom grant (gitvault-checkpoint-cadence design D3) ──
3922
+ /** Thin passthrough to the transport — see {@link GitvaultTransport.openCompactionGrant}. */
3923
+ async openCompactionGrant() {
3924
+ return this.transport.openCompactionGrant({ repo_id: this.repoId });
3925
+ }
3926
+ /** Thin passthrough to the transport — see {@link GitvaultTransport.closeCompactionGrant}. Always safe best-effort; never throws by construction of the route (idempotent). */
3927
+ async closeCompactionGrant() {
3928
+ return this.transport.closeCompactionGrant({ repo_id: this.repoId });
3929
+ }
3847
3930
  }
3848
3931
  /** §4.7 cross-field equality: covers_through agree; the claim set's ordered pack ids/hashes/sizes/total equal the manifest's (shared stored fields only). */
3849
3932
  export function checkClaimSetEquality(claimSet, manifest, headCoversThrough) {