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,192 @@
|
|
|
1
|
+
import failureMessages from '../../transport/partition-errors.json' with { type: 'json' };
|
|
2
|
+
import { canonicalize } from '../../artifacts/canonical-json.js';
|
|
3
|
+
import { sha256Hex } from '../../artifacts/signatures.js';
|
|
4
|
+
|
|
5
|
+
/** Shared orchestration bindings. Tensor semantics remain supplied by Doppler. */
|
|
6
|
+
export const partitionIdentityKeys = Object.freeze(['modelId', 'modelIdentity', 'planId', 'threadId', 'attemptId', 'participantA', 'participantB']);
|
|
7
|
+
export const partitionActions = Object.freeze(['mesh.execute_partition_a', 'mesh.execute_partition_b',
|
|
8
|
+
'mesh.transfer_intermediate_activation', 'mesh.transfer_token_context', 'mesh.transfer_partition_output']);
|
|
9
|
+
export const assertPartition = (value, message) => { if (!value) throw new Error(message); };
|
|
10
|
+
/** A declared failure whose code and message cross every channel hop together; an uncoded error
|
|
11
|
+
* becomes EXECUTION_FAILED at the next hop. `detail` adds non-message fields, such as a bound. */
|
|
12
|
+
export function partitionFailure(code, detail = {}) {
|
|
13
|
+
assertPartition(Object.hasOwn(failureMessages, code), 'Undeclared partition failure code');
|
|
14
|
+
return Object.assign(new Error(failureMessages[code]), detail, { code });
|
|
15
|
+
}
|
|
16
|
+
/** Throw CAPACITY_UNAVAILABLE unless `value` holds: an exhausted or closed admission slot, never a defect. */
|
|
17
|
+
export const assertCapacity = value => { if (!value) throw partitionFailure('CAPACITY_UNAVAILABLE'); };
|
|
18
|
+
export const samePartitionIdentity = (actual, expected) => actual && [...partitionIdentityKeys, 'requesterId', 'placementGeneration'].every(key => actual[key] === expected[key]);
|
|
19
|
+
/** Ask the injected provider for tensor layout; retain the accepted v1 adapter. */
|
|
20
|
+
export function partitionActivationContract(runtime, plan) {
|
|
21
|
+
if (runtime.getPartitionActivationContract) {
|
|
22
|
+
assertPartition(typeof runtime.validateActivationFrameForPlan === 'function', 'Doppler activation validator required');
|
|
23
|
+
return Object.freeze({ ...runtime.getPartitionActivationContract(plan) });
|
|
24
|
+
}
|
|
25
|
+
assertPartition(plan?.schema === runtime.LAYER_PARTITION_SCHEMA
|
|
26
|
+
&& plan.activationPrecision === undefined && plan.activationComponents === undefined
|
|
27
|
+
&& plan.activationElementStrideBytes === undefined, 'Unsupported Doppler partition activation contract');
|
|
28
|
+
return Object.freeze({ schema: runtime.ACTIVATION_TENSOR_SCHEMA, dtype: plan.activationDtype,
|
|
29
|
+
hiddenSize: plan.hiddenSize, precision: plan.activationDtype, components: 1,
|
|
30
|
+
elementStrideBytes: plan.activationDtype === 'f16' ? 2 : 4 });
|
|
31
|
+
}
|
|
32
|
+
export function matchingPartitionActivation(value, contract, frame = false) {
|
|
33
|
+
if (frame && contract.components === 1) {
|
|
34
|
+
return value?.dtype === contract.dtype && ['precision', 'components', 'elementStrideBytes']
|
|
35
|
+
.every(key => value[key] === undefined);
|
|
36
|
+
}
|
|
37
|
+
return value?.dtype === contract.dtype && ['precision', 'components', 'elementStrideBytes']
|
|
38
|
+
.every(key => value[key] === contract[key] || contract.components === 1 && value[key] === undefined);
|
|
39
|
+
}
|
|
40
|
+
export function validatePartitionIdentity(identity) {
|
|
41
|
+
assertPartition(identity && typeof identity === 'object' && !Array.isArray(identity) && partitionIdentityKeys.every(key => typeof identity[key] === 'string'
|
|
42
|
+
&& identity[key].length > 0 && identity[key].length <= 256)
|
|
43
|
+
&& /^sha256:[a-f0-9]{64}$/.test(identity.modelIdentity)
|
|
44
|
+
&& identity.participantA !== identity.participantB, 'Invalid partition identity');
|
|
45
|
+
if (identity.requesterId !== undefined || identity.placementGeneration !== undefined) {
|
|
46
|
+
assertPartition(typeof identity.requesterId === 'string' && identity.requesterId.length > 0 && identity.requesterId.length <= 256
|
|
47
|
+
&& Number.isSafeInteger(identity.placementGeneration) && identity.placementGeneration >= 0, 'Invalid requester placement binding');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Snapshot the complete validated identity; optional placement fields travel as a pair. */
|
|
51
|
+
export function canonicalPartitionBinding(identity) {
|
|
52
|
+
validatePartitionIdentity(identity);
|
|
53
|
+
const keys = identity.requesterId === undefined ? partitionIdentityKeys
|
|
54
|
+
: [...partitionIdentityKeys, 'requesterId', 'placementGeneration'];
|
|
55
|
+
return Object.freeze(Object.fromEntries(keys.map(key => [key, identity[key]])));
|
|
56
|
+
}
|
|
57
|
+
const assertFiniteJson = value => {
|
|
58
|
+
if (Array.isArray(value)) return value.forEach(item => assertFiniteJson(item));
|
|
59
|
+
if (value && typeof value === 'object') return Object.keys(value).forEach(key => assertFiniteJson(value[key]));
|
|
60
|
+
assertPartition(value === null || ['string', 'boolean'].includes(typeof value)
|
|
61
|
+
|| typeof value === 'number' && Number.isFinite(value), 'Partition metadata must be finite JSON');
|
|
62
|
+
};
|
|
63
|
+
/** The library's canonical JSON (artifacts/canonical-json.js) of finite JSON metadata. */
|
|
64
|
+
export function canonicalPartitionJson(value) {
|
|
65
|
+
assertFiniteJson(value);
|
|
66
|
+
return canonicalize(value);
|
|
67
|
+
}
|
|
68
|
+
export async function partitionFingerprint(metadata, bytes = new Uint8Array()) {
|
|
69
|
+
const header = new TextEncoder().encode(canonicalPartitionJson(metadata));
|
|
70
|
+
const input = new Uint8Array(4 + header.length + bytes.length);
|
|
71
|
+
new DataView(input.buffer).setUint32(0, header.length);
|
|
72
|
+
input.set(header, 4); input.set(bytes, 4 + header.length);
|
|
73
|
+
return sha256Hex(input);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const PARTITION_ROUTE_SCHEMA = 'reploid.partition-route/v1';
|
|
77
|
+
export const ROUTE_ATTEMPT_SCHEMA = 'reploid.partition-attempt/v2';
|
|
78
|
+
const identifier = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
79
|
+
const exactFields = (value, keys) => value && typeof value === 'object' && !Array.isArray(value)
|
|
80
|
+
&& Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
81
|
+
const freezeRouteJson = value => {
|
|
82
|
+
if (value && typeof value === 'object') { Object.values(value).forEach(freezeRouteJson); Object.freeze(value); }
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
/** Structural checks also apply when comparing a received identity without a runtime. */
|
|
86
|
+
export function canonicalPartitionRoute(route) {
|
|
87
|
+
assertPartition(exactFields(route, ['schema', 'modelId', 'modelIdentity', 'planId', 'stages', 'activation'])
|
|
88
|
+
&& route.schema === PARTITION_ROUTE_SCHEMA && identifier(route.modelId) && /^sha256:[a-f0-9]{64}$/.test(route.planId)
|
|
89
|
+
&& /^sha256:[a-f0-9]{64}$/.test(route.modelIdentity)
|
|
90
|
+
&& Array.isArray(route.stages) && route.stages.length >= 2, 'Invalid partition route');
|
|
91
|
+
const participants = new Set(), transports = new Set();
|
|
92
|
+
route.stages.forEach((stage, index) => {
|
|
93
|
+
assertPartition(exactFields(stage, ['index', 'participantId', 'transportId', 'residentId', 'executionId'])
|
|
94
|
+
&& stage.index === index && ['participantId', 'transportId', 'residentId', 'executionId'].every(key => identifier(stage[key]))
|
|
95
|
+
&& !participants.has(stage.participantId) && !transports.has(stage.transportId), 'Invalid or duplicate route stage');
|
|
96
|
+
participants.add(stage.participantId); transports.add(stage.transportId);
|
|
97
|
+
});
|
|
98
|
+
const layout = route.activation;
|
|
99
|
+
assertPartition(exactFields(layout, ['schema', 'dtype', 'hiddenSize', 'precision', 'components', 'elementStrideBytes'])
|
|
100
|
+
&& layout.schema === (layout.precision === 'two-f32' ? 'doppler.activation-tensor/v2' : 'doppler.activation-tensor/v1')
|
|
101
|
+
&& Number.isSafeInteger(layout.hiddenSize) && layout.hiddenSize > 0
|
|
102
|
+
&& (layout.precision === 'two-f32' ? layout.dtype === 'f32' && layout.components === 2 && layout.elementStrideBytes === 8
|
|
103
|
+
: ['f32', 'f16'].includes(layout.dtype) && layout.precision === layout.dtype && layout.components === 1
|
|
104
|
+
&& layout.elementStrideBytes === (layout.dtype === 'f16' ? 2 : 4)), 'Invalid route activation layout');
|
|
105
|
+
return freezeRouteJson(structuredClone(route));
|
|
106
|
+
}
|
|
107
|
+
/** Provider hashing validates its plan geometry; host admission separately binds the selected manifest. */
|
|
108
|
+
export async function validatePartitionRoute(runtime, plan, route) {
|
|
109
|
+
const snapshot = canonicalPartitionRoute(route), planSnapshot = structuredClone(plan);
|
|
110
|
+
assertPartition(typeof runtime?.hashLayerPartitionPlan === 'function'
|
|
111
|
+
&& typeof runtime.getPartitionActivationContract === 'function', 'Ordered Doppler contracts required');
|
|
112
|
+
assertPartition(planSnapshot?.schema === 'doppler.layer-partition-contract/v3'
|
|
113
|
+
&& planSnapshot.modelId === snapshot.modelId && planSnapshot.stageCount === snapshot.stages.length
|
|
114
|
+
&& Array.isArray(planSnapshot.partitions) && planSnapshot.partitions.length === snapshot.stages.length
|
|
115
|
+
&& planSnapshot.partitions.every((part, index) => part.index === index), 'Route plan stage mismatch');
|
|
116
|
+
const layout = partitionActivationContract(runtime, planSnapshot);
|
|
117
|
+
assertPartition(canonicalPartitionJson(layout) === canonicalPartitionJson(snapshot.activation), 'Route activation mismatch');
|
|
118
|
+
assertPartition(await runtime.hashLayerPartitionPlan(planSnapshot) === snapshot.planId, 'Route plan identity mismatch');
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
export function canonicalRouteAttemptBinding(identity) {
|
|
122
|
+
assertPartition(exactFields(identity, ['schema', 'threadId', 'attemptId', 'requesterId', 'placementGeneration', 'route'])
|
|
123
|
+
&& identity.schema === ROUTE_ATTEMPT_SCHEMA && ['threadId', 'attemptId', 'requesterId'].every(key => identifier(identity[key]))
|
|
124
|
+
&& Number.isSafeInteger(identity.placementGeneration) && identity.placementGeneration >= 0, 'Invalid route attempt identity');
|
|
125
|
+
return Object.freeze({ schema: identity.schema, threadId: identity.threadId, attemptId: identity.attemptId,
|
|
126
|
+
requesterId: identity.requesterId, placementGeneration: identity.placementGeneration, route: canonicalPartitionRoute(identity.route) });
|
|
127
|
+
}
|
|
128
|
+
export function sameRouteAttemptIdentity(actual, expected) {
|
|
129
|
+
try { return canonicalPartitionJson(canonicalRouteAttemptBinding(actual)) === canonicalPartitionJson(canonicalRouteAttemptBinding(expected)); }
|
|
130
|
+
catch { return false; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A ready stage is inventory, not a complete chat path. This checks advertised
|
|
134
|
+
* binding only; entry refresh, grants and reservations remain authoritative. */
|
|
135
|
+
export function isOrderedPartitionEntryModel(model, peer) {
|
|
136
|
+
try {
|
|
137
|
+
const selection = model?.partition;
|
|
138
|
+
if (peer.description?.schema !== 'reploid.partition-capability/v2' || model.index !== 0
|
|
139
|
+
|| selection?.schema !== 'reploid.partition-selection/v2') return false;
|
|
140
|
+
const route = canonicalPartitionRoute(selection.route), first = route.stages[0];
|
|
141
|
+
return model.id === route.modelId && model.identity === route.modelIdentity
|
|
142
|
+
&& model.planId === route.planId && selection.planId === route.planId
|
|
143
|
+
&& selection.coordinatorId === first.participantId && first.participantId === peer.participantId
|
|
144
|
+
&& first.transportId === peer.transportId && first.residentId === model.residentId
|
|
145
|
+
&& first.executionId === model.executionId
|
|
146
|
+
&& canonicalPartitionJson(route.activation) === canonicalPartitionJson(model.activation);
|
|
147
|
+
} catch { return false; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One observed inventory version, bounded by host policy. These hints never authorize execution. */
|
|
151
|
+
export function validateOrderedPartitionCapability(description, participantId, maxOffersPerPeer) {
|
|
152
|
+
assertPartition(Number.isSafeInteger(maxOffersPerPeer) && maxOffersPerPeer > 0, 'Explicit stage offer limit required');
|
|
153
|
+
assertPartition(description?.schema === 'reploid.partition-capability/v2' && description.participantId === participantId
|
|
154
|
+
&& ['idle', 'waiting', 'loading', 'ready', 'stopping', 'failed'].includes(description.phase)
|
|
155
|
+
&& Array.isArray(description.models) && description.models.length <= maxOffersPerPeer,
|
|
156
|
+
'Ordered partition capability identity or inventory mismatch');
|
|
157
|
+
const capacity = description.capacity;
|
|
158
|
+
assertPartition(capacity && typeof capacity.closed === 'boolean' && Number.isSafeInteger(capacity.active) && capacity.active >= 0
|
|
159
|
+
&& Number.isSafeInteger(capacity.availableSlots) && capacity.availableSlots >= 0, 'Ordered partition capacity required');
|
|
160
|
+
for (const model of description.models) {
|
|
161
|
+
assertPartition(model && typeof model.id === 'string' && model.id.length > 0 && model.id.length <= 256
|
|
162
|
+
&& /^sha256:[a-f0-9]{64}$/.test(model.identity) && Number.isSafeInteger(model.index) && model.index >= 0
|
|
163
|
+
&& ['planId', 'residentId', 'executionId'].every(key => typeof model[key] === 'string' && model[key].length > 0 && model[key].length <= 256)
|
|
164
|
+
&& ['ready', 'unavailable'].includes(model.availability), 'Invalid ordered partition offer');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Exact adapter selection travels in the plan digest, separate from the base manifest. */
|
|
169
|
+
export function partitionAdapterIdentities(model) {
|
|
170
|
+
const adapters = model?.adapters ?? [];
|
|
171
|
+
assertPartition(Array.isArray(adapters) && adapters.length <= 1 && adapters.every(adapter =>
|
|
172
|
+
/^sha256:[a-f0-9]{64}$/.test(adapter.identity) && adapter.baseModelIdentity === model.identity),
|
|
173
|
+
'Partition adapter must target the exact base artifact');
|
|
174
|
+
return adapters.map(adapter => adapter.identity);
|
|
175
|
+
}
|
|
176
|
+
export function samePartitionAdapters(left, right) {
|
|
177
|
+
return canonicalPartitionJson(partitionAdapterIdentities(left)) === canonicalPartitionJson(partitionAdapterIdentities(right));
|
|
178
|
+
}
|
|
179
|
+
export function selectPartitionContribution(model, adapters = model.adapters ?? []) {
|
|
180
|
+
const selection = { ...model, adapters };
|
|
181
|
+
partitionAdapterIdentities(selection);
|
|
182
|
+
selection.adapters = adapters.map(adapter => {
|
|
183
|
+
const pinned = model.availableAdapters?.find(entry => entry.identity === adapter.identity
|
|
184
|
+
&& entry.baseModelIdentity === model.identity);
|
|
185
|
+
assertPartition(pinned, 'Partition adapter is absent from the authorized catalog');
|
|
186
|
+
return structuredClone(pinned);
|
|
187
|
+
});
|
|
188
|
+
if (selection.partitionPlan && adapters.length) selection.partitionPlan = {
|
|
189
|
+
...selection.partitionPlan, adapterIdentities: partitionAdapterIdentities(selection)
|
|
190
|
+
};
|
|
191
|
+
return structuredClone(selection);
|
|
192
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import failureMessages from '../../transport/partition-errors.json' with { type: 'json' };
|
|
2
|
+
import { assertPartition as assert } from './partition-contract.js';
|
|
3
|
+
|
|
4
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
5
|
+
const unavailable = () => Object.assign(new Error(failureMessages.CAPACITY_UNAVAILABLE), { code: 'CAPACITY_UNAVAILABLE' });
|
|
6
|
+
|
|
7
|
+
/** One admission budget per executing device (Launch 3, opt-in). It charges declared application
|
|
8
|
+
* allocations, not measured physical memory: each resident's weights once, plus `bytes` and one
|
|
9
|
+
* slot per admitted attempt, across every resident and route on the device. Callers release an
|
|
10
|
+
* attempt only after its cleanup settles, and a resident only after its provider closed.
|
|
11
|
+
*
|
|
12
|
+
* With `queue`, an attempt that does not fit waits instead of failing. Waiting is bounded in
|
|
13
|
+
* length, per requester and in time; each bound fails with CAPACITY_UNAVAILABLE. Grants go to the
|
|
14
|
+
* route owner (coordinator) with the fewest attempts admitted on this device, then the one served
|
|
15
|
+
* least recently, then first come, first served, with no overtaking by smaller requests. */
|
|
16
|
+
export function createDeviceAdmission({ maxBytes, maxConcurrentAttempts, queue = null }) {
|
|
17
|
+
assert(positive(maxBytes) && positive(maxConcurrentAttempts), 'Explicit device admission budget required');
|
|
18
|
+
assert(queue === null || ['maxQueued', 'maxQueuedPerRequester', 'maxWaitMs'].every(key => positive(queue[key])),
|
|
19
|
+
'Explicit bounded admission queue required');
|
|
20
|
+
const residents = new Map(), attempts = new Map(), waiting = [], served = new Map();
|
|
21
|
+
let sequence = 0, grants = 0;
|
|
22
|
+
const attemptRows = () => [...attempts.values()].flatMap(rows => [...rows.values()]);
|
|
23
|
+
const residentBytes = () => [...residents.values()].reduce((total, bytes) => total + bytes, 0);
|
|
24
|
+
const attemptBytes = () => attemptRows().reduce((total, row) => total + row.bytes, 0);
|
|
25
|
+
const free = () => maxBytes - residentBytes() - attemptBytes();
|
|
26
|
+
const fits = bytes => attemptRows().length < maxConcurrentAttempts && bytes <= free();
|
|
27
|
+
const admittedFor = routeKey => attemptRows().filter(row => row.routeKey === routeKey).length;
|
|
28
|
+
function charge(entry) {
|
|
29
|
+
attempts.get(entry.owner).set(entry.attemptId, { bytes: entry.bytes, routeKey: entry.routeKey });
|
|
30
|
+
served.set(entry.routeKey, grants++);
|
|
31
|
+
// Forget idle route owners so this record stays bounded; a returning owner counts as least recent.
|
|
32
|
+
if (served.size > maxConcurrentAttempts + (queue?.maxQueued ?? 0)) for (const key of served.keys()) {
|
|
33
|
+
if (!admittedFor(key) && !waiting.some(row => row.routeKey === key)) served.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function settle(entry, error) {
|
|
37
|
+
const at = waiting.indexOf(entry);
|
|
38
|
+
if (at < 0) return;
|
|
39
|
+
waiting.splice(at, 1);
|
|
40
|
+
clearTimeout(entry.timer); entry.signal?.removeEventListener('abort', entry.onAbort);
|
|
41
|
+
if (error) entry.reject(error); else { charge(entry); entry.resolve(); }
|
|
42
|
+
}
|
|
43
|
+
function pump() {
|
|
44
|
+
while (waiting.length) {
|
|
45
|
+
const next = waiting.reduce((best, entry) => {
|
|
46
|
+
const order = admittedFor(entry.routeKey) - admittedFor(best.routeKey)
|
|
47
|
+
|| (served.get(entry.routeKey) ?? -1) - (served.get(best.routeKey) ?? -1) || entry.sequence - best.sequence;
|
|
48
|
+
return order < 0 ? entry : best;
|
|
49
|
+
});
|
|
50
|
+
if (!fits(next.bytes)) return;
|
|
51
|
+
settle(next, null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
/** Charge resident weights once; a resident that does not fit is never offered. */
|
|
56
|
+
admitResident(owner, bytes) {
|
|
57
|
+
assert(owner && typeof owner === 'object' && positive(bytes), 'Device resident charge requires an owner and weight bytes');
|
|
58
|
+
if (residents.has(owner)) return;
|
|
59
|
+
if (bytes > free()) throw Object.assign(new Error('Device allocation budget cannot hold this resident'), { code: 'RESOURCE_EXHAUSTED' });
|
|
60
|
+
residents.set(owner, bytes); attempts.set(owner, new Map());
|
|
61
|
+
},
|
|
62
|
+
/** Admit one attempt on one resident; repeated admission of the same attempt charges once. */
|
|
63
|
+
async admit({ owner, attemptId, bytes, requesterId = null, routeKey = null, signal }) {
|
|
64
|
+
assert(residents.has(owner) && typeof attemptId === 'string' && attemptId.length > 0 && positive(bytes),
|
|
65
|
+
'Device attempt admission requires an admitted resident, attempt and allocation');
|
|
66
|
+
if (attempts.get(owner).has(attemptId)) return;
|
|
67
|
+
const queued = waiting.find(entry => entry.owner === owner && entry.attemptId === attemptId);
|
|
68
|
+
if (queued) return queued.promise;
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
if (!waiting.length && fits(bytes)) { charge({ owner, attemptId, bytes, routeKey }); return; }
|
|
71
|
+
if (!queue || waiting.length >= queue.maxQueued
|
|
72
|
+
|| waiting.filter(entry => entry.requesterId === requesterId).length >= queue.maxQueuedPerRequester) throw unavailable();
|
|
73
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
74
|
+
const entry = { owner, attemptId, bytes, requesterId, routeKey, sequence: sequence++, promise, resolve, reject, signal };
|
|
75
|
+
entry.timer = setTimeout(() => settle(entry, unavailable()), queue.maxWaitMs);
|
|
76
|
+
entry.onAbort = () => settle(entry, signal.reason);
|
|
77
|
+
signal?.addEventListener('abort', entry.onAbort, { once: true });
|
|
78
|
+
waiting.push(entry); pump();
|
|
79
|
+
return promise;
|
|
80
|
+
},
|
|
81
|
+
release(owner, attemptId) { attempts.get(owner)?.delete(attemptId); pump(); },
|
|
82
|
+
releaseResident(owner) {
|
|
83
|
+
for (const entry of waiting.filter(row => row.owner === owner)) settle(entry, unavailable());
|
|
84
|
+
residents.delete(owner); attempts.delete(owner); pump();
|
|
85
|
+
},
|
|
86
|
+
/** Attempts of `bytes` each that could still be admitted without waiting. */
|
|
87
|
+
available(bytes) {
|
|
88
|
+
assert(positive(bytes), 'Device availability requires an attempt allocation');
|
|
89
|
+
if (waiting.length) return 0;
|
|
90
|
+
return Math.max(0, Math.min(maxConcurrentAttempts - attemptRows().length, Math.floor(free() / bytes)));
|
|
91
|
+
},
|
|
92
|
+
getState: () => ({ maxBytes, maxConcurrentAttempts, residents: residents.size, residentBytes: residentBytes(),
|
|
93
|
+
attemptBytes: attemptBytes(), active: attemptRows().length, availableBytes: free(),
|
|
94
|
+
...(queue ? { queued: waiting.length, maxQueued: queue.maxQueued } : {}) })
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createPartitionDataChannel } from '../../transport/partition-data-channel.js';
|
|
2
|
+
import { assertPartition as assert, validateOrderedPartitionCapability, isOrderedPartitionEntryModel } from './partition-contract.js';
|
|
3
|
+
|
|
4
|
+
/** Owns bounded capability observation, never placement, loading or permission. */
|
|
5
|
+
export function createPartitionDiscovery({ identity, catalog, policy, peers, createNetwork,
|
|
6
|
+
describe, onChange = () => {}, now = () => Date.now() }) {
|
|
7
|
+
const connections = new Map(), lifetime = new AbortController();
|
|
8
|
+
let refreshing = null, closed = false, closing = null, networkSettled = false;
|
|
9
|
+
const snapshot = () => [...connections.entries()].map(([transportId, peer]) => {
|
|
10
|
+
const expiresAt = peer.observedAt + policy.inputLimits.descriptorTtlMs;
|
|
11
|
+
return { transportId, participantId: peer.participantId, observedAt: peer.observedAt,
|
|
12
|
+
expiresAt, available: !closed && !!peer.description && now() < expiresAt,
|
|
13
|
+
reason: closed ? 'closed' : !peer.description ? peer.reason || 'unobserved'
|
|
14
|
+
: now() >= expiresAt ? 'expired' : 'observed', description: structuredClone(peer.description) };
|
|
15
|
+
});
|
|
16
|
+
const notify = () => onChange(snapshot());
|
|
17
|
+
const network = createNetwork({ label: 'reploid-partitions-control',
|
|
18
|
+
maxPeers: policy.maxPeers, timeoutMs: policy.connectTimeoutMs,
|
|
19
|
+
createEndpoint: ({ channel, remoteParticipantId }) => {
|
|
20
|
+
const endpoint = createPartitionDataChannel({ channel, localParticipantId: identity.peerId,
|
|
21
|
+
remoteParticipantId, limits: policy.controlChannel,
|
|
22
|
+
authorize: async ({ metadata, byteLength }) => metadata.operation === 'describe' && byteLength === 0,
|
|
23
|
+
serve: async () => structuredClone(describe()) });
|
|
24
|
+
const peer = { endpoint, participantId: remoteParticipantId, description: null, observedAt: 0, revision: 0,
|
|
25
|
+
close: () => endpoint.close() };
|
|
26
|
+
channel.addEventListener('close', () => {
|
|
27
|
+
for (const [id, value] of connections) if (value === peer) connections.delete(id);
|
|
28
|
+
notify();
|
|
29
|
+
}, { once: true });
|
|
30
|
+
return peer;
|
|
31
|
+
},
|
|
32
|
+
onPeer(transportId, peer) { if (!closed) connections.set(transportId, peer); }
|
|
33
|
+
});
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
getSnapshot: snapshot,
|
|
36
|
+
getModels: () => snapshot().filter(peer => peer.available).flatMap(peer => peer.description.models
|
|
37
|
+
.filter(model => peer.description.schema !== 'reploid.partition-capability/v2'
|
|
38
|
+
|| isOrderedPartitionEntryModel(model, peer))),
|
|
39
|
+
invalidateModel(transportId, model) {
|
|
40
|
+
const peer = connections.get(transportId);
|
|
41
|
+
if (!peer || closed) return;
|
|
42
|
+
peer.revision++;
|
|
43
|
+
if (peer.description) peer.description.models = peer.description.models.filter(candidate =>
|
|
44
|
+
candidate.id !== model.id || candidate.identity !== model.identity);
|
|
45
|
+
notify();
|
|
46
|
+
},
|
|
47
|
+
refresh() {
|
|
48
|
+
if (closed) return Promise.resolve(snapshot());
|
|
49
|
+
if (refreshing) return refreshing;
|
|
50
|
+
refreshing = (async () => {
|
|
51
|
+
for (const peer of peers().slice(0, policy.maxPeers)) {
|
|
52
|
+
if (closed) break;
|
|
53
|
+
if (!connections.has(peer.id) && peer.localTransportId < peer.id) {
|
|
54
|
+
try { await network.connect(peer.id); } catch { /* The next bounded refresh may retry. */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await Promise.allSettled([...connections.values()].map(async peer => {
|
|
58
|
+
const revision = peer.revision;
|
|
59
|
+
try {
|
|
60
|
+
const description = await peer.endpoint.request({ operation: 'describe' }, new Uint8Array(), { signal: lifetime.signal });
|
|
61
|
+
if (description?.schema === 'reploid.partition-capability/v2') {
|
|
62
|
+
validateOrderedPartitionCapability(description, peer.participantId, policy.maxStageOffersPerPeer);
|
|
63
|
+
} else assert(description.participantId === peer.participantId && Array.isArray(description.models)
|
|
64
|
+
&& (description.eligibleIndexes === undefined || Array.isArray(description.eligibleIndexes)
|
|
65
|
+
&& description.eligibleIndexes.length > 0 && description.eligibleIndexes.length <= 2
|
|
66
|
+
&& description.eligibleIndexes.every(index => index === 0 || index === 1))
|
|
67
|
+
&& description.models.length <= catalog.length && [null, 0, 1].includes(description.index)
|
|
68
|
+
&& (!description.capacity || Number.isSafeInteger(description.capacity.availableSlots)
|
|
69
|
+
&& description.capacity.availableSlots >= 0 && Number.isSafeInteger(description.capacity.active)
|
|
70
|
+
&& description.capacity.active >= 0 && typeof description.capacity.closed === 'boolean')
|
|
71
|
+
&& ['idle', 'waiting', 'loading', 'ready', 'stopping', 'failed'].includes(description.phase),
|
|
72
|
+
'Partition capability identity or inventory mismatch');
|
|
73
|
+
// A describe already in flight cannot restore a hint rejected by a
|
|
74
|
+
// newer authoritative entry check. The next observation may restore it.
|
|
75
|
+
if (!closed && revision === peer.revision) { peer.description = structuredClone(description); peer.observedAt = now(); peer.reason = null; }
|
|
76
|
+
} catch { if (revision === peer.revision) { peer.description = null; peer.reason = 'description-unavailable'; } }
|
|
77
|
+
}));
|
|
78
|
+
notify(); return snapshot();
|
|
79
|
+
})().finally(() => { refreshing = null; });
|
|
80
|
+
return refreshing;
|
|
81
|
+
},
|
|
82
|
+
close() {
|
|
83
|
+
if (closing) return closing;
|
|
84
|
+
closed = true; lifetime.abort(new Error('Partition discovery closed'));
|
|
85
|
+
closing = (async () => {
|
|
86
|
+
const results = await Promise.allSettled([Promise.resolve().then(async () => {
|
|
87
|
+
if (!networkSettled) { await network.close(); networkSettled = true; }
|
|
88
|
+
}), refreshing]);
|
|
89
|
+
connections.clear();
|
|
90
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
91
|
+
if (failures.length) throw new AggregateError(failures, 'Partition discovery settlement failed');
|
|
92
|
+
})().catch(error => { closing = null; throw error; });
|
|
93
|
+
return closing;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { assertPartition as assert, canonicalPartitionJson, samePartitionIdentity,
|
|
2
|
+
validatePartitionIdentity, canonicalRouteAttemptBinding } from './partition-contract.js';
|
|
3
|
+
import { ORDERED_PUBLICATION_ATTEMPT_SCHEMA, validateOrderedPublicationContext } from './partition-ordered-publication.js';
|
|
4
|
+
|
|
5
|
+
export const ENTRY_PUBLICATION_SCHEMA = 'reploid.partition-publication/v1';
|
|
6
|
+
const same = (a, b) => canonicalPartitionJson(a) === canonicalPartitionJson(b);
|
|
7
|
+
const epoch = value => typeof value === 'string' && /^[a-f0-9]{32}$/.test(value);
|
|
8
|
+
const digest = value => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
9
|
+
const methods = ['head', 'admit', 'publish', 'publish-batch', 'cancel'];
|
|
10
|
+
const orderedInput = request => request?.schema === 'reploid.partition-input/v3';
|
|
11
|
+
const orderedKeys = ['meshId', 'requesterId', 'coordinatorId', 'modelId', 'modelIdentity', 'planId', 'threadId',
|
|
12
|
+
'attemptId', 'placementGeneration'];
|
|
13
|
+
/** The owner interface a coordinator calls; the requester's scope answers it. */
|
|
14
|
+
export function entryPublicationPort(call) {
|
|
15
|
+
assert(typeof call === 'function', 'Publication call port required');
|
|
16
|
+
return Object.freeze({ requiresCommitEvidence: true,
|
|
17
|
+
reconcile: () => call('head', {}), admitAttempt: body => call('admit', body),
|
|
18
|
+
publish: (value, { evidence } = {}) => call('publish', { publication: value, evidence }),
|
|
19
|
+
// Consecutive records committed by the requester in one transaction and projected once.
|
|
20
|
+
publishBatch: async (values, { evidence } = {}) => (await call('publish-batch', { publications: values, evidence }))?.receipts,
|
|
21
|
+
cancel: (attemptId, coordinatorEpoch) => call('cancel', { attemptId, coordinatorEpoch }) });
|
|
22
|
+
}
|
|
23
|
+
export function validateEntryPublicationContext(value, request) {
|
|
24
|
+
assert(value?.schema === 'reploid.partition-publication-context/v1' && epoch(value.publisherEpoch)
|
|
25
|
+
&& epoch(value.coordinatorEpoch) && digest(value.routeId) && digest(value.generation?.generationId)
|
|
26
|
+
&& digest(value.generation.executionIdentity) && value.generation.policy
|
|
27
|
+
&& Number.isSafeInteger(value.generation.policy.maxGeneratedTokens) && value.generation.policy.maxGeneratedTokens >= 0
|
|
28
|
+
&& Number.isSafeInteger(value.vocabSize) && value.vocabSize > 0
|
|
29
|
+
&& Number.isSafeInteger(value.deadlineAt) && value.deadlineAt > 0
|
|
30
|
+
&& Array.isArray(value.stages) && value.stages.length === 2
|
|
31
|
+
&& value.stages.every((stage, i) => stage.participantId === [request.participantA, request.participantB][i]
|
|
32
|
+
&& stage.partitionIndex === i && digest(stage.stageId) && epoch(stage.participantEpoch)
|
|
33
|
+
&& epoch(stage.deviceEpoch) && typeof stage.residentId === 'string' && stage.residentId.length > 0
|
|
34
|
+
&& digest(stage.executionId)), 'Invalid entry publication context');
|
|
35
|
+
return structuredClone(value);
|
|
36
|
+
}
|
|
37
|
+
/** A publication request travels on the already authenticated requester/A channel.
|
|
38
|
+
* It never conveys another conversation's history, a tensor or a raw text delta. */
|
|
39
|
+
export function entryPublicationEnvelope(request, method, body) {
|
|
40
|
+
assert(methods.includes(method), 'Unknown publication method');
|
|
41
|
+
if (orderedInput(request)) {
|
|
42
|
+
canonicalRouteAttemptBinding({ schema: 'reploid.partition-attempt/v2', route: request.route, threadId: request.threadId,
|
|
43
|
+
attemptId: request.attemptId, requesterId: request.requesterId, placementGeneration: request.placementGeneration });
|
|
44
|
+
return { schema: ENTRY_PUBLICATION_SCHEMA, operation: 'publication', method,
|
|
45
|
+
...Object.fromEntries(orderedKeys.map(key => [key, request[key]])), route: structuredClone(request.route),
|
|
46
|
+
publication: validateOrderedPublicationContext(request.publication), body: structuredClone(body) };
|
|
47
|
+
}
|
|
48
|
+
validatePartitionIdentity(request);
|
|
49
|
+
const { meshId, requesterId, participantA, participantB, modelId, modelIdentity, planId,
|
|
50
|
+
threadId, attemptId, placementGeneration, canonical } = request;
|
|
51
|
+
return { schema: ENTRY_PUBLICATION_SCHEMA, operation: 'publication', method, meshId, requesterId,
|
|
52
|
+
participantA, participantB, modelId, modelIdentity, planId, threadId, attemptId, placementGeneration,
|
|
53
|
+
canonical: structuredClone(canonical), body: structuredClone(body) };
|
|
54
|
+
}
|
|
55
|
+
export function matchingEntryPublication(message, request) {
|
|
56
|
+
if (orderedInput(request)) {
|
|
57
|
+
try {
|
|
58
|
+
return request.publication !== undefined && message?.schema === ENTRY_PUBLICATION_SCHEMA && message.operation === 'publication'
|
|
59
|
+
&& methods.includes(message.method) && orderedKeys.every(key => message[key] === request[key])
|
|
60
|
+
&& message.canonical === undefined && message.participantA === undefined && message.participantB === undefined
|
|
61
|
+
&& same(message.route, request.route) && same(message.publication, request.publication);
|
|
62
|
+
} catch { return false; }
|
|
63
|
+
}
|
|
64
|
+
return request?.schema === 'reploid.partition-input/v2' && message.schema === ENTRY_PUBLICATION_SCHEMA
|
|
65
|
+
&& message.operation === 'publication' && methods.includes(message.method)
|
|
66
|
+
&& message.meshId === request.meshId && samePartitionIdentity(message, request)
|
|
67
|
+
&& same(message.canonical, request.canonical);
|
|
68
|
+
}
|
|
69
|
+
/** This port belongs to the requester, not the input executor. The transaction
|
|
70
|
+
* owner validates cursors/signatures and fences uncertain storage outcomes. */
|
|
71
|
+
export function createEntryPublicationScope({ request, owner, authorize, onPublished, maxControlBytes, maxOutputCharacters }) {
|
|
72
|
+
assert(owner?.requiresCommitEvidence === true && ['reconcile', 'admitAttempt', 'publish', 'cancel']
|
|
73
|
+
.every(name => typeof owner[name] === 'function') && typeof authorize === 'function'
|
|
74
|
+
&& typeof onPublished === 'function', 'Requester durable publication and projection ports required');
|
|
75
|
+
assert(Number.isSafeInteger(maxControlBytes) && maxControlBytes > 0
|
|
76
|
+
&& Number.isSafeInteger(maxOutputCharacters) && maxOutputCharacters > 0, 'Publication channel bounds required');
|
|
77
|
+
const copy = value => {
|
|
78
|
+
const json = canonicalPartitionJson(value);
|
|
79
|
+
assert(new TextEncoder().encode(json).byteLength <= maxControlBytes, 'Publication control budget exhausted');
|
|
80
|
+
return JSON.parse(json);
|
|
81
|
+
};
|
|
82
|
+
const selected = copy(request), ordered = orderedInput(selected);
|
|
83
|
+
const context = ordered ? validateOrderedPublicationContext(selected.publication) : validateEntryPublicationContext(selected.canonical, selected);
|
|
84
|
+
let closed = false, admitted = null, admittedPolicy = null, content = '', count = 0, terminal = false;
|
|
85
|
+
const projections = new Map(), pending = new Set();
|
|
86
|
+
let cancellation = null;
|
|
87
|
+
const permit = async method => {
|
|
88
|
+
assert(!closed && await authorize(method) === true && !closed, 'Publication delivery authority closed or revoked');
|
|
89
|
+
};
|
|
90
|
+
// An ordered coordinator chooses its epoch at admission; canonical routes advertise it.
|
|
91
|
+
const coordinatorEpoch = () => ordered ? admitted?.coordinatorEpoch : context.coordinatorEpoch;
|
|
92
|
+
const sequenceLimit = () => ordered ? admittedPolicy.maxTokens : Math.max(1, context.generation.policy.maxGeneratedTokens);
|
|
93
|
+
const identityMatches = identity => ordered
|
|
94
|
+
? identity?.schema === ORDERED_PUBLICATION_ATTEMPT_SCHEMA && identity.attemptId === selected.attemptId
|
|
95
|
+
&& identity.threadId === selected.threadId && identity.requesterId === selected.requesterId
|
|
96
|
+
&& identity.placementGeneration === selected.placementGeneration && identity.publisherEpoch === context.publisherEpoch
|
|
97
|
+
&& epoch(identity.coordinatorEpoch) && same(identity.route, selected.route)
|
|
98
|
+
: identity?.attemptId === selected.attemptId
|
|
99
|
+
&& identity.requesterId === selected.requesterId && identity.publisherEpoch === context.publisherEpoch
|
|
100
|
+
&& identity.coordinatorEpoch === context.coordinatorEpoch && identity.routeId === context.routeId
|
|
101
|
+
&& identity.generationId === context.generation.generationId && identity.routeKind === 'two-stage-layer'
|
|
102
|
+
&& same(identity.stages, context.stages);
|
|
103
|
+
const admissionMatches = value => identityMatches(value.identity) && (ordered
|
|
104
|
+
? value.executionIdentity === value.identity.generationDigest && Number.isSafeInteger(value.vocabSize) && value.vocabSize > 0
|
|
105
|
+
&& Number.isSafeInteger(value.generationPolicy?.maxTokens) && value.generationPolicy.maxTokens > 0
|
|
106
|
+
: same(value.generationPolicy, context.generation.policy)
|
|
107
|
+
&& value.executionIdentity === context.generation.executionIdentity && value.vocabSize === context.vocabSize);
|
|
108
|
+
const call = async (method, body) => {
|
|
109
|
+
assert(methods.includes(method), 'Unknown publication method');
|
|
110
|
+
const value = copy(body); // Snapshot before any asynchronous authorization or storage.
|
|
111
|
+
await permit(method);
|
|
112
|
+
if (method === 'head') {
|
|
113
|
+
assert(same(value, {}), 'Unexpected publication head input');
|
|
114
|
+
const state = await owner.reconcile(); await permit(method);
|
|
115
|
+
return copy({ head: state.head, attempts: state.attempts.filter(item => item.identity.attemptId === selected.attemptId
|
|
116
|
+
&& (ordered || item.identity.coordinatorEpoch === context.coordinatorEpoch)).map(item => ({ identity: item.identity })) });
|
|
117
|
+
}
|
|
118
|
+
if (method === 'admit') {
|
|
119
|
+
assert(admissionMatches(value) && value.deadlineAt === context.deadlineAt && (!admitted || same(admitted, value.identity)),
|
|
120
|
+
'Publication admission differs from approved context');
|
|
121
|
+
const result = await owner.admitAttempt(value); admitted = value.identity; admittedPolicy = value.generationPolicy;
|
|
122
|
+
await permit(method); return copy(result);
|
|
123
|
+
}
|
|
124
|
+
assert(admitted, 'Publication attempt has not been admitted');
|
|
125
|
+
if (method === 'cancel') {
|
|
126
|
+
assert(same(value, { attemptId: selected.attemptId, coordinatorEpoch: coordinatorEpoch() }),
|
|
127
|
+
'Cancellation differs from admitted attempt');
|
|
128
|
+
const result = await owner.cancel(value.attemptId, value.coordinatorEpoch);
|
|
129
|
+
await permit(method); return copy(result);
|
|
130
|
+
}
|
|
131
|
+
const batch = method === 'publish-batch';
|
|
132
|
+
const publications = batch ? value.publications : [value.publication], evidence = batch ? value.evidence : [value.evidence];
|
|
133
|
+
assert(Array.isArray(publications) && publications.length > 0 && Array.isArray(evidence) && evidence.length === publications.length
|
|
134
|
+
&& publications.every((publication, index) => publication?.attemptId === selected.attemptId && publication.coordinatorEpoch === coordinatorEpoch()
|
|
135
|
+
&& Number.isSafeInteger(publication.sequence) && publication.sequence >= 0
|
|
136
|
+
&& publication.sequence < sequenceLimit() && Array.isArray(evidence[index])), 'Publication differs from admitted attempt');
|
|
137
|
+
const receipts = copy(batch ? await owner.publishBatch(publications, { evidence })
|
|
138
|
+
: [await owner.publish(publications[0], { evidence: evidence[0] })]);
|
|
139
|
+
assert(Array.isArray(receipts) && receipts.length === publications.length && receipts.every((receipt, index) =>
|
|
140
|
+
receipt.conversationId === selected.threadId && receipt.writerEpoch === context.publisherEpoch
|
|
141
|
+
&& receipt.attemptId === selected.attemptId && receipt.coordinatorEpoch === coordinatorEpoch()
|
|
142
|
+
&& receipt.sequence === publications[index].sequence && receipt.publicationDigest === publications[index].publicationDigest),
|
|
143
|
+
'Durable publication receipt identity mismatch');
|
|
144
|
+
await permit(method);
|
|
145
|
+
// Duplicate delivery reuses the same projection promise. A failed projection
|
|
146
|
+
// cannot be acknowledged or regenerated; the host reconciles saved history.
|
|
147
|
+
// A batch is projected once, from its last durable record.
|
|
148
|
+
const last = receipts.length - 1;
|
|
149
|
+
let projected = projections.get(receipts[last].publicationDigest);
|
|
150
|
+
if (!projected) {
|
|
151
|
+
const fresh = publications.filter(publication => publication.sequence >= count);
|
|
152
|
+
const text = fresh.map(publication => publication.delta?.text ?? '').join('');
|
|
153
|
+
assert(fresh.every(publication => typeof (publication.delta?.text ?? '') === 'string')
|
|
154
|
+
&& content.length + text.length <= maxOutputCharacters, 'Published output exceeds entry budget');
|
|
155
|
+
assert(fresh.length > 0 && fresh[0].sequence === count, 'Published projection sequence mismatch');
|
|
156
|
+
const publication = publications[last];
|
|
157
|
+
projected = Promise.resolve().then(async () => {
|
|
158
|
+
await permit(method);
|
|
159
|
+
await onPublished(copy({ publication, receipt: receipts[last], evidence: evidence[last] }));
|
|
160
|
+
content += text; count += fresh.length;
|
|
161
|
+
terminal = publication.schema === 'doppler.admission-publication/v1' || publication.successor?.terminal === true;
|
|
162
|
+
});
|
|
163
|
+
projections.set(receipts[last].publicationDigest, projected);
|
|
164
|
+
}
|
|
165
|
+
await projected; await permit(method); return batch ? { receipts } : receipts[0];
|
|
166
|
+
};
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
async close({ cancel = false } = {}) {
|
|
169
|
+
closed = true;
|
|
170
|
+
await Promise.allSettled([...pending]);
|
|
171
|
+
if (cancel && admitted) {
|
|
172
|
+
if (!cancellation) cancellation = Promise.resolve().then(() => owner.cancel(selected.attemptId, coordinatorEpoch()));
|
|
173
|
+
try { await cancellation; } catch (error) { cancellation = null; throw error; }
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
getProjection() { return { content, count, terminal }; },
|
|
177
|
+
call(method, body) {
|
|
178
|
+
const operation = call(method, body); pending.add(operation);
|
|
179
|
+
operation.then(() => pending.delete(operation), () => pending.delete(operation));
|
|
180
|
+
return operation;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|