@amalgm/shell 0.1.100 → 0.1.102

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/PURPOSE.md CHANGED
@@ -284,10 +284,13 @@ shell only assembles.
284
284
  baseline: the newest retained Inbox result that exactly equals each
285
285
  installed row advances that entity's resolved and materialized sequence
286
286
  without replaying historical cargo, while every later different result
287
- remains ordinary Apply work. Startup adopts that baseline while holding
288
- every watched ground cone: an older Detect transaction settles first and
289
- any newer Watch generation waits until adoption finishes. Apply starts
290
- only after that finite boundary. Register
287
+ remains ordinary Apply work. Watch may establish native coverage before
288
+ startup Apply is ready, but its suspicions remain unconsumed until Apply
289
+ has adopted or resumed durable materialization. Startup adopts that
290
+ baseline while holding every watched ground cone; only after Apply starts
291
+ does one Detect generation consume the rings accumulated during boot. An
292
+ older Detect transaction settles first and any newer Watch generation
293
+ waits until adoption finishes. Register
291
294
  and Watch enumerate the same durable binding inventory, so a missed
292
295
  notification for rendering the portable reference cannot make either the
293
296
  reference row or its declared external
@@ -154,6 +154,12 @@ export declare class UserGroundHost {
154
154
  * runs unchanged. */
155
155
  private readInlineCargo;
156
156
  private uploadSnapshotContent;
157
+ /** Download a set of immutable artifacts straight from content storage.
158
+ * The set is the primitive: every storage object the set still needs —
159
+ * manifests and blocks alike — shares one wide lane bound, so no artifact
160
+ * serializes behind another's plan or seal. Each object is still its own
161
+ * storage request. A lone artifact is a set of one. */
162
+ private downloadContentSet;
157
163
  private downloadContent;
158
164
  private ensureWatchers;
159
165
  private scheduleRescan;
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
3
3
  import { open } 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, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, download as downloadArtifact, 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, 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";
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,13 +27,12 @@ import { GroundCoordinator } from "./ground-coordination.js";
27
27
  import { submitMutationOperation } from "./paged-mutation-submit.js";
28
28
  import { WireClient, WireRequestError } from "./wire-client.js";
29
29
  const SMALL_CONTENT_UPLOAD_CONCURRENCY = 16;
30
- /** Artifact lanes for a download phase. Direct storage fetches cost the
31
- * gateway nothing, so parallelism is bounded by this machine's pipe rather
32
- * than authority fairness. */
33
- const FILE_CONTENT_DOWNLOAD_CONCURRENCY = 16;
34
- /** Block lanes within one artifact. Most files are a single block; large
35
- * repository packs fan their chunks across this. */
36
- const DIRECT_BLOCK_DOWNLOAD_CONCURRENCY = 8;
30
+ /** Storage-object lanes for one download set. Manifests and blocks are all
31
+ * independent content-addressed objects fetched straight from storage, so
32
+ * twelve thousand small files and one large pack share the same wide bound;
33
+ * direct fetches cost the gateway nothing, and memory stays bounded by
34
+ * lanes × one block. */
35
+ const DIRECT_OBJECT_DOWNLOAD_CONCURRENCY = 128;
37
36
  const DETECT_QUIET_MS = 12;
38
37
  const DETECT_MAX_DEFERRAL_MS = 75;
39
38
  const DETECT_RETRY_MS = 250;
@@ -87,7 +86,14 @@ export class UserGroundHost {
87
86
  });
88
87
  this.watchHost = new NodeWatchHost({
89
88
  open: options.openWatch,
90
- onSignal: () => this.scheduleRescan(),
89
+ // Watch establishes the native evidence boundary during convergence,
90
+ // before the durable Apply baseline has been adopted. Keep every ring
91
+ // in Watch, but do not let Detect consume that evidence until Apply has
92
+ // resumed any revealed intent and owns materialization truth.
93
+ onSignal: () => {
94
+ if (this.converged)
95
+ this.scheduleRescan();
96
+ },
91
97
  onEvent: (evidence) => this.stage({
92
98
  primitive: "watch",
93
99
  stage: "native-suspicion",
@@ -164,6 +170,11 @@ export class UserGroundHost {
164
170
  await this.startRecordRail(this.activeIdentity);
165
171
  await this.startApplyRail(this.activeIdentity);
166
172
  this.converged = true;
173
+ // Native rings collected while convergence was establishing Watch
174
+ // coverage are ordinary evidence, not a startup exception. They get
175
+ // one Detect generation now that Apply recovery is authoritative.
176
+ if (this.watchHost.hasPending)
177
+ this.scheduleRescan();
167
178
  return evidence;
168
179
  },
169
180
  };
@@ -288,7 +299,7 @@ export class UserGroundHost {
288
299
  records: input.records,
289
300
  destinationParent: input.destinationParent,
290
301
  cloudHead: input.cloudHead,
291
- readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact, 10, { journey: "add", workspaceId: input.workspace.uuid }),
302
+ readContent: (artifacts) => this.downloadContentSet(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: input.workspace.uuid }),
292
303
  adoptMaterialization: (rootUUID) => {
293
304
  const settled = this.applyHost?.adoptVerifiedMaterialization(rootUUID) ?? 0;
294
305
  this.applyRail?.recordsAvailable();
@@ -414,7 +425,7 @@ export class UserGroundHost {
414
425
  records: intent.records,
415
426
  destinationParent: dirname(intent.destinationPath),
416
427
  cloudHead: intent.cloudHead,
417
- readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact, 10, { journey: "add", workspaceId: intent.workspaceId }),
428
+ readContent: (artifacts) => this.downloadContentSet(intent.resourceId, this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: intent.workspaceId }),
418
429
  adoptMaterialization: (rootUUID) => {
419
430
  this.applyHost?.adoptVerifiedMaterialization(rootUUID);
420
431
  this.applyRail?.recordsAvailable();
@@ -1050,7 +1061,7 @@ export class UserGroundHost {
1050
1061
  database,
1051
1062
  cacheDir,
1052
1063
  cloud,
1053
- readContent: (artifact) => this.downloadContent(cloud.resourceId, cacheDir, artifact),
1064
+ readContent: (artifacts) => this.downloadContentSet(cloud.resourceId, cacheDir, artifacts.map((artifact) => ({ artifact }))),
1054
1065
  });
1055
1066
  return localValue(identity, userRoot, database);
1056
1067
  },
@@ -1268,13 +1279,24 @@ export class UserGroundHost {
1268
1279
  throw error;
1269
1280
  }
1270
1281
  }
1271
- async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10, trace, knownManifest) {
1282
+ /** Download a set of immutable artifacts straight from content storage.
1283
+ * The set is the primitive: every storage object the set still needs —
1284
+ * manifests and blocks alike — shares one wide lane bound, so no artifact
1285
+ * serializes behind another's plan or seal. Each object is still its own
1286
+ * storage request. A lone artifact is a set of one. */
1287
+ async downloadContentSet(resourceId, cacheDir, wants, retryAttempts = 10, trace) {
1288
+ const unique = new Map(wants.map((want) => [want.artifact.contentHash, want]));
1289
+ const sole = unique.size === 1 ? [...unique.values()][0]?.artifact : undefined;
1272
1290
  const started = performance.now();
1291
+ let declaredBytes = 0;
1273
1292
  let downloadedBytes = 0;
1293
+ let downloadedChunks = 0;
1294
+ let reusedChunks = 0;
1274
1295
  let requests = 0;
1296
+ const entityId = trace?.entityId ?? sole?.entityId;
1275
1297
  const identifiers = {
1276
1298
  ...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
1277
- entityId: trace?.entityId ?? artifact.entityId,
1299
+ ...(entityId ? { entityId } : {}),
1278
1300
  ...(trace?.mutationId ? { mutationId: trace.mutationId } : {}),
1279
1301
  ...(trace?.globalSequence ? { globalSequence: trace.globalSequence } : {}),
1280
1302
  };
@@ -1283,9 +1305,19 @@ export class UserGroundHost {
1283
1305
  stage: "immutable-content",
1284
1306
  status: "started",
1285
1307
  ...identifiers,
1286
- measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind },
1308
+ measurements: {
1309
+ journey: trace?.journey ?? "bootstrap",
1310
+ artifacts: unique.size,
1311
+ ...(sole ? { kind: sole.kind } : {}),
1312
+ },
1287
1313
  });
1288
- const cache = { target: null };
1314
+ const targets = new Map();
1315
+ const target = (artifact) => {
1316
+ const found = targets.get(artifact.contentHash);
1317
+ if (!found)
1318
+ throw new Error("download cache has no content manifest");
1319
+ return found;
1320
+ };
1289
1321
  // Every storage object — the manifest and each block — is fetched
1290
1322
  // straight from content storage over a presigned URL and opened locally;
1291
1323
  // the wire carries only the key handoff and signing requests. A retry
@@ -1297,13 +1329,14 @@ export class UserGroundHost {
1297
1329
  return bytes;
1298
1330
  };
1299
1331
  try {
1300
- const receipt = await downloadArtifact(artifact, {
1332
+ const receipts = await downloadAllArtifacts([...unique.values()].map((want) => want.artifact), {
1301
1333
  sha256Hex,
1302
- getManifest: async () => {
1334
+ getManifest: async (artifact) => {
1303
1335
  // The record's plan or this machine's own manifest cache already
1304
1336
  // proves chunk identity; downloaded bytes are still hash-verified,
1305
1337
  // so a stale local manifest can only fail loudly, never corrupt.
1306
- const local = knownManifest ?? readCachedManifest(cacheDir, artifact.contentHash);
1338
+ const local = unique.get(artifact.contentHash)?.manifest
1339
+ ?? readCachedManifest(cacheDir, artifact.contentHash);
1307
1340
  const manifest = local ?? await (async () => {
1308
1341
  const bytes = await fetchObject({ kind: "manifest", sha256: artifact.contentHash });
1309
1342
  const checked = checkContentManifest(JSON.parse(bytes.toString("utf8")), artifact.contentHash);
@@ -1311,32 +1344,31 @@ export class UserGroundHost {
1311
1344
  throw new Error(checked.error);
1312
1345
  return checked.value;
1313
1346
  })();
1314
- cache.target = new ContentCacheDownload(cacheDir, artifact, manifest);
1347
+ targets.set(artifact.contentHash, new ContentCacheDownload(cacheDir, artifact, manifest));
1315
1348
  return manifest;
1316
1349
  },
1317
- hasChunk: async (_candidate, _manifest, index) => {
1318
- if (!cache.target)
1319
- throw new Error("download cache has no content manifest");
1320
- return cache.target.hasChunk(index);
1321
- },
1322
- getChunk: async (_candidate, manifest, index) => {
1350
+ hasChunk: async (artifact, _manifest, index) => target(artifact).hasChunk(index),
1351
+ getChunk: async (_artifact, manifest, index) => {
1323
1352
  const chunk = manifest.chunks[index];
1324
1353
  if (!chunk)
1325
1354
  throw new Error(`content manifest has no chunk ${index}`);
1326
1355
  return fetchObject({ kind: "block", sha256: chunk.sha256 });
1327
1356
  },
1328
- putChunk: async (_candidate, _manifest, index, bytes) => {
1329
- if (!cache.target)
1330
- throw new Error("download cache has no content manifest");
1331
- await cache.target.putChunk(index, bytes);
1357
+ putChunk: async (artifact, _manifest, index, bytes) => {
1358
+ await target(artifact).putChunk(index, bytes);
1332
1359
  },
1333
- sealArtifact: async () => {
1334
- if (!cache.target)
1335
- throw new Error("download cache has no content manifest");
1336
- const sealed = await cache.target.seal();
1360
+ sealArtifact: async (artifact) => {
1361
+ const sealed = await target(artifact).seal();
1337
1362
  return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
1338
1363
  },
1339
- }, { concurrency: DIRECT_BLOCK_DOWNLOAD_CONCURRENCY });
1364
+ }, { concurrency: DIRECT_OBJECT_DOWNLOAD_CONCURRENCY });
1365
+ const content = new Map();
1366
+ for (const receipt of receipts) {
1367
+ declaredBytes += receipt.manifest.bytes;
1368
+ downloadedChunks += receipt.downloadedChunks;
1369
+ reusedChunks += receipt.reusedChunks;
1370
+ content.set(receipt.artifact.contentHash, receipt.local);
1371
+ }
1340
1372
  this.stage({
1341
1373
  primitive: "download",
1342
1374
  stage: "immutable-content",
@@ -1345,15 +1377,16 @@ export class UserGroundHost {
1345
1377
  durationMs: performance.now() - started,
1346
1378
  measurements: {
1347
1379
  journey: trace?.journey ?? "bootstrap",
1348
- kind: artifact.kind,
1349
- declaredBytes: receipt.manifest.bytes,
1380
+ artifacts: unique.size,
1381
+ ...(sole ? { kind: sole.kind } : {}),
1382
+ declaredBytes,
1350
1383
  downloadedBytes,
1351
- downloadedChunks: receipt.downloadedChunks,
1352
- reusedChunks: receipt.reusedChunks,
1384
+ downloadedChunks,
1385
+ reusedChunks,
1353
1386
  requests,
1354
1387
  },
1355
1388
  });
1356
- return receipt.local;
1389
+ return content;
1357
1390
  }
1358
1391
  catch (error) {
1359
1392
  this.stage({
@@ -1362,14 +1395,26 @@ export class UserGroundHost {
1362
1395
  status: "failed",
1363
1396
  ...identifiers,
1364
1397
  durationMs: performance.now() - started,
1365
- measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind, requests },
1398
+ measurements: {
1399
+ journey: trace?.journey ?? "bootstrap",
1400
+ artifacts: unique.size,
1401
+ ...(sole ? { kind: sole.kind } : {}),
1402
+ requests,
1403
+ },
1366
1404
  });
1367
1405
  throw error;
1368
1406
  }
1369
1407
  finally {
1370
- await cache.target?.close();
1408
+ await Promise.all([...targets.values()].map((open) => open.close()));
1371
1409
  }
1372
1410
  }
1411
+ async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10, trace, knownManifest) {
1412
+ const content = await this.downloadContentSet(resourceId, cacheDir, [{ artifact, ...(knownManifest ? { manifest: knownManifest } : {}) }], retryAttempts, trace);
1413
+ const local = content.get(artifact.contentHash);
1414
+ if (!local)
1415
+ throw new Error(`download set did not seal ${artifact.contentHash}`);
1416
+ return local;
1417
+ }
1373
1418
  ensureWatchers(identity, authoritativeBaselineRootIds = []) {
1374
1419
  if (this.closing)
1375
1420
  return this.watchHost.health();
@@ -4259,62 +4304,76 @@ async function materializeRecords(input) {
4259
4304
  const { records, rootPath, cacheDir, bindingDir, readContent } = input;
4260
4305
  const repositories = [];
4261
4306
  const repositoriesByUuid = new Map();
4262
- const fileArtifacts = new Map();
4307
+ // One download set covers everything the wire owes this add up front:
4308
+ // every ordinary file artifact and every repository transport head, side
4309
+ // by side under one wide object bound. Only transport parents — links a
4310
+ // chain reveals inside downloaded history — cost further rounds, and each
4311
+ // round is again one set across every chain. Disk application below keeps
4312
+ // its ordered, validated walk.
4313
+ const wanted = new Map();
4314
+ const repositoryHeads = [];
4263
4315
  for (const record of records) {
4264
- if (record.type !== "file.text" && record.type !== "file.binary" && record.type !== "link")
4265
- continue;
4266
- const artifact = artifactForRecord(record);
4267
- if (artifact)
4268
- fileArtifacts.set(artifact.contentHash, artifact);
4269
- }
4270
- const resolveRepositoryChain = async (record) => {
4271
- if (!record.payloadVersion || !record.transportVersion) {
4272
- throw new Error(`cloud repository ${record.uuid} has no state transport`);
4316
+ if (record.type === "repo.git") {
4317
+ if (!record.payloadVersion || !record.transportVersion) {
4318
+ throw new Error(`cloud repository ${record.uuid} has no state transport`);
4319
+ }
4320
+ const artifact = artifactForRecord(record);
4321
+ if (!artifact)
4322
+ throw new Error(`cloud repository ${record.uuid} has no transfer artifact`);
4323
+ wanted.set(artifact.contentHash, artifact);
4324
+ repositoryHeads.push({ record, artifact });
4273
4325
  }
4274
- const artifact = artifactForRecord(record);
4275
- if (!artifact)
4276
- throw new Error(`cloud repository ${record.uuid} has no transfer artifact`);
4277
- const local = await readContent(artifact);
4326
+ else if (["file.text", "file.binary", "link"].includes(record.type)) {
4327
+ const artifact = artifactForRecord(record);
4328
+ if (artifact)
4329
+ wanted.set(artifact.contentHash, artifact);
4330
+ }
4331
+ }
4332
+ const fileContent = await readContent([...wanted.values()]);
4333
+ const repositoryChains = new Map();
4334
+ const walking = repositoryHeads.map(({ record, artifact }) => {
4335
+ const local = fileContent.get(artifact.contentHash);
4336
+ if (!local)
4337
+ throw new Error(`cloud repository ${record.uuid} transport was not downloaded`);
4278
4338
  if (local.contentHash !== record.transportVersion) {
4279
4339
  throw new Error(`cloud repository ${record.uuid} transport is corrupt`);
4280
4340
  }
4281
- const chain = [{ file: local.file, transportVersion: local.contentHash }];
4282
- const versions = new Set([record.transportVersion]);
4283
- let state = inspectRepositoryTransportFile(local.file);
4284
- while (state.parentTransportVersion) {
4285
- if (versions.has(state.parentTransportVersion)) {
4286
- throw new Error(`cloud repository ${record.uuid} transport chain has a cycle`);
4287
- }
4288
- const parentVersion = state.parentTransportVersion;
4289
- versions.add(parentVersion);
4290
- const parent = await readContent({ ...artifact, contentHash: parentVersion });
4291
- if (parent.contentHash !== parentVersion) {
4292
- throw new Error(`cloud repository ${record.uuid} parent transport is corrupt`);
4293
- }
4294
- chain.unshift({ file: parent.file, transportVersion: parent.contentHash });
4295
- state = inspectRepositoryTransportFile(parent.file);
4296
- }
4297
4341
  const latest = inspectRepositoryTransportFile(local.file);
4298
4342
  if (latest.stateId !== record.payloadVersion) {
4299
4343
  throw new Error(`cloud repository ${record.uuid} transport does not name its declared state`);
4300
4344
  }
4301
- return chain;
4302
- };
4303
- // One download phase covers everything the wire owes this add: ordinary
4304
- // file artifacts and every repository transport chain, side by side under
4305
- // one bound. A chain's own links stay serial (each names its parent), but
4306
- // no chain waits behind the file wave or another repository's chain.
4307
- // Disk application below keeps its ordered, validated walk.
4308
- const fileContent = new Map();
4309
- const repositoryChains = new Map();
4310
- await boundedForEach([
4311
- ...[...fileArtifacts.values()].map((artifact) => async () => {
4312
- fileContent.set(artifact.contentHash, await readContent(artifact));
4313
- }),
4314
- ...records.filter((record) => record.type === "repo.git").map((record) => async () => {
4315
- repositoryChains.set(record.uuid, await resolveRepositoryChain(record));
4316
- }),
4317
- ], FILE_CONTENT_DOWNLOAD_CONCURRENCY, (download) => download());
4345
+ const chain = [{ file: local.file, transportVersion: local.contentHash }];
4346
+ repositoryChains.set(record.uuid, chain);
4347
+ return {
4348
+ record,
4349
+ artifact,
4350
+ chain,
4351
+ versions: new Set([local.contentHash]),
4352
+ parent: latest.parentTransportVersion,
4353
+ };
4354
+ });
4355
+ for (let frontier = walking.filter((walk) => walk.parent !== null); frontier.length > 0; frontier = frontier.filter((walk) => walk.parent !== null)) {
4356
+ const parents = await readContent(frontier.map((walk) => {
4357
+ const parent = walk.parent;
4358
+ if (walk.versions.has(parent)) {
4359
+ throw new Error(`cloud repository ${walk.record.uuid} transport chain has a cycle`);
4360
+ }
4361
+ return { ...walk.artifact, contentHash: parent };
4362
+ }));
4363
+ for (const walk of frontier) {
4364
+ const wantedParent = walk.parent;
4365
+ const parent = parents.get(wantedParent);
4366
+ if (!parent) {
4367
+ throw new Error(`cloud repository ${walk.record.uuid} parent transport was not downloaded`);
4368
+ }
4369
+ if (parent.contentHash !== wantedParent) {
4370
+ throw new Error(`cloud repository ${walk.record.uuid} parent transport is corrupt`);
4371
+ }
4372
+ walk.versions.add(parent.contentHash);
4373
+ walk.chain.unshift({ file: parent.file, transportVersion: parent.contentHash });
4374
+ walk.parent = inspectRepositoryTransportFile(parent.file).parentTransportVersion;
4375
+ }
4376
+ }
4318
4377
  const materializeRepository = async (record, destination) => {
4319
4378
  const chain = repositoryChains.get(record.uuid);
4320
4379
  if (!chain)