@amalgm/shell 0.1.119 → 0.1.121

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.
@@ -0,0 +1,114 @@
1
+ import { AutomationRunExecutor, createAutomationClient, createMachineRunsClient, } from '@amalgm/automations';
2
+ import { createChannelsClient } from '@amalgm/channels';
3
+ export function createPlatformProductHost(options) {
4
+ const automations = createAutomationClient({
5
+ baseUrl: options.automationsBaseUrl,
6
+ headers: headersFor(options.authorization, 'amalgm-automations', automationScopes),
7
+ ...(options.fetch ? { fetch: options.fetch } : {}),
8
+ });
9
+ const channels = createChannelsClient({
10
+ baseUrl: options.channelsBaseUrl,
11
+ headers: headersFor(options.authorization, 'amalgm-channels', channelScopes),
12
+ ...(options.fetch ? { fetch: options.fetch } : {}),
13
+ });
14
+ const runs = createMachineRunsClient({
15
+ baseUrl: options.automationsBaseUrl,
16
+ headers: headersFor(options.authorization, 'amalgm-automations', () => ['runs:execute']),
17
+ ...(options.fetch ? { fetch: options.fetch } : {}),
18
+ });
19
+ const executor = new AutomationRunExecutor(runs, actionRegistry({
20
+ 'channels.notify_user': channelNotification(channels),
21
+ }));
22
+ const runner = createRunner({
23
+ claim: () => runs.claim({ limit: 8, leaseSeconds: 60 }),
24
+ execute: (run) => executor.execute(run),
25
+ intervalMs: options.pollIntervalMs ?? 1_000,
26
+ log: options.log,
27
+ });
28
+ return Object.freeze({ automations, channels, runner });
29
+ }
30
+ function headersFor(authorization, audience, scopes) {
31
+ return async (method, url) => {
32
+ const grant = await authorization.authorize({ audience, scopes: scopes(method, url), method, url });
33
+ return { authorization: grant.authorization, dpop: grant.dpop };
34
+ };
35
+ }
36
+ function automationScopes(method, url) {
37
+ if (method.toUpperCase() !== 'GET')
38
+ return ['automations:write'];
39
+ return new URL(url).pathname.includes('/runs') ? ['runs:read'] : ['automations:read'];
40
+ }
41
+ function channelScopes(method) {
42
+ return [method.toUpperCase() === 'GET' ? 'channels:read' : 'channels:send'];
43
+ }
44
+ function actionRegistry(actions) {
45
+ return Object.freeze({
46
+ async call(input) {
47
+ const { actionId, payload, idempotencyKey } = input;
48
+ const action = actions[actionId];
49
+ if (!action)
50
+ throw new Error(`Automation action is not installed: ${actionId}`);
51
+ return action(payload, idempotencyKey);
52
+ },
53
+ });
54
+ }
55
+ function channelNotification(channels) {
56
+ return async (payload, idempotencyKey) => {
57
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
58
+ throw new Error('channels.notify_user input must be an object');
59
+ }
60
+ const delivery = await channels.notify({
61
+ ...payload,
62
+ idempotencyKey,
63
+ });
64
+ return delivery;
65
+ };
66
+ }
67
+ function createRunner(options) {
68
+ let active = null;
69
+ let timer = null;
70
+ const drain = () => {
71
+ if (active)
72
+ return active;
73
+ active = options.claim()
74
+ .then(async (runs) => {
75
+ if (runs.length)
76
+ options.log?.('automations.claimed', { count: runs.length });
77
+ const settled = await Promise.allSettled(runs.map(options.execute));
78
+ settled.forEach((result) => {
79
+ if (result.status === 'rejected')
80
+ options.log?.('automations.run.failed', { error: safe(result.reason) });
81
+ });
82
+ })
83
+ .catch((error) => options.log?.('automations.poll.failed', { error: safe(error) }))
84
+ .finally(() => { active = null; });
85
+ return active;
86
+ };
87
+ return Object.freeze({
88
+ start() {
89
+ if (timer)
90
+ return;
91
+ timer = setInterval(drain, options.intervalMs);
92
+ timer.unref();
93
+ void drain();
94
+ },
95
+ async close() {
96
+ if (timer)
97
+ clearInterval(timer);
98
+ timer = null;
99
+ await active;
100
+ },
101
+ });
102
+ }
103
+ function safe(error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ const details = error && typeof error === 'object'
106
+ ? [field(error, 'code'), field(error, 'status')].filter(Boolean).join(' ')
107
+ : '';
108
+ return `${message}${details ? ` (${details})` : ''}`.replace(/[\r\n]+/g, ' ').slice(0, 500);
109
+ }
110
+ function field(value, key) {
111
+ const item = Reflect.get(value, key);
112
+ return typeof item === 'string' || typeof item === 'number' ? `${key}=${item}` : '';
113
+ }
114
+ //# sourceMappingURL=platform-product-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-product-host.js","sourceRoot":"","sources":["../src/platform-product-host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type ToolDefinition } from '@amalgm/tools';
2
+ interface ToolServerLike {
3
+ readonly tools: readonly {
4
+ readonly name: string;
5
+ readonly description?: string;
6
+ readonly inputSchema?: Readonly<Record<string, unknown>>;
7
+ }[];
8
+ }
9
+ export interface PlatformToolDefinitionOptions {
10
+ readonly agents: ToolServerLike;
11
+ readonly apps: ToolServerLike;
12
+ readonly browser: ToolServerLike;
13
+ readonly products?: Readonly<{
14
+ automations: boolean;
15
+ channels: boolean;
16
+ }>;
17
+ }
18
+ /** One system Toolbox record per public Amalgm product capability. */
19
+ export declare function platformToolDefinitions(options: PlatformToolDefinitionOptions): Promise<readonly ToolDefinition[]>;
20
+ export {};
@@ -0,0 +1,108 @@
1
+ import { createAutomationMcpServer } from '@amalgm/automations/mcp';
2
+ import { createChannelsMcpServer } from '@amalgm/channels/mcp';
3
+ import { chatMcpToolDescriptors } from '@amalgm/chat/mcp';
4
+ import { defineHostMcpTool, managementToolDescriptors, } from '@amalgm/tools';
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
7
+ function descriptors(server) {
8
+ return server.tools.map((tool) => ({
9
+ name: tool.name,
10
+ ...(tool.description ? { description: tool.description } : {}),
11
+ inputSchema: (tool.inputSchema || {
12
+ type: 'object', properties: {}, additionalProperties: false,
13
+ }),
14
+ }));
15
+ }
16
+ function platformTool(input) {
17
+ return defineHostMcpTool({
18
+ id: input.id,
19
+ name: input.name,
20
+ description: input.description,
21
+ serverName: input.id,
22
+ route: input.route,
23
+ tools: input.tools,
24
+ metadata: { repository: input.repository },
25
+ });
26
+ }
27
+ /** Project an owning SDK's official MCP descriptors into one Toolbox record. */
28
+ async function officialDescriptors(server) {
29
+ const client = new Client({ name: 'amalgm-shell-tool-projection', version: '1.0.0' });
30
+ const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
31
+ try {
32
+ await server.connect(serverTransport);
33
+ await client.connect(clientTransport);
34
+ const listed = await client.listTools();
35
+ return listed.tools.map((tool) => ({
36
+ name: tool.name,
37
+ ...(tool.description ? { description: tool.description } : {}),
38
+ inputSchema: tool.inputSchema,
39
+ }));
40
+ }
41
+ finally {
42
+ await client.close().catch(() => undefined);
43
+ await server.close().catch(() => undefined);
44
+ }
45
+ }
46
+ /** One system Toolbox record per public Amalgm product capability. */
47
+ export async function platformToolDefinitions(options) {
48
+ const products = options.products ?? { automations: true, channels: true };
49
+ return Object.freeze([
50
+ platformTool({
51
+ id: 'browser',
52
+ name: 'Browser',
53
+ description: 'Browse and operate the web in visible Electron or isolated headless Chromium sessions.',
54
+ repository: 'amalgm-browser',
55
+ route: '/mcp/browser',
56
+ tools: descriptors(options.browser),
57
+ }),
58
+ ...(products.automations ? [platformTool({
59
+ id: 'automations',
60
+ name: 'Automations',
61
+ description: 'Create, inspect, change, and review durable Amalgm automations.',
62
+ repository: 'amalgm-automations',
63
+ route: '/mcp/automations',
64
+ tools: await officialDescriptors(createAutomationMcpServer({})),
65
+ })] : []),
66
+ platformTool({
67
+ id: 'agents',
68
+ name: 'Agents',
69
+ description: 'Manage Amalgm agent identities, installations, and immutable revisions.',
70
+ repository: 'amalgm-agents',
71
+ route: '/mcp/agents',
72
+ tools: descriptors(options.agents),
73
+ }),
74
+ ...(products.channels ? [platformTool({
75
+ id: 'channels',
76
+ name: 'Channels',
77
+ description: 'Send and inspect durable notifications for the current Amalgm user.',
78
+ repository: 'amalgm-channels',
79
+ route: '/mcp/channels',
80
+ tools: await officialDescriptors(createChannelsMcpServer({})),
81
+ })] : []),
82
+ platformTool({
83
+ id: 'chat',
84
+ name: 'Chat',
85
+ description: 'Inspect durable conversations and send messages to existing agent conversations.',
86
+ repository: 'amalgm-chat',
87
+ route: '/mcp/chat',
88
+ tools: chatMcpToolDescriptors(),
89
+ }),
90
+ platformTool({
91
+ id: 'apps',
92
+ name: 'Apps',
93
+ description: 'Validate, deploy, inspect, start, and stop versioned Amalgm applications.',
94
+ repository: 'amalgm-apps',
95
+ route: '/mcp/apps',
96
+ tools: descriptors(options.apps),
97
+ }),
98
+ platformTool({
99
+ id: 'tools',
100
+ name: 'Tools',
101
+ description: 'Inspect and manage the canonical Toolbox catalog and immutable tool deployments.',
102
+ repository: 'amalgm-tools',
103
+ route: '/mcp/platform-tools',
104
+ tools: managementToolDescriptors(),
105
+ }),
106
+ ]);
107
+ }
108
+ //# sourceMappingURL=platform-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-tools.js","sourceRoot":"","sources":["../src/platform-tools.ts"],"names":[],"mappings":"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"}
@@ -197,6 +197,11 @@ export declare class UserGroundHost {
197
197
  * the inline bound; anything else returns null and the block upload path
198
198
  * runs unchanged. */
199
199
  private readInlineCargo;
200
+ /** One self-contained cargo segment on the wire: declared artifacts in
201
+ * header order, their bytes concatenated behind them, one verified round
202
+ * trip. Returns the authority's sealed hashes for the stream law to
203
+ * prove coverage. */
204
+ private putCargo;
200
205
  private uploadSnapshotContent;
201
206
  /** Download a set of immutable artifacts straight from content storage.
202
207
  * The set is the primitive: every storage object the set still needs —
@@ -229,8 +234,8 @@ export declare class UserGroundHost {
229
234
  private publishPendingSnapshots;
230
235
  /** Seal this publication's graph pages. Pages are immutable, content
231
236
  * addressed, and already in memory, so the authority's seal inventory alone
232
- * decides what still has to move and whole pages ride cargo waves. The head
233
- * may not commit until every page it newly names is sealed. */
237
+ * decides what still has to move and whole pages ride the cargo stream. The
238
+ * head may not commit until every page it newly names is sealed. */
234
239
  private uploadGraphPages;
235
240
  private publishLoadedSnapshot;
236
241
  }
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
3
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, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, pathIsSuspect, planCargoWaves, PRESIGN_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } 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, pathIsSuspect, PRESIGN_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } 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,9 +27,10 @@ import { NodeWatchHost, } from "./watching/index.js";
27
27
  import { GroundCoordinator } from "./ground-coordination.js";
28
28
  import { adoptSnapshotDeliveryBaseline } from "./snapshot-delivery-baseline.js";
29
29
  import { WireClient, WireRequestError } from "./wire-client.js";
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;
30
+ /** One cargo segment loads its ground bytes across this many concurrent
31
+ * reads. Reading and hashing overlap the wire without letting a
32
+ * 256-artifact segment claim hundreds of file descriptors at once. */
33
+ const CARGO_SEGMENT_LOAD_CONCURRENCY = 8;
33
34
  /** Storage-object lanes for one download set. Tiny objects use the wide
34
35
  * request window, while the separate byte budget narrows large chunks so
35
36
  * request concurrency can never silently become an unbounded memory claim. */
@@ -1309,6 +1310,25 @@ export class UserGroundHost {
1309
1310
  }
1310
1311
  return inline;
1311
1312
  }
1313
+ /** One self-contained cargo segment on the wire: declared artifacts in
1314
+ * header order, their bytes concatenated behind them, one verified round
1315
+ * trip. Returns the authority's sealed hashes for the stream law to
1316
+ * prove coverage. */
1317
+ async putCargo(resourceId, artifacts, payload) {
1318
+ const encoded = await encodeContentWireBytes(payload);
1319
+ const frame = await retryContent(() => this.wire.requestBinary({
1320
+ type: "private.entity-content.put-cargo",
1321
+ resource_id: resourceId,
1322
+ artifacts,
1323
+ bytes: payload.byteLength,
1324
+ wire_bytes: encoded.bytes.byteLength,
1325
+ ...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
1326
+ }, encoded.bytes, ["private.entity-content.stored-cargo"]));
1327
+ return {
1328
+ sealed: Array.isArray(frame.sealed) ? frame.sealed.map(String) : [],
1329
+ wireBytes: encoded.bytes.byteLength,
1330
+ };
1331
+ }
1312
1332
  async uploadSnapshotContent(identity, resourceId, records, trace, manifestHints) {
1313
1333
  const cache = this.cacheDir(identity);
1314
1334
  const byHash = new Map();
@@ -1401,19 +1421,22 @@ export class UserGroundHost {
1401
1421
  }
1402
1422
  throw new StaleCaptureError(`${source.artifact.contentHash} no longer matches its source`, { cause: error });
1403
1423
  };
1404
- // Complete small artifacts ride cargo waves: whole artifacts per frame,
1405
- // verified and sealed by the authority in the same round trip, so the
1406
- // frame count scales with bytes moved, never with file count. Large
1407
- // artifacts keep per-artifact transfer where chunk reuse pays.
1424
+ // Complete small artifacts ride the cargo stream: whole artifacts per
1425
+ // self-contained segment, each verified and sealed by the authority in
1426
+ // its own round trip, so the frame count scales with bytes moved, never
1427
+ // with file count — and Live's bounded window keeps later segments on
1428
+ // the wire while earlier durability acknowledgements are still
1429
+ // outstanding. Large artifacts keep per-artifact transfer where chunk
1430
+ // reuse pays.
1408
1431
  const small = sources.filter((source) => source.bytes <= CHUNK_BYTES);
1409
1432
  const large = sources.filter((source) => source.bytes > CHUNK_BYTES);
1410
- let cargoWaves = 0;
1411
- const transferCargoWave = async (wave) => {
1412
- const loaded = await Promise.all(wave.items.map(async (source) => {
1433
+ let cargoSegments = 0;
1434
+ const sendCargoSegment = async (segment) => {
1435
+ const loaded = new Array(segment.items.length);
1436
+ await boundedForEach(segment.items.map((item, index) => ({ source: item.source, index })), CARGO_SEGMENT_LOAD_CONCURRENCY, async ({ source, index }) => {
1413
1437
  try {
1414
1438
  const bytes = source.linkTarget ?? await readFile(source.file);
1415
- return {
1416
- source,
1439
+ loaded[index] = {
1417
1440
  bytes,
1418
1441
  manifest: deriveUploadManifest(source.artifact, bytes, sha256Hex),
1419
1442
  };
@@ -1423,30 +1446,17 @@ export class UserGroundHost {
1423
1446
  stale(source, error);
1424
1447
  throw error;
1425
1448
  }
1426
- }));
1427
- const payload = Buffer.concat(loaded.map((item) => item.bytes));
1428
- const encoded = await encodeContentWireBytes(payload);
1429
- const frame = await retryContent(() => this.wire.requestBinary({
1430
- type: "private.entity-content.put-cargo",
1431
- resource_id: resourceId,
1432
- artifacts: loaded.map((item) => ({
1433
- content_hash: item.source.artifact.contentHash,
1434
- bytes: item.manifest.bytes,
1435
- chunks: item.manifest.chunks.map((chunk) => ({ sha256: chunk.sha256, bytes: chunk.bytes })),
1436
- })),
1437
- bytes: payload.byteLength,
1438
- wire_bytes: encoded.bytes.byteLength,
1439
- ...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
1440
- }, encoded.bytes, ["private.entity-content.stored-cargo"]));
1441
- const sealed = new Set(Array.isArray(frame.sealed) ? frame.sealed.map(String) : []);
1442
- for (const item of loaded) {
1443
- if (!sealed.has(item.source.artifact.contentHash)) {
1444
- throw new Error(`content authority did not seal ${item.source.artifact.contentHash}`);
1445
- }
1449
+ });
1450
+ const { sealed, wireBytes } = await this.putCargo(resourceId, loaded.map((item, index) => ({
1451
+ content_hash: segment.items[index].contentHash,
1452
+ bytes: item.manifest.bytes,
1453
+ chunks: item.manifest.chunks.map((chunk) => ({ sha256: chunk.sha256, bytes: chunk.bytes })),
1454
+ })), Buffer.concat(loaded.map((item) => item.bytes)));
1455
+ for (const item of loaded)
1446
1456
  uploadedChunks += item.manifest.chunks.length;
1447
- }
1448
- uploadedBytes += encoded.bytes.byteLength;
1449
- cargoWaves += 1;
1457
+ uploadedBytes += wireBytes;
1458
+ cargoSegments += 1;
1459
+ return sealed;
1450
1460
  };
1451
1461
  const transfer = async (source) => {
1452
1462
  const { artifact, file, manifest: knownManifest, bytes: fileBytes } = source;
@@ -1546,7 +1556,11 @@ export class UserGroundHost {
1546
1556
  }
1547
1557
  };
1548
1558
  try {
1549
- await boundedForEach([...planCargoWaves(small)], CARGO_UPLOAD_CONCURRENCY, transferCargoWave);
1559
+ await streamCargo(small.map((source) => ({
1560
+ contentHash: source.artifact.contentHash,
1561
+ bytes: source.bytes,
1562
+ source,
1563
+ })), { send: sendCargoSegment });
1550
1564
  for (const source of large) {
1551
1565
  await transfer(source);
1552
1566
  }
@@ -1565,7 +1579,7 @@ export class UserGroundHost {
1565
1579
  uploadedChunks,
1566
1580
  reusedChunks,
1567
1581
  inventoryRequests,
1568
- cargoWaves,
1582
+ cargoSegments,
1569
1583
  },
1570
1584
  });
1571
1585
  }
@@ -2291,8 +2305,8 @@ export class UserGroundHost {
2291
2305
  }
2292
2306
  /** Seal this publication's graph pages. Pages are immutable, content
2293
2307
  * addressed, and already in memory, so the authority's seal inventory alone
2294
- * decides what still has to move and whole pages ride cargo waves. The head
2295
- * may not commit until every page it newly names is sealed. */
2308
+ * decides what still has to move and whole pages ride the cargo stream. The
2309
+ * head may not commit until every page it newly names is sealed. */
2296
2310
  async uploadGraphPages(resourceId, pages, trace) {
2297
2311
  if (pages.length === 0)
2298
2312
  return;
@@ -2300,32 +2314,21 @@ export class UserGroundHost {
2300
2314
  const sealed = await this.sealedContentHashes(resourceId, pages.map((page) => page.sha256));
2301
2315
  const unsealed = pages
2302
2316
  .filter((page) => !sealed.has(page.sha256))
2303
- .map((page) => ({ page, bytes: page.bytes.byteLength }));
2317
+ .map((page) => ({ contentHash: page.sha256, bytes: page.bytes.byteLength, page }));
2304
2318
  let uploadedBytes = 0;
2305
- await boundedForEach([...planCargoWaves(unsealed)], CARGO_UPLOAD_CONCURRENCY, async (wave) => {
2306
- const payload = Buffer.concat(wave.items.map((item) => item.page.bytes));
2307
- const encoded = await encodeContentWireBytes(payload);
2308
- const frame = await retryContent(() => this.wire.requestBinary({
2309
- type: "private.entity-content.put-cargo",
2310
- resource_id: resourceId,
2319
+ await streamCargo(unsealed, {
2320
+ send: async (segment) => {
2321
+ const { sealed: stored, wireBytes } = await this.putCargo(resourceId,
2311
2322
  // A page is a single-chunk artifact: its identity, its only chunk,
2312
2323
  // and its bytes are one and the same sha.
2313
- artifacts: wave.items.map((item) => ({
2314
- content_hash: item.page.sha256,
2324
+ segment.items.map((item) => ({
2325
+ content_hash: item.contentHash,
2315
2326
  bytes: item.bytes,
2316
- chunks: [{ sha256: item.page.sha256, bytes: item.bytes }],
2317
- })),
2318
- bytes: payload.byteLength,
2319
- wire_bytes: encoded.bytes.byteLength,
2320
- ...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
2321
- }, encoded.bytes, ["private.entity-content.stored-cargo"]));
2322
- const stored = new Set(Array.isArray(frame.sealed) ? frame.sealed.map(String) : []);
2323
- for (const item of wave.items) {
2324
- if (!stored.has(item.page.sha256)) {
2325
- throw new Error(`content authority did not seal graph page ${item.page.sha256}`);
2326
- }
2327
- }
2328
- uploadedBytes += encoded.bytes.byteLength;
2327
+ chunks: [{ sha256: item.contentHash, bytes: item.bytes }],
2328
+ })), Buffer.concat(segment.items.map((item) => item.page.bytes)));
2329
+ uploadedBytes += wireBytes;
2330
+ return stored;
2331
+ },
2329
2332
  });
2330
2333
  this.stage({
2331
2334
  primitive: "upload",