@amalgm/shell 0.1.108 → 0.1.110

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.
@@ -1,9 +1,9 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, } from "node:fs";
3
- import { open } from "node:fs/promises";
3
+ import { open, readFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
6
- import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, PRESIGN_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, rootReplacementFromRecords, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
6
+ import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, planCargoWaves, PRESIGN_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, rootReplacementFromRecords, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
7
7
  import Database from "better-sqlite3";
8
8
  import { atomicCopy, atomicWrite, ensurePrivateDir, ensureUserDir } from "./filesystem.js";
9
9
  import { emitFilesStage, } from "./files-observability.js";
@@ -27,7 +27,9 @@ import { GroundCoordinator } from "./ground-coordination.js";
27
27
  import { submitMutationOperation } from "./paged-mutation-submit.js";
28
28
  import { adoptSnapshotDeliveryBaseline } from "./snapshot-delivery-baseline.js";
29
29
  import { WireClient, WireRequestError } from "./wire-client.js";
30
- const SMALL_CONTENT_UPLOAD_CONCURRENCY = 16;
30
+ /** Cargo waves hold up to 16MiB of payload each; two in flight bounds the
31
+ * upload path's memory while keeping the wire busy. */
32
+ const CARGO_UPLOAD_CONCURRENCY = 2;
31
33
  /** Storage-object lanes for one download set. Tiny objects use the wide
32
34
  * request window, while the separate byte budget narrows large chunks so
33
35
  * request concurrency can never silently become an unbounded memory claim. */
@@ -44,6 +46,13 @@ const DETECT_RETRY_MS = 250;
44
46
  /** A Detect walk yields the event loop this often so Send, Receive, and
45
47
  * Apply progress during a large reconciliation instead of queueing behind it. */
46
48
  const DETECT_WALK_YIELD_ENTRIES = 256;
49
+ /** File captures during a walk overlap reads and hashing across this many
50
+ * lanes; the directory traversal itself stays ordered and serial. */
51
+ const WALK_CAPTURE_CONCURRENCY = 8;
52
+ /** How many times one register attempt re-derives after ground moved under
53
+ * its capture. Each attempt re-walks at read+hash speed, so steady churn is
54
+ * surfaced instead of chased forever. */
55
+ const REGISTER_STALE_CAPTURE_ATTEMPTS = 3;
47
56
  const DOWNLOAD_CONTENT_RETRY_ATTEMPTS = 3;
48
57
  const APPLY_GROUND_WAIT_TIMEOUT_MS = 15_000;
49
58
  const GROUND_ROW_COLUMNS = [
@@ -529,19 +538,30 @@ export class UserGroundHost {
529
538
  // unchanged while a contract cutover moved its cargo namespace, so
530
539
  // the publish path re-proves seal evidence and repairs any missing
531
540
  // uploads before this answer is lawful.
541
+ let staleRepair = false;
532
542
  try {
533
543
  const deliveryWatermark = await this.prepareSnapshotPublication(identity, { journey: "register", workspaceId });
534
544
  await this.publishMaterializedSnapshot(identity, new Set([workspaceId]), deliveryWatermark, { journey: "register", workspaceId });
535
545
  }
536
- finally {
546
+ catch (error) {
547
+ // A stale capture means a repair upload's evidence no longer matches
548
+ // the ground it was captured from. The full walk below recaptures
549
+ // and republishes current truth instead of failing the register.
550
+ if (!staleCapture(error)) {
551
+ this.finishColdOperation();
552
+ throw error;
553
+ }
554
+ staleRepair = true;
555
+ }
556
+ if (!staleRepair) {
537
557
  this.finishColdOperation();
558
+ return {
559
+ registered: true,
560
+ entityCount: countDescendantRows(database, workspaceId),
561
+ cloudHead: this.cloudState.checksum,
562
+ watch,
563
+ };
538
564
  }
539
- return {
540
- registered: true,
541
- entityCount: countDescendantRows(database, workspaceId),
542
- cloudHead: this.cloudState.checksum,
543
- watch,
544
- };
545
565
  }
546
566
  // Register is a cold declaration, not a filesystem edit. It performs one
547
567
  // complete reconciliation and publishes one graph head; it must not mint
@@ -575,75 +595,88 @@ export class UserGroundHost {
575
595
  const observations = this.watchHost.observations().filter((observation) => observation.directory === this.userRoot(identity)
576
596
  || pathWithin(contentOwner, observation.directory));
577
597
  this.watchDirty = false;
578
- const registrationStarted = performance.now();
579
- this.stage({
580
- primitive: "register", stage: "entity-registration", status: "started", workspaceId,
581
- });
582
- const scanned = await reconcileGround({
583
- identity,
584
- userRoot: this.userRoot(identity),
585
- database: this.databasePath(identity),
586
- cacheDir: this.cacheDir(identity),
587
- onScan: (evidence) => this.reportDetectScan(evidence),
588
- registrationWorkspaceId: workspaceId,
589
- suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
590
- });
591
- const retry = scanned.detection.find((plan) => plan.kind === "retry");
592
- if (retry?.kind === "retry") {
593
- throw new Error(retry.reasons.join("; "));
594
- }
595
- const registrationDuration = performance.now() - registrationStarted;
596
- const commitStarted = performance.now();
597
- this.stage({
598
- primitive: "register", stage: "entity-sqlite-commit", status: "started", workspaceId,
599
- });
600
- const committed = commitDetectedState(this.databasePath(identity), {
601
- notebookBasis: scanned.notebookBasis,
602
- acceptedMaterializedRows: scanned.acceptedMaterializedRows,
603
- materializedRemovals: scanned.materializedRemovals,
604
- acceptedNotebookRows: scanned.acceptedNotebookRows,
605
- notebookRemovals: scanned.notebookRemovals,
606
- });
607
- if (!committed) {
608
- throw new Error("verified ground advanced while registration was observing; retry");
609
- }
610
- this.stage({
611
- primitive: "register",
612
- stage: "entity-sqlite-commit",
613
- status: "completed",
614
- workspaceId,
615
- durationMs: performance.now() - commitStarted,
616
- });
617
- if (scanned.transferFailures.length > 0) {
618
- const details = scanned.transferFailures
619
- .map(({ rootId, reason }) => `${rootId}: ${reason}`)
620
- .join("; ");
621
- this.stage({
622
- primitive: "register",
623
- stage: "entity-registration",
624
- status: "failed",
625
- workspaceId,
626
- durationMs: registrationDuration,
627
- measurements: { roots: scanned.transferFailures.length },
628
- });
629
- throw pipelineStageError("entity registration", new Error(`repository state could not be captured: ${details}`));
630
- }
631
- this.stage({
632
- primitive: "register",
633
- stage: "entity-registration",
634
- status: "completed",
635
- workspaceId,
636
- durationMs: registrationDuration,
637
- measurements: {
638
- roots: scanned.publicationRootIds?.length ?? 0,
639
- rows: scanned.acceptedMaterializedRows.length,
640
- },
641
- });
642
- this.namedDetect?.refreshEnrollmentPolicy();
643
- if (!scanned.publicationRootIds) {
644
- throw new Error("cold registration reconciliation did not name its publication roots");
598
+ // Ground may move between a walk's capture and its upload. Each retry
599
+ // re-walks and republishes the ground as it now is — bounded, because
600
+ // a workspace under continuous mutation belongs to Watch afterwards,
601
+ // not to an endless register.
602
+ for (let attempt = 1;; attempt++) {
603
+ try {
604
+ const registrationStarted = performance.now();
605
+ this.stage({
606
+ primitive: "register", stage: "entity-registration", status: "started", workspaceId,
607
+ });
608
+ const scanned = await reconcileGround({
609
+ identity,
610
+ userRoot: this.userRoot(identity),
611
+ database: this.databasePath(identity),
612
+ cacheDir: this.cacheDir(identity),
613
+ onScan: (evidence) => this.reportDetectScan(evidence),
614
+ registrationWorkspaceId: workspaceId,
615
+ suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
616
+ });
617
+ const retry = scanned.detection.find((plan) => plan.kind === "retry");
618
+ if (retry?.kind === "retry") {
619
+ throw new Error(retry.reasons.join("; "));
620
+ }
621
+ const registrationDuration = performance.now() - registrationStarted;
622
+ const commitStarted = performance.now();
623
+ this.stage({
624
+ primitive: "register", stage: "entity-sqlite-commit", status: "started", workspaceId,
625
+ });
626
+ const committed = commitDetectedState(this.databasePath(identity), {
627
+ notebookBasis: scanned.notebookBasis,
628
+ acceptedMaterializedRows: scanned.acceptedMaterializedRows,
629
+ materializedRemovals: scanned.materializedRemovals,
630
+ acceptedNotebookRows: scanned.acceptedNotebookRows,
631
+ notebookRemovals: scanned.notebookRemovals,
632
+ });
633
+ if (!committed) {
634
+ throw new Error("verified ground advanced while registration was observing; retry");
635
+ }
636
+ this.stage({
637
+ primitive: "register",
638
+ stage: "entity-sqlite-commit",
639
+ status: "completed",
640
+ workspaceId,
641
+ durationMs: performance.now() - commitStarted,
642
+ });
643
+ if (scanned.transferFailures.length > 0) {
644
+ const details = scanned.transferFailures
645
+ .map(({ rootId, reason }) => `${rootId}: ${reason}`)
646
+ .join("; ");
647
+ this.stage({
648
+ primitive: "register",
649
+ stage: "entity-registration",
650
+ status: "failed",
651
+ workspaceId,
652
+ durationMs: registrationDuration,
653
+ measurements: { roots: scanned.transferFailures.length },
654
+ });
655
+ throw pipelineStageError("entity registration", new Error(`repository state could not be captured: ${details}`));
656
+ }
657
+ this.stage({
658
+ primitive: "register",
659
+ stage: "entity-registration",
660
+ status: "completed",
661
+ workspaceId,
662
+ durationMs: registrationDuration,
663
+ measurements: {
664
+ roots: scanned.publicationRootIds?.length ?? 0,
665
+ rows: scanned.acceptedMaterializedRows.length,
666
+ },
667
+ });
668
+ this.namedDetect?.refreshEnrollmentPolicy();
669
+ if (!scanned.publicationRootIds) {
670
+ throw new Error("cold registration reconciliation did not name its publication roots");
671
+ }
672
+ await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds), deliveryWatermark, { journey: "register", workspaceId }, scanned.largeManifests);
673
+ break;
674
+ }
675
+ catch (error) {
676
+ if (!staleCapture(error) || attempt >= REGISTER_STALE_CAPTURE_ATTEMPTS)
677
+ throw error;
678
+ }
645
679
  }
646
- await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds), deliveryWatermark, { journey: "register", workspaceId });
647
680
  this.watchHost.settle(observations);
648
681
  }
649
682
  catch (error) {
@@ -762,10 +795,9 @@ export class UserGroundHost {
762
795
  throw new Error("entity Record rail already started");
763
796
  await this.wire.open();
764
797
  // One floor, no per-feature gates: this shell and its gateway release
765
- // together, and the gateway ships first. 11 = snapshot delivery
766
- // watermarks; an older gateway cannot atomically bind Add's graph to the
767
- // official entity-record prefix it already includes.
768
- if (this.wire.protocolVersion < 11) {
798
+ // together, and the gateway ships first. 12 = bounded cold-Register cargo
799
+ // waves; an older gateway cannot verify and seal that upload frame.
800
+ if (this.wire.protocolVersion < 12) {
769
801
  throw new Error("gateway speaks an older wire protocol than this shell");
770
802
  }
771
803
  this.adoptCloudDeliveryBaseline(identity, this.cloudState.deliveryWatermark);
@@ -1152,27 +1184,15 @@ export class UserGroundHost {
1152
1184
  }
1153
1185
  return inline;
1154
1186
  }
1155
- async uploadSnapshotContent(identity, resourceId, records, trace) {
1187
+ async uploadSnapshotContent(identity, resourceId, records, trace, manifestHints) {
1156
1188
  const cache = this.cacheDir(identity);
1157
1189
  const byHash = new Map();
1158
1190
  for (const record of records) {
1159
1191
  const artifact = artifactForRecord(record);
1160
- if (artifact)
1161
- byHash.set(artifact.contentHash, artifact);
1192
+ if (artifact && !byHash.has(artifact.contentHash)) {
1193
+ byHash.set(artifact.contentHash, { artifact, uuid: record.uuid });
1194
+ }
1162
1195
  }
1163
- const sources = [...byHash.values()].map((artifact) => {
1164
- const file = contentCacheFile(cache, artifact.contentHash);
1165
- const manifest = readCachedManifest(cache, artifact.contentHash);
1166
- return {
1167
- artifact,
1168
- file,
1169
- manifest,
1170
- // A retained manifest can prove the remote inventory without local
1171
- // cargo bytes. The source file is required only when the authority
1172
- // reports a missing block.
1173
- bytes: manifest?.bytes ?? statSync(file).size,
1174
- };
1175
- });
1176
1196
  const started = performance.now();
1177
1197
  let uploadedChunks = 0;
1178
1198
  let reusedChunks = 0;
@@ -1190,14 +1210,121 @@ export class UserGroundHost {
1190
1210
  ...identifiers,
1191
1211
  measurements: {
1192
1212
  journey: trace?.journey ?? "bootstrap",
1193
- artifacts: sources.length,
1194
- declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
1213
+ artifacts: byHash.size,
1195
1214
  },
1196
1215
  });
1216
+ // The authority's seal inventory is the only permission to skip an
1217
+ // upload — asked before any source is touched, so sealed artifacts cost
1218
+ // zero local reads and a retried publication whose upload already sealed
1219
+ // completely never needs its captured ground to still exist.
1220
+ const alreadySealed = await this.sealedContentHashes(resourceId, [...byHash.keys()]);
1221
+ const unsealed = [...byHash.values()]
1222
+ .filter(({ artifact }) => !alreadySealed.has(artifact.contentHash));
1223
+ const rows = readGroundRowsByUuid(this.databasePath(identity), unsealed.map(({ uuid }) => uuid));
1224
+ // Each artifact uploads from the strongest evidence available: a cache
1225
+ // copy when another journey pinned one, otherwise the ground entity
1226
+ // itself. Register stages nothing, so the ground path is the common
1227
+ // case — and every read is re-proven against the captured hash, turning
1228
+ // ground churn into a stale capture instead of a wrong upload.
1229
+ const sources = unsealed.map(({ artifact, uuid }) => {
1230
+ const cached = contentCacheFile(cache, artifact.contentHash);
1231
+ const fromCache = existsSync(cached);
1232
+ const row = rows.get(uuid);
1233
+ if (!fromCache && artifact.kind === "git") {
1234
+ // Ground holds the working tree, never the transport parcel; a
1235
+ // lost parcel means recapturing the repository as it now is.
1236
+ throw new StaleCaptureError(`repository parcel ${artifact.contentHash} is no longer captured`);
1237
+ }
1238
+ if (!fromCache && !row) {
1239
+ throw new StaleCaptureError(`entity ${uuid} left the ground before uploading ${artifact.contentHash}`);
1240
+ }
1241
+ const file = fromCache ? cached : row.absolutePath;
1242
+ let linkTarget;
1243
+ let bytes;
1244
+ try {
1245
+ if (!fromCache && artifact.kind === "link") {
1246
+ linkTarget = Buffer.from(readlinkSync(file), "utf8");
1247
+ bytes = linkTarget.byteLength;
1248
+ }
1249
+ else {
1250
+ bytes = statSync(file).size;
1251
+ }
1252
+ }
1253
+ catch (error) {
1254
+ throw new StaleCaptureError(`source for ${artifact.contentHash} is no longer readable`, { cause: error });
1255
+ }
1256
+ const declared = manifestHints?.get(artifact.contentHash)
1257
+ ?? readCachedManifest(cache, artifact.contentHash)
1258
+ ?? undefined;
1259
+ return {
1260
+ artifact,
1261
+ file,
1262
+ fromCache,
1263
+ ...(linkTarget ? { linkTarget } : {}),
1264
+ // Retained evidence only counts while it still describes the exact
1265
+ // source being read; a disagreeing manifest is re-derived instead.
1266
+ ...(declared && declared.bytes === bytes ? { manifest: declared } : {}),
1267
+ bytes,
1268
+ };
1269
+ });
1270
+ const stale = (source, error) => {
1271
+ if (source.fromCache) {
1272
+ // A cache copy that no longer proves its hash is torn evidence:
1273
+ // purge it so the retry reads the ground itself.
1274
+ rmSync(source.file, { force: true });
1275
+ rmSync(manifestCacheFile(cache, source.artifact.contentHash), { force: true });
1276
+ }
1277
+ throw new StaleCaptureError(`${source.artifact.contentHash} no longer matches its source`, { cause: error });
1278
+ };
1279
+ // Complete small artifacts ride cargo waves: whole artifacts per frame,
1280
+ // verified and sealed by the authority in the same round trip, so the
1281
+ // frame count scales with bytes moved, never with file count. Large
1282
+ // artifacts keep per-artifact transfer where chunk reuse pays.
1197
1283
  const small = sources.filter((source) => source.bytes <= CHUNK_BYTES);
1198
1284
  const large = sources.filter((source) => source.bytes > CHUNK_BYTES);
1285
+ let cargoWaves = 0;
1286
+ const transferCargoWave = async (wave) => {
1287
+ const loaded = await Promise.all(wave.items.map(async (source) => {
1288
+ try {
1289
+ const bytes = source.linkTarget ?? await readFile(source.file);
1290
+ return {
1291
+ source,
1292
+ bytes,
1293
+ manifest: deriveUploadManifest(source.artifact, bytes, sha256Hex),
1294
+ };
1295
+ }
1296
+ catch (error) {
1297
+ if (sourceMismatch(error))
1298
+ stale(source, error);
1299
+ throw error;
1300
+ }
1301
+ }));
1302
+ const payload = Buffer.concat(loaded.map((item) => item.bytes));
1303
+ const encoded = await encodeContentWireBytes(payload);
1304
+ const frame = await retryContent(() => this.wire.requestBinary({
1305
+ type: "private.entity-content.put-cargo",
1306
+ resource_id: resourceId,
1307
+ artifacts: loaded.map((item) => ({
1308
+ content_hash: item.source.artifact.contentHash,
1309
+ bytes: item.manifest.bytes,
1310
+ chunks: item.manifest.chunks.map((chunk) => ({ sha256: chunk.sha256, bytes: chunk.bytes })),
1311
+ })),
1312
+ bytes: payload.byteLength,
1313
+ wire_bytes: encoded.bytes.byteLength,
1314
+ ...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
1315
+ }, encoded.bytes, ["private.entity-content.stored-cargo"]));
1316
+ const sealed = new Set(Array.isArray(frame.sealed) ? frame.sealed.map(String) : []);
1317
+ for (const item of loaded) {
1318
+ if (!sealed.has(item.source.artifact.contentHash)) {
1319
+ throw new Error(`content authority did not seal ${item.source.artifact.contentHash}`);
1320
+ }
1321
+ uploadedChunks += item.manifest.chunks.length;
1322
+ }
1323
+ uploadedBytes += encoded.bytes.byteLength;
1324
+ cargoWaves += 1;
1325
+ };
1199
1326
  const transfer = async (source) => {
1200
- const { artifact, file, manifest: cachedManifest, bytes: fileBytes } = source;
1327
+ const { artifact, file, manifest: knownManifest, bytes: fileBytes } = source;
1201
1328
  const sourceFile = { handle: null };
1202
1329
  let manifestPresent = false;
1203
1330
  const putChunk = async (manifest, index, bytes) => {
@@ -1219,8 +1346,7 @@ export class UserGroundHost {
1219
1346
  try {
1220
1347
  const receipt = await uploadArtifact(artifact, {
1221
1348
  bytes: fileBytes,
1222
- ...(cachedManifest ? { manifest: cachedManifest } : {}),
1223
- ...(cachedManifest ? { manifestVerified: true } : {}),
1349
+ ...(knownManifest ? { manifest: knownManifest, manifestVerified: true } : {}),
1224
1350
  verify: async (contentHash) => await hashContentFile(file) === contentHash,
1225
1351
  read: async (offset, bytes) => {
1226
1352
  sourceFile.handle ??= open(file, "r");
@@ -1283,7 +1409,11 @@ export class UserGroundHost {
1283
1409
  });
1284
1410
  uploadedChunks += receipt.uploadedChunks;
1285
1411
  reusedChunks += receipt.reusedChunks;
1286
- atomicWrite(manifestCacheFile(cache, artifact.contentHash), stableJson(receipt.manifest));
1412
+ }
1413
+ catch (error) {
1414
+ if (sourceMismatch(error))
1415
+ stale(source, error);
1416
+ throw error;
1287
1417
  }
1288
1418
  finally {
1289
1419
  if (sourceFile.handle)
@@ -1291,7 +1421,7 @@ export class UserGroundHost {
1291
1421
  }
1292
1422
  };
1293
1423
  try {
1294
- await boundedForEach(small, SMALL_CONTENT_UPLOAD_CONCURRENCY, transfer);
1424
+ await boundedForEach([...planCargoWaves(small)], CARGO_UPLOAD_CONCURRENCY, transferCargoWave);
1295
1425
  for (const source of large) {
1296
1426
  await transfer(source);
1297
1427
  }
@@ -1303,12 +1433,14 @@ export class UserGroundHost {
1303
1433
  durationMs: performance.now() - started,
1304
1434
  measurements: {
1305
1435
  journey: trace?.journey ?? "bootstrap",
1306
- artifacts: sources.length,
1436
+ artifacts: byHash.size,
1437
+ reusedArtifacts: byHash.size - sources.length,
1307
1438
  declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
1308
1439
  uploadedBytes,
1309
1440
  uploadedChunks,
1310
1441
  reusedChunks,
1311
1442
  inventoryRequests,
1443
+ cargoWaves,
1312
1444
  },
1313
1445
  });
1314
1446
  }
@@ -1815,9 +1947,11 @@ export class UserGroundHost {
1815
1947
  catch (error) {
1816
1948
  // A conclusive refusal settles the intent as rejected: the lookup that
1817
1949
  // follows fetches authoritative state and a later register supersedes
1818
- // the settled row, so a refused publication never wedges startup. An
1950
+ // the settled row, so a refused publication never wedges startup. A
1951
+ // stale capture already deleted its unsubmitted row — the intent will
1952
+ // be re-derived from current ground by the next register boundary. An
1819
1953
  // unknown outcome must keep blocking — the mutation may have landed.
1820
- if (!conclusivelyRefused(error))
1954
+ if (!conclusivelyRefused(error) && !staleCapture(error))
1821
1955
  throw error;
1822
1956
  this.cloudState = null;
1823
1957
  }
@@ -1828,12 +1962,13 @@ export class UserGroundHost {
1828
1962
  async prepareSnapshotPublication(identity, trace) {
1829
1963
  // An older durable publication resolves before the new boundary derives.
1830
1964
  // Unknown outcomes remain pending; a conclusive refusal may be replaced
1831
- // by the newer complete snapshot below.
1965
+ // by the newer complete snapshot below; a stale capture already deleted
1966
+ // its unsubmitted row and the boundary being prepared re-derives it.
1832
1967
  try {
1833
1968
  await this.publishPendingSnapshots(identity, trace);
1834
1969
  }
1835
1970
  catch (error) {
1836
- if (!conclusivelyRefused(error))
1971
+ if (!conclusivelyRefused(error) && !staleCapture(error))
1837
1972
  throw error;
1838
1973
  }
1839
1974
  await this.recordRail?.flush();
@@ -1851,7 +1986,7 @@ export class UserGroundHost {
1851
1986
  await this.applyRail?.flush();
1852
1987
  return deliveryWatermark;
1853
1988
  }
1854
- async publishMaterializedSnapshot(identity, publicationRootIds, deliveryWatermark, trace) {
1989
+ async publishMaterializedSnapshot(identity, publicationRootIds, deliveryWatermark, trace, manifestHints) {
1855
1990
  const state = this.cloudState;
1856
1991
  if (!state)
1857
1992
  throw new Error("cloud state is unavailable for graph publication");
@@ -1893,7 +2028,7 @@ export class UserGroundHost {
1893
2028
  // repair on the next Register.
1894
2029
  if (uploadRecords.length > 0) {
1895
2030
  try {
1896
- await this.uploadSnapshotContent(identity, state.resourceId, uploadRecords, trace);
2031
+ await this.uploadSnapshotContent(identity, state.resourceId, uploadRecords, trace, manifestHints);
1897
2032
  }
1898
2033
  catch (error) {
1899
2034
  throw pipelineStageError("cloud upload", error);
@@ -1924,7 +2059,7 @@ export class UserGroundHost {
1924
2059
  replacementRecords: replacement.records,
1925
2060
  status: "pending",
1926
2061
  });
1927
- await this.publishPendingSnapshots(identity, trace);
2062
+ await this.publishPendingSnapshots(identity, trace, manifestHints);
1928
2063
  }
1929
2064
  /** Which of these heads does the authority hold sealed under the current
1930
2065
  * content contract? Batched so a whole workspace costs a handful of
@@ -1949,7 +2084,7 @@ export class UserGroundHost {
1949
2084
  }
1950
2085
  return sealed;
1951
2086
  }
1952
- async publishPendingSnapshots(identity, trace) {
2087
+ async publishPendingSnapshots(identity, trace, manifestHints) {
1953
2088
  for (const pending of readSnapshotPublications(this.databasePath(identity))) {
1954
2089
  // A rejected row is a settled fact, not an intent: the authority
1955
2090
  // conclusively refused this exact mutation, and only a newer complete
@@ -1959,9 +2094,17 @@ export class UserGroundHost {
1959
2094
  const snapshot = snapshotFromRecords(JSON.parse(pending.snapshotJson).records || []);
1960
2095
  const replacement = rootReplacementFromRecords(pending.replacementRootIds, pending.replacementRecords);
1961
2096
  try {
1962
- await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords, trace);
2097
+ await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords, trace, manifestHints);
1963
2098
  }
1964
2099
  catch (error) {
2100
+ // An unsubmitted pending row is a local intent the authority has
2101
+ // never seen. When its captured ground moved before upload, the
2102
+ // intent is simply stale: delete it and let the caller re-derive
2103
+ // one from current ground. A submitted row never lands here — its
2104
+ // upload sealed completely before the first submit frame left.
2105
+ if (staleCapture(error) && !pending.submitted) {
2106
+ deleteJournalEntry(this.databasePath(identity), pending.mutationId);
2107
+ }
1965
2108
  throw pipelineStageError("cloud upload", error);
1966
2109
  }
1967
2110
  let frame;
@@ -1975,6 +2118,9 @@ export class UserGroundHost {
1975
2118
  ...(trace.mutationId ? { mutationId: trace.mutationId } : {}),
1976
2119
  });
1977
2120
  }
2121
+ if (!pending.submitted) {
2122
+ markSnapshotJournalSubmitted(this.databasePath(identity), pending.mutationId);
2123
+ }
1978
2124
  try {
1979
2125
  frame = await submitMutationOperation(this.wire, {
1980
2126
  resourceId: pending.resourceId,
@@ -2221,7 +2367,8 @@ function initializeDatabase(file) {
2221
2367
  resource_id TEXT NOT NULL,
2222
2368
  publication_json TEXT NOT NULL,
2223
2369
  created_at INTEGER NOT NULL,
2224
- status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected'))
2370
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected')),
2371
+ submitted_at INTEGER
2225
2372
  );
2226
2373
  CREATE TABLE IF NOT EXISTS workspace_add_intents (
2227
2374
  workspace_uuid TEXT PRIMARY KEY,
@@ -2262,6 +2409,9 @@ function initializeDatabase(file) {
2262
2409
  if (!publicationColumns.has("status")) {
2263
2410
  database.exec("ALTER TABLE snapshot_publications ADD COLUMN status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected'))");
2264
2411
  }
2412
+ if (!publicationColumns.has("submitted_at")) {
2413
+ database.exec("ALTER TABLE snapshot_publications ADD COLUMN submitted_at INTEGER");
2414
+ }
2265
2415
  database.exec("DROP INDEX IF EXISTS record_inbox_delivery");
2266
2416
  database.exec("DROP TABLE IF EXISTS mutation_journal");
2267
2417
  migratedDatabases.add(file);
@@ -2303,6 +2453,31 @@ function readGroundRowsForRoots(file, table, rootUUIDs) {
2303
2453
  database.close();
2304
2454
  }
2305
2455
  }
2456
+ /** Exact ground rows for a UUID set, batched under SQLite's parameter bound.
2457
+ * Upload resolves each traveling artifact's source path and recorded size
2458
+ * from these rows — content bytes live in the ground itself, never in a
2459
+ * staged register copy. */
2460
+ function readGroundRowsByUuid(file, uuids) {
2461
+ if (!existsSync(file) || uuids.length === 0)
2462
+ return new Map();
2463
+ const database = initializeDatabase(file);
2464
+ try {
2465
+ const found = new Map();
2466
+ for (let start = 0; start < uuids.length; start += 500) {
2467
+ const batch = uuids.slice(start, start + 500);
2468
+ const rows = database.prepare(`
2469
+ SELECT ${GROUND_ROW_PROJECTION}
2470
+ FROM entities WHERE uuid IN (${batch.map(() => "?").join(", ")})
2471
+ `).all(...batch);
2472
+ for (const row of rows)
2473
+ found.set(row.uuid, row);
2474
+ }
2475
+ return found;
2476
+ }
2477
+ finally {
2478
+ database.close();
2479
+ }
2480
+ }
2306
2481
  /** Type and UUID settle locally even when immutable repository capture must
2307
2482
  * retry. Those rows are identity evidence for the next observation, while
2308
2483
  * their absence from the notebook still means no verified logical result has
@@ -2687,7 +2862,8 @@ function readSnapshotPublications(file) {
2687
2862
  try {
2688
2863
  return database.prepare(`
2689
2864
  SELECT mutation_id AS mutationId, resource_id AS resourceId,
2690
- publication_json AS publicationJson, status
2865
+ publication_json AS publicationJson, status,
2866
+ submitted_at AS submittedAt
2691
2867
  FROM snapshot_publications ORDER BY sequence
2692
2868
  `).all().map((row) => {
2693
2869
  const publication = JSON.parse(row.publicationJson);
@@ -2714,6 +2890,7 @@ function readSnapshotPublications(file) {
2714
2890
  replacementRootIds: publication.replacementRootIds.map(String),
2715
2891
  replacementRecords: snapshotFromRecords(publication.replacementRecords).records,
2716
2892
  status: row.status === "rejected" ? "rejected" : "pending",
2893
+ submitted: row.submittedAt !== null,
2717
2894
  };
2718
2895
  });
2719
2896
  }
@@ -2857,26 +3034,28 @@ function commitDetectedState(file, input) {
2857
3034
  }
2858
3035
  }
2859
3036
  /** Journal the newest snapshot publication for its resource. A journal row
2860
- * has exactly two meanings: `pending` — the submit outcome is unknown, so
2861
- * the same mutation id must retry until the authority answers (the mutation
2862
- * may already be accepted with only the acknowledgement lost); `rejected` —
2863
- * the authority conclusively refused it, so it is never replayed. Only a
2864
- * rejected row may be superseded — last write wins — and supersession and
2865
- * insertion share one transaction, so a crash leaves exactly one durable
2866
- * intent. Content uploads are content-addressed and idempotent, so a
2867
- * superseded row loses no uploaded work. */
3037
+ * has exactly three meanings: `pending` and never submitted — a local intent
3038
+ * the authority has never seen, superseded freely (last write wins);
3039
+ * `pending` and submitted — the submit outcome is unknown, so the same
3040
+ * mutation id must retry until the authority answers (the mutation may
3041
+ * already be accepted with only the acknowledgement lost) and nothing may
3042
+ * supersede it; `rejected` — the authority conclusively refused it, so it is
3043
+ * never replayed and awaits supersession. Supersession and insertion share
3044
+ * one transaction, so a crash leaves exactly one durable intent. Content
3045
+ * uploads are content-addressed and idempotent, so a superseded row loses no
3046
+ * uploaded work. */
2868
3047
  function supersedeSnapshotJournal(file, entry) {
2869
3048
  const database = initializeDatabase(file);
2870
3049
  try {
2871
3050
  database.transaction(() => {
2872
3051
  const unresolved = database.prepare(`
2873
3052
  SELECT 1 AS present FROM snapshot_publications
2874
- WHERE resource_id = ? AND status = 'pending' LIMIT 1
3053
+ WHERE resource_id = ? AND status = 'pending' AND submitted_at IS NOT NULL LIMIT 1
2875
3054
  `).get(entry.resourceId);
2876
3055
  if (unresolved) {
2877
- throw new Error("a pending publication with an unknown outcome must resolve before supersession");
3056
+ throw new Error("a submitted publication with an unknown outcome must resolve before supersession");
2878
3057
  }
2879
- database.prepare("DELETE FROM snapshot_publications WHERE resource_id = ? AND status = 'rejected'")
3058
+ database.prepare("DELETE FROM snapshot_publications WHERE resource_id = ?")
2880
3059
  .run(entry.resourceId);
2881
3060
  database.prepare(`
2882
3061
  INSERT INTO snapshot_publications(
@@ -2898,6 +3077,19 @@ function supersedeSnapshotJournal(file, entry) {
2898
3077
  database.close();
2899
3078
  }
2900
3079
  }
3080
+ /** Durably declare a submit attempt before the first frame leaves. From this
3081
+ * moment the row's outcome may exist only at the authority, so it must retry
3082
+ * the same mutation id and may never be superseded or rebuilt locally. */
3083
+ function markSnapshotJournalSubmitted(file, mutationId) {
3084
+ const database = initializeDatabase(file);
3085
+ try {
3086
+ database.prepare("UPDATE snapshot_publications SET submitted_at = ? WHERE mutation_id = ?")
3087
+ .run(Date.now(), mutationId);
3088
+ }
3089
+ finally {
3090
+ database.close();
3091
+ }
3092
+ }
2901
3093
  /** Settle a journal row as conclusively refused. It is never replayed again;
2902
3094
  * a newer complete snapshot supersedes it on the next publication. */
2903
3095
  function markSnapshotJournalRejected(file, mutationId) {
@@ -2919,6 +3111,32 @@ function conclusivelyRefused(error) {
2919
3111
  return error.retryable === false;
2920
3112
  return error instanceof Error && conclusivelyRefused(error.cause);
2921
3113
  }
3114
+ /** Ground moved between capture and upload: a source no longer yields the
3115
+ * bytes its recorded content head names, so the publication built on that
3116
+ * capture can never complete. It was never submitted, so the lawful recovery
3117
+ * is a fresh walk — capture current ground and publish that truth instead. */
3118
+ class StaleCaptureError extends Error {
3119
+ constructor(detail, options) {
3120
+ super(`captured ground moved before upload: ${detail}`, options);
3121
+ }
3122
+ }
3123
+ function staleCapture(error) {
3124
+ if (error instanceof StaleCaptureError)
3125
+ return true;
3126
+ return error instanceof Error && staleCapture(error.cause);
3127
+ }
3128
+ /** Local read evidence disagreeing with the recorded content head — the
3129
+ * upload law's typed mismatch or the source file being gone entirely. */
3130
+ function sourceMismatch(error) {
3131
+ const code = error?.code;
3132
+ return code === "UPLOAD_SOURCE_MISMATCH" || code === "ENOENT";
3133
+ }
3134
+ /** The walked file moved while its bytes were being captured — ordinary
3135
+ * ground behavior, answered by recapturing what stands there now. */
3136
+ function captureChurn(error) {
3137
+ const code = error?.code;
3138
+ return code === "CAPTURE_SOURCE_CHANGED" || code === "ENOENT";
3139
+ }
2922
3140
  function deleteJournalEntry(file, mutationId) {
2923
3141
  const database = initializeDatabase(file);
2924
3142
  try {
@@ -3389,6 +3607,7 @@ async function reconcileGround(input) {
3389
3607
  const notebookRemovals = new Set();
3390
3608
  const detection = [];
3391
3609
  const transferFailures = [];
3610
+ const largeManifests = new Map();
3392
3611
  const scan = async (parameters) => {
3393
3612
  const counters = {
3394
3613
  entries: 0,
@@ -3466,6 +3685,8 @@ async function reconcileGround(input) {
3466
3685
  : coreSuspicion,
3467
3686
  existingRows: existing.filter((row) => row.rootUUID === coreUUID),
3468
3687
  suspicion: observationFor(userRoot),
3688
+ plansSendRecords: suspicions !== undefined,
3689
+ largeManifests,
3469
3690
  });
3470
3691
  }
3471
3692
  const physicalRoots = registrationOwner ? [registrationOwner] : outerBoundRoots;
@@ -3501,6 +3722,11 @@ async function reconcileGround(input) {
3501
3722
  existingRows: existingWorkspaceRows,
3502
3723
  registeredBoundaries,
3503
3724
  suspicion: observationFor(rootPath),
3725
+ // A register scan publishes one complete snapshot; it never mints Send
3726
+ // records for the same initial contents, so it plans no detection and
3727
+ // stages no cache bytes — upload reads the ground itself.
3728
+ plansSendRecords: suspicions !== undefined && registrationWorkspaceId === undefined,
3729
+ largeManifests,
3504
3730
  });
3505
3731
  }
3506
3732
  const changedUUIDs = new Set([
@@ -3524,10 +3750,11 @@ async function reconcileGround(input) {
3524
3750
  notebookRemovals: [...notebookRemovals],
3525
3751
  publicationRootIds: selectedRoots ? [...selectedRoots] : null,
3526
3752
  transferFailures,
3753
+ largeManifests,
3527
3754
  };
3528
3755
  }
3529
3756
  async function reconcileRoot(input) {
3530
- const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, } = input;
3757
+ const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, plansSendRecords, largeManifests, } = input;
3531
3758
  // Prior rows reach this scan from every binding this root encloses. A row's
3532
3759
  // stored relativePath is relative to the root that last committed it, which
3533
3760
  // is a different frame when an enclosing boundary was just registered
@@ -3567,7 +3794,7 @@ async function reconcileRoot(input) {
3567
3794
  transferable: true,
3568
3795
  transferFailure: null,
3569
3796
  notebookRemovals: [],
3570
- detection: input.suspicion ? planGroundDetection({
3797
+ detection: input.suspicion && plansSendRecords ? planGroundDetection({
3571
3798
  rootId: rootUUID,
3572
3799
  suspicion: input.suspicion,
3573
3800
  before: existingRows.map(portableRecord),
@@ -3695,6 +3922,11 @@ async function reconcileRoot(input) {
3695
3922
  return [...new Set(paths)].sort();
3696
3923
  };
3697
3924
  let visitedEntries = 0;
3925
+ // File captures overlap reads and hashing across a bounded pool while the
3926
+ // directory traversal itself stays ordered and serial. Each task settles
3927
+ // its own entry in place; every task is awaited before entries are read.
3928
+ const captureTasks = [];
3929
+ const captureGate = concurrencyGate(WALK_CAPTURE_CONCURRENCY);
3698
3930
  const visit = async (directory, parentUUID, parentType, base = "", activeRepository = parentType === "repo.git"
3699
3931
  ? {
3700
3932
  root: directory,
@@ -3770,6 +4002,7 @@ async function reconcileRoot(input) {
3770
4002
  let type;
3771
4003
  let payloadVersion = null;
3772
4004
  let linkTarget;
4005
+ let deferredCapture = false;
3773
4006
  let contentVerifiedAtMs = existing?.contentVerifiedAtMs ?? null;
3774
4007
  if (!observe && existing && !stats.isDirectory()) {
3775
4008
  type = existing.type;
@@ -3795,7 +4028,7 @@ async function reconcileRoot(input) {
3795
4028
  linkTarget = readlinkSync(absolutePath);
3796
4029
  const bytes = Buffer.from(linkTarget, "utf8");
3797
4030
  payloadVersion = sha256Hex(bytes);
3798
- if (!activeRepository)
4031
+ if (!activeRepository && plansSendRecords)
3799
4032
  sealCapturedArtifact(cacheDir, {
3800
4033
  entityId: uuid,
3801
4034
  entityType: type,
@@ -3815,9 +4048,10 @@ async function reconcileRoot(input) {
3815
4048
  }
3816
4049
  else {
3817
4050
  evidence && (evidence.contentReads += 1);
3818
- const captured = await captureContentFile(absolutePath, stats, activeRepository ? null : cacheDir);
3819
- type = classifyFile({ binary: captured.binary });
3820
- payloadVersion = captured.contentHash;
4051
+ // Provisional identity; the pooled capture scheduled below settles
4052
+ // type, content head, and fingerprint before entries are read.
4053
+ type = "file.binary";
4054
+ deferredCapture = true;
3821
4055
  }
3822
4056
  }
3823
4057
  else {
@@ -3839,6 +4073,44 @@ async function reconcileRoot(input) {
3839
4073
  ...(linkTarget !== undefined ? { linkTarget } : {}),
3840
4074
  ...statFingerprint(stats),
3841
4075
  });
4076
+ if (deferredCapture) {
4077
+ const entry = entries[entries.length - 1];
4078
+ captureTasks.push(captureGate(async () => {
4079
+ let current = stats;
4080
+ for (let attempt = 1;; attempt++) {
4081
+ try {
4082
+ const captured = await captureContentFile(absolutePath, current, activeRepository || !plansSendRecords ? null : cacheDir);
4083
+ entry.type = classifyFile({ binary: captured.binary });
4084
+ entry.payloadVersion = captured.contentHash;
4085
+ entry.contentVerifiedAtMs = Date.now();
4086
+ Object.assign(entry, statFingerprint(current));
4087
+ if (captured.bytes > CHUNK_BYTES) {
4088
+ largeManifests?.set(captured.contentHash, captured.manifest);
4089
+ }
4090
+ return;
4091
+ }
4092
+ catch (error) {
4093
+ if (!captureChurn(error))
4094
+ throw error;
4095
+ if (attempt >= REGISTER_STALE_CAPTURE_ATTEMPTS) {
4096
+ throw new StaleCaptureError(`${absolutePath} kept changing while being captured`, { cause: error });
4097
+ }
4098
+ // The file moved mid-read. Capture what stands there now —
4099
+ // the walk publishes point-in-time truth, not the first bytes
4100
+ // it happened to observe.
4101
+ try {
4102
+ current = lstatSync(absolutePath);
4103
+ }
4104
+ catch (statError) {
4105
+ throw new StaleCaptureError(`${absolutePath} disappeared during capture`, { cause: statError });
4106
+ }
4107
+ if (!current.isFile()) {
4108
+ throw new StaleCaptureError(`${absolutePath} is no longer a file`, { cause: error });
4109
+ }
4110
+ }
4111
+ }
4112
+ }));
4113
+ }
3842
4114
  if (stats.isDirectory() && !stats.isSymbolicLink()) {
3843
4115
  const childRailChanged = existing !== undefined && existing.type !== type;
3844
4116
  await visit(absolutePath, uuid, type, relativePath, type === "repo.git"
@@ -3852,6 +4124,7 @@ async function reconcileRoot(input) {
3852
4124
  }
3853
4125
  };
3854
4126
  await visit(rootPath, rootUUID, rootType);
4127
+ await Promise.all(captureTasks);
3855
4128
  // When every entry sits at its stored address with its stored type and
3856
4129
  // physical fingerprint (honoring any fixed UUID), path-first claiming makes
3857
4130
  // the multi-pass reconciliation the identity map — resolve directly. Any
@@ -3987,7 +4260,17 @@ async function reconcileRoot(input) {
3987
4260
  for (const path of registration.dirtyPaths) {
3988
4261
  const entry = ownedByPath.get(path);
3989
4262
  const absolutePath = join(repository.absolutePath, ...path.split("/"));
3990
- if (!existsSync(absolutePath)) {
4263
+ // Existence must observe the path itself: a tracked symlink with an
4264
+ // absent target is present ground, never a deleted entry.
4265
+ let stats = null;
4266
+ try {
4267
+ stats = lstatSync(absolutePath);
4268
+ }
4269
+ catch (error) {
4270
+ if (error.code !== "ENOENT")
4271
+ throw error;
4272
+ }
4273
+ if (stats === null) {
3991
4274
  if (entry)
3992
4275
  removed.add(path);
3993
4276
  continue;
@@ -3995,7 +4278,6 @@ async function reconcileRoot(input) {
3995
4278
  if (!entry) {
3996
4279
  throw new Error(`repository dirty path ${path} has no registered identity`);
3997
4280
  }
3998
- const stats = lstatSync(absolutePath);
3999
4281
  if (stats.isSymbolicLink()) {
4000
4282
  const target = readlinkSync(absolutePath);
4001
4283
  entry.type = "link";
@@ -4229,7 +4511,9 @@ async function reconcileRoot(input) {
4229
4511
  const before = existingRows.map(portableRecord);
4230
4512
  const beforeByUuid = new Map(before.map((record) => [record.uuid, record]));
4231
4513
  const entryByUuid = new Map(entries.map((entry) => [entry.uuid, entry]));
4232
- const detected = rows.map((row) => {
4514
+ // Replays exist to mint Send records; a scan that plans none builds none —
4515
+ // and its walk staged no cache bytes for replays to read.
4516
+ const detected = !plansSendRecords ? [] : rows.map((row) => {
4233
4517
  const base = beforeByUuid.get(row.record.uuid) ?? null;
4234
4518
  const entry = entryByUuid.get(row.record.uuid);
4235
4519
  if (row.record.type !== "repo.git" && entry && territory.ownerOf(entry)) {
@@ -4324,7 +4608,7 @@ async function reconcileRoot(input) {
4324
4608
  transferable: true,
4325
4609
  transferFailure: null,
4326
4610
  notebookRemovals,
4327
- detection: input.suspicion ? planGroundDetection({
4611
+ detection: input.suspicion && plansSendRecords ? planGroundDetection({
4328
4612
  rootId: rootUUID,
4329
4613
  suspicion: input.suspicion,
4330
4614
  before: plannedBefore,
@@ -5031,4 +5315,25 @@ async function boundedForEach(items, concurrency, perform) {
5031
5315
  };
5032
5316
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
5033
5317
  }
5318
+ /** Admit queued async tasks with at most `limit` in flight. Scheduling never
5319
+ * blocks the caller; each task's settlement is its returned promise. The
5320
+ * admitted task re-checks the bound because a released slot may be claimed
5321
+ * synchronously before a woken waiter resumes. */
5322
+ function concurrencyGate(limit) {
5323
+ let active = 0;
5324
+ const waiting = [];
5325
+ return async (task) => {
5326
+ while (active >= limit) {
5327
+ await new Promise((grant) => waiting.push(grant));
5328
+ }
5329
+ active += 1;
5330
+ try {
5331
+ return await task();
5332
+ }
5333
+ finally {
5334
+ active -= 1;
5335
+ waiting.shift()?.();
5336
+ }
5337
+ };
5338
+ }
5034
5339
  //# sourceMappingURL=user-ground-host.js.map