@amalgm/shell 0.1.132 → 0.1.134
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/dist/content-cache-host.d.ts +27 -3
- package/dist/content-cache-host.js +82 -7
- package/dist/content-cache-host.js.map +1 -1
- package/dist/register-upload-fusion.d.ts +83 -0
- package/dist/register-upload-fusion.js +169 -0
- package/dist/register-upload-fusion.js.map +1 -0
- package/dist/user-ground-host.d.ts +8 -0
- package/dist/user-ground-host.js +143 -60
- package/dist/user-ground-host.js.map +1 -1
- package/dist/wire-client.js +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -11,7 +11,8 @@ import { EntityApplyHost, } from "./entity-apply-host.js";
|
|
|
11
11
|
import { EntityRecordSqliteStore } from "./entity-record-store.js";
|
|
12
12
|
import { observeEntityRecordPorts } from "./entity-record-observability.js";
|
|
13
13
|
import { indexRowsByOutermostRoot } from "./ground-root-index.js";
|
|
14
|
-
import { ContentCacheDownloadSet, captureContentFile, hasSealedContent, hashContentFile, pruneSettledContentCache, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
14
|
+
import { ContentCacheDownloadSet, captureContentFile, contentBlockCollector, hasSealedContent, hashContentFile, pruneSettledContentCache, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
15
|
+
import { RegisterUploadFusion } from "./register-upload-fusion.js";
|
|
15
16
|
import { StagedContentSet, addStagingEvidenceDir, clearAddStagingEvidence, } from "./workspace-add-staging.js";
|
|
16
17
|
import { DirectContentPlane } from "./content-direct.js";
|
|
17
18
|
import { encodeContentWireBytes } from "./content-wire-codec.js";
|
|
@@ -676,20 +677,75 @@ export class UserGroundHost {
|
|
|
676
677
|
this.stage({
|
|
677
678
|
primitive: "register", stage: "entity-registration", status: "started", workspaceId,
|
|
678
679
|
});
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
680
|
+
// The walk's own read is also the upload: smalls ride cargo
|
|
681
|
+
// segments, larges stream blocks and seal last. The flush settles
|
|
682
|
+
// every fused effect before publication plans, so the upload
|
|
683
|
+
// stage's seal inventory — the guarantor for anything fusion
|
|
684
|
+
// could not ship — already sees these seals.
|
|
685
|
+
const fusionResourceId = this.cloudState.resourceId;
|
|
686
|
+
const fusionLegs = new TransferLegs();
|
|
687
|
+
const fusion = new RegisterUploadFusion({
|
|
688
|
+
sealedContentHashes: (hashes) => this.sealedContentHashes(fusionResourceId, hashes),
|
|
689
|
+
sendCargo: async (artifacts, payload) => {
|
|
690
|
+
const { sealed } = await this.putCargo(fusionResourceId, artifacts, payload, fusionLegs);
|
|
691
|
+
return sealed;
|
|
692
|
+
},
|
|
693
|
+
putBlocks: async (chunks, bytes) => {
|
|
694
|
+
await this.putContentBlocks(fusionResourceId, chunks, bytes, fusionLegs);
|
|
695
|
+
},
|
|
696
|
+
sealManifest: async (manifest) => {
|
|
697
|
+
await this.putContentManifest(fusionResourceId, manifest, fusionLegs);
|
|
698
|
+
},
|
|
688
699
|
});
|
|
700
|
+
const scanned = await (async () => {
|
|
701
|
+
try {
|
|
702
|
+
return await reconcileGround({
|
|
703
|
+
identity,
|
|
704
|
+
userRoot: this.userRoot(identity),
|
|
705
|
+
database: this.databasePath(identity),
|
|
706
|
+
cacheDir: this.cacheDir(identity),
|
|
707
|
+
onScan: (evidence) => this.reportDetectScan(evidence),
|
|
708
|
+
measuring: this.measuring(),
|
|
709
|
+
registrationWorkspaceId: workspaceId,
|
|
710
|
+
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
|
|
711
|
+
fusion,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
finally {
|
|
715
|
+
// Even an abandoned walk settles its in-flight cargo, and a
|
|
716
|
+
// stale-capture re-walk then skips whatever already sealed.
|
|
717
|
+
const fused = await fusion.flush();
|
|
718
|
+
this.stage({
|
|
719
|
+
primitive: "register",
|
|
720
|
+
stage: "fused-upload",
|
|
721
|
+
status: "completed",
|
|
722
|
+
workspaceId,
|
|
723
|
+
measurements: {
|
|
724
|
+
smallArtifacts: fused.smallArtifacts,
|
|
725
|
+
smallBytes: fused.smallBytes,
|
|
726
|
+
cargoSegments: fused.cargoSegments,
|
|
727
|
+
sealedSkipped: fused.sealedSkipped,
|
|
728
|
+
largeArtifacts: fused.largeArtifacts,
|
|
729
|
+
largeBytes: fused.largeBytes,
|
|
730
|
+
...(fused.disabledReason !== null ? { disabled: fused.disabledReason } : {}),
|
|
731
|
+
...fusionLegs.report(),
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
})();
|
|
689
736
|
const retry = scanned.detection.find((plan) => plan.kind === "retry");
|
|
690
737
|
if (retry?.kind === "retry") {
|
|
691
738
|
throw new Error(retry.reasons.join("; "));
|
|
692
739
|
}
|
|
740
|
+
// The classic upload re-proved every source byte against its
|
|
741
|
+
// captured hash when it read it. The fused rail already shipped
|
|
742
|
+
// those bytes, so the equivalent proof is the fingerprint law at
|
|
743
|
+
// stat cost: any fused source that moved after capture makes this
|
|
744
|
+
// attempt stale, and the re-walk publishes what stands there now.
|
|
745
|
+
const movedGround = await fusion.movedGround();
|
|
746
|
+
if (movedGround !== null) {
|
|
747
|
+
throw new StaleCaptureError(`${movedGround} changed after its fused upload`);
|
|
748
|
+
}
|
|
693
749
|
const registrationDuration = performance.now() - registrationStarted;
|
|
694
750
|
const commitStarted = performance.now();
|
|
695
751
|
this.stage({
|
|
@@ -867,10 +923,10 @@ export class UserGroundHost {
|
|
|
867
923
|
throw new Error("entity Record rail already started");
|
|
868
924
|
await this.wire.open();
|
|
869
925
|
// One floor, no per-feature gates: this shell and its gateway release
|
|
870
|
-
// together, and the gateway ships first.
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
if (this.wire.protocolVersion <
|
|
926
|
+
// together, and the gateway ships first. 15 = one-read Register capture:
|
|
927
|
+
// large-file blocks become durable while capture owns their bytes, then
|
|
928
|
+
// the manifest seals last. An older gateway cannot accept that rail.
|
|
929
|
+
if (this.wire.protocolVersion < 15) {
|
|
874
930
|
throw new Error("gateway speaks an older wire protocol than this shell");
|
|
875
931
|
}
|
|
876
932
|
this.adoptCloudDeliveryBaseline(identity, this.cloudState.deliveryWatermark);
|
|
@@ -1413,6 +1469,40 @@ export class UserGroundHost {
|
|
|
1413
1469
|
wireBytes: encoded.bytes.byteLength,
|
|
1414
1470
|
};
|
|
1415
1471
|
}
|
|
1472
|
+
/** Store one wave of provisional content blocks, addressed by their own
|
|
1473
|
+
* digests and bound to no artifact. Resolving means every block is
|
|
1474
|
+
* durable at the authority. Answers the wire bytes actually sent. */
|
|
1475
|
+
async putContentBlocks(resourceId, blocks, payload, legs) {
|
|
1476
|
+
const encoded = await legs.time("encodeMs", () => encodeContentWireBytes(payload));
|
|
1477
|
+
const frame = await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1478
|
+
type: "private.entity-content.put-blocks",
|
|
1479
|
+
resource_id: resourceId,
|
|
1480
|
+
blocks,
|
|
1481
|
+
bytes: payload.byteLength,
|
|
1482
|
+
wire_bytes: encoded.bytes.byteLength,
|
|
1483
|
+
...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
|
|
1484
|
+
}, encoded.bytes, ["private.entity-content.stored-blocks"])));
|
|
1485
|
+
legs.add("gatewayVerifyMs", Number(frame.verify_ms));
|
|
1486
|
+
legs.add("gatewayStoreMs", Number(frame.store_ms));
|
|
1487
|
+
return encoded.bytes.byteLength;
|
|
1488
|
+
}
|
|
1489
|
+
/** Seal one artifact's manifest. The authority re-proves every named
|
|
1490
|
+
* block durable before anything becomes visible, so this frame is only
|
|
1491
|
+
* lawful after the complete file went over the wire. */
|
|
1492
|
+
async putContentManifest(resourceId, manifest, legs) {
|
|
1493
|
+
const bytes = Buffer.from(stableJson(manifest), "utf8");
|
|
1494
|
+
const frame = await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1495
|
+
type: "private.entity-content.put",
|
|
1496
|
+
resource_id: resourceId,
|
|
1497
|
+
content_hash: manifest.contentHash,
|
|
1498
|
+
sha256: sha256Hex(bytes),
|
|
1499
|
+
bytes: bytes.length,
|
|
1500
|
+
}, bytes, ["private.entity-content.stored"])));
|
|
1501
|
+
legs.add("gatewayVerifyMs", Number(frame.verify_ms));
|
|
1502
|
+
legs.add("gatewayStoreMs", Number(frame.store_ms));
|
|
1503
|
+
legs.add("gatewaySealMs", Number(frame.seal_ms));
|
|
1504
|
+
return bytes.length;
|
|
1505
|
+
}
|
|
1416
1506
|
async uploadSnapshotContent(identity, resourceId, records, trace, manifestHints) {
|
|
1417
1507
|
const cache = this.cacheDir(identity);
|
|
1418
1508
|
const byHash = new Map();
|
|
@@ -1550,22 +1640,12 @@ export class UserGroundHost {
|
|
|
1550
1640
|
const { artifact, file, manifest: knownManifest, bytes: fileBytes } = source;
|
|
1551
1641
|
const sourceFile = { handle: null };
|
|
1552
1642
|
let manifestPresent = false;
|
|
1553
|
-
const
|
|
1643
|
+
const chunkBlocks = (manifest, indexes) => indexes.map((index) => {
|
|
1554
1644
|
const chunk = manifest.chunks[index];
|
|
1555
1645
|
if (!chunk)
|
|
1556
1646
|
throw new Error(`content manifest has no chunk ${index}`);
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
resource_id: resourceId,
|
|
1560
|
-
content_hash: artifact.contentHash,
|
|
1561
|
-
kind: "chunk",
|
|
1562
|
-
part_index: index,
|
|
1563
|
-
part_count: manifest.chunks.length,
|
|
1564
|
-
sha256: chunk.sha256,
|
|
1565
|
-
bytes: chunk.bytes,
|
|
1566
|
-
}, bytes, ["private.entity-content.stored"])));
|
|
1567
|
-
uploadedBytes += bytes.byteLength;
|
|
1568
|
-
};
|
|
1647
|
+
return { sha256: chunk.sha256, bytes: chunk.bytes };
|
|
1648
|
+
});
|
|
1569
1649
|
try {
|
|
1570
1650
|
const receipt = await uploadArtifact(artifact, {
|
|
1571
1651
|
bytes: fileBytes,
|
|
@@ -1595,40 +1675,16 @@ export class UserGroundHost {
|
|
|
1595
1675
|
}
|
|
1596
1676
|
return frame.missing;
|
|
1597
1677
|
},
|
|
1598
|
-
putChunk: async (_candidate, manifest, index, bytes) =>
|
|
1678
|
+
putChunk: async (_candidate, manifest, index, bytes) => {
|
|
1679
|
+
uploadedBytes += await this.putContentBlocks(resourceId, chunkBlocks(manifest, [index]), bytes, legs);
|
|
1680
|
+
},
|
|
1599
1681
|
putChunkBatch: async (_candidate, manifest, indexes, bytes) => {
|
|
1600
|
-
|
|
1601
|
-
await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1602
|
-
type: "private.entity-content.put-batch",
|
|
1603
|
-
resource_id: resourceId,
|
|
1604
|
-
content_hash: artifact.contentHash,
|
|
1605
|
-
part_count: manifest.chunks.length,
|
|
1606
|
-
chunks: indexes.map((index) => ({
|
|
1607
|
-
part_index: index,
|
|
1608
|
-
sha256: manifest.chunks[index]?.sha256,
|
|
1609
|
-
bytes: manifest.chunks[index]?.bytes,
|
|
1610
|
-
})),
|
|
1611
|
-
bytes: bytes.byteLength,
|
|
1612
|
-
wire_bytes: encoded.bytes.byteLength,
|
|
1613
|
-
...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
|
|
1614
|
-
}, encoded.bytes, ["private.entity-content.stored-batch"])));
|
|
1615
|
-
uploadedBytes += encoded.bytes.byteLength;
|
|
1682
|
+
uploadedBytes += await this.putContentBlocks(resourceId, chunkBlocks(manifest, indexes), bytes, legs);
|
|
1616
1683
|
},
|
|
1617
1684
|
putManifest: async (_candidate, manifest) => {
|
|
1618
1685
|
if (manifestPresent)
|
|
1619
1686
|
return;
|
|
1620
|
-
|
|
1621
|
-
await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1622
|
-
type: "private.entity-content.put",
|
|
1623
|
-
resource_id: resourceId,
|
|
1624
|
-
content_hash: artifact.contentHash,
|
|
1625
|
-
kind: "manifest",
|
|
1626
|
-
part_index: manifest.chunks.length,
|
|
1627
|
-
part_count: manifest.chunks.length,
|
|
1628
|
-
sha256: sha256Hex(bytes),
|
|
1629
|
-
bytes: bytes.length,
|
|
1630
|
-
}, bytes, ["private.entity-content.stored"])));
|
|
1631
|
-
uploadedBytes += bytes.byteLength;
|
|
1687
|
+
uploadedBytes += await this.putContentManifest(resourceId, manifest, legs);
|
|
1632
1688
|
},
|
|
1633
1689
|
});
|
|
1634
1690
|
uploadedChunks += receipt.uploadedChunks;
|
|
@@ -1664,6 +1720,14 @@ export class UserGroundHost {
|
|
|
1664
1720
|
artifacts: byHash.size,
|
|
1665
1721
|
reusedArtifacts: byHash.size - sources.length,
|
|
1666
1722
|
declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
|
|
1723
|
+
// The same declared source bytes split by rail: whole small
|
|
1724
|
+
// artifacts ride shared cargo segments, larger ones ride
|
|
1725
|
+
// per-artifact chunk transfer. Any change to either rail's shape
|
|
1726
|
+
// is sized from a production trace by these four numbers.
|
|
1727
|
+
cargoArtifacts: small.length,
|
|
1728
|
+
cargoBytes: small.reduce((total, source) => total + source.bytes, 0),
|
|
1729
|
+
largeArtifacts: large.length,
|
|
1730
|
+
largeBytes: large.reduce((total, source) => total + source.bytes, 0),
|
|
1667
1731
|
uploadedBytes,
|
|
1668
1732
|
loadedBytes,
|
|
1669
1733
|
uploadedChunks,
|
|
@@ -4094,7 +4158,7 @@ function materializedWorkspaceBindings(bindingDir) {
|
|
|
4094
4158
|
return bindings.sort((left, right) => left.workspaceId.localeCompare(right.workspaceId));
|
|
4095
4159
|
}
|
|
4096
4160
|
async function reconcileGround(input) {
|
|
4097
|
-
const { identity, userRoot, database, cacheDir, suspicions, coreScan, registrationWorkspaceId, onScan, measuring = false, } = input;
|
|
4161
|
+
const { identity, userRoot, database, cacheDir, suspicions, coreScan, registrationWorkspaceId, fusion, onScan, measuring = false, } = input;
|
|
4098
4162
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
4099
4163
|
const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
|
|
4100
4164
|
const boundRootEntries = materializedWorkspaceBindings(bindingDir);
|
|
@@ -4320,9 +4384,11 @@ async function reconcileGround(input) {
|
|
|
4320
4384
|
suspicion: observationFor(rootPath),
|
|
4321
4385
|
// A register scan publishes one complete snapshot; it never mints Send
|
|
4322
4386
|
// records for the same initial contents, so it plans no detection and
|
|
4323
|
-
// stages no cache bytes —
|
|
4387
|
+
// stages no cache bytes — the fused rail ships content from the walk's
|
|
4388
|
+
// own read, and upload re-reads only what fusion could not seal.
|
|
4324
4389
|
plansSendRecords: suspicions !== undefined && registrationWorkspaceId === undefined,
|
|
4325
4390
|
largeManifests,
|
|
4391
|
+
fusion,
|
|
4326
4392
|
});
|
|
4327
4393
|
}
|
|
4328
4394
|
const changedUUIDs = new Set([
|
|
@@ -4350,7 +4416,7 @@ async function reconcileGround(input) {
|
|
|
4350
4416
|
};
|
|
4351
4417
|
}
|
|
4352
4418
|
async function reconcileRoot(input) {
|
|
4353
|
-
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, measureEvidence = false, plansSendRecords, largeManifests, } = input;
|
|
4419
|
+
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, measureEvidence = false, plansSendRecords, largeManifests, fusion, } = input;
|
|
4354
4420
|
// Prior rows reach this scan from every binding this root encloses. A row's
|
|
4355
4421
|
// stored relativePath is relative to the root that last committed it, which
|
|
4356
4422
|
// is a different frame when an enclosing boundary was just registered
|
|
@@ -4703,9 +4769,20 @@ async function reconcileRoot(input) {
|
|
|
4703
4769
|
captureTasks.push(captureGate(async () => {
|
|
4704
4770
|
const taskStarted = measureEvidence ? performance.now() : 0;
|
|
4705
4771
|
let current = stats;
|
|
4772
|
+
// Repository leaves ship on the Git-native rail; everything else
|
|
4773
|
+
// may fuse its capture read with upload. Fusion is opportunistic:
|
|
4774
|
+
// once disabled, captures continue exactly as the classic path
|
|
4775
|
+
// and the upload stage ships what the walk could not.
|
|
4776
|
+
const fused = activeRepository ? undefined : fusion;
|
|
4706
4777
|
for (let attempt = 1;; attempt++) {
|
|
4707
4778
|
try {
|
|
4708
|
-
const
|
|
4779
|
+
const collect = fused?.enabled && current.size <= CHUNK_BYTES
|
|
4780
|
+
? contentBlockCollector(current.size)
|
|
4781
|
+
: null;
|
|
4782
|
+
const captured = fused && current.size > CHUNK_BYTES
|
|
4783
|
+
? (await fused.captureLarge(absolutePath, current))
|
|
4784
|
+
?? await captureContentFile(absolutePath, current, activeRepository || !plansSendRecords ? null : cacheDir)
|
|
4785
|
+
: await captureContentFile(absolutePath, current, activeRepository || !plansSendRecords ? null : cacheDir, collect);
|
|
4709
4786
|
if (measureEvidence && evidence) {
|
|
4710
4787
|
evidence.captureBusyMs += performance.now() - taskStarted;
|
|
4711
4788
|
evidence.captureBytes += captured.bytes;
|
|
@@ -4718,6 +4795,12 @@ async function reconcileRoot(input) {
|
|
|
4718
4795
|
if (captured.bytes > CHUNK_BYTES) {
|
|
4719
4796
|
largeManifests?.set(captured.contentHash, captured.manifest);
|
|
4720
4797
|
}
|
|
4798
|
+
// The capture proved these exact bytes against the fingerprint
|
|
4799
|
+
// law; hand them to the cargo rail. Waiting here is window
|
|
4800
|
+
// backpressure, bounding memory by the capture pool's width.
|
|
4801
|
+
if (collect && fused) {
|
|
4802
|
+
await fused.addSmall(absolutePath, current, captured, collect.collected);
|
|
4803
|
+
}
|
|
4721
4804
|
return;
|
|
4722
4805
|
}
|
|
4723
4806
|
catch (error) {
|