reploid 0.0.0-stage → 0.1.0
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/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +534 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +83 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createPartitionPreparationOwner } from '../packages/reploid/src/mesh/partitions/preparation-owner.js';
|
|
3
|
+
/** Native Node host for the same authenticated partition protocol as the browser.
|
|
4
|
+
* No browser, model arithmetic, or implicit execution fallback lives here.
|
|
5
|
+
*/
|
|
6
|
+
export { createChatCompletions } from './chat-completions.js';
|
|
7
|
+
export { createResponses } from './responses.js';
|
|
8
|
+
export { createMessages } from './messages.js';
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import { readNativeModelConfiguration } from './native-model-config.js';
|
|
12
|
+
import { createWebAccessNetwork } from '../packages/reploid/src/mesh/web-access-network.js';
|
|
13
|
+
import { createPublicWebWorker } from './public-web-worker.js';
|
|
14
|
+
import { createNativeWholeModel } from './native-whole-model.js';
|
|
15
|
+
import { openNativeGenerationJournal } from './native-generation-journal.js';
|
|
16
|
+
import { createWholeModelRequester } from './whole-model-requester.js';
|
|
17
|
+
import { createNativeModelFiles } from './native-model-files.js';
|
|
18
|
+
import { createCustodyKeyCertifier } from '../packages/reploid/src/artifacts/custody-certificate.js';
|
|
19
|
+
import { openNativeOwnerControls, nativeOwnerStatus, NO_OWNER_POLICY } from './native-owner/controls.js';
|
|
20
|
+
import { openNativeCanonicalEpochs } from './native-canonical-epochs.js';
|
|
21
|
+
import { openNativeReleaseCheckpoints } from './native-release-storage.js';
|
|
22
|
+
import { installedPackageDirectory, nativeShaderCacheIdentity, openNativeShaderCache } from './native-shader-cache.js';
|
|
23
|
+
import { openNativeReadinessLog } from './native-readiness-log.js';
|
|
24
|
+
import { createReadinessRecorder } from '../packages/reploid/src/measurement/index.js';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { sha256Digest as hash, sha256Hex } from './hashing.js';
|
|
27
|
+
import { EventEmitter } from 'node:events';
|
|
28
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
29
|
+
import { parseArgs } from 'node:util';
|
|
30
|
+
import { realpathSync } from 'node:fs';
|
|
31
|
+
import { createAutomaticPartitions, createPeerGenerationMesh, createPartitionNetwork, createResidentPartition, createSignedResidentFactory, createPartitionStageCommitAuthority, residentPreparationProgress } from '../packages/reploid/src/mesh/index.js';
|
|
32
|
+
import { createSwarmTransport, createRoomIntroductionAdmission } from '../packages/reploid/src/transport/index.js';
|
|
33
|
+
import { createSigningIdentity, createPeerIdFromPublicJwk } from '../packages/reploid/src/artifacts/identity.js';
|
|
34
|
+
import { signGenerationTerminal } from '../packages/reploid/src/artifacts/generation-terminal.js';
|
|
35
|
+
import { createAttemptEvidenceSigner } from '../packages/reploid/src/mesh/partitions/partition-attempt-evidence.js';
|
|
36
|
+
import { resolveConfig, requireResolvedConfig } from '../packages/reploid/src/config/index.js';
|
|
37
|
+
import { createWorkAdapterResolver } from '../apps/poolday/providers/work-adapter.js';
|
|
38
|
+
import models from '../shared/config/chat-models.json' with { type: 'json' };
|
|
39
|
+
import custodyPolicy from '../apps/poolday/config/chat-files.json' with { type: 'json' };
|
|
40
|
+
import policy from '../apps/poolday/config/partition-policy.json' with { type: 'json' };
|
|
41
|
+
import profile from '../shared/config/reploid-library.json' with { type: 'json' };
|
|
42
|
+
import bootstrap from '../shared/config/swarm-bootstrap.json' with { type: 'json' };
|
|
43
|
+
import dopplerPin from '../shared/config/doppler-package.json' with { type: 'json' };
|
|
44
|
+
import { resolvePartitionGeneration } from '../apps/poolday/config/partition-generation.js';
|
|
45
|
+
import { prepareLocalPackRelease } from '../apps/poolday/pool/pack-release-policy.js';
|
|
46
|
+
import { createChangeOnlyLog } from './native-change-log.js';
|
|
47
|
+
import { snapshotWorkPartitionSelection, createWorkPartitionPlan,
|
|
48
|
+
snapshotWorkPartitionStage, workOrderedResidentOptions, snapshotWorkOrderedParticipation, bindWorkOrderedParticipation } from '../apps/poolday/host/work-partition-plan.js';
|
|
49
|
+
import { assertWorkResidentCapsule } from '../apps/poolday/host/work-partition-capsule.js';
|
|
50
|
+
import { createPartitionTokenizerGuard } from '../apps/poolday/host/work-partition-tokenizer-guard.js';
|
|
51
|
+
import { resolveServedFile } from './browser-roots.js';
|
|
52
|
+
|
|
53
|
+
const requireValue = (condition, message) => { if (!condition) throw new Error(message); };
|
|
54
|
+
|
|
55
|
+
/** CLI status only. Detailed receipts remain available through getState(); routine
|
|
56
|
+
* notifications must not serialize signed artifact bodies or conversation text.
|
|
57
|
+
* `at` (ISO 8601, UTC) is when the host observed this state. */
|
|
58
|
+
export function summarizeNativePartitionState(state, { at = Date.now() } = {}) {
|
|
59
|
+
const text = (value, limit = 256) => typeof value === 'string'
|
|
60
|
+
? value.length > limit ? value.slice(0, limit) + '…' : value : null;
|
|
61
|
+
const scalar = value => typeof value === 'string' ? text(value)
|
|
62
|
+
: typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
63
|
+
const pick = (value, keys) => Object.fromEntries(keys.filter(key => value?.[key] !== undefined)
|
|
64
|
+
.map(key => [key, scalar(value[key])]));
|
|
65
|
+
const rows = (values, project) => {
|
|
66
|
+
const entries = Array.isArray(values) ? values : [];
|
|
67
|
+
return { count: entries.length, omitted: Math.max(0, entries.length - 16), records: entries.slice(0, 16).map(project) };
|
|
68
|
+
};
|
|
69
|
+
const placement = Array.isArray(state.placement) ? state.placement : [];
|
|
70
|
+
const acquisition = state.acquisition;
|
|
71
|
+
return { event: 'partition-state', at: new Date(at).toISOString(), phase: text(state.phase), error: text(state.error, 1024),
|
|
72
|
+
offering: state.offering === true, modelId: text(state.modelId), unsettledOwners: scalar(state.unsettledOwners),
|
|
73
|
+
progress: state.progress ? pick(state.progress, ['stage', 'phase', 'message', 'loaded', 'total', 'progress',
|
|
74
|
+
'status', 'prepared', 'key', 'elapsedMs', 'startedAt', 'maxDurationMs', 'outstanding']) : null,
|
|
75
|
+
placement: placement.slice(0, 16).map(value => text(value)), placementOmitted: Math.max(0, placement.length - 16),
|
|
76
|
+
descriptor: state.descriptor ? { ...pick(state.descriptor, ['schema', 'modelId', 'modelIdentity', 'planId', 'index',
|
|
77
|
+
'participantId', 'ready', 'residentWeightBytes', 'generationDigest', 'canonicalGenerationId']),
|
|
78
|
+
layerRange: Array.isArray(state.descriptor.layerRange) ? state.descriptor.layerRange.slice(0, 2).map(scalar) : null } : null,
|
|
79
|
+
reservations: state.reservations ? { ...pick(state.reservations, ['active', 'availableSlots', 'closed']),
|
|
80
|
+
...rows(state.reservations.records, record => ({ attemptId: text(record.identity?.attemptId),
|
|
81
|
+
...pick(record, ['phase', 'failure', 'reservedAt', 'settlingAt', 'settledAt']) })) } : null,
|
|
82
|
+
attempts: rows(state.attempts, record => pick(record, ['attemptId', 'threadId', 'status', 'phase'])),
|
|
83
|
+
acquisition: acquisition ? { openingMode: text(acquisition.openingMode),
|
|
84
|
+
memory: pick(acquisition.memory, ['liveBytes', 'allocatedBytes', 'maxBytes', 'peakBytes', 'allocations', 'rejected', 'lost']),
|
|
85
|
+
preparation: pick(acquisition.preparation, ['openMs', 'acquisitionAndMaterializationMs']) } : null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const events = () => {
|
|
89
|
+
const emitter = new EventEmitter();
|
|
90
|
+
return { emit: (...args) => emitter.emit(...args), on(name, handler) {
|
|
91
|
+
emitter.on(name, handler); return () => emitter.off(name, handler);
|
|
92
|
+
} };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** node-webrtc exposes UINT16_MAX for unspecified SCTP retransmission limits.
|
|
96
|
+
* Normalize that provider representation at the host boundary, not in the
|
|
97
|
+
* shared protocol's reliable-channel requirement. */
|
|
98
|
+
export function normalizeNativeDataChannel(channel) {
|
|
99
|
+
for (const key of ['maxRetransmits', 'maxPacketLifeTime']) {
|
|
100
|
+
if (channel[key] === 65535) Object.defineProperty(channel, key, { value: null, configurable: true });
|
|
101
|
+
}
|
|
102
|
+
normalizeNativeDrainEvents(channel);
|
|
103
|
+
return channel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** wrtc 0.10 exposes bufferedAmount but no threshold or low-water event.
|
|
107
|
+
* Poll only for an active drain waiter; protocol timeouts/cancellation remove
|
|
108
|
+
* that waiter. Providers with the standard contract retain their own events. */
|
|
109
|
+
function normalizeNativeDrainEvents(channel) {
|
|
110
|
+
if ('bufferedAmountLowThreshold' in channel || typeof channel.addEventListener !== 'function') return;
|
|
111
|
+
const add = channel.addEventListener.bind(channel), remove = channel.removeEventListener.bind(channel);
|
|
112
|
+
const send = channel.send.bind(channel);
|
|
113
|
+
const waiters = new Map();
|
|
114
|
+
let timer = null, previous = channel.bufferedAmount;
|
|
115
|
+
Object.defineProperty(channel, 'bufferedAmountLowThreshold', { value: 0, writable: true, configurable: true });
|
|
116
|
+
function stopTimer() { clearTimeout(timer); timer = null; }
|
|
117
|
+
function removeWaiter(listener) {
|
|
118
|
+
const entry = waiters.get(listener);
|
|
119
|
+
if (!entry) return;
|
|
120
|
+
waiters.delete(listener);
|
|
121
|
+
remove('bufferedamountlow', entry.wrapper);
|
|
122
|
+
entry.signal?.removeEventListener('abort', entry.abort);
|
|
123
|
+
if (!waiters.size) stopTimer();
|
|
124
|
+
}
|
|
125
|
+
function schedule() {
|
|
126
|
+
if (timer === null && waiters.size && channel.readyState === 'open') timer = setTimeout(poll, 16);
|
|
127
|
+
}
|
|
128
|
+
function poll() {
|
|
129
|
+
timer = null;
|
|
130
|
+
if (!waiters.size) return;
|
|
131
|
+
if (channel.readyState !== 'open') { dispose(); return; }
|
|
132
|
+
const current = channel.bufferedAmount;
|
|
133
|
+
const crossed = previous > channel.bufferedAmountLowThreshold && current <= channel.bufferedAmountLowThreshold;
|
|
134
|
+
previous = current;
|
|
135
|
+
if (crossed) channel.dispatchEvent(new Event('bufferedamountlow'));
|
|
136
|
+
if (current > channel.bufferedAmountLowThreshold) schedule();
|
|
137
|
+
}
|
|
138
|
+
function dispose() { stopTimer(); for (const listener of waiters.keys()) removeWaiter(listener); }
|
|
139
|
+
channel.addEventListener = function(type, listener, options) {
|
|
140
|
+
if (type !== 'bufferedamountlow') return add(type, listener, options);
|
|
141
|
+
if (!listener || waiters.has(listener) || options?.signal?.aborted) return;
|
|
142
|
+
const entry = { signal: options?.signal, abort: () => removeWaiter(listener), wrapper(event) {
|
|
143
|
+
if (options?.once) removeWaiter(listener);
|
|
144
|
+
if (typeof listener === 'function') listener.call(channel, event); else listener.handleEvent(event);
|
|
145
|
+
} };
|
|
146
|
+
waiters.set(listener, entry);
|
|
147
|
+
add(type, entry.wrapper);
|
|
148
|
+
entry.signal?.addEventListener('abort', entry.abort, { once: true });
|
|
149
|
+
previous = channel.bufferedAmount;
|
|
150
|
+
schedule();
|
|
151
|
+
};
|
|
152
|
+
channel.removeEventListener = function(type, listener, options) {
|
|
153
|
+
if (type === 'bufferedamountlow') removeWaiter(listener); else remove(type, listener, options);
|
|
154
|
+
};
|
|
155
|
+
channel.send = function(...args) {
|
|
156
|
+
const result = send(...args);
|
|
157
|
+
if (channel.bufferedAmount > channel.bufferedAmountLowThreshold) { previous = channel.bufferedAmount; schedule(); }
|
|
158
|
+
return result;
|
|
159
|
+
};
|
|
160
|
+
add('close', dispose); add('error', dispose);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createNativeRtcAdapter(rtc) {
|
|
164
|
+
class RTCPeerConnection extends rtc.RTCPeerConnection {
|
|
165
|
+
constructor(...args) {
|
|
166
|
+
super(...args);
|
|
167
|
+
this.addEventListener('datachannel', event => normalizeNativeDataChannel(event.channel));
|
|
168
|
+
}
|
|
169
|
+
createDataChannel(...args) { return normalizeNativeDataChannel(super.createDataChannel(...args)); }
|
|
170
|
+
}
|
|
171
|
+
return { ...rtc, RTCPeerConnection };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Read only complete authenticated pieces requested by Doppler's public reader. */
|
|
175
|
+
export async function openNativeModelSource({ model, modelDirectory, signal, runtime, files }) {
|
|
176
|
+
if (files) return files.preparePartitionSource(model, { signal });
|
|
177
|
+
const root = await fs.realpath(modelDirectory);
|
|
178
|
+
const read = async (name, offset, size) => {
|
|
179
|
+
signal?.throwIfAborted();
|
|
180
|
+
requireValue(/^[a-zA-Z0-9_.-]+$/.test(name), 'Model file must be a plain filename');
|
|
181
|
+
const filename = await fs.realpath(path.join(root, name));
|
|
182
|
+
requireValue(path.dirname(filename) === root, 'Model file escapes its approved directory');
|
|
183
|
+
const handle = await fs.open(filename, 'r');
|
|
184
|
+
try {
|
|
185
|
+
const stat = await handle.stat();
|
|
186
|
+
requireValue(stat.isFile(), 'Model source must be a regular file');
|
|
187
|
+
if (offset === undefined) return new Uint8Array(await handle.readFile());
|
|
188
|
+
requireValue(Number.isSafeInteger(offset) && offset >= 0 && Number.isSafeInteger(size) && size > 0
|
|
189
|
+
&& offset + size <= stat.size, 'Model piece exceeds its file');
|
|
190
|
+
const bytes = new Uint8Array(size);
|
|
191
|
+
let received = 0;
|
|
192
|
+
while (received < size) {
|
|
193
|
+
signal?.throwIfAborted();
|
|
194
|
+
const result = await handle.read(bytes, received, size - received, offset + received);
|
|
195
|
+
requireValue(result.bytesRead > 0, 'Model piece read was truncated'); received += result.bytesRead;
|
|
196
|
+
}
|
|
197
|
+
signal?.throwIfAborted(); return bytes;
|
|
198
|
+
} finally { await handle.close(); }
|
|
199
|
+
};
|
|
200
|
+
const manifestBytes = await read('manifest.json');
|
|
201
|
+
requireValue(hash(manifestBytes) === model.identity, 'Native manifest differs from the approved catalog');
|
|
202
|
+
const descriptor = model.source.files.find(row => row.role === 'model-piece-index');
|
|
203
|
+
requireValue(descriptor?.hashAlgorithm === 'sha256', 'Native model requires a pinned piece index');
|
|
204
|
+
// Catalog metadata is bundled with the application; the weight directory need
|
|
205
|
+
// not contain a second hand-maintained copy of it.
|
|
206
|
+
const indexBytes = descriptor.url?.startsWith('/config/model-pieces/')
|
|
207
|
+
? new Uint8Array(await fs.readFile(resolveServedFile(descriptor.url)))
|
|
208
|
+
: await read(descriptor.path);
|
|
209
|
+
requireValue(hash(indexBytes) === 'sha256:' + descriptor.hash, 'Native piece index differs from the approved catalog');
|
|
210
|
+
return runtime.createVerifiedPieceStorage({ manifestBytes, indexBytes, indexIdentity: 'sha256:' + descriptor.hash,
|
|
211
|
+
signal, acquire: piece => read(piece.path, piece.offset, piece.size) });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The standalone host owns the Node provider and the only device in this
|
|
215
|
+
* process. Final close follows resident settlement; stopping contribution alone
|
|
216
|
+
* keeps the provider reusable. No private Doppler imports are needed. */
|
|
217
|
+
export function createNativeGpuHost(loadProvider, { backend = process.platform === 'darwin' ? 'metal' : 'vulkan' } = {}) {
|
|
218
|
+
requireValue(['metal', 'vulkan'].includes(backend), 'Native GPU backend must be metal or vulkan');
|
|
219
|
+
let opening = null, closing = null, restore = null, retirement = null, stopped = false;
|
|
220
|
+
let ownedDevice = null, lost = null, queueSettled = false, queueRejected = false, lossResolved = false, poolDestroyed = false, deviceDestroyed = false;
|
|
221
|
+
const retire = () => retirement ||= (async () => {
|
|
222
|
+
await opening?.catch(() => {});
|
|
223
|
+
if (!restore) return;
|
|
224
|
+
const { getDevice, destroyBufferPool } = await import('doppler-gpu/tooling');
|
|
225
|
+
ownedDevice ||= getDevice();
|
|
226
|
+
if (!ownedDevice) return;
|
|
227
|
+
// Capture this device's loss completion before destruction can clear the runtime handle.
|
|
228
|
+
if (!lost) {
|
|
229
|
+
requireValue(typeof ownedDevice.lost?.then === 'function', 'Native device loss settlement promise is unavailable');
|
|
230
|
+
lost = Promise.resolve(ownedDevice.lost).then(value => { lossResolved = true; return { value }; }, error => ({ error }));
|
|
231
|
+
}
|
|
232
|
+
if (!queueSettled) {
|
|
233
|
+
// The first queue failure remains observable. An explicit retry may settle
|
|
234
|
+
// an actually lost device even if its queue barrier permanently rejects.
|
|
235
|
+
if (queueRejected && lossResolved) queueSettled = true;
|
|
236
|
+
else {
|
|
237
|
+
try { await ownedDevice.queue.onSubmittedWorkDone(); queueSettled = true; }
|
|
238
|
+
catch (error) { queueRejected = true; throw error; }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!poolDestroyed) { destroyBufferPool(ownedDevice); poolDestroyed = true; }
|
|
242
|
+
if (!deviceDestroyed) { ownedDevice.destroy(); deviceDestroyed = true; }
|
|
243
|
+
const completion = await lost;
|
|
244
|
+
if ('error' in completion) throw completion.error;
|
|
245
|
+
})().catch(error => { retirement = null; throw error; });
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
open() {
|
|
248
|
+
if (stopped) throw new Error('Native GPU host is closing');
|
|
249
|
+
if (!opening) opening = (async () => {
|
|
250
|
+
requireValue(!globalThis.navigator?.gpu, 'Native GPU host cannot replace a borrowed provider');
|
|
251
|
+
const provider = await loadProvider();
|
|
252
|
+
const saved = new Map(Object.keys(provider.globals).concat('navigator')
|
|
253
|
+
.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]));
|
|
254
|
+
restore = () => { for (const [key, descriptor] of saved) {
|
|
255
|
+
if (descriptor) Object.defineProperty(globalThis, key, descriptor); else delete globalThis[key];
|
|
256
|
+
} };
|
|
257
|
+
try {
|
|
258
|
+
Object.assign(globalThis, provider.globals);
|
|
259
|
+
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { gpu: provider.create(['backend=' + backend]) } });
|
|
260
|
+
} catch (error) { restore(); restore = null; throw error; }
|
|
261
|
+
})().catch(error => { opening = null; throw error; });
|
|
262
|
+
return opening;
|
|
263
|
+
},
|
|
264
|
+
close() {
|
|
265
|
+
if (closing) return closing;
|
|
266
|
+
stopped = true;
|
|
267
|
+
let timer;
|
|
268
|
+
const deadline = new Promise((_, reject) => {
|
|
269
|
+
timer = setTimeout(() => reject(new Error('Native GPU ownership settlement exceeded 10000ms; provider retained')), 10000);
|
|
270
|
+
});
|
|
271
|
+
// No forced GC or sleep: navigator.gpu keeps the Dawn instance reachable
|
|
272
|
+
// through queue completion, pool/device destruction and the exact lost promise.
|
|
273
|
+
closing = Promise.race([retire(), deadline]).then(() => {
|
|
274
|
+
restore?.(); restore = null;
|
|
275
|
+
}).catch(error => { closing = null; throw error; }).finally(() => clearTimeout(timer));
|
|
276
|
+
return closing;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function openNativePartition({ model: selected, modelDirectory, nativeGpu, index, signal, participantId,
|
|
282
|
+
files, releaseDirectory, registerCleanup, canonical = null, ordered = null, device = null, attemptBytes, onProgress = () => {},
|
|
283
|
+
gpuBufferBytes, evidenceSigner = null, onEvidence = null, shaderCache = null }) {
|
|
284
|
+
selected = snapshotWorkPartitionSelection(selected);
|
|
285
|
+
const stageBinding = snapshotWorkPartitionStage(selected, index, participantId, ordered, canonical);
|
|
286
|
+
if (selected.capsule !== undefined) requireValue(files && selected.packOpenOptions && selected.executablePack && releaseDirectory,
|
|
287
|
+
'Signed native resident requires release policy, checkpoint directory and pinned artifact source');
|
|
288
|
+
const configured = { ...selected, generation: resolvePartitionGeneration(selected) };
|
|
289
|
+
const runtime = await import('doppler-gpu/partitions');
|
|
290
|
+
const orderedOptions = workOrderedResidentOptions(runtime, stageBinding);
|
|
291
|
+
if (canonical) {
|
|
292
|
+
requireValue(typeof runtime.canonicalGenerationIdentity === 'function' && selected.canonicalGeneration
|
|
293
|
+
&& canonical.identity?.peerId === participantId && canonical.participantEpoch === canonical.epochs?.participantEpoch,
|
|
294
|
+
'Canonical native contribution requires declared generation, provider capability and durable epochs');
|
|
295
|
+
canonical.epochs.assertCurrent();
|
|
296
|
+
}
|
|
297
|
+
await nativeGpu(); signal.throwIfAborted();
|
|
298
|
+
await runtime.configureDeviceMemoryBudget({ maxBytes: gpuBufferBytes ?? selected.partitionRuntime?.maxGpuBufferBytes?.[index] ?? policy.maxGpuBufferBytes });
|
|
299
|
+
const source = await openNativeModelSource({ model: selected, modelDirectory, signal, runtime, files });
|
|
300
|
+
let resident, release = null, capsuleVerification = null, commitAuthority = null;
|
|
301
|
+
const ownerId = canonical ? crypto.randomUUID().replaceAll('-', '') : null;
|
|
302
|
+
let ownershipRecorded = false;
|
|
303
|
+
const cleanupOwner = createPartitionPreparationOwner({ source, registerCleanup,
|
|
304
|
+
errorMessage: 'Native preparation cleanup failed',
|
|
305
|
+
onSettled() { if (ownershipRecorded) { canonical.epochs.settle(ownerId); ownershipRecorded = false; } } });
|
|
306
|
+
const { closeSources, trackSession } = cleanupOwner;
|
|
307
|
+
const tokenizer = createPartitionTokenizerGuard(source.storage);
|
|
308
|
+
const onLoadProgress = event => { const progress = residentPreparationProgress(event); if (progress) onProgress(progress); };
|
|
309
|
+
try {
|
|
310
|
+
signal.throwIfAborted();
|
|
311
|
+
const manifest = source.manifest;
|
|
312
|
+
const plan = createWorkPartitionPlan(runtime, manifest, selected, index);
|
|
313
|
+
const planId = runtime.hashLayerPartitionPlan(plan);
|
|
314
|
+
let canonicalOptions = {};
|
|
315
|
+
if (canonical) {
|
|
316
|
+
canonical.epochs.assertCurrent();
|
|
317
|
+
const generation = runtime.canonicalGenerationIdentity(selected.canonicalGeneration.policy,
|
|
318
|
+
selected.canonicalGeneration.executionIdentity, plan.vocabSize);
|
|
319
|
+
const stage = { participantId, participantEpoch: canonical.participantEpoch,
|
|
320
|
+
deviceEpoch: crypto.randomUUID().replaceAll('-', ''), residentId: crypto.randomUUID(), partitionIndex: index,
|
|
321
|
+
executionId: generation.executionIdentity };
|
|
322
|
+
stage.stageId = runtime.metadataDigest('reploid.partition-resident-stage/v1', { modelIdentity: selected.identity, planId, ...stage });
|
|
323
|
+
commitAuthority = createPartitionStageCommitAuthority({ runtime, identity: canonical.identity, stage });
|
|
324
|
+
cleanupOwner.commitAuthority = commitAuthority;
|
|
325
|
+
canonicalOptions = { canonicalGeneration: selected.canonicalGeneration, canonicalRuntime: runtime,
|
|
326
|
+
canonicalCommitAuthority: { getStage: () => { canonical.epochs.assertCurrent(); return commitAuthority.getStage(); },
|
|
327
|
+
async sign(request) { canonical.epochs.assertCurrent(); const result = await commitAuthority.sign(request); canonical.epochs.assertCurrent(); return result; },
|
|
328
|
+
close: () => commitAuthority.close() } };
|
|
329
|
+
}
|
|
330
|
+
let factory;
|
|
331
|
+
if (selected.capsule !== undefined) {
|
|
332
|
+
requireValue(source.artifactStore, 'Signed native resident requires a pinned artifact source');
|
|
333
|
+
release = await prepareLocalPackRelease({ model: configured,
|
|
334
|
+
openCheckpoints: () => openNativeReleaseCheckpoints({ directory: releaseDirectory }) });
|
|
335
|
+
cleanupOwner.release = release;
|
|
336
|
+
const { openCapsule } = await import('doppler-gpu/host');
|
|
337
|
+
factory = createSignedResidentFactory({ createFactory: runtime.createResidentPartitionFactory,
|
|
338
|
+
openCapsule, model: configured, artifactStore: source.artifactStore, release,
|
|
339
|
+
assertSession: assertWorkResidentCapsule, closeSources, trackSession, canonical: !!canonical,
|
|
340
|
+
onVerification: value => { capsuleVerification = value; }, onLoadProgress });
|
|
341
|
+
} else factory = runtime.createManifestResidentPartitionFactory({ resolveAdapter: createWorkAdapterResolver({ models: [selected], acquire: files?.acquireAdapter }), manifest, manifestIdentity: selected.identity, onLoadProgress,
|
|
342
|
+
runtimeConfig: selected.partitionRuntime?.runtimeConfig ?? { shared: { bufferPool: policy.bufferPool, debug: { profiler: { enabled: policy.profileGpu } } },
|
|
343
|
+
inference: { session: { kvcache: { maxSeqLen: configured.generation.maxSeqLen },
|
|
344
|
+
prefillChunkLayers: policy.prefillChunkLayers, prefillTokenChunkSize: policy.prefillTokenChunkSize } } },
|
|
345
|
+
createStorage: async () => tokenizer.storage });
|
|
346
|
+
resident = createResidentPartition({ runtime: factory, model: configured, plan, planId, index, participantId,
|
|
347
|
+
limits: policy.limits, onEvidence, ...canonicalOptions, ...orderedOptions, ...(device ? { device, attemptBytes } : {}),
|
|
348
|
+
// Ordered stages sign receipts for charged attempts with this host's participant identity.
|
|
349
|
+
...(orderedOptions.ordered && evidenceSigner?.participantId === participantId ? { evidence: evidenceSigner } : {}) });
|
|
350
|
+
cleanupOwner.resident = resident;
|
|
351
|
+
onProgress({ stage: 'opening', message: 'Opening native assigned partition' });
|
|
352
|
+
if (canonical) { canonical.epochs.begin(ownerId); ownershipRecorded = true; }
|
|
353
|
+
const prepared = await resident.prepare({ approved: true, signal });
|
|
354
|
+
tokenizer.assertAvailable();
|
|
355
|
+
// A complete preparation lets the process seal its shader cache; the outcome is reported, never thrown.
|
|
356
|
+
shaderCache?.seal({ ...prepared?.descriptor?.pipelinePreparation, planId, index, modelIdentity: selected.identity });
|
|
357
|
+
return { runtime, model: configured, plan, planId, resident: canonical ? Object.freeze(Object.fromEntries(
|
|
358
|
+
Object.entries(resident).map(([name, value]) => [name, typeof value !== 'function' ? value : name === 'close'
|
|
359
|
+
? () => cleanupOwner.close() : name === 'closeAttempt' ? (...args) => value(...args) : (...args) => {
|
|
360
|
+
canonical.epochs.assertCurrent(); const result = value(...args);
|
|
361
|
+
if (result?.then) return result.then(output => { canonical.epochs.assertCurrent(); return output; });
|
|
362
|
+
canonical.epochs.assertCurrent(); return result;
|
|
363
|
+
}]))) : resident,
|
|
364
|
+
getReceipt: () => ({ ...(selected.capsule === undefined ? source.getReceipt() : { sourceAcquisition: source.getReceipt() }),
|
|
365
|
+
host: 'node', openingMode: selected.capsule === undefined ? 'pinned-manifest' : 'signed-capsule',
|
|
366
|
+
capsuleVerification, memory: runtime.inspectDeviceMemory() }) };
|
|
367
|
+
} catch (error) {
|
|
368
|
+
const openingError = tokenizer.explain(error);
|
|
369
|
+
const failures = [openingError];
|
|
370
|
+
try { await cleanupOwner.close(); } catch (cleanup) { failures.push(cleanup); }
|
|
371
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Native opening and cleanup failed');
|
|
372
|
+
throw openingError;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** `owner` is the optional owner gate from openNativeOwnerControls. It defers or reduces
|
|
377
|
+
* new contribution, stage attempts and piece supply; it never cancels admitted work.
|
|
378
|
+
* The persistent identity certifies the per-process custody key at startup and on
|
|
379
|
+
* renewal; `onCustodyCertificate` observes each certificate (public, no secrets).
|
|
380
|
+
* `earning` (self/host/earning/contribution.js over an explicitly configured client)
|
|
381
|
+
* claims this host's settled stage shares and supplied deliveries and submits the
|
|
382
|
+
* evidence of deliveries it acquired; it needs the owner policy and is off by default.
|
|
383
|
+
* Claim failures are reported to the logger and never stop contribution.
|
|
384
|
+
* `readiness` (createReadinessRecorder from reploid/measurement, off by default) observes every
|
|
385
|
+
* partition state and each prepared resident's admissions; the caller owns and ends it. */
|
|
386
|
+
export async function createNativeParticipant({ model, modelDirectory, identity, rtc, WebSocket,
|
|
387
|
+
signalingUrl, roomId, roomToken, nativeGpu, cacheDirectory, canonical = null, ordered = null, onFileChange = () => {}, onChange = () => {}, logger = console,
|
|
388
|
+
owner: ownerGate = null, onCustodyCertificate = () => {}, earning = null, shaderCache = null, readiness = null, executionMode = 'partition', webWorker = null,
|
|
389
|
+
generationJournalFile = null, executionAdmission = null, onModelFill = () => {} }) {
|
|
390
|
+
requireValue(['partition', 'whole-model'].includes(executionMode), 'Unknown native execution mode');
|
|
391
|
+
const wholeMode = executionMode === 'whole-model';
|
|
392
|
+
requireValue(executionAdmission === null || ['whole', 'partition', 'partitionReceipt', 'receipt'].every(key => typeof executionAdmission[key] === 'function'),
|
|
393
|
+
'Execution admission requires whole-model, partition and receipt ports');
|
|
394
|
+
requireValue(!wholeMode || canonical === null && ordered === null && readiness === null,
|
|
395
|
+
'Whole-model execution cannot inherit ordered, canonical or stage-readiness configuration');
|
|
396
|
+
requireValue(earning === null || ownerGate && typeof earning.observe === 'function', 'Native earning requires the owner policy and an earning contribution port');
|
|
397
|
+
requireValue(!wholeMode || !earning || typeof earning.prepareWholeModel === 'function' && typeof earning.recordWholeModelReceipt === 'function',
|
|
398
|
+
'Whole-model earning requires admission and receipt persistence');
|
|
399
|
+
requireValue(readiness === null || typeof readiness.observe === 'function' && typeof readiness.watchResident === 'function',
|
|
400
|
+
'Native readiness recording requires a readiness recorder');
|
|
401
|
+
if (ordered !== null) model = structuredClone(model);
|
|
402
|
+
ordered = snapshotWorkOrderedParticipation(ordered, canonical, [model]);
|
|
403
|
+
requireValue(identity?.privateJwk && await createPeerIdFromPublicJwk(identity.publicJwk) === identity.peerId,
|
|
404
|
+
'Native participant requires its own valid signing identity');
|
|
405
|
+
const evidenceSigner = createAttemptEvidenceSigner({ identity });
|
|
406
|
+
requireValue(typeof rtc?.RTCPeerConnection === 'function' && typeof WebSocket === 'function', 'Native WebRTC and WebSocket required');
|
|
407
|
+
requireValue(model && modelDirectory && typeof nativeGpu === 'function', 'Explicit native model, directory and GPU provider required');
|
|
408
|
+
let canonicalRuntime = null, epochs = null;
|
|
409
|
+
if (canonical !== null) {
|
|
410
|
+
requireValue(canonical && typeof canonical.epochDirectory === 'string' && model.canonicalGeneration,
|
|
411
|
+
'Canonical native participation requires explicit durable epochs and declared model generation');
|
|
412
|
+
canonicalRuntime = await import('doppler-gpu/partitions');
|
|
413
|
+
for (const name of ['canonicalGenerationIdentity', 'metadataDigest', 'tokenDigest', 'validateResidentAttemptIdentity',
|
|
414
|
+
'residentAttemptDigest', 'residentPublicationDecisionDigest', 'validateExecutionPublication', 'validateAdmissionPublication']) {
|
|
415
|
+
requireValue(typeof canonicalRuntime[name] === 'function', 'Installed Doppler lacks canonical capability: ' + name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const endpoint = new URL(signalingUrl);
|
|
419
|
+
endpoint.searchParams.set('scope', roomId === bootstrap.publicRoomId ? 'public' : 'private');
|
|
420
|
+
endpoint.searchParams.set('roomId', roomId);
|
|
421
|
+
const config = resolveConfig({ profile: profile.profile, overrides: {
|
|
422
|
+
mesh: { enabled: true, executeJobs: wholeMode, maxInboundJobs: 1, roomId },
|
|
423
|
+
webrtc: { signalingUrl: endpoint.href, broadcastRoomId: roomId, transportOrder: ['webrtc'] }
|
|
424
|
+
} });
|
|
425
|
+
const resolvedConfig = requireResolvedConfig(config);
|
|
426
|
+
const eventBus = events(), bridge = events(), utils = { logger, generateId: () => crypto.randomUUID() };
|
|
427
|
+
const admission = roomId === bootstrap.publicRoomId ? null : createRoomIntroductionAdmission({ roomId, token: roomToken });
|
|
428
|
+
const generationJournal = wholeMode && generationJournalFile ? await openNativeGenerationJournal({ file: generationJournalFile,
|
|
429
|
+
maxResults: resolvedConfig.mesh.maxRetainedJobs, maxResultBytes: resolvedConfig.webrtc.maxMessageBytes }) : null;
|
|
430
|
+
let transport, partitions, files, webNetwork, wholeRequester, closePromise, closed = false, gpuPromise;
|
|
431
|
+
const networks = new Set();
|
|
432
|
+
const earningWrites = new Set();
|
|
433
|
+
let wholeEarningReady = !earning;
|
|
434
|
+
const wholeVariants = () => [[], ...(whole?.getState().adapterIdentities || []).map(value => [value])]
|
|
435
|
+
.map(adapterIdentities => ({ modelId: model.id, modelIdentity: model.identity, executionMode: 'whole-model', adapterIdentities }));
|
|
436
|
+
// A whole-model contributor completes its model directory from authorized peers before loading.
|
|
437
|
+
const whole = wholeMode ? createNativeWholeModel({ model, modelDirectory, nativeGpu, owner: ownerGate,
|
|
438
|
+
fill: cacheDirectory ? ({ signal }) => files.fillWholeModel(model, { signal, onProgress: onModelFill }) : null,
|
|
439
|
+
resolveAdapter: createWorkAdapterResolver({ models: [model], ...(cacheDirectory ? { acquire: (artifact, controls) => files.acquireAdapter(artifact, controls) } : {}) }),
|
|
440
|
+
onChange: state => { onChange(state); mesh.refreshAdvertisement(); } }) : null;
|
|
441
|
+
const mesh = createPeerGenerationMesh({ config, ports: {
|
|
442
|
+
generationJournal,
|
|
443
|
+
settleGeneration: whole ? async () => {
|
|
444
|
+
// The resident returns ready only after generation-state reset succeeds.
|
|
445
|
+
if (!whole.getState().ready) await whole.stop();
|
|
446
|
+
return true;
|
|
447
|
+
} : undefined,
|
|
448
|
+
instanceId: 'node:' + identity.peerId, modelConfig: wholeMode ? model : null, utils, eventBus, events: bridge,
|
|
449
|
+
getExecutionState: () => whole ? { ...whole.getExecutionState(), ...(!wholeEarningReady ? { phase: 'idle' } : {}) } : { phase: 'idle' },
|
|
450
|
+
identity: { ensure: async () => identity, save: async () => {}, sync: async () => {},
|
|
451
|
+
rotate: async () => { throw new Error('Stop the native participant before rotating its identity'); } },
|
|
452
|
+
authorize: async request => !closed && (request.action === 'mesh.connect'
|
|
453
|
+
|| request.action === 'mesh.execute' && whole?.getState().ready === true && wholeEarningReady
|
|
454
|
+
&& (!executionAdmission || await executionAdmission.whole({ request: request.request,
|
|
455
|
+
consumer: await mesh.verifyPeerIdentity(request.peerId) }))
|
|
456
|
+
|| request.action === 'mesh.dispatch' && await wholeRequester?.authorize(request) === true),
|
|
457
|
+
generate: async (messages, onUpdate, controls) => {
|
|
458
|
+
requireValue(!closed && whole, 'Native whole-model execution was not enabled');
|
|
459
|
+
if (earning) await earning.prepareWholeModel({ requestId: controls.requestId, consumer: controls.consumer,
|
|
460
|
+
model: { modelId: model.id, modelIdentity: model.identity, executionMode: 'whole-model', adapterIdentities: controls.adapterIdentities || [] } });
|
|
461
|
+
controls.signal.throwIfAborted();
|
|
462
|
+
return whole.generate(messages, onUpdate, controls);
|
|
463
|
+
},
|
|
464
|
+
onGenerationTerminal: async terminal => {
|
|
465
|
+
if (!executionAdmission) return;
|
|
466
|
+
const write = executionAdmission.terminal(terminal); earningWrites.add(write);
|
|
467
|
+
try { await write; }
|
|
468
|
+
catch (error) { logger.warn('Fleet terminal evidence deferred', error?.code || error?.message); }
|
|
469
|
+
finally { earningWrites.delete(write); }
|
|
470
|
+
},
|
|
471
|
+
onReceipt: async receipt => {
|
|
472
|
+
if (!earning?.recordWholeModelReceipt && !executionAdmission) return;
|
|
473
|
+
const write = Promise.all([earning?.recordWholeModelReceipt(receipt), executionAdmission?.receipt(receipt)]); earningWrites.add(write);
|
|
474
|
+
try { await write; observeEarning(); }
|
|
475
|
+
catch (error) { logger.warn('Earning receipt deferred', error?.code || error?.message); }
|
|
476
|
+
finally { earningWrites.delete(write); }
|
|
477
|
+
},
|
|
478
|
+
createTransport: () => transport = createSwarmTransport({ config, Utils: utils, EventBus: eventBus,
|
|
479
|
+
WebSocket, RTCPeerConnection: rtc.RTCPeerConnection, RTCSessionDescription: rtc.RTCSessionDescription,
|
|
480
|
+
RTCIceCandidate: rtc.RTCIceCandidate,
|
|
481
|
+
getSessionId: () => identity.peerId, getRoomCredentials: () => ({ roomId, token: roomToken }),
|
|
482
|
+
introductions: { getIdentity: () => identity, ...(admission ? { createAdmission: admission.create } : {}),
|
|
483
|
+
authorize: async request => !closed && request.roomId === roomId && request.action !== 'forward'
|
|
484
|
+
&& (!admission || (!request.contact && !request.from) || await admission.verify(request.contact || request.from)) }
|
|
485
|
+
})
|
|
486
|
+
} });
|
|
487
|
+
if (!wholeMode && !canonical) wholeRequester = createWholeModelRequester({ mesh, models: [model] });
|
|
488
|
+
// Claims read this host's own records only; they never change admission or settlement.
|
|
489
|
+
const observeEarning = () => {
|
|
490
|
+
if (!earning) return;
|
|
491
|
+
earning.observe({ admissions: partitions?.getState().reservations?.records ?? [], deliveries: files?.getDeliveryEvidence() ?? [],
|
|
492
|
+
...(whole ? { wholeModels: wholeVariants() } : {}) })
|
|
493
|
+
.catch(error => logger.warn('Earning claims deferred', error?.code || error?.message));
|
|
494
|
+
};
|
|
495
|
+
const settledOwners = new Set();
|
|
496
|
+
const closeOwner = async owner => {
|
|
497
|
+
if (!owner || settledOwners.has(owner)) return;
|
|
498
|
+
await owner.close(); settledOwners.add(owner);
|
|
499
|
+
};
|
|
500
|
+
// Contribution or file supply waiting for owner admission owns nothing yet; stopping only ends the wait.
|
|
501
|
+
const ownerWaits = { contribute: null, share: null };
|
|
502
|
+
// Resolves false when stop or close ended the wait, like stopping an unfinished preparation.
|
|
503
|
+
const ownerAdmission = async kind => {
|
|
504
|
+
const controller = new AbortController(); ownerWaits[kind]?.abort(new Error('Superseded owner admission wait'));
|
|
505
|
+
ownerWaits[kind] = controller;
|
|
506
|
+
try { await ownerGate.waitForAdmission({ kind: kind === 'share' ? 'file supply' : 'contribution', signal: controller.signal }); }
|
|
507
|
+
catch (error) { if (controller.signal.aborted) return false; throw error; }
|
|
508
|
+
finally { if (ownerWaits[kind] === controller) ownerWaits[kind] = null; }
|
|
509
|
+
return !closed;
|
|
510
|
+
};
|
|
511
|
+
const endOwnerWait = (kind, reason) => ownerWaits[kind]?.abort(new Error(reason));
|
|
512
|
+
const close = () => closePromise ||= (async () => {
|
|
513
|
+
closed = true;
|
|
514
|
+
endOwnerWait('contribute', 'Native participant closed'); endOwnerWait('share', 'Native participant closed');
|
|
515
|
+
const results = await Promise.allSettled([webNetwork, whole, partitions, ...networks]
|
|
516
|
+
.map(owner => Promise.resolve().then(() => closeOwner(owner))));
|
|
517
|
+
// Sources and the mesh outlive resident cancellation/opening settlement.
|
|
518
|
+
for (const owner of [files, mesh]) {
|
|
519
|
+
results.push(...await Promise.allSettled([Promise.resolve().then(() => closeOwner(owner))]));
|
|
520
|
+
}
|
|
521
|
+
results.push(...await Promise.allSettled([...earningWrites]));
|
|
522
|
+
if (settledOwners.has(mesh)) results.push(...await Promise.allSettled([Promise.resolve().then(() => generationJournal?.close())]));
|
|
523
|
+
// Resident cleanup still needs its epoch journal on explicit retry. A failed
|
|
524
|
+
// transport close does not revoke that ownership or clear its durable debt.
|
|
525
|
+
if (!partitions || settledOwners.has(partitions)) {
|
|
526
|
+
results.push(...await Promise.allSettled([Promise.resolve().then(() => closeOwner(epochs))]));
|
|
527
|
+
}
|
|
528
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
529
|
+
if (failures.length) throw new AggregateError(failures, 'Native contribution did not settle cleanly');
|
|
530
|
+
})().catch(error => { closePromise = null; throw error; });
|
|
531
|
+
try {
|
|
532
|
+
if (generationJournal) {
|
|
533
|
+
for (const priorBinding of await generationJournal.unfinished()) {
|
|
534
|
+
if (priorBinding?.provider !== identity.peerId) continue;
|
|
535
|
+
const terminal = await signGenerationTerminal(priorBinding, 'failed', identity);
|
|
536
|
+
await generationJournal.finish(priorBinding, {
|
|
537
|
+
type: 'reploid:generation-error',
|
|
538
|
+
payload: { requestId: priorBinding.requestId, error: 'Provider restarted after unfinished execution', terminal }
|
|
539
|
+
});
|
|
540
|
+
if (executionAdmission) {
|
|
541
|
+
await executionAdmission.terminal(terminal).catch(error => logger.warn('Fleet terminal recovery deferred', error?.code || error?.message));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (canonical) epochs = await openNativeCanonicalEpochs({ directory: canonical.epochDirectory, participantId: identity.peerId, meshId: roomId });
|
|
546
|
+
await mesh.connect();
|
|
547
|
+
webNetwork = createWebAccessNetwork({ transport, worker: webWorker, enabled: () => !closed && webWorker !== null,
|
|
548
|
+
verifyPeer: (peer, signal) => mesh.verifyPeerIdentity(peer, signal) });
|
|
549
|
+
if (cacheDirectory) {
|
|
550
|
+
files = await createNativeModelFiles({ transport, modelDirectory, cacheDirectory, onChange: onFileChange, owner: ownerGate,
|
|
551
|
+
certifyCustodyKey: createCustodyKeyCertifier({ identity, onIssue: onCustodyCertificate }).certify });
|
|
552
|
+
await files.custodyCertificate();
|
|
553
|
+
}
|
|
554
|
+
if (!wholeMode) partitions = createAutomaticPartitions({ identity, meshId: roomId, models: [model], ...bindWorkOrderedParticipation(ordered, identity.peerId, ordered ? transport._getPeerId() : null, policy),
|
|
555
|
+
canonical: epochs ? { runtime: canonicalRuntime, participantEpoch: epochs.participantEpoch, coordinatorEpoch: epochs.coordinatorEpoch } : null,
|
|
556
|
+
peers: () => transport.getConnectedPeers().map(peer => ({ ...peer, localTransportId: transport._getPeerId() })),
|
|
557
|
+
createNetwork(options) {
|
|
558
|
+
const network = createPartitionNetwork({ ...options, transport,
|
|
559
|
+
verifyPeer: async (peerId, signal) => {
|
|
560
|
+
try { return await mesh.verifyPeerIdentity(peerId, signal); }
|
|
561
|
+
catch (error) { logger.warn('Native participant identity verification failed', peerId, error.message); throw error; }
|
|
562
|
+
} });
|
|
563
|
+
networks.add(network); return network;
|
|
564
|
+
},
|
|
565
|
+
loadProgram: async (selected, index, controls) => {
|
|
566
|
+
const program = await openNativePartition({ model: selected, index, modelDirectory,
|
|
567
|
+
releaseDirectory: cacheDirectory && path.join(path.dirname(cacheDirectory), 'release-checkpoints'),
|
|
568
|
+
nativeGpu: () => gpuPromise ||= nativeGpu().catch(error => { gpuPromise = null; throw error; }), files, ...controls, evidenceSigner, shaderCache,
|
|
569
|
+
onEvidence: executionAdmission ? bundle => executionAdmission.partitionReceipt(bundle) : null,
|
|
570
|
+
...(epochs ? { canonical: { ...controls.canonical, epochs } } : {}),
|
|
571
|
+
...(ownerGate ? { gpuBufferBytes: ownerGate.gpuBufferBytes(selected.partitionRuntime?.maxGpuBufferBytes?.[index] ?? policy.maxGpuBufferBytes) } : {}) });
|
|
572
|
+
let prepared = ownerGate ? { ...program, resident: ownerGate.gateResident(program.resident) } : program;
|
|
573
|
+
if (executionAdmission) {
|
|
574
|
+
const resident = prepared.resident;
|
|
575
|
+
prepared = { ...prepared, resident: { ...resident, async reserve(request) {
|
|
576
|
+
await executionAdmission.partition({ ...request, partitionIndex: index });
|
|
577
|
+
return resident.reserve(request);
|
|
578
|
+
} } };
|
|
579
|
+
}
|
|
580
|
+
readiness?.watchResident(prepared.resident);
|
|
581
|
+
return prepared;
|
|
582
|
+
}, onChange: earning || readiness ? state => { onChange(state); readiness?.observe(state); if (earning) observeEarning(); } : onChange
|
|
583
|
+
});
|
|
584
|
+
return Object.freeze({ identity: identity.peerId, roomId, executionClass: roomId === bootstrap.publicRoomId ? 'public-pool' : 'private-pool',
|
|
585
|
+
getState: () => {
|
|
586
|
+
if (whole) { const state = whole.getState(); return { ...state, ready: state.ready && wholeEarningReady }; }
|
|
587
|
+
const state = partitions.getState();
|
|
588
|
+
return wholeRequester ? { ...state, models: [...wholeRequester.getModels(), ...(state.models || [])] } : state;
|
|
589
|
+
},
|
|
590
|
+
generate(request, controls) {
|
|
591
|
+
requireValue(!closed && !canonical && partitions, 'Native ordinary requester is unavailable');
|
|
592
|
+
requireValue(typeof controls?.requestApproval === 'function', 'Explicit requester disclosure authorization required');
|
|
593
|
+
return request.model.partition ? partitions.generate(request, controls) : wholeRequester.generate(request, controls);
|
|
594
|
+
},
|
|
595
|
+
recoverGeneration(request, reference, controls) {
|
|
596
|
+
requireValue(!closed && wholeRequester, 'Native whole-model recovery unavailable');
|
|
597
|
+
return wholeRequester.recover(request, reference, controls);
|
|
598
|
+
},
|
|
599
|
+
contribute: ownerGate ? async approved => {
|
|
600
|
+
if (approved === true && !await ownerAdmission('contribute')) return;
|
|
601
|
+
if (!whole) return partitions.contribute(model.id, approved);
|
|
602
|
+
if (earning && approved === true) {
|
|
603
|
+
wholeEarningReady = false;
|
|
604
|
+
await earning.ensureOffer(wholeVariants()[0]);
|
|
605
|
+
await whole.contribute(approved);
|
|
606
|
+
for (const variant of wholeVariants()) await earning.ensureOffer(variant);
|
|
607
|
+
requireValue(!closed, 'Native contribution closed during offer acceptance');
|
|
608
|
+
wholeEarningReady = true; mesh.refreshAdvertisement();
|
|
609
|
+
return whole.getState();
|
|
610
|
+
}
|
|
611
|
+
return whole.contribute(approved);
|
|
612
|
+
} : approved => whole ? whole.contribute(approved) : partitions.contribute(model.id, approved),
|
|
613
|
+
stop: ownerGate ? () => { endOwnerWait('contribute', 'Native contribution stopped'); return whole ? whole.stop() : partitions.stop(); } : () => whole ? whole.stop() : partitions.stop(), close,
|
|
614
|
+
getNetworkState: () => mesh.getSwarmSnapshot(), getFileState: options => files?.getState(options) || null,
|
|
615
|
+
shareFiles: ownerGate ? async approved => {
|
|
616
|
+
requireValue(files, 'Native cache directory required');
|
|
617
|
+
if (approved === true && !await ownerAdmission('share')) return null;
|
|
618
|
+
return files.share(model, approved);
|
|
619
|
+
} : approved => { requireValue(files, 'Native cache directory required'); return files.share(model, approved); },
|
|
620
|
+
stopSharing: ownerGate ? () => { endOwnerWait('share', 'Native file contribution stopped'); return files?.stopSupply(); } : () => files?.stopSupply(),
|
|
621
|
+
announceFiles: () => { files?.announce(); if (whole) mesh.refreshAdvertisement(); observeEarning(); }, getOwnerState: () => ownerGate ? ownerGate.summary() : NO_OWNER_POLICY,
|
|
622
|
+
getCustodyCertificate: async () => files ? files.custodyCertificate() : null });
|
|
623
|
+
} catch (error) { try { await close(); } catch (cleanup) { throw new AggregateError([error, cleanup], 'Native startup and cleanup failed'); } throw error; }
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Attempt provider cleanup even when participant settlement reports failure. */
|
|
627
|
+
export function createNativeShutdown(participant, gpuHost) {
|
|
628
|
+
let closing;
|
|
629
|
+
return () => closing ||= (async () => {
|
|
630
|
+
const failures = [];
|
|
631
|
+
try { await participant.close(); } catch (error) { failures.push(error); }
|
|
632
|
+
try { await gpuHost.close(); } catch (error) { failures.push(error); }
|
|
633
|
+
if (failures.length) throw new AggregateError(failures, 'Native host cleanup failed');
|
|
634
|
+
})();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Reads the private room capability once from an operator file. The path must be absolute and
|
|
638
|
+
* name a regular file owned by the current user with mode 0600. The final path component is
|
|
639
|
+
* opened without following a symbolic link and the open descriptor is checked before it is read.
|
|
640
|
+
* One trailing newline (LF or CRLF) is removed; the token is otherwise unchanged. */
|
|
641
|
+
export async function readNativeRoomTokenFile(file, { uid = process.getuid?.() } = {}) {
|
|
642
|
+
requireValue(typeof file === 'string' && path.isAbsolute(file), '--room-token-file requires an absolute path');
|
|
643
|
+
requireValue(Number.isSafeInteger(uid), '--room-token-file requires a POSIX host that reports the current user id');
|
|
644
|
+
let handle;
|
|
645
|
+
try { handle = await fs.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); }
|
|
646
|
+
catch (error) {
|
|
647
|
+
const reason = error.code === 'ENOENT' ? 'does not exist'
|
|
648
|
+
: error.code === 'ELOOP' ? 'is a symbolic link, not a regular file' : `cannot be opened (${error.code || error.message})`;
|
|
649
|
+
throw new Error(`--room-token-file ${reason}: ${file}`);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const stat = await handle.stat();
|
|
653
|
+
requireValue(stat.isFile(), `--room-token-file must be a regular file: ${file}`);
|
|
654
|
+
requireValue(stat.uid === uid, `--room-token-file must be owned by the current user (uid ${uid}), not uid ${stat.uid}: ${file}`);
|
|
655
|
+
const mode = stat.mode & 0o7777;
|
|
656
|
+
requireValue(mode === 0o600, `--room-token-file must have mode 0600 (owner read/write only), not ${mode.toString(8).padStart(4, '0')}: ${file}`);
|
|
657
|
+
const token = (await handle.readFile('utf8')).replace(/\r?\n$/, '');
|
|
658
|
+
requireValue(token.length > 0, `--room-token-file is empty: ${file}`);
|
|
659
|
+
return token;
|
|
660
|
+
} finally { await handle.close(); }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** One source for the room capability. The value then follows the same path as --room-token
|
|
664
|
+
* (createRoomIntroductionAdmission and the signaling join), so it is validated identically.
|
|
665
|
+
* --room-token remains for compatibility; it exposes the token in process arguments. */
|
|
666
|
+
export async function resolveNativeRoomToken(values, { warn = message => console.error(message), uid } = {}) {
|
|
667
|
+
const inline = values['room-token'], file = values['room-token-file'];
|
|
668
|
+
requireValue(inline === undefined || file === undefined, 'Choose either --room-token or --room-token-file, not both');
|
|
669
|
+
if (file !== undefined) return readNativeRoomTokenFile(file, { uid });
|
|
670
|
+
if (inline !== undefined) warn('Warning: --room-token exposes the room token in process arguments; use --room-token-file');
|
|
671
|
+
return inline;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function main() {
|
|
675
|
+
const { values } = parseArgs({ options: Object.fromEntries(['model', 'model-directory', 'identity-file', 'rtc-module',
|
|
676
|
+
'ws-module', 'gpu-module', 'adapter', 'execution-mode', 'partition-index', 'ordered-config', 'model-config', 'signaling-url', 'room', 'room-token', 'room-token-file', 'origin', 'cache-directory',
|
|
677
|
+
'owner-policy', 'shader-cache-root', 'earning-config', 'readiness-log', 'browser-search-profile'].map(key => [key, { type: 'string' }]).concat([
|
|
678
|
+
['web-search', { type: 'boolean', default: false }], ['help', { type: 'boolean', default: false }], ['canonical', { type: 'boolean', default: false }], ['share-files', { type: 'boolean', default: false }], ['contribute', { type: 'boolean', default: false }], ['connect-only', { type: 'boolean', default: false }]])) });
|
|
679
|
+
if (values.help) {
|
|
680
|
+
console.log(`Reploid native participant (Node 22.13 or newer)
|
|
681
|
+
Usage: reploid-participant --connect-only|--share-files|--contribute
|
|
682
|
+
--model <catalog id> --model-directory <read-only weights directory>
|
|
683
|
+
--identity-file <private state/identity.json>
|
|
684
|
+
--web-search (opt in to public Bing search and page extraction; no browser sign-ins)
|
|
685
|
+
--browser-search-profile <dedicated absolute directory> (Google in owned Chrome; human checks stay manual)
|
|
686
|
+
--rtc-module @roamhq/wrtc --ws-module ws [--gpu-module webgpu]
|
|
687
|
+
[--cache-directory <verified piece cache>] [--signaling-url <ws(s) URL>]
|
|
688
|
+
[--room <namespace>] [--room-token-file <absolute path>] [--origin <allowed origin>]
|
|
689
|
+
[--adapter <catalog adapter id>] (prepare this exact adapter with its base model)
|
|
690
|
+
[--execution-mode partition|whole-model] (default: partition; never falls back;
|
|
691
|
+
whole-model fills an incomplete --model-directory from verified peer pieces)
|
|
692
|
+
[--partition-index 0|1] (explicit eligible stage for a two-stage route)
|
|
693
|
+
[--ordered-config <absolute JSON path>] (explicit stage assignment and ordered limits)
|
|
694
|
+
[--model-config <absolute JSON path>] (pinned operator model and partition request; private rooms only)
|
|
695
|
+
[--canonical] (requires explicit catalog generation and a capable installed runtime)
|
|
696
|
+
[--owner-policy <absolute JSON path>] (owner schedule, memory, rate, power, temperature and foreground limits)
|
|
697
|
+
[--earning-config <absolute JSON path>] (reploid.native-earning/v1: earning service, sign-in and contribution offers; needs --owner-policy)
|
|
698
|
+
[--shader-cache-root <absolute directory>] (sealed Mesa shader cache; default: shader-cache beside the identity file)
|
|
699
|
+
[--readiness-log <absolute JSONL path>] (appends readiness spans and stage admissions for scripts/capacity-utilization.js; needs --contribute)
|
|
700
|
+
|
|
701
|
+
--room-token-file reads the private room token once at startup from a regular
|
|
702
|
+
file owned by this user with mode 0600; one trailing newline is removed.
|
|
703
|
+
--room-token <join marker> still works but exposes the token in process
|
|
704
|
+
arguments and prints a warning; it cannot be combined with --room-token-file.
|
|
705
|
+
|
|
706
|
+
File sharing and compute are separate explicit choices; both may be enabled.
|
|
707
|
+
Compute requires --gpu-module. Missing assigned pieces use authenticated peers.
|
|
708
|
+
SIGINT/SIGTERM stop admission, settle work and release owned resources.
|
|
709
|
+
On Linux, --contribute compiles GPU pipelines into a private Mesa shader cache that is
|
|
710
|
+
sealed only after a complete stage preparation and reused only for the same runtime,
|
|
711
|
+
provider, driver and program; MESA_SHADER_CACHE_DIR must not be set by the caller.
|
|
712
|
+
Native GPU uses Metal on macOS and Vulkan elsewhere. Model and route qualification
|
|
713
|
+
is separate from backend selection; --help opens no provider.`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
requireValue(values.contribute || values['share-files'] || values['connect-only'], 'Choose --contribute, --share-files or --connect-only explicitly');
|
|
717
|
+
requireValue(!values['web-search'] || !values['browser-search-profile'], 'Choose one search service');
|
|
718
|
+
if (values.contribute) requireValue(values['gpu-module'], '--gpu-module is required for compute');
|
|
719
|
+
requireValue(values['readiness-log'] === undefined || values.contribute && path.isAbsolute(values['readiness-log']),
|
|
720
|
+
'--readiness-log requires --contribute and an absolute file path');
|
|
721
|
+
for (const name of ['model', 'model-directory', 'identity-file', 'rtc-module', 'ws-module']) requireValue(values[name], '--' + name + ' is required');
|
|
722
|
+
requireValue(values['execution-mode'] === undefined || ['partition', 'whole-model'].includes(values['execution-mode']), 'Unknown native execution mode');
|
|
723
|
+
requireValue(values['execution-mode'] !== 'whole-model' || !values.canonical && !values['ordered-config'] && !values['readiness-log'],
|
|
724
|
+
'Whole-model execution cannot inherit ordered, canonical or stage-readiness configuration');
|
|
725
|
+
let configuredModels = models;
|
|
726
|
+
if (values['model-config'] !== undefined) {
|
|
727
|
+
requireValue(values.room && values.room !== bootstrap.publicRoomId,
|
|
728
|
+
'Operator model configuration requires an explicit private room; public routes require reviewed catalog/release approval');
|
|
729
|
+
configuredModels = [await readNativeModelConfiguration(values['model-config'], {
|
|
730
|
+
modelId: values.model, modelDirectory: values['model-directory'], models,
|
|
731
|
+
})];
|
|
732
|
+
}
|
|
733
|
+
// Before identity, module or connection side effects; read once.
|
|
734
|
+
const roomToken = await resolveNativeRoomToken(values);
|
|
735
|
+
let logNetwork = () => {}, observeReadiness = () => {};
|
|
736
|
+
const owner = values['owner-policy'] === undefined ? null : await openNativeOwnerControls({ file: values['owner-policy'],
|
|
737
|
+
// An owner decision changes admitted capacity without a partition notification.
|
|
738
|
+
custody: custodyPolicy, onChange: () => { logNetwork(); observeReadiness(); },
|
|
739
|
+
// Doppler's application GPU buffer accounting; null until compute opens a device.
|
|
740
|
+
readGpuBuffers: values.contribute ? async () => (await import('doppler-gpu/partitions')).inspectDeviceMemory() : null });
|
|
741
|
+
let ordered = null;
|
|
742
|
+
if (values['ordered-config']) {
|
|
743
|
+
requireValue(!values.canonical && path.isAbsolute(values['ordered-config']), 'Ordered config requires an absolute file path and ordinary execution');
|
|
744
|
+
ordered = JSON.parse(await fs.readFile(values['ordered-config'], 'utf8'));
|
|
745
|
+
requireValue(ordered && typeof ordered === 'object', 'Ordered config requires an explicit configuration object');
|
|
746
|
+
snapshotWorkOrderedParticipation(ordered, null, configuredModels);
|
|
747
|
+
}
|
|
748
|
+
requireValue(values['partition-index'] === undefined || ['0', '1'].includes(values['partition-index'])
|
|
749
|
+
&& !values['ordered-config'] && values['execution-mode'] !== 'whole-model', 'Partition index requires a two-stage partition contributor');
|
|
750
|
+
const identityFile = path.resolve(values['identity-file']);
|
|
751
|
+
let model = configuredModels.find(row => row.id === values.model); requireValue(model, 'Model is absent from the application catalog');
|
|
752
|
+
if (values.adapter) {
|
|
753
|
+
const adapter = model.availableAdapters?.find(entry => entry.id === values.adapter);
|
|
754
|
+
requireValue(adapter && adapter.baseModelIdentity === model.identity, 'Adapter is not authorized for this exact base artifact');
|
|
755
|
+
model = { ...model, adapters: [structuredClone(adapter)] };
|
|
756
|
+
}
|
|
757
|
+
if (values['partition-index'] !== undefined) model = { ...model, partitionContributionIndexes: [Number(values['partition-index'])] };
|
|
758
|
+
const moduleSpecifier = value => {
|
|
759
|
+
requireValue(path.isAbsolute(value) || ['@roamhq/wrtc', 'ws', 'webgpu'].includes(value),
|
|
760
|
+
'Native modules require an operator-selected absolute local path or supported package name');
|
|
761
|
+
return path.isAbsolute(value) ? pathToFileURL(value).href : value;
|
|
762
|
+
};
|
|
763
|
+
requireValue(values['shader-cache-root'] === undefined || values.contribute, '--shader-cache-root requires --contribute');
|
|
764
|
+
// Bound before any native module loads, so MESA_SHADER_CACHE_DIR is set before the Vulkan driver initializes.
|
|
765
|
+
let shaderCache = null;
|
|
766
|
+
if (values.contribute && (process.platform === 'linux' || values['shader-cache-root'] !== undefined)) {
|
|
767
|
+
const gpuModule = values['gpu-module'];
|
|
768
|
+
const { key, identity: cacheIdentity } = await nativeShaderCacheIdentity({
|
|
769
|
+
runtime: { directory: await installedPackageDirectory(fileURLToPath(import.meta.resolve('doppler-gpu/partitions')), 'doppler-gpu'),
|
|
770
|
+
archiveIntegrity: dopplerPin.integrity },
|
|
771
|
+
provider: { module: gpuModule, directory: await installedPackageDirectory(fileURLToPath(import.meta.resolve(moduleSpecifier(gpuModule))),
|
|
772
|
+
path.isAbsolute(gpuModule) ? null : gpuModule) },
|
|
773
|
+
program: { model, policy, stage: Number.isSafeInteger(ordered?.stage?.index) ? ordered.stage.index : null, canonical: values.canonical } });
|
|
774
|
+
shaderCache = await openNativeShaderCache({ root: values['shader-cache-root'] ?? path.join(path.dirname(identityFile), 'shader-cache'),
|
|
775
|
+
key, identity: cacheIdentity, onEvent: event => console.log(JSON.stringify({ event: 'shader-cache', ...event })) });
|
|
776
|
+
}
|
|
777
|
+
let identity;
|
|
778
|
+
try { identity = JSON.parse(await fs.readFile(identityFile, 'utf8')); }
|
|
779
|
+
catch (error) { if (error.code !== 'ENOENT') throw error;
|
|
780
|
+
identity = await createSigningIdentity({ algorithm: 'Ed25519' });
|
|
781
|
+
await fs.mkdir(path.dirname(identityFile), { recursive: true, mode: 0o700 });
|
|
782
|
+
await fs.writeFile(identityFile, JSON.stringify(identity), { flag: 'wx', mode: 0o600 }); }
|
|
783
|
+
const importModule = value => { moduleSpecifier(value); return import(path.isAbsolute(value) ? pathToFileURL(value).href : value); };
|
|
784
|
+
const [rtcModule, wsModule] = await Promise.all([importModule(values['rtc-module']), importModule(values['ws-module'])]);
|
|
785
|
+
const rtc = createNativeRtcAdapter(rtcModule.default || rtcModule);
|
|
786
|
+
const Socket = wsModule.default || wsModule.WebSocket;
|
|
787
|
+
class HostWebSocket extends Socket { constructor(url) { super(url, { origin: values.origin || 'http://localhost:8000' }); } }
|
|
788
|
+
const gpuHost = createNativeGpuHost(() => importModule(values['gpu-module']));
|
|
789
|
+
// Optional readiness records (reploid.readiness-span/v1, reploid.stage-admission/v1) for the utilization CLI;
|
|
790
|
+
// identities, times and slot counts only. The session starts here, before any stage is prepared.
|
|
791
|
+
const readinessLog = values['readiness-log'] === undefined ? null : await openNativeReadinessLog({ file: values['readiness-log'],
|
|
792
|
+
onError: error => console.error('Readiness record not written:', error.code || error.message) });
|
|
793
|
+
const readiness = readinessLog && createReadinessRecorder({ participantId: identity.peerId, host: 'node', write: readinessLog.write });
|
|
794
|
+
const closeReadiness = async reason => { readiness?.close(reason); await readinessLog?.close(); };
|
|
795
|
+
// Optional earning: signs this host in, registers its identity as a native device, records the owner policy
|
|
796
|
+
// and claims settled work under service-authored offers (native-earning.js); state lives beside the identity.
|
|
797
|
+
if (values['earning-config']) requireValue(owner, '--earning-config requires --owner-policy');
|
|
798
|
+
const earning = values['earning-config'] ? await import('./native-earning.js').then(async native => native.openNativeEarning({
|
|
799
|
+
settings: await native.readNativeEarningFile(values['earning-config']), identity, ownerAuthorization: { policy: owner.policy, digest: owner.digest },
|
|
800
|
+
storage: native.createNativeEarningStorage(path.join(path.dirname(identityFile), 'earning-state.json')) }))
|
|
801
|
+
.catch(async error => { owner?.close(); await shaderCache?.close(); await closeReadiness('startup-failed'); throw error; }) : null;
|
|
802
|
+
// Status lines only when their content changes, plus a periodic heartbeat (policy.statusHeartbeatMs).
|
|
803
|
+
const statusLog = createChangeOnlyLog({ heartbeatMs: policy.statusHeartbeatMs, write: line => console.log(line) });
|
|
804
|
+
const webWorker = values['browser-search-profile']
|
|
805
|
+
? await import('./browser-web-worker.js').then(module => module.openBrowserWebWorker({ profileDirectory: values['browser-search-profile'] }))
|
|
806
|
+
.catch(async error => { owner?.close(); await shaderCache?.close(); await closeReadiness('startup-failed'); throw error; })
|
|
807
|
+
: values['web-search'] ? createPublicWebWorker() : null;
|
|
808
|
+
const participant = await createNativeParticipant({ model, modelDirectory: values['model-directory'], identity, rtc,
|
|
809
|
+
WebSocket: HostWebSocket, signalingUrl: values['signaling-url'] || bootstrap.signalingUrl,
|
|
810
|
+
roomId: values.room || bootstrap.publicRoomId, roomToken: roomToken || bootstrap.publicJoinMarker,
|
|
811
|
+
cacheDirectory: values['cache-directory'] || path.join(path.dirname(identityFile), 'model-cache'),
|
|
812
|
+
executionMode: values['execution-mode'] || 'partition',
|
|
813
|
+
onModelFill: (() => {
|
|
814
|
+
// Piece lines at most every 5 s; every other fill event is logged.
|
|
815
|
+
let last = 0;
|
|
816
|
+
return event => {
|
|
817
|
+
if (event.phase === 'piece' && Date.now() - last < 5000) return;
|
|
818
|
+
last = Date.now(); console.log(JSON.stringify({ event: 'model-fill', at: new Date(last).toISOString(), ...event }));
|
|
819
|
+
};
|
|
820
|
+
})(),
|
|
821
|
+
generationJournalFile: path.join(path.dirname(identityFile), 'generation-journal.sqlite'),
|
|
822
|
+
webWorker,
|
|
823
|
+
ordered, canonical: values.canonical ? { epochDirectory: path.join(path.dirname(identityFile), 'canonical-epochs',
|
|
824
|
+
sha256Hex(values.room || bootstrap.publicRoomId)) } : null,
|
|
825
|
+
nativeGpu: () => gpuHost.open(), owner, shaderCache, earning, readiness,
|
|
826
|
+
onCustodyCertificate: certificate => console.log(JSON.stringify({ event: 'custody-certificate', certificate })),
|
|
827
|
+
onChange: state => {
|
|
828
|
+
statusLog('partition-state', { ...summarizeNativePartitionState(state), owner: nativeOwnerStatus(owner) });
|
|
829
|
+
// A prepared stage is offered for before any route admits it.
|
|
830
|
+
if (earning && state.descriptor?.planId) earning.ensureOffer(state.descriptor).catch(error => console.error('Earning offer deferred:', error.code || error.message));
|
|
831
|
+
} })
|
|
832
|
+
.catch(async error => { owner?.close(); await shaderCache?.close(); await closeReadiness('startup-failed'); await webWorker?.close?.(); throw error; });
|
|
833
|
+
logNetwork = () => statusLog('network', { event: 'network', at: new Date().toISOString(), ...participant.getNetworkState(),
|
|
834
|
+
files: participant.getFileState({ includeReceipts: false }), owner: participant.getOwnerState() });
|
|
835
|
+
if (readiness) observeReadiness = () => readiness.observe(participant.getState());
|
|
836
|
+
// Polling also renews the custody certificate before it expires, and observes readiness even while a partition tick waits.
|
|
837
|
+
const timer = setInterval(() => { participant.announceFiles(); logNetwork(); observeReadiness();
|
|
838
|
+
participant.getCustodyCertificate().catch(error => console.error('Custody certificate renewal failed:', error.message)); }, policy.pollMs);
|
|
839
|
+
const shutdown = createNativeShutdown(participant, gpuHost);
|
|
840
|
+
// Readiness ends when shutdown begins and admission closes; admissions settled by shutdown are still recorded.
|
|
841
|
+
const stop = () => { clearInterval(timer); logNetwork = () => {}; observeReadiness = () => {}; readiness?.end('shutdown');
|
|
842
|
+
return shutdown().finally(async () => { owner?.close(); await shaderCache?.close(); await closeReadiness('shutdown'); await webWorker?.close?.(); }); };
|
|
843
|
+
const completion = new Promise((resolve, reject) => {
|
|
844
|
+
const signalStop = () => stop().then(resolve, reject);
|
|
845
|
+
process.once('SIGINT', signalStop); process.once('SIGTERM', signalStop);
|
|
846
|
+
});
|
|
847
|
+
console.log(JSON.stringify({ event: 'native-initialized', participantId: identity.peerId, modelId: model.id,
|
|
848
|
+
owner: owner ? owner.declaration() : NO_OWNER_POLICY, shaderCache: shaderCache ? shaderCache.status()
|
|
849
|
+
: values.contribute ? { status: 'not-applicable', platform: process.platform } : null }));
|
|
850
|
+
try {
|
|
851
|
+
await Promise.all([completion, (async () => {
|
|
852
|
+
if (values['share-files']) await participant.shareFiles(true);
|
|
853
|
+
if (values.contribute) await participant.contribute(true);
|
|
854
|
+
})()]);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
try { await stop(); }
|
|
857
|
+
catch (cleanup) { if (cleanup !== error) throw new AggregateError([error, cleanup], 'Native preparation and cleanup failed'); }
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// npm starts a package command through a symbolic link, so real paths are compared:
|
|
862
|
+
// a plain path comparison made the installed command exit without doing anything.
|
|
863
|
+
const entered = (() => { try { return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); } catch { return false; } })();
|
|
864
|
+
if (entered) {
|
|
865
|
+
await main();
|
|
866
|
+
}
|