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.
Files changed (233) hide show
  1. package/apps/poolday/config/chat-files.json +27 -0
  2. package/apps/poolday/config/partition-generation.js +21 -0
  3. package/apps/poolday/config/partition-policy.json +74 -0
  4. package/apps/poolday/config/partition-releases.json +3 -0
  5. package/apps/poolday/config/work-profile.json +122 -0
  6. package/apps/poolday/host/chat-view.js +47 -0
  7. package/apps/poolday/host/earning/client.js +199 -0
  8. package/apps/poolday/host/earning/contribution.js +168 -0
  9. package/apps/poolday/host/partition-release.js +33 -0
  10. package/apps/poolday/host/work-partition-artifact-source.js +37 -0
  11. package/apps/poolday/host/work-partition-capsule.js +39 -0
  12. package/apps/poolday/host/work-partition-plan.js +167 -0
  13. package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
  14. package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
  15. package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
  16. package/apps/poolday/pool/pack-operation.js +161 -0
  17. package/apps/poolday/pool/pack-release-policy.js +44 -0
  18. package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
  19. package/apps/poolday/providers/work-adapter.js +67 -0
  20. package/apps/poolday/providers/work-device.js +33 -0
  21. package/apps/poolday/providers/work-generation-completion.js +55 -0
  22. package/apps/poolday/providers/work-provider.js +209 -0
  23. package/apps/poolday/providers/work-resident-provider.js +113 -0
  24. package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
  25. package/docs/fleet-service.md +214 -0
  26. package/docs/native-participant.md +534 -0
  27. package/native-delivery.json +1174 -0
  28. package/package.json +29 -4
  29. package/packages/reploid/src/artifacts/binary.js +25 -0
  30. package/packages/reploid/src/artifacts/canonical-json.js +23 -0
  31. package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
  32. package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
  33. package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
  34. package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
  35. package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
  36. package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
  37. package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
  38. package/packages/reploid/src/artifacts/identity.js +91 -0
  39. package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
  40. package/packages/reploid/src/artifacts/receipt.js +131 -0
  41. package/packages/reploid/src/artifacts/signatures.js +94 -0
  42. package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
  43. package/packages/reploid/src/cancellation.js +44 -0
  44. package/packages/reploid/src/config/defaults.json +110 -0
  45. package/packages/reploid/src/config/index.js +209 -0
  46. package/packages/reploid/src/config/request-overrides.json +9 -0
  47. package/packages/reploid/src/config/transport-defaults.json +7 -0
  48. package/packages/reploid/src/hashing.js +30 -0
  49. package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
  50. package/packages/reploid/src/measurement/index.js +5 -0
  51. package/packages/reploid/src/measurement/readiness-records.js +310 -0
  52. package/packages/reploid/src/mesh/computer-identity.js +50 -0
  53. package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
  54. package/packages/reploid/src/mesh/contribution.js +141 -0
  55. package/packages/reploid/src/mesh/generation-recovery.js +58 -0
  56. package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
  57. package/packages/reploid/src/mesh/gpu-signature.js +136 -0
  58. package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
  59. package/packages/reploid/src/mesh/index.js +66 -0
  60. package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
  61. package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
  62. package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
  63. package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
  64. package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
  65. package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
  66. package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
  67. package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
  68. package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
  69. package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
  70. package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
  71. package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
  72. package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
  73. package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
  74. package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
  75. package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
  76. package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
  77. package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
  78. package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
  79. package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
  80. package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
  81. package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
  82. package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
  83. package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
  84. package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
  85. package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
  86. package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
  87. package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
  88. package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
  89. package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
  90. package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
  91. package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
  92. package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
  93. package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
  94. package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
  95. package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
  96. package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
  97. package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
  98. package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
  99. package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
  100. package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
  101. package/packages/reploid/src/mesh/peer-generation.js +896 -0
  102. package/packages/reploid/src/mesh/peer-identity.js +87 -0
  103. package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
  104. package/packages/reploid/src/mesh/preparation-progress.js +46 -0
  105. package/packages/reploid/src/mesh/provider-hardware.js +11 -0
  106. package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
  107. package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
  108. package/packages/reploid/src/mesh/web-access-network.js +112 -0
  109. package/packages/reploid/src/mesh/web-access-policy.json +17 -0
  110. package/packages/reploid/src/retrieval/citations.js +156 -0
  111. package/packages/reploid/src/retrieval/claim-support.js +113 -0
  112. package/packages/reploid/src/retrieval/costs.js +101 -0
  113. package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
  114. package/packages/reploid/src/retrieval/index.js +20 -0
  115. package/packages/reploid/src/retrieval/lexical-support.js +27 -0
  116. package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
  117. package/packages/reploid/src/retrieval/page-fetch.json +10 -0
  118. package/packages/reploid/src/retrieval/page-text.js +182 -0
  119. package/packages/reploid/src/retrieval/passage.js +37 -0
  120. package/packages/reploid/src/retrieval/policy.json +14 -0
  121. package/packages/reploid/src/retrieval/retrieval.js +23 -0
  122. package/packages/reploid/src/retrieval/review-queue.js +76 -0
  123. package/packages/reploid/src/retrieval/sources.js +263 -0
  124. package/packages/reploid/src/retrieval/support-review.js +155 -0
  125. package/packages/reploid/src/retrieval/text.js +21 -0
  126. package/packages/reploid/src/rules/contribution.rules.json +8 -0
  127. package/packages/reploid/src/rules/routing.rules.json +24 -0
  128. package/packages/reploid/src/transport/assignment.js +571 -0
  129. package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
  130. package/packages/reploid/src/transport/index.js +11 -0
  131. package/packages/reploid/src/transport/introduction-admission.js +24 -0
  132. package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
  133. package/packages/reploid/src/transport/introduction-policy.json +12 -0
  134. package/packages/reploid/src/transport/introduction-records.js +90 -0
  135. package/packages/reploid/src/transport/introductions.js +209 -0
  136. package/packages/reploid/src/transport/partition-data-channel.js +421 -0
  137. package/packages/reploid/src/transport/partition-errors.json +16 -0
  138. package/packages/reploid/src/transport/retry-policy.js +36 -0
  139. package/packages/reploid/src/transport/room.js +502 -0
  140. package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
  141. package/packages/reploid/src/transport/signaling.js +571 -0
  142. package/packages/reploid/src/transport/swarm.js +1247 -0
  143. package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
  144. package/server/browser-roots.js +42 -0
  145. package/server/browser-web-worker.js +94 -0
  146. package/server/chat-completions.js +45 -0
  147. package/server/fleet/browser-bridge.js +145 -0
  148. package/server/fleet/contract.js +42 -0
  149. package/server/fleet/credit-evidence.js +103 -0
  150. package/server/fleet/credit-recovery.js +175 -0
  151. package/server/fleet/credits.js +178 -0
  152. package/server/fleet/execution-admission.js +32 -0
  153. package/server/fleet/native-host.js +817 -0
  154. package/server/fleet/placement.js +132 -0
  155. package/server/fleet/requester.js +346 -0
  156. package/server/fleet/service.js +311 -0
  157. package/server/fleet/terminal-policy.js +83 -0
  158. package/server/fleet/text-api.js +94 -0
  159. package/server/hashing.js +35 -0
  160. package/server/http-request-owner.js +49 -0
  161. package/server/messages.js +57 -0
  162. package/server/native-canonical-epochs.js +90 -0
  163. package/server/native-change-log.js +40 -0
  164. package/server/native-earning.js +203 -0
  165. package/server/native-generation-journal.js +46 -0
  166. package/server/native-model-config.js +57 -0
  167. package/server/native-model-files.js +382 -0
  168. package/server/native-model-origin.js +54 -0
  169. package/server/native-model-origins.json +12 -0
  170. package/server/native-node.js +83 -0
  171. package/server/native-owner/controls.js +162 -0
  172. package/server/native-owner/monitor.js +251 -0
  173. package/server/native-owner/policy.js +273 -0
  174. package/server/native-owner/rate.js +92 -0
  175. package/server/native-participant.js +866 -0
  176. package/server/native-readiness-log.js +30 -0
  177. package/server/native-release-storage.js +64 -0
  178. package/server/native-shader-cache.js +369 -0
  179. package/server/native-supply-budget.js +66 -0
  180. package/server/native-whole-earning.js +67 -0
  181. package/server/native-whole-model-fill.js +182 -0
  182. package/server/native-whole-model.js +130 -0
  183. package/server/public-web-worker.js +66 -0
  184. package/server/responses.js +92 -0
  185. package/server/search/errors.js +47 -0
  186. package/server/search/page-fetch.js +185 -0
  187. package/server/text-api.js +83 -0
  188. package/server/text-recovery.js +45 -0
  189. package/server/text-request-options.js +23 -0
  190. package/server/text-requester.js +231 -0
  191. package/server/token-auth.js +9 -0
  192. package/server/whole-model-requester.js +99 -0
  193. package/shared/config/chat-models.json +4685 -0
  194. package/shared/config/doppler-execution-contracts.js +32 -0
  195. package/shared/config/doppler-execution-contracts.json +43 -0
  196. package/shared/config/doppler-generation-contract.js +223 -0
  197. package/shared/config/doppler-local-models.js +79 -0
  198. package/shared/config/doppler-package.json +7 -0
  199. package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
  200. package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
  201. package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
  202. package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
  203. package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
  204. package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
  205. package/shared/config/reploid-library.json +49 -0
  206. package/shared/config/swarm-bootstrap.json +21 -0
  207. package/shared/infrastructure/doppler-runtime-service.js +261 -0
  208. package/shared/pool/executable-pack.js +87 -0
  209. package/shared/pool/pack-operation-adapters.js +124 -0
  210. package/shared/pool/pack-operation-policy.js +75 -0
  211. package/shared/pool/pool-config.json +2280 -0
  212. package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
  213. package/shared/vendor/reploid/artifacts/binary.js +25 -0
  214. package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
  215. package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
  216. package/shared/vendor/reploid/artifacts/identity.js +91 -0
  217. package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
  218. package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
  219. package/shared/vendor/reploid/artifacts/signatures.js +94 -0
  220. package/shared/vendor/reploid/config/defaults.json +110 -0
  221. package/shared/vendor/reploid/config/index.js +209 -0
  222. package/shared/vendor/reploid/config/request-overrides.json +9 -0
  223. package/shared/vendor/reploid/hashing.js +30 -0
  224. package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
  225. package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
  226. package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
  227. package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
  228. package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
  229. package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
  230. package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
  231. package/shared/vendor/reploid/rules/routing.rules.json +24 -0
  232. package/shared/vendor/reploid/transport/partition-errors.json +16 -0
  233. package/README.md +0 -3
@@ -0,0 +1,319 @@
1
+ import { createOrderedPartitionExecution } from './partition-route-execution.js';
2
+ import { executeCanonicalPartition } from './partition-canonical-execution.js';
3
+ import failureMessages from '../../transport/partition-errors.json' with { type: 'json' };
4
+ /** Coordinates one Doppler token step through both partitions. No model math lives here. */
5
+ import { partitionFingerprint, partitionActivationContract, matchingPartitionActivation } from './partition-contract.js';
6
+ const assert = (ok, message) => { if (!ok) throw new Error(message); };
7
+ const identifier = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
8
+ const positive = value => Number.isSafeInteger(value) && value > 0;
9
+ const snapshot = value => structuredClone(value);
10
+
11
+ function freezeRecord(value) {
12
+ for (const child of Object.values(value)) {
13
+ if (child && typeof child === 'object') freezeRecord(child);
14
+ }
15
+ return Object.freeze(value);
16
+ }
17
+
18
+ function checkTokens(tokens, vocabSize) {
19
+ assert(Array.isArray(tokens) && tokens.length > 0
20
+ && tokens.every(token => Number.isSafeInteger(token) && token >= 0 && token < vocabSize),
21
+ 'tokenIds must contain valid Doppler vocabulary IDs');
22
+ }
23
+
24
+ function sameBinding(actual, expected) {
25
+ return actual && Object.entries(expected).every(([key, value]) => actual[key] === value);
26
+ }
27
+
28
+ /**
29
+ * Device ports are authenticated local/remote Doppler session facades. Each call
30
+ * performs exactly one prefill/decode step, never an independent generation.
31
+ * The host authorizer verifies grants against live identities/revocation state.
32
+ * This coordinator cannot verify physical execution or manufacture that proof.
33
+ */
34
+ export function createLayerPartitionRunner({ runtime, plan: suppliedPlan, deviceA, deviceB,
35
+ transport, authorize, limits, canonical = false, route, stages }) {
36
+ if (route !== undefined || stages !== undefined) {
37
+ return createOrderedPartitionExecution({ runtime, plan: suppliedPlan, route, stages, transport, authorize, limits, canonical });
38
+ }
39
+ assert(runtime && typeof runtime.LAYER_PARTITION_SCHEMA === 'string'
40
+ && typeof runtime.ACTIVATION_TENSOR_SCHEMA === 'string'
41
+ && typeof runtime.serializeActivationFrame === 'function'
42
+ && typeof runtime.deserializeActivationFrame === 'function', 'Doppler partition runtime required');
43
+ assert(typeof canonical === 'boolean', 'Invalid canonical runner capability');
44
+ if (canonical) {
45
+ for (const name of ['canonicalGenerationIdentity', 'admitResidentPrompt', 'validateResidentAttemptIdentity',
46
+ 'metadataDigest', 'tokenDigest', 'validateExecutionPublication', 'hashLayerPartitionPlan']) {
47
+ assert(typeof runtime[name] === 'function', 'Canonical Doppler runtime capability required');
48
+ }
49
+ for (const [device, methods] of [[deviceA, ['reserve', 'admitCanonicalPrompt', 'executeCanonicalGroup0', 'commitCanonicalTransition']],
50
+ [deviceB, ['reserveCanonical', 'admitCanonicalPrompt', 'executeCanonicalFrame', 'acknowledgeCanonicalPublication']]]) {
51
+ for (const method of [...methods, 'getCanonicalStage', 'getCanonicalGeneration']) {
52
+ assert(typeof device?.[method] === 'function', 'Canonical participant capability required');
53
+ }
54
+ }
55
+ }
56
+ assert(suppliedPlan && typeof suppliedPlan === 'object', 'Valid Doppler layer partition plan required');
57
+ const plan = freezeRecord(snapshot(suppliedPlan));
58
+ const activationContract = partitionActivationContract(runtime, plan);
59
+ assert(identifier(plan.modelId) && positive(plan.vocabSize) && positive(plan.hiddenSize) && positive(plan.totalLayers)
60
+ && positive(plan.splitLayer) && plan.splitLayer < plan.totalLayers
61
+ && ['f16', 'f32'].includes(plan.activationDtype), 'Invalid Doppler partition dimensions');
62
+ assert(typeof authorize === 'function', 'A verifying host authorization port is required');
63
+ for (const [device, method] of [[deviceA, 'executeGroup0'], [deviceB, typeof deviceB?.executeFrame === 'function' ? 'executeFrame' : 'executeGroup1']]) {
64
+ assert(identifier(device?.id) && typeof device[method] === 'function'
65
+ && typeof device.closeAttempt === 'function', 'Partition device identity, execution and settlement ports required');
66
+ }
67
+ assert(deviceA.id !== deviceB.id, 'Partition participants must be distinct');
68
+ assert(typeof deviceB.executeFrame === 'function' || typeof transport?.transferActivation === 'function', 'Activation transport required');
69
+ assert(limits && ['maxTokens', 'maxPromptTokens', 'maxActivationBytes', 'maxOutputCharacters', 'maxAttempts', 'maxConcurrentAttempts']
70
+ .every(key => positive(limits[key])), 'Explicit positive partition allocation limits required');
71
+ const policy = Object.freeze({ ...limits });
72
+ const participants = Object.freeze([deviceA.id, deviceB.id]);
73
+ const usedAttempts = new Set();
74
+ const active = new Map();
75
+ const receipts = new Map();
76
+ let closed = false;
77
+ // FIFO token leases let two conversations share resident weights fairly.
78
+ let tail = Promise.resolve();
79
+ const lease = async task => {
80
+ const previous = tail;
81
+ let release;
82
+ tail = new Promise(resolve => { release = resolve; });
83
+ await previous;
84
+ try { return await task(); } finally { release(); }
85
+ };
86
+
87
+ async function execute({ tokenIds, generation, identity, grants, maxTokens, signal, onDelta = () => {}, canonical: canonicalRequest }) {
88
+ assert(!closed, 'Partition runner is closed');
89
+ signal?.throwIfAborted();
90
+ assert(!canonical || canonicalRequest, 'Canonical runner requires its explicit execution contract');
91
+ checkTokens(tokenIds, plan.vocabSize);
92
+ assert(tokenIds.length <= policy.maxPromptTokens, 'Prompt exceeds partition allocation');
93
+ assert((canonicalRequest ? canonical && Number.isSafeInteger(maxTokens) && maxTokens >= 0 : positive(maxTokens)) && maxTokens <= policy.maxTokens, 'Invalid partition output allocation');
94
+ assert(generation && typeof generation === 'object' && !Array.isArray(generation)
95
+ && (canonicalRequest ? canonicalRequest.generation.policy.maxGeneratedTokens === maxTokens : generation.maxTokens === maxTokens), 'Resolved Doppler generation settings must match the effective output limit');
96
+ assert(identity && ['modelIdentity', 'planId', 'threadId', 'attemptId'].every(key => identifier(identity[key])),
97
+ 'Exact model, plan, thread and attempt identities required');
98
+ assert(/^sha256:[a-f0-9]{64}$/.test(identity.modelIdentity), 'Pinned model digest required');
99
+ assert(grants?.executionA && grants?.executionB && grants?.activation && grants?.tokenContext && grants?.output,
100
+ 'Execution, activation, token-context and output grants are required');
101
+ assert(typeof onDelta === 'function', 'onDelta must be a function');
102
+ const binding = Object.freeze({ modelId: plan.modelId, modelIdentity: identity.modelIdentity,
103
+ planId: identity.planId, threadId: identity.threadId, attemptId: identity.attemptId,
104
+ participantA: participants[0], participantB: participants[1],
105
+ ...(identity.requesterId ? { requesterId: identity.requesterId, placementGeneration: identity.placementGeneration } : {}) });
106
+ const authority = freezeRecord(snapshot(grants));
107
+ const settings = freezeRecord(snapshot(generation));
108
+ const inputTokens = [...tokenIds];
109
+ assert(!usedAttempts.has(binding.attemptId), 'Attempt already used; resume requires a supported checkpoint');
110
+ assert(usedAttempts.size < policy.maxAttempts, 'Partition runner attempt budget exhausted');
111
+ assert(active.size < policy.maxConcurrentAttempts, 'Partition concurrency budget exhausted');
112
+ usedAttempts.add(binding.attemptId);
113
+ const controller = new AbortController();
114
+ const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
115
+ let settle;
116
+ const settled = new Promise(resolve => { settle = resolve; });
117
+ active.set(binding.attemptId, { controller, settled });
118
+ const receipt = { identity: binding, phase: 'executing', startedAt: Date.now(), failure: null,
119
+ reservations: [], cleanupStartedAt: null, cleanupSettledAt: null, cleanup: [] };
120
+ receipts.set(binding.attemptId, receipt);
121
+ const generationDigestPromise = canonicalRequest ? null : partitionFingerprint(settings);
122
+ let input = inputTokens, continuationA = null, continuationB = null;
123
+ let content = '', activationBytes = 0, position = 0, logits = null;
124
+ let stopReason = 'length', failure = null;
125
+ const canonicalOwners = new Set();
126
+ const outputTokens = [];
127
+ const steps = [];
128
+ const permit = async (action, grant, step) => {
129
+ combined.throwIfAborted();
130
+ assert(await authorize({ action, grant, plan, ...binding, ...step, signal: combined }) === true,
131
+ 'Partition authorization declined: ' + action);
132
+ combined.throwIfAborted();
133
+ };
134
+ try {
135
+ if (canonicalRequest) return await executeCanonicalPartition({ runtime, plan, deviceA, deviceB, policy, binding, authority,
136
+ canonical: canonicalRequest, tokenIds: inputTokens, signal: combined, lease, permit, onDelta, receipt,
137
+ ownDevice: id => canonicalOwners.add(id) });
138
+ if (deviceA.reserve) receipt.reservations.push(snapshot(await deviceA.reserve(binding)));
139
+ if (deviceB.reserve) {
140
+ const generationDigest = await generationDigestPromise;
141
+ combined.throwIfAborted();
142
+ const step = { step: 0, tokenPosition: 0, inputTokenCount: inputTokens.length, maxTokens, generationDigest };
143
+ await permit('mesh.execute_partition_b', authority.executionB, step);
144
+ await permit('mesh.transfer_token_context', authority.tokenContext, step);
145
+ const admission = await deviceB.reserve({ identity: binding, grant: authority.executionB, generation: settings,
146
+ generationDigest, inputTokenCount: inputTokens.length, signal: combined });
147
+ receipt.reservations.push(snapshot(admission.reservation));
148
+ }
149
+ for (let index = 0; index < maxTokens; index++) {
150
+ const queuedAt = performance.now();
151
+ const delivery = await lease(async () => {
152
+ const leaseWaitMs = performance.now() - queuedAt;
153
+ combined.throwIfAborted();
154
+ const generationDigest = await generationDigestPromise;
155
+ combined.throwIfAborted();
156
+ const step = Object.freeze({ step: index, tokenPosition: position, inputTokenCount: input.length,
157
+ maxTokens, generationDigest });
158
+ const started = performance.now();
159
+ // Both execution recipients must be eligible before exposing any input.
160
+ await permit('mesh.execute_partition_a', authority.executionA, step);
161
+ await permit('mesh.execute_partition_b', authority.executionB, step);
162
+ await permit('mesh.transfer_intermediate_activation', authority.activation, step);
163
+ await permit('mesh.transfer_token_context', authority.tokenContext, step);
164
+ await permit('mesh.transfer_partition_output', authority.output, step);
165
+ const localStarted = performance.now();
166
+ const resultA = snapshot(await deviceA.executeGroup0({ tokenIds: [...input], continuation: continuationA,
167
+ generation: settings, identity: binding, executionGrant: authority.executionA, ...step, signal: combined }));
168
+ const localStepMs = performance.now() - localStarted;
169
+ combined.throwIfAborted();
170
+ const tensor = resultA?.activationTensor;
171
+ assert(matchingPartitionActivation(tensor, activationContract) && tensor.shape?.length === 3
172
+ && tensor.shape[0] === 1 && tensor.shape[1] === input.length && tensor.shape[2] === plan.hiddenSize
173
+ && tensor.seqOffset === position && tensor.step === index, 'Partition A activation contract mismatch');
174
+ assert(tensor.data?.byteLength <= policy.maxActivationBytes, 'Activation exceeds transfer allocation');
175
+ const metadata = { ...binding, ...step, from: participants[0], to: participants[1] };
176
+ const serializationStarted = performance.now();
177
+ const frame = runtime.serializeActivationFrame({ ...tensor, metadata });
178
+ assert(frame.schema === activationContract.schema && matchingPartitionActivation(frame, activationContract, true),
179
+ 'Serialized activation contract mismatch');
180
+ runtime.validateActivationFrameForPlan?.(frame, plan);
181
+ assert(frame.byteLength <= policy.maxActivationBytes, 'Activation exceeds transfer allocation');
182
+ // Retain bytes separately: a transport cannot mutate the comparison baseline.
183
+ const sent = snapshot(frame);
184
+ const serializationMs = performance.now() - serializationStarted;
185
+ await permit('mesh.transfer_intermediate_activation', authority.activation, step);
186
+ const transferStarted = performance.now();
187
+ let resultB, transferMs = null, remoteStepMs = null;
188
+ if (typeof deviceB.executeFrame === 'function') {
189
+ await permit('mesh.execute_partition_b', authority.executionB, step);
190
+ resultB = snapshot(await deviceB.executeFrame({ frame, continuation: continuationB,
191
+ inputTokenIds: [...input], generation: settings, identity: binding,
192
+ executionGrant: authority.executionB, outputGrant: authority.output,
193
+ ...step, signal: combined }));
194
+ // Includes remote compute and disclosure; never label this as network-only latency.
195
+ remoteStepMs = performance.now() - transferStarted;
196
+ } else {
197
+ const received = snapshot(await transport.transferActivation(frame, { identity: binding, ...step, signal: combined }));
198
+ combined.throwIfAborted();
199
+ transferMs = performance.now() - transferStarted;
200
+ assert(received?.schema === activationContract.schema && matchingPartitionActivation(received, activationContract, true)
201
+ && sameBinding(received.metadata, metadata) && received.seqOffset === position
202
+ && received.step === index && received.dtype === sent.dtype
203
+ && JSON.stringify(received.shape) === JSON.stringify(sent.shape)
204
+ && received.byteLength === sent.byteLength && received.buffer instanceof ArrayBuffer
205
+ && received.buffer.byteLength === sent.byteLength, 'Received activation identity or shape mismatch');
206
+ const before = new Uint8Array(sent.buffer), after = new Uint8Array(received.buffer);
207
+ assert(before.every((byte, offset) => byte === after[offset]), 'Received activation bytes differ');
208
+ await permit('mesh.execute_partition_b', authority.executionB, step);
209
+ const activation = runtime.validateActivationFrameForPlan
210
+ ? runtime.validateActivationFrameForPlan(received, plan) : runtime.deserializeActivationFrame(received);
211
+ resultB = snapshot(await deviceB.executeGroup1({ activation,
212
+ inputTokenIds: [...input], generation: settings, continuation: continuationB,
213
+ identity: binding, executionGrant: authority.executionB,
214
+ outputGrant: authority.output, ...step, signal: combined }));
215
+ }
216
+ combined.throwIfAborted();
217
+ // B owns sampling, decoding and stop semantics. It must return ONE selected token.
218
+ assert(resultB && sameBinding(resultB.identity, binding) && resultB.step === index
219
+ && resultB.tokenPosition === position, 'Partition B response identity mismatch');
220
+ assert(Number.isSafeInteger(resultB.tokenId) && resultB.tokenId >= 0
221
+ && resultB.tokenId < plan.vocabSize && typeof resultB.done === 'boolean'
222
+ && typeof resultB.delta === 'string', 'Partition B must return exactly one selected token');
223
+ assert(!('tokenIds' in resultB) && !('content' in resultB), 'Independent generation from B is forbidden');
224
+ assert(content.length + resultB.delta.length <= policy.maxOutputCharacters, 'Partition output exceeds allocation');
225
+ if (resultB.done) assert(identifier(resultB.stopReason), 'Doppler stopping reason required');
226
+ assert(index + 1 < maxTokens || resultB.done, 'Doppler must finalize decoding at the request token limit');
227
+ await permit('mesh.transfer_partition_output', authority.output, step);
228
+ continuationA = resultA.continuation;
229
+ continuationB = resultB.continuation;
230
+ logits = resultB.logits == null ? null : snapshot(resultB.logits);
231
+ outputTokens.push(resultB.tokenId);
232
+ content += resultB.delta;
233
+ activationBytes += sent.byteLength;
234
+ steps.push({ ...step, tokenId: resultB.tokenId, activationBytes: sent.byteLength,
235
+ leaseWaitMs, outputDeliveryMs: 0,
236
+ localStepMs, serializationMs, computationA: resultA.metrics ?? null, computationB: resultB.metrics ?? null,
237
+ transferMs, remoteStepMs, transportTiming: resultB.transportTiming ?? null,
238
+ elapsedMs: performance.now() - started });
239
+ combined.throwIfAborted();
240
+ position += input.length;
241
+ input = [resultB.tokenId];
242
+ if (resultB.done) stopReason = resultB.stopReason;
243
+ return { delta: resultB.delta, terminal: resultB.done };
244
+ });
245
+ // Per-attempt backpressure must not hold another conversation's GPU lease.
246
+ combined.throwIfAborted();
247
+ if (delivery.delta) {
248
+ const deliveryStarted = performance.now();
249
+ try { await onDelta(delivery.delta); }
250
+ finally { steps.at(-1).outputDeliveryMs = performance.now() - deliveryStarted; }
251
+ }
252
+ combined.throwIfAborted();
253
+ if (delivery.terminal) break;
254
+ }
255
+ return { content, tokenIds: outputTokens, logits, stopReason,
256
+ execution: { schema: 'reploid.mesh.partition-execution/v2', ...binding,
257
+ placement: 'two-device-layer-partition', splitLayer: plan.splitLayer,
258
+ activationBytes, steps, stopReason, settlement: receipt } };
259
+ } catch (error) {
260
+ failure = error;
261
+ receipt.failure = String(error?.message || error);
262
+ throw error;
263
+ } finally {
264
+ // Settle both owned attempts even when cancellation arrives during a port call.
265
+ // Shared resident weights remain owned by the host, not this conversation.
266
+ receipt.phase = 'settling'; receipt.cleanupStartedAt = Date.now();
267
+ const cleanupDevices = canonicalRequest ? [deviceA, deviceB].filter(device => canonicalOwners.has(device.id)) : [deviceA, deviceB];
268
+ const results = await Promise.allSettled(cleanupDevices.map(async device => {
269
+ try {
270
+ await device.closeAttempt({ identity: binding });
271
+ receipt.cleanup.push({ participantId: device.id, status: 'settled', settledAt: Date.now(), error: null });
272
+ } catch (cause) {
273
+ receipt.cleanup.push({ participantId: device.id, status: 'failed', settledAt: null,
274
+ error: String(cause?.message || cause) });
275
+ throw cause;
276
+ }
277
+ }));
278
+ active.delete(binding.attemptId);
279
+ settle();
280
+ const cleanupErrors = results.filter(result => result.status === 'rejected').map(result => result.reason);
281
+ receipt.cleanupSettledAt = cleanupErrors.length ? null : Date.now();
282
+ receipt.phase = cleanupErrors.length ? 'cleanup-failed' : combined.aborted ? 'cancelled' : failure ? 'failed' : 'completed';
283
+ if (cleanupErrors.length) throw Object.assign(
284
+ new AggregateError(failure ? [failure, ...cleanupErrors] : cleanupErrors, failureMessages.CLEANUP_FAILED),
285
+ { code: 'CLEANUP_FAILED' });
286
+ }
287
+ }
288
+
289
+ return Object.freeze({ plan, execute,
290
+ executeCanonical(request) {
291
+ assert(canonical && request?.canonical, 'Canonical runner was not explicitly enabled');
292
+ return execute({ ...request, generation: request.canonical.generation.policy,
293
+ maxTokens: request.canonical.generation.policy.maxGeneratedTokens });
294
+ },
295
+ getState: () => ({ closed, active: active.size, attempts: snapshot([...receipts.values()]) }),
296
+ async close() {
297
+ closed = true;
298
+ const pending = [...active.values()];
299
+ for (const item of pending) item.controller.abort(new Error('Partition runner closed'));
300
+ await Promise.all(pending.map(item => item.settled));
301
+ }
302
+ });
303
+ }
304
+
305
+ /** Final-logit comparison only. Full acceptance additionally compares every boundary/token. */
306
+ export async function verifySplitParity({ runtime, splitRunner, referenceRunner, tokenIds, generation,
307
+ identity, grants, maxTokens, tolerance }) {
308
+ assert(typeof runtime?.comparePartitionExecution === 'function', 'Doppler comparison runtime required');
309
+ assert(Number.isFinite(tolerance) && tolerance >= 0, 'Explicit numerical tolerance required');
310
+ const splitResult = await splitRunner.execute({ tokenIds, generation, identity, grants, maxTokens });
311
+ const refResult = await referenceRunner.execute({ tokenIds, maxTokens });
312
+ assert(splitResult.logits?.length > 0 && refResult.logits?.length > 0
313
+ && [...splitResult.logits, ...refResult.logits].every(Number.isFinite), 'Finite non-empty logits required');
314
+ const comparison = runtime.comparePartitionExecution({ splitOutput: splitResult.logits,
315
+ referenceOutput: refResult.logits, tolerance });
316
+ const tokensMatch = JSON.stringify(splitResult.tokenIds) === JSON.stringify(refResult.tokenIds);
317
+ return { ...comparison, matches: comparison.matches && tokensMatch, tokensMatch,
318
+ splitResult, refResult, comparison };
319
+ }
@@ -0,0 +1,109 @@
1
+ import { assertPartition as assert, validateOrderedPartitionCapability } from './partition-contract.js';
2
+
3
+ /** A contributor's own claim while it chooses or prepares a stage. A hint, never an assignment. */
4
+ export const STAGE_ASSIGNMENT_SCHEMA = 'reploid.stage-assignment/v1';
5
+ /** A ranking coordinator's measured decode cost per stage of its current route. A hint, never an assignment. */
6
+ export const STAGE_COSTS_SCHEMA = 'reploid.stage-costs/v1';
7
+
8
+ const matchesPlan = (model, planId) => typeof model.partitionPlanId === 'string' && model.partitionPlanId === planId;
9
+
10
+ const observedPeers = (capabilities, now, maxOffersPerPeer) => capabilities.filter(peer => {
11
+ if (peer?.available !== true || !(peer.expiresAt > now)) return false;
12
+ try { validateOrderedPartitionCapability(peer.description, peer.participantId, maxOffersPerPeer); return true; } catch { return false; }
13
+ });
14
+
15
+ /** Opt-in replica demand from observed discovery: a stage whose advertised queue exceeds `queueBound`,
16
+ * else the stage a ranking coordinator measured at least `bottleneckRatio` times slower than every
17
+ * other stage. Returns the one stage that would gain most from another replica, or none. */
18
+ export function stageDemand({ model, stageCount, capabilities, now, maxOffersPerPeer, queueBound, bottleneckRatio }) {
19
+ assert(Number.isSafeInteger(queueBound) && queueBound >= 0 && typeof bottleneckRatio === 'number' && bottleneckRatio > 1,
20
+ 'Explicit replica demand bounds required');
21
+ const peers = observedPeers(capabilities, now, maxOffersPerPeer), queued = Array(stageCount).fill(0);
22
+ for (const peer of peers) {
23
+ const waiting = peer.description.capacity.queued;
24
+ if (peer.description.phase !== 'ready' || !Number.isSafeInteger(waiting) || waiting < 0) continue;
25
+ for (const offer of peer.description.models) if (offer.id === model.id && offer.identity === model.identity && matchesPlan(model, offer.planId)
26
+ && offer.availability === 'ready' && Number.isSafeInteger(offer.index) && offer.index < stageCount) {
27
+ queued[offer.index] = Math.max(queued[offer.index], waiting);
28
+ }
29
+ }
30
+ const busiest = queued.reduce((best, value, index) => value > queued[best] ? index : best, 0);
31
+ if (queued[busiest] > queueBound) return Object.freeze({ index: busiest, reason: 'queue', queued: Object.freeze(queued) });
32
+ // Coordinators are read in participant-id order so every observer resolves the same claim.
33
+ for (const peer of [...peers].sort((a, b) => a.participantId < b.participantId ? -1 : a.participantId > b.participantId ? 1 : 0)) {
34
+ const costs = peer.description.stageCosts, decode = costs?.decodeMs;
35
+ if (costs?.schema !== STAGE_COSTS_SCHEMA || costs.modelId !== model.id || costs.modelIdentity !== model.identity || !matchesPlan(model, costs.planId)
36
+ || !Array.isArray(decode) || decode.length !== stageCount || !decode.every(value => typeof value === 'number' && Number.isFinite(value) && value >= 0)) continue;
37
+ const slowest = decode.reduce((best, value, index) => value > decode[best] ? index : best, 0);
38
+ const next = Math.max(...decode.filter((_, index) => index !== slowest));
39
+ if (decode[slowest] > 0 && decode[slowest] >= bottleneckRatio * next) {
40
+ return Object.freeze({ index: slowest, reason: 'bottleneck', queued: Object.freeze(queued) });
41
+ }
42
+ }
43
+ return Object.freeze({ index: null, reason: null, queued: Object.freeze(queued) });
44
+ }
45
+
46
+ /** Launch 3, opt-in. An operator's `auto` choice authorizes this participant's own policy to pick
47
+ * its stage from observed discovery. Discovery grants nothing, and nothing here assigns another
48
+ * participant. Supply counts distinct participants: ready offers plus auto claims of peers
49
+ * still preparing. Unassigned auto peers are joiners. The caller re-evaluates only while idle. */
50
+ export function planStageAssignment({ participantId, model, stageCount, capabilities, now, current = null,
51
+ maxOffersPerPeer, demand = null }) {
52
+ assert(typeof participantId === 'string' && participantId.length > 0 && typeof model?.id === 'string'
53
+ && /^sha256:[a-f0-9]{64}$/.test(model.identity) && typeof model.partitionPlanId === 'string' && model.partitionPlanId.length > 0 && Number.isSafeInteger(stageCount) && stageCount >= 2
54
+ && Array.isArray(capabilities) && Number.isSafeInteger(now) && Number.isSafeInteger(maxOffersPerPeer)
55
+ && (current === null || Number.isSafeInteger(current) && current >= 0 && current < stageCount),
56
+ 'Explicit stage assignment input required');
57
+ const stageIndex = value => Number.isSafeInteger(value) && value >= 0 && value < stageCount;
58
+ const wanted = demand && stageIndex(demand.index) ? demand.index : null;
59
+ const supply = Array.from({ length: stageCount }, () => new Set()), joiners = new Set();
60
+ let replicaPreparing = false;
61
+ for (const peer of capabilities) {
62
+ if (peer?.participantId === participantId || peer?.available !== true || !(peer.expiresAt > now)) continue;
63
+ const description = peer.description;
64
+ try { validateOrderedPartitionCapability(description, peer.participantId, maxOffersPerPeer); } catch { continue; }
65
+ if (description.phase === 'ready') for (const offer of description.models) {
66
+ if (offer.id === model.id && offer.identity === model.identity && matchesPlan(model, offer.planId) && offer.availability === 'ready'
67
+ && stageIndex(offer.index)) supply[offer.index].add(peer.participantId);
68
+ }
69
+ const claim = description.assignment;
70
+ if (claim?.schema === STAGE_ASSIGNMENT_SCHEMA && claim.modelId === model.id && claim.modelIdentity === model.identity && matchesPlan(model, claim.planId)
71
+ && ['waiting', 'loading', 'ready'].includes(description.phase)) {
72
+ if (claim.index === null) joiners.add(peer.participantId);
73
+ else if (stageIndex(claim.index)) {
74
+ supply[claim.index].add(peer.participantId);
75
+ if (claim.index === wanted && description.phase !== 'ready') replicaPreparing = true;
76
+ }
77
+ }
78
+ }
79
+ for (const id of joiners) if (supply.some(set => set.has(id))) joiners.delete(id);
80
+ const counts = supply.map(set => set.size);
81
+ // Least supplied; ties go to the lowest index so every observer orders stages alike.
82
+ const least = (rows, skip = -1) => rows.reduce((best, count, index) =>
83
+ index !== skip && (best < 0 || count < rows[best]) ? index : best, -1);
84
+ const decide = (index, reason) => Object.freeze({ index, reason, move: current !== null && index !== current,
85
+ supply: Object.freeze([...counts]), joiners: joiners.size });
86
+ // A replica is useful only once every stage has a supplier: completing a route comes first.
87
+ const replicate = wanted !== null && counts.every(count => count > 0);
88
+ if (current === null) {
89
+ // Preparation consumes the owner's resources. Wait until observed suppliers
90
+ // and willing joiners can cover a route; excess joiners remain unprepared.
91
+ const missing = counts.flatMap((count, index) => count === 0 ? [index] : []);
92
+ const rank = [participantId, ...joiners].sort().indexOf(participantId);
93
+ if (missing.length > joiners.size + 1) return decide(null, 'waiting-for-route');
94
+ if (rank < missing.length) return decide(missing[rank], 'cold-least-supplied');
95
+ // One joiner addresses the observed demand. An in-flight replica already
96
+ // addresses that observation; wait for fresh demand after it becomes ready.
97
+ if (replicate && rank === 0 && !replicaPreparing) return decide(wanted, 'replicate-' + demand.reason);
98
+ return decide(null, 'no-useful-assignment');
99
+ }
100
+ // Hysteresis: move only when this stage has at least two more contributors than the least
101
+ // supplied one, no joiner is about to fill the gap, and only its highest participant id moves.
102
+ const others = supply[current], target = least(counts, current);
103
+ const highest = [participantId, ...others].sort().at(-1) === participantId;
104
+ if (joiners.size || !highest) return decide(current, 'retain');
105
+ // Leave for a demanded stage only while another contributor keeps this stage supplied.
106
+ if (replicate && wanted !== current && others.size >= 1) return decide(wanted, 'replicate-' + demand.reason);
107
+ if (others.size < counts[target] + 1) return decide(current, 'retain');
108
+ return decide(target, 'rebalance');
109
+ }
@@ -0,0 +1,103 @@
1
+ import { createPeerIdFromPublicJwk, importSigningKey, importVerificationKey, getIdentitySignAlgorithm,
2
+ encodeBytes, toBase64Url, fromBase64Url } from '../../artifacts/identity.js';
3
+ import { assertPartition as assert } from './partition-contract.js';
4
+
5
+ const evidenceSchema = 'reploid.partition-stage-evidence/v1';
6
+ const claimSchema = 'reploid.partition-stage-claim/v1';
7
+ const acknowledgmentKeys = ['schema', 'stageId', 'attemptDigest', 'sequence', 'baseCursorId', 'decisionId',
8
+ 'successorCursorId', 'participantEpoch', 'deviceEpoch', 'residentId'];
9
+ const exact = (value, keys) => assert(value && typeof value === 'object' && !Array.isArray(value)
10
+ && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)), 'Invalid stage evidence fields');
11
+ const digest = (runtime, domain, value) => runtime.metadataDigest(domain, value);
12
+ const copy = value => structuredClone(value);
13
+ const same = (runtime, domain, a, b) => digest(runtime, domain, a) === digest(runtime, domain, b);
14
+ function boundStage(runtime, attempt, stage) {
15
+ const accepted = runtime.validateResidentAttemptIdentity(attempt);
16
+ const found = accepted.stages.find(value => value.stageId === stage.stageId);
17
+ assert(found && same(runtime, claimSchema, found, stage), 'Stage differs from admitted attempt');
18
+ return { attempt: accepted, stage: found, attemptDigest: runtime.residentAttemptDigest(accepted) };
19
+ }
20
+ function checkClaim(runtime, claim, attempt, stage) {
21
+ exact(claim, ['schema', 'stage', 'acknowledgment']);
22
+ const bound = boundStage(runtime, attempt, stage), ack = claim.acknowledgment;
23
+ exact(ack, acknowledgmentKeys);
24
+ assert(claim.schema === claimSchema && same(runtime, claimSchema, claim.stage, bound.stage)
25
+ && ack.schema === 'doppler.stage-commit/v1' && ack.stageId === stage.stageId
26
+ && ack.attemptDigest === bound.attemptDigest && Number.isSafeInteger(ack.sequence) && ack.sequence >= 0
27
+ && (ack.baseCursorId === null || /^sha256:[a-f0-9]{64}$/.test(ack.baseCursorId))
28
+ && /^sha256:[a-f0-9]{64}$/.test(ack.decisionId) && /^sha256:[a-f0-9]{64}$/.test(ack.successorCursorId)
29
+ && ack.participantEpoch === stage.participantEpoch && ack.deviceEpoch === stage.deviceEpoch
30
+ && ack.residentId === stage.residentId, 'Stage commitment binding mismatch');
31
+ return bound;
32
+ }
33
+
34
+ /** Signs an assertion by the existing contributor identity. The resident calls
35
+ * this only after its actual transition settles; signatures do not attest math.
36
+ * The receiver's bounded phase-result owner retains/replays these exact bytes. */
37
+ export function createPartitionStageCommitAuthority({ runtime, identity, stage }) {
38
+ for (const method of ['metadataDigest', 'validateResidentAttemptIdentity', 'residentAttemptDigest', 'residentPublicationDecisionDigest']) {
39
+ assert(typeof runtime?.[method] === 'function', 'Canonical Doppler stage contracts required');
40
+ }
41
+ assert(identity?.privateJwk && identity?.publicJwk && identity.peerId === stage?.participantId,
42
+ 'Stage signing identity must equal the bound participant');
43
+ const signer = copy(identity), selectedStage = copy(stage);
44
+ let closed = false;
45
+ return Object.freeze({
46
+ getStage: () => copy(selectedStage),
47
+ async sign({ attempt, sequence, baseCursorId, transition }) {
48
+ assert(!closed, 'Stage commitment authority closed');
49
+ const bound = boundStage(runtime, attempt, selectedStage);
50
+ assert(transition?.successor?.generationId === bound.attempt.generationId,
51
+ 'Target transition generation differs from attempt');
52
+ const decision = { attemptDigest: bound.attemptDigest, sequence, baseCursorId,
53
+ acceptedTokenIds: transition.acceptedTokenIds, tokenRange: transition.tokenRange,
54
+ successor: transition.successor, delta: { text: transition.delta, outputDigest: transition.outputDigest },
55
+ stoppingOutcome: transition.successor.stopReason };
56
+ const acknowledgment = { schema: 'doppler.stage-commit/v1', stageId: selectedStage.stageId,
57
+ attemptDigest: bound.attemptDigest, sequence, baseCursorId,
58
+ decisionId: runtime.residentPublicationDecisionDigest(decision), successorCursorId: transition.successor.cursorId,
59
+ participantEpoch: selectedStage.participantEpoch, deviceEpoch: selectedStage.deviceEpoch, residentId: selectedStage.residentId };
60
+ const claim = { schema: claimSchema, stage: selectedStage, acknowledgment };
61
+ checkClaim(runtime, claim, bound.attempt, selectedStage);
62
+ assert(await createPeerIdFromPublicJwk(signer.publicJwk) === selectedStage.participantId, 'Signing key does not identify stage participant');
63
+ const key = await importSigningKey(signer);
64
+ const signature = await crypto.subtle.sign(getIdentitySignAlgorithm(signer), key, encodeBytes(digest(runtime, claimSchema, claim)));
65
+ assert(!closed, 'Stage commitment authority closed');
66
+ const evidence = { schema: evidenceSchema, claim: copy(claim), publicJwk: copy(signer.publicJwk), signature: toBase64Url(signature) };
67
+ return { acknowledgment: { ...acknowledgment, authenticationDigest: digest(runtime, evidenceSchema, evidence) }, evidence };
68
+ },
69
+ close() { closed = true; }
70
+ });
71
+ }
72
+
73
+ /** Validates durable evidence against the exact publication acknowledgment and
74
+ * admitted stage, using existing signing formats. No global trust/key lookup. */
75
+ export async function verifyPartitionStageCommit({ runtime, attempt, stage, acknowledgment, evidence,
76
+ transition, sequence, baseCursorId }) {
77
+ try {
78
+ // The runtime's canonical encoding enforces bounded metadata before copying.
79
+ const input = { attempt, stage, acknowledgment, evidence,
80
+ ...(transition === undefined ? {} : { transition, sequence, baseCursorId }) };
81
+ digest(runtime, evidenceSchema, input);
82
+ ({ attempt, stage, acknowledgment, evidence, transition, sequence, baseCursorId } = copy(input));
83
+ exact(evidence, ['schema', 'claim', 'publicJwk', 'signature']);
84
+ exact(acknowledgment, [...acknowledgmentKeys, 'authenticationDigest']);
85
+ if (evidence.schema !== evidenceSchema || typeof evidence.signature !== 'string'
86
+ || !/^[A-Za-z0-9_-]{1,256}$/.test(evidence.signature)
87
+ || acknowledgment.authenticationDigest !== digest(runtime, evidenceSchema, evidence)) return false;
88
+ const { authenticationDigest, ...body } = acknowledgment;
89
+ checkClaim(runtime, evidence.claim, attempt, stage);
90
+ if (transition !== undefined && (acknowledgment.sequence !== sequence || acknowledgment.baseCursorId !== baseCursorId
91
+ || acknowledgment.successorCursorId !== transition.successor.cursorId
92
+ || acknowledgment.decisionId !== runtime.residentPublicationDecisionDigest({
93
+ attemptDigest: runtime.residentAttemptDigest(attempt), sequence, baseCursorId,
94
+ acceptedTokenIds: transition.acceptedTokenIds, tokenRange: transition.tokenRange,
95
+ successor: transition.successor, delta: { text: transition.delta, outputDigest: transition.outputDigest },
96
+ stoppingOutcome: transition.successor.stopReason }))) return false;
97
+ if (!same(runtime, claimSchema, body, evidence.claim.acknowledgment)
98
+ || await createPeerIdFromPublicJwk(evidence.publicJwk) !== stage.participantId) return false;
99
+ const key = await importVerificationKey(evidence.publicJwk);
100
+ return await crypto.subtle.verify(getIdentitySignAlgorithm(evidence.publicJwk), key,
101
+ fromBase64Url(evidence.signature), encodeBytes(digest(runtime, claimSchema, evidence.claim)));
102
+ } catch { return false; }
103
+ }