reploid 0.0.0-stage → 0.1.1

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 +542 -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 +85 -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,817 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import { constants, readFileSync, realpathSync } from 'node:fs';
4
+ import { execFileSync } from 'node:child_process';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { createHash } from 'node:crypto';
8
+ import { pathToFileURL, fileURLToPath } from 'node:url';
9
+ import { createInterface } from 'node:readline';
10
+ import { inspectDeviceMemory } from 'doppler-gpu/partitions';
11
+ import { getDevice, getKernelCapabilities } from 'doppler-gpu/tooling/device';
12
+ import { createSigningIdentity, createPeerIdFromPublicJwk } from '../../packages/reploid/src/artifacts/identity.js';
13
+ import { signGenerationTerminal } from '../../packages/reploid/src/artifacts/generation-terminal.js';
14
+ import { hashJson } from '../../packages/reploid/src/artifacts/signatures.js';
15
+ import { createNativeParticipant, createNativeRtcAdapter, createNativeGpuHost, readNativeRoomTokenFile } from '../native-participant.js';
16
+ import { openNativeOwnerControls } from '../native-owner/controls.js';
17
+ import custodyPolicy from '../../apps/poolday/config/chat-files.json' with { type: 'json' };
18
+ import bootstrap from '../../shared/config/swarm-bootstrap.json' with { type: 'json' };
19
+ import { startFleetBrowserBridge } from './browser-bridge.js';
20
+ import { startFleetTextApi } from './text-api.js';
21
+ import { createFleetClient } from './service.js';
22
+ import { createFleetRequester, buildFleetRequestOutcome } from './requester.js';
23
+ import { createFleetExecutionAdmission } from './execution-admission.js';
24
+ import { assert, text, same } from './contract.js';
25
+ import { partitionAdapterIdentities } from '../../packages/reploid/src/mesh/partitions/partition-contract.js';
26
+
27
+ export async function openComputerIdentity(file) {
28
+ assert(path.isAbsolute(file), 'Absolute computer identity path required');
29
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
30
+ let identity;
31
+ try { identity = JSON.parse(await readOwnFile(file)); }
32
+ catch (error) {
33
+ if (error.code !== 'ENOENT') throw error;
34
+ identity = await createSigningIdentity({ algorithm: 'Ed25519' });
35
+ await fs.writeFile(file, JSON.stringify(identity), { flag: 'wx', mode: 0o600 });
36
+ }
37
+ assert(identity.privateJwk && await createPeerIdFromPublicJwk(identity.publicJwk) === identity.peerId, 'Invalid persistent computer identity');
38
+ return identity;
39
+ }
40
+
41
+ async function readOwnFile(file) {
42
+ assert(path.isAbsolute(file), 'Absolute operator file path required');
43
+ assert(!(await fs.lstat(file)).isSymbolicLink(), 'Operator file cannot be a symbolic link');
44
+ const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
45
+ try {
46
+ const stat = await handle.stat();
47
+ assert(stat.isFile(), 'Operator file must be regular');
48
+ if (process.getuid) assert(stat.uid === process.getuid() && (stat.mode & 0o077) === 0, 'Operator secrets must be owner-only');
49
+ return await handle.readFile('utf8');
50
+ } finally { await handle.close(); }
51
+ }
52
+
53
+ export function activeProviderRequests(state) {
54
+ if (!state) return 0;
55
+ const reservations = state.reservations?.records || state.resident?.reservations?.records || [];
56
+ return Math.max(['executing', 'loading', 'stopping', 'draining'].includes(state.phase) ? 1 : 0,
57
+ state.reservations?.active || 0, state.unsettledOwners || 0, state.queue?.queued || 0,
58
+ reservations.filter(row => row.settledAt === null).length,
59
+ (state.attempts || []).filter(row => row.cleanupSettledAt == null).length,
60
+ state.unknownAttempts?.length || 0);
61
+ }
62
+
63
+ function availableHostBytes() {
64
+ const fallback = os.freemem();
65
+ try {
66
+ if (process.platform === 'darwin') {
67
+ const output = execFileSync('vm_stat', [], { encoding: 'utf8', timeout: 1000 });
68
+ const pageSize = Number(output.match(/page size of (\d+) bytes/i)?.[1] || 0);
69
+ const pages = label => Number(output.match(new RegExp(`${label}:\\s+(\\d+)\\.`))?.[1] || 0);
70
+ if (pageSize > 0) {
71
+ const reclaimable = (pages('Pages free') + pages('File-backed pages')) * pageSize;
72
+ if (Number.isSafeInteger(reclaimable) && reclaimable > fallback) return Math.min(os.totalmem(), reclaimable);
73
+ }
74
+ } else if (process.platform === 'linux') {
75
+ const meminfo = readFileSync('/proc/meminfo', 'utf8');
76
+ const availableKb = Number(meminfo.match(/^MemAvailable:\s+(\d+)\s+kB/m)?.[1] || 0);
77
+ if (availableKb > 0) return Math.min(os.totalmem(), availableKb * 1024);
78
+ }
79
+ } catch {
80
+ // Fall back to os.freemem() when platform virtual-memory counters are unavailable.
81
+ }
82
+ return fallback;
83
+ }
84
+
85
+ export function fleetHardwareSnapshot() {
86
+ const device = getDevice(), memory = inspectDeviceMemory();
87
+ return { platform: process.platform, totalHostBytes: os.totalmem(), freeHostBytes: availableHostBytes(),
88
+ processResidentBytes: process.memoryUsage().rss,
89
+ adapter: device ? getKernelCapabilities().adapterInfo : null,
90
+ maxStorageBufferBindingSize: device?.limits.maxStorageBufferBindingSize ?? null,
91
+ maxBufferSize: device?.limits.maxBufferSize ?? null,
92
+ gpuBuffers: memory && { scope: memory.scope, liveBytes: memory.liveBytes, peakBytes: memory.peakBytes,
93
+ maxBytes: memory.maxBytes, categories: memory.categories } };
94
+ }
95
+
96
+ /** A direct reply proves this requester process is online, not that it contributes GPU capacity. */
97
+ export function includeRequesterPresence(computers, state, at) {
98
+ return computers.map(computer => computer.id !== state.computerId ? computer : {
99
+ ...computer, online: true, observedAt: at, requester: true,
100
+ activeRequests: Math.max(computer.online ? computer.activeRequests || 0 : 0, state.activeRequests),
101
+ ...(state.queuedRequests > 0 ? { queuedRequests: state.queuedRequests } : {}),
102
+ preparing: state.preparing,
103
+ ...(!computer.online ? { resident: null, freeHostBytes: state.hardware.freeHostBytes } : {})
104
+ });
105
+ }
106
+
107
+ /** Fair round-robin admission scheduler across conversation lanes, including fast-path dispatches. */
108
+ export function createFairLaneScheduler({
109
+ enabled = true,
110
+ capacityWaitMs = 30000,
111
+ canDrain = () => true,
112
+ findRoute,
113
+ freeSlots,
114
+ onPoll = () => {}
115
+ }) {
116
+ const queuedEntries = new Map(), laneBuckets = new Map(), laneLastDispatched = new Map();
117
+ const activeProvidersByAttempt = new Map();
118
+ let startingDispatches = 0, dispatchSequence = 0, queuePollTimer = null, closed = false;
119
+
120
+ function recordLaneDispatch(lane) {
121
+ laneLastDispatched.set(lane, ++dispatchSequence);
122
+ if (laneLastDispatched.size > 256) {
123
+ for (const key of laneLastDispatched.keys()) {
124
+ if (!laneBuckets.has(key)) {
125
+ laneLastDispatched.delete(key);
126
+ if (laneLastDispatched.size <= 192) break;
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ function computeFairOrder() {
133
+ const copies = new Map();
134
+ const virtualLast = new Map();
135
+ let virtualSeq = dispatchSequence;
136
+ for (const [lane, items] of laneBuckets.entries()) {
137
+ if (items?.length) {
138
+ copies.set(lane, [...items]);
139
+ virtualLast.set(lane, laneLastDispatched.get(lane) ?? 0);
140
+ }
141
+ }
142
+ const order = [];
143
+ while (copies.size > 0) {
144
+ let bestLane = null, bestSeq = Infinity, bestQueuedAt = Infinity;
145
+ for (const [lane, items] of copies.entries()) {
146
+ const seq = virtualLast.get(lane) ?? 0, queuedAt = items[0].queuedAt;
147
+ if (seq < bestSeq || (seq === bestSeq && queuedAt < bestQueuedAt)) {
148
+ bestLane = lane; bestSeq = seq; bestQueuedAt = queuedAt;
149
+ }
150
+ }
151
+ if (bestLane === null) break;
152
+ const items = copies.get(bestLane);
153
+ order.push(items.shift());
154
+ virtualLast.set(bestLane, ++virtualSeq);
155
+ if (items.length === 0) copies.delete(bestLane);
156
+ }
157
+ return order;
158
+ }
159
+
160
+ function notifyQueuePositions() {
161
+ const ordered = computeFairOrder();
162
+ const total = ordered.length;
163
+ ordered.forEach((entry, index) => {
164
+ const position = index + 1;
165
+ if (entry.lastPosition !== position || entry.lastTotal !== total) {
166
+ entry.lastPosition = position;
167
+ entry.lastTotal = total;
168
+ entry.onAdmission?.({ status: 'queued', queuePosition: position, queuedCount: total, queuedAt: entry.queuedAt });
169
+ }
170
+ });
171
+ }
172
+
173
+ function removeQueuedEntry(entry) {
174
+ queuedEntries.delete(entry.attemptId);
175
+ clearTimeout(entry.timer);
176
+ entry.signal?.removeEventListener('abort', entry.onAbort);
177
+ const items = laneBuckets.get(entry.lane);
178
+ if (items) {
179
+ const index = items.indexOf(entry);
180
+ if (index >= 0) items.splice(index, 1);
181
+ if (!items.length) laneBuckets.delete(entry.lane);
182
+ }
183
+ if (queuedEntries.size === 0 && queuePollTimer) {
184
+ clearInterval(queuePollTimer);
185
+ queuePollTimer = null;
186
+ }
187
+ }
188
+
189
+ function drain() {
190
+ if (closed || !canDrain() || !queuedEntries.size) return;
191
+ let progressed = true;
192
+ while (progressed && laneBuckets.size > 0) {
193
+ progressed = false;
194
+ const candidates = [...laneBuckets.entries()]
195
+ .filter(([, items]) => items?.length > 0)
196
+ .sort(([laneA, itemsA], [laneB, itemsB]) => {
197
+ const seqDiff = (laneLastDispatched.get(laneA) ?? 0) - (laneLastDispatched.get(laneB) ?? 0);
198
+ return seqDiff !== 0 ? seqDiff : itemsA[0].queuedAt - itemsB[0].queuedAt;
199
+ });
200
+ for (const [lane, items] of candidates) {
201
+ const entry = items[0];
202
+ const chosen = findRoute(entry.requestedModel, entry.allowPartitionFallback);
203
+ if (chosen && freeSlots(chosen, activeProvidersByAttempt, startingDispatches) > 0) {
204
+ items.shift();
205
+ if (items.length === 0) laneBuckets.delete(lane);
206
+ queuedEntries.delete(entry.attemptId);
207
+ clearTimeout(entry.timer);
208
+ entry.signal?.removeEventListener('abort', entry.onAbort);
209
+ recordLaneDispatch(lane);
210
+ startingDispatches += 1;
211
+ entry.resolve({ chosen, queuedMs: Math.max(0, Date.now() - entry.queuedAt), releaseStarting: true });
212
+ progressed = true;
213
+ break;
214
+ }
215
+ }
216
+ }
217
+ if (queuedEntries.size === 0 && queuePollTimer) {
218
+ clearInterval(queuePollTimer);
219
+ queuePollTimer = null;
220
+ }
221
+ notifyQueuePositions();
222
+ }
223
+
224
+ async function acquire({ attemptId, threadId, requestedModel, allowPartitionFallback = false, signal, onAdmission }) {
225
+ signal.throwIfAborted();
226
+ if (!enabled) {
227
+ const immediate = findRoute(requestedModel, allowPartitionFallback);
228
+ return { chosen: immediate, queuedMs: 0, markDisclosed() {}, release() {} };
229
+ }
230
+ assert(Number.isSafeInteger(capacityWaitMs) && capacityWaitMs > 0, 'Explicit capacity wait bound required');
231
+ const lane = typeof threadId === 'string' && threadId ? threadId : attemptId;
232
+ if (queuedEntries.size === 0) {
233
+ const immediate = findRoute(requestedModel, allowPartitionFallback);
234
+ if (immediate && freeSlots(immediate, activeProvidersByAttempt, startingDispatches) > 0) {
235
+ recordLaneDispatch(lane);
236
+ startingDispatches += 1;
237
+ let startingHeld = true;
238
+ return {
239
+ chosen: immediate,
240
+ queuedMs: 0,
241
+ markDisclosed(providerKey) {
242
+ if (providerKey) activeProvidersByAttempt.set(attemptId, providerKey);
243
+ if (startingHeld) { startingHeld = false; startingDispatches = Math.max(0, startingDispatches - 1); }
244
+ drain();
245
+ },
246
+ release() {
247
+ activeProvidersByAttempt.delete(attemptId);
248
+ if (startingHeld) { startingHeld = false; startingDispatches = Math.max(0, startingDispatches - 1); }
249
+ drain();
250
+ }
251
+ };
252
+ }
253
+ }
254
+ const queuedAt = Date.now();
255
+ const acquired = await new Promise((resolve, reject) => {
256
+ const entry = {
257
+ attemptId, lane, requestedModel, allowPartitionFallback, signal, onAdmission, queuedAt,
258
+ lastPosition: 0, lastTotal: 0, resolve, reject,
259
+ timer: setTimeout(() => {
260
+ removeQueuedEntry(entry);
261
+ notifyQueuePositions();
262
+ reject(Error('No ready capacity within the configured wait'));
263
+ }, capacityWaitMs),
264
+ onAbort: () => {
265
+ removeQueuedEntry(entry);
266
+ notifyQueuePositions();
267
+ reject(signal.reason || new DOMException('Aborted', 'AbortError'));
268
+ }
269
+ };
270
+ signal.addEventListener('abort', entry.onAbort, { once: true });
271
+ let items = laneBuckets.get(lane);
272
+ if (!items) { items = []; laneBuckets.set(lane, items); }
273
+ items.push(entry);
274
+ queuedEntries.set(attemptId, entry);
275
+ if (!queuePollTimer) {
276
+ queuePollTimer = setInterval(() => {
277
+ void Promise.resolve(onPoll()).catch(() => {});
278
+ drain();
279
+ }, 500);
280
+ queuePollTimer.unref?.();
281
+ }
282
+ notifyQueuePositions();
283
+ drain();
284
+ });
285
+ let startingHeld = acquired.releaseStarting === true;
286
+ onAdmission?.({ status: 'admitted', queuedMs: acquired.queuedMs });
287
+ return {
288
+ chosen: acquired.chosen,
289
+ queuedMs: acquired.queuedMs,
290
+ markDisclosed(providerKey) {
291
+ if (providerKey) activeProvidersByAttempt.set(attemptId, providerKey);
292
+ if (startingHeld) { startingHeld = false; startingDispatches = Math.max(0, startingDispatches - 1); }
293
+ drain();
294
+ },
295
+ release() {
296
+ activeProvidersByAttempt.delete(attemptId);
297
+ if (startingHeld) { startingHeld = false; startingDispatches = Math.max(0, startingDispatches - 1); }
298
+ drain();
299
+ }
300
+ };
301
+ }
302
+
303
+ return Object.freeze({
304
+ acquire,
305
+ drain,
306
+ queuedCount: () => queuedEntries.size,
307
+ close() {
308
+ closed = true;
309
+ if (queuePollTimer) { clearInterval(queuePollTimer); queuePollTimer = null; }
310
+ }
311
+ });
312
+ }
313
+
314
+ /** One process owns one GPU host. Model changes drain the old resident before preparing another. */
315
+ export async function openFleetNativeHost({ configuration, loadRtc = () => import('@roamhq/wrtc'), loadSocket = () => import('ws'),
316
+ loadGpu = () => import('webgpu'), onChange = () => {} }) {
317
+ const config = structuredClone(configuration);
318
+ assert(config.schema === 'reploid.fleet-native/v1' && text(config.computerId) && text(config.name)
319
+ && Array.isArray(config.models) && config.models.length > 0
320
+ && config.models.every(row => ['partition', 'whole-model'].includes(row.mode)), 'Explicit fleet native configuration required');
321
+ assert(text(config.roomId) && config.roomId !== bootstrap.publicRoomId && config.roomTokenFile,
322
+ 'Operator fleet models require an explicit private room');
323
+ const identity = await openComputerIdentity(config.identityFile);
324
+ const [rtcModule, socketModule] = await Promise.all([loadRtc(), loadSocket()]);
325
+ const rtc = createNativeRtcAdapter(rtcModule.default ?? rtcModule), Socket = socketModule.default ?? socketModule.WebSocket;
326
+ class WebSocket extends Socket { constructor(url) { super(url, { origin: config.origin || 'http://localhost:8000' }); } }
327
+ const serviceCa = config.serviceCaFile ? (assert(path.isAbsolute(config.serviceCaFile), 'Absolute service CA certificate path required'),
328
+ await fs.readFile(config.serviceCaFile)) : null;
329
+ const gpu = createNativeGpuHost(loadGpu), credits = config.serviceUrl ? createFleetClient({ url: config.serviceUrl, identity, ca: serviceCa,
330
+ outboxFile: path.join(path.dirname(config.identityFile), 'service-credit-outbox.json') }) : null;
331
+ const owner = config.ownerPolicyFile ? await openNativeOwnerControls({ file: config.ownerPolicyFile, custody: custodyPolicy,
332
+ readGpuBuffers: async () => (await import('doppler-gpu/partitions')).inspectDeviceMemory() }) : null;
333
+ const roomToken = config.roomTokenFile ? process.getuid ? await readNativeRoomTokenFile(config.roomTokenFile)
334
+ : (await readOwnFile(config.roomTokenFile)).replace(/\r?\n$/, '') : null;
335
+ let participant = null, selected = null, preparing = false, closed = false, lastError = null, preparedAt = null;
336
+ const requests = new Map(), outcomesByAttempt = new Map(), outcomesByReservation = new Map();
337
+
338
+ function recordOutcome(outcome) {
339
+ if (!outcome) return outcome;
340
+ if (outcome.attemptId) {
341
+ outcomesByAttempt.delete(outcome.attemptId);
342
+ outcomesByAttempt.set(outcome.attemptId, outcome);
343
+ while (outcomesByAttempt.size > 128) outcomesByAttempt.delete(outcomesByAttempt.keys().next().value);
344
+ }
345
+ if (outcome.reservationId) {
346
+ outcomesByReservation.delete(outcome.reservationId);
347
+ outcomesByReservation.set(outcome.reservationId, outcome);
348
+ while (outcomesByReservation.size > 128) outcomesByReservation.delete(outcomesByReservation.keys().next().value);
349
+ }
350
+ return outcome;
351
+ }
352
+ const lookupOutcome = id => outcomesByReservation.get(id) ?? outcomesByAttempt.get(id) ?? null;
353
+ const resolveProviderId = (_reservationId, binding) => {
354
+ const target = binding?.providers?.[0]?.participantId;
355
+ if (!target) return null;
356
+ const peers = participant?.getNetworkState()?.peers || [];
357
+ return peers.find(peer => peer.participantId === target)?.peerId ?? null;
358
+ };
359
+
360
+ function findMatchingRoute(requested, allowPartitionFallback = false) {
361
+ const available = (participant?.getState()?.models || []).filter(model => model.id === requested.id
362
+ && model.identity === requested.identity
363
+ && (requested.partition ? model.availability === 'ready' : ['ready', 'busy'].includes(model.availability))
364
+ && same(partitionAdapterIdentities(model), partitionAdapterIdentities(requested)));
365
+ return requested.partition ? available.find(model => same(model.partition, requested.partition))
366
+ : available.find(model => !model.partition) || (allowPartitionFallback ? available.find(model => model.partition && model.availability === 'ready') : null);
367
+ }
368
+
369
+ function freeDispatchSlots(chosen, activeProvidersByAttempt, startingDispatches) {
370
+ if (!chosen) return 0;
371
+ const busyKeys = new Set();
372
+ for (const value of activeProvidersByAttempt.values()) {
373
+ for (const key of Array.isArray(value) ? value : [value]) {
374
+ if (typeof key === 'string' && key) busyKeys.add(key);
375
+ }
376
+ }
377
+ if (chosen.partition) {
378
+ return chosen.availability === 'ready' && busyKeys.size === 0 && startingDispatches === 0 ? 1 : 0;
379
+ }
380
+ const peers = participant?.getNetworkState()?.peers || [];
381
+ const matching = peers.filter(peer => peer.readiness === 'ready' && peer.model === chosen.id
382
+ && (!chosen.identity || peer.modelIdentity === chosen.identity));
383
+ if (matching.length > 0) {
384
+ const seenParticipants = new Set();
385
+ const freePeers = matching.reduce((sum, peer) => {
386
+ const participantKey = peer.participantId || peer.peerId;
387
+ if (busyKeys.has(peer.peerId) || (participantKey && (busyKeys.has(participantKey) || seenParticipants.has(participantKey)))) return sum;
388
+ if (participantKey) seenParticipants.add(participantKey);
389
+ const slots = Number.isSafeInteger(peer.availableSlots) && peer.availableSlots > 0 ? peer.availableSlots : 0;
390
+ return sum + slots;
391
+ }, 0);
392
+ return Math.max(0, freePeers - startingDispatches);
393
+ }
394
+ return chosen.availability === 'ready' && busyKeys.size === 0 && startingDispatches === 0 ? 1 : 0;
395
+ }
396
+
397
+ const laneScheduler = createFairLaneScheduler({
398
+ enabled: config.followPlacement === true,
399
+ capacityWaitMs: config.capacityWaitMs,
400
+ canDrain: () => !closed && !preparing,
401
+ findRoute: findMatchingRoute,
402
+ freeSlots: freeDispatchSlots,
403
+ onPoll: () => publishDemand()
404
+ });
405
+ const acquireDispatchSlot = options => laneScheduler.acquire(options);
406
+ const drainRequesterQueue = () => laneScheduler.drain();
407
+ const state = () => ({ computerId: config.computerId, name: config.name, participantId: identity.peerId,
408
+ selected: selected && { modelId: selected.model.id, modelIdentity: selected.model.identity, mode: selected.mode },
409
+ preparing, activeRequests: requests.size, queuedRequests: laneScheduler.queuedCount(), error: lastError, hardware: fleetHardwareSnapshot(),
410
+ execution: participant?.getState() ?? null, network: participant?.getNetworkState() ?? null });
411
+
412
+ let startupProviderCleaned = false;
413
+ async function reconcileCredits() {
414
+ if (!credits) return [];
415
+ const results = [];
416
+ if (!startupProviderCleaned && !participant && !preparing) {
417
+ try {
418
+ const orphaned = await credits.call('providerOpenMine', {});
419
+ for (const row of orphaned) {
420
+ if (row.binding?.providers?.[0]?.participantId !== identity.peerId) continue;
421
+ const terminal = await signGenerationTerminal({
422
+ requestId: row.binding.executionId,
423
+ provider: identity.peerId,
424
+ consumer: row.binding.consumer,
425
+ model: row.binding.model.id,
426
+ modelIdentity: row.binding.model.identity,
427
+ adapterIdentities: row.binding.model.adapterIdentities || [],
428
+ inputDigest: row.binding.inputDigest
429
+ }, 'failed', identity);
430
+ results.push(await credits.collect(row.id, { terminal }));
431
+ }
432
+ startupProviderCleaned = true;
433
+ } catch {
434
+ // Retry startup provider orphan cleanup on the next pre-mesh reconcile if service was unreachable.
435
+ }
436
+ }
437
+ const reconciled = await credits.reconcile(requests.size === 0 ? { activeIds: [] } : {});
438
+ for (const row of reconciled) {
439
+ if (!results.some(prior => prior.id === row.id)) results.push(row);
440
+ }
441
+ return results;
442
+ }
443
+ await reconcileCredits().catch(() => {});
444
+
445
+ async function select(index, contribute) {
446
+ assert(!closed && !preparing && requests.size === 0, 'Computer is busy or closing');
447
+ const entry = config.models[index]; assert(entry, 'Model is outside owner preferences');
448
+ const activity = participant?.getState();
449
+ assert(activeProviderRequests(activity) === 0, 'Active provider cannot move');
450
+ preparing = true; lastError = null;
451
+ try {
452
+ if (participant) await participant.close();
453
+ participant = null; selected = null;
454
+ const bytes = await fs.readFile(path.join(entry.directory, 'manifest.json'));
455
+ assert('sha256:' + createHash('sha256').update(bytes).digest('hex') === entry.model.identity, 'Owner-selected model manifest changed');
456
+ participant = await createNativeParticipant({ model: entry.model, modelDirectory: entry.directory, identity, rtc, WebSocket,
457
+ signalingUrl: config.signalingUrl, roomId: config.roomId, roomToken, nativeGpu: () => gpu.open(),
458
+ cacheDirectory: null, owner, executionMode: entry.mode, ordered: entry.ordered ?? null,
459
+ executionAdmission: credits ? createFleetExecutionAdmission({ credits, model: entry.model }) : null,
460
+ generationJournalFile: path.join(path.dirname(config.identityFile), 'generation.sqlite'),
461
+ logger: { info() {}, log() {}, debug() {}, warn: (...args) => onChange({ warning: args.join(' ') }), error: (...args) => onChange({ error: args.join(' ') }) },
462
+ onChange: () => { drainRequesterQueue(); onChange(state()); } });
463
+ selected = entry;
464
+ if (contribute) await participant.contribute(true);
465
+ assert(participant.getState()?.phase !== 'failed', participant.getState()?.error || 'Model preparation failed');
466
+ preparedAt = Date.now(); return state();
467
+ } catch (error) { lastError = error.message; throw error; }
468
+ finally { preparing = false; drainRequesterQueue(); onChange(state()); }
469
+ }
470
+ async function reconcile() {
471
+ assert(credits && config.observationFile, 'Configured credit service and measured envelopes required for automatic placement');
472
+ await reconcileCredits();
473
+ const observation = JSON.parse(await fs.readFile(config.observationFile, 'utf8'));
474
+ // Generated after the package closure is collected; it cannot hash itself as an input.
475
+ const delivery = await fs.readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'native-delivery.json'));
476
+ observation.softwareIdentity = 'sha256:' + createHash('sha256').update(delivery).digest('hex');
477
+ const activity = participant?.getState();
478
+ const hardware = fleetHardwareSnapshot();
479
+ observation.id = config.computerId; observation.participantId = identity.peerId; observation.observedAt = Date.now();
480
+ observation.preparing = preparing;
481
+ observation.activeRequests = requests.size + activeProviderRequests(activity);
482
+ const residentActive = Boolean(selected && (activity?.ready || ['ready', 'executing', 'draining'].includes(activity?.phase)));
483
+ const residentHostBytes = residentActive ? (hardware.processResidentBytes ?? 0) : 0;
484
+ const ownedHostBudget = Math.min(observation.preferences.maxHostBytes,
485
+ owner?.policy.memory?.maxProcessResidentBytes ?? observation.preferences.maxHostBytes);
486
+ observation.freeHostBytes = Math.max(0, Math.min(hardware.freeHostBytes, ownedHostBudget - residentHostBytes));
487
+ observation.capacityScope = 'owned-gpu-buffer-budget';
488
+ const ownedBudget = Math.min(observation.preferences.maxGpuBytes, owner?.policy.memory?.maxGpuBufferBytes ?? observation.preferences.maxGpuBytes);
489
+ observation.freeGpuBytes = Math.max(0, ownedBudget - (hardware.gpuBuffers?.liveBytes ?? 0));
490
+ if (hardware.maxStorageBufferBindingSize !== null) observation.maxStorageBufferBindingSize = hardware.maxStorageBufferBindingSize;
491
+ const contextCache = activity?.contextCache ?? null;
492
+ observation.resident = residentActive ? {
493
+ modelIdentity: selected.model.identity, mode: selected.mode,
494
+ preparedAt, planId: selected.model.partitionPlanId ?? null, stageIndex: selected.ordered?.stage?.index ?? null,
495
+ gpuBytes: hardware.gpuBuffers?.liveBytes ?? 0,
496
+ peakGpuBytes: hardware.gpuBuffers?.peakBytes ?? 0,
497
+ hostBytes: residentHostBytes,
498
+ ...(contextCache ? { cachedContexts: contextCache.entries, contextCacheBytes: contextCache.allocatedBytes } : {})
499
+ } : null;
500
+ await credits.call('observe', observation);
501
+ const plan = await credits.call('plan', {}), decision = plan.decisions.find(row => row.computerId === config.computerId);
502
+ if (decision?.action === 'stop' && observation.activeRequests === 0 && participant) {
503
+ await participant.stop();
504
+ }
505
+ if (decision?.action === 'prepare' && observation.activeRequests === 0) {
506
+ const index = config.models.findIndex(entry => entry.model.id === decision.modelId && entry.model.identity === decision.modelIdentity
507
+ && entry.mode === decision.mode && (entry.model.partitionPlanId ?? null) === decision.planId
508
+ && (entry.ordered?.stage?.index ?? null) === decision.stageIndex);
509
+ assert(index >= 0, 'Plan does not match an owner-approved runtime configuration');
510
+ await select(index, true);
511
+ }
512
+ return plan;
513
+ }
514
+ async function publishDemand() {
515
+ if (!credits || config.followPlacement !== true) return;
516
+ const demand = new Map();
517
+ for (const { request, since } of requests.values()) {
518
+ const key = request.model.id + request.model.identity, prior = demand.get(key);
519
+ // This is a scheduling estimate, never a token meter or a credit charge.
520
+ const promptTokens = Math.max(1, Math.ceil(Buffer.byteLength(request.messages.map(row => row.content).join('\n')) / 4));
521
+ demand.set(key, { id: request.model.id, identity: request.model.identity, queued: (prior?.queued || 0) + 1,
522
+ since: Math.min(prior?.since ?? since, since), promptTokens: Math.max(prior?.promptTokens || 0, promptTokens),
523
+ outputTokens: config.maxOutputTokens, estimate: 'input-utf8-bytes-div4' });
524
+ }
525
+ await credits.call('demand', { id: config.computerId, requests: [...demand.values()] });
526
+ }
527
+ const textParticipant = Object.freeze({
528
+ roomId: config.roomId,
529
+ executionClass: 'private-pool',
530
+ getState: () => participant?.getState() ?? { models: [] },
531
+ generate: (request, controls) => {
532
+ assert(participant && !closed && !preparing, 'Requester unavailable');
533
+ return participant.generate(request, controls);
534
+ },
535
+ recoverGeneration: (request, reference, controls) => {
536
+ assert(participant && !closed && !preparing, 'Requester unavailable');
537
+ return participant.recoverGeneration(request, reference, controls);
538
+ }
539
+ });
540
+ const requesterPorts = extra => ({
541
+ identity, credits, maxOutputTokens: config.maxOutputTokens,
542
+ onOutcome: recordOutcome, lookupOutcome, resolveProviderId, ...extra
543
+ });
544
+ async function recordPreDispatchFailure(request, error) {
545
+ const recordedAt = Date.now();
546
+ const outcome = recordOutcome(buildFleetRequestOutcome({
547
+ attemptId: request.attemptId ?? null,
548
+ threadId: request.threadId ?? null,
549
+ mode: request.model?.partition ? 'partition' : 'whole-model',
550
+ model: request.model,
551
+ inputDigest: await hashJson(request.messages),
552
+ phase: 'queued',
553
+ dispatched: false,
554
+ acknowledged: { deltaCount: 0, outputCharacters: 0, acknowledgedOutputTokens: 0 },
555
+ settlement: { status: 'cancelled_queued', charge: 0, outputTokens: 0, settledAt: recordedAt },
556
+ maximum: 0,
557
+ recordedAt
558
+ }));
559
+ if (error && typeof error === 'object' && !error.disconnect) error.disconnect = outcome;
560
+ return outcome;
561
+ }
562
+ return Object.freeze({ identity, state, select, reconcile, reconcileCredits, textParticipant,
563
+ outcome: id => lookupOutcome(id),
564
+ async inspectOutcome(id) {
565
+ const prior = lookupOutcome(id);
566
+ const reservationId = prior?.reservationId ?? (typeof id === 'string' && id.startsWith('service:') ? id : null);
567
+ if (!reservationId || !credits) return prior;
568
+ let row;
569
+ try { row = await credits.call('request', { id: reservationId }); }
570
+ catch { return prior; }
571
+ if (!row) return prior;
572
+ const dispatched = row.state !== 'reserved' && row.state !== 'abandoned';
573
+ const settlement = row.state === 'settled' ? row.outcome
574
+ : row.state === 'abandoned' ? { status: 'abandoned', charge: 0, outputTokens: 0, settledAt: row.binding.createdAt }
575
+ : { id: reservationId, status: 'pending' };
576
+ const settledCompleted = row.state === 'settled' && !['cancelled', 'failed'].includes(row.outcome?.status);
577
+ return recordOutcome(buildFleetRequestOutcome({
578
+ attemptId: prior?.attemptId ?? null,
579
+ threadId: prior?.threadId ?? null,
580
+ reservationId,
581
+ executionId: row.binding.executionId,
582
+ mode: row.binding.mode,
583
+ model: row.binding.model,
584
+ providerId: prior?.providerId ?? null,
585
+ providerIdentity: row.binding.providers[0]?.participantId ?? null,
586
+ inputDigest: row.binding.inputDigest,
587
+ phase: dispatched ? 'in-flight' : 'queued',
588
+ dispatched,
589
+ acknowledged: {
590
+ deltaCount: prior?.acknowledged?.deltaCount ?? 0,
591
+ outputCharacters: prior?.acknowledged?.outputCharacters ?? 0,
592
+ acknowledgedOutputTokens: settledCompleted ? (row.outcome.outputTokens ?? 0) : (prior?.acknowledged?.acknowledgedOutputTokens ?? 0)
593
+ },
594
+ settlement,
595
+ maximum: dispatched ? row.binding.maximum : 0,
596
+ recordedAt: Date.now()
597
+ }));
598
+ },
599
+ allowsModel: model => config.models.some(entry => entry.model.id === model?.id && entry.model.identity === model?.identity),
600
+ async enrolledRecipients() {
601
+ assert(credits, 'Service credits are not configured');
602
+ const network = await credits.call('computers', {});
603
+ return new Set((network.computers || []).flatMap(row => row.participantIds || []));
604
+ },
605
+ async runAccountedGeneration(request, controls, execute) {
606
+ assert(participant && !closed && !preparing && !requests.has(request.attemptId), 'Requester unavailable or attempt already active');
607
+ assert(credits, 'Service credits are not configured');
608
+ assert(Number.isSafeInteger(config.maxOutputTokens) && config.maxOutputTokens > 0, 'Explicit fleet output limit required');
609
+ if (request.generationRequest && request.generationRequest.maxOutputTokens > config.maxOutputTokens) {
610
+ throw Object.assign(Error('max_output_tokens exceeds fleet credit policy'), { status: 400, param: 'max_output_tokens' });
611
+ }
612
+ const controller = new AbortController();
613
+ const abort = () => controller.abort(controls.signal.reason);
614
+ controls.signal.addEventListener('abort', abort, { once: true });
615
+ if (controls.signal.aborted) abort();
616
+ requests.set(request.attemptId, { controller, request, since: Date.now() });
617
+ let slot = null;
618
+ try {
619
+ await publishDemand();
620
+ try {
621
+ slot = await acquireDispatchSlot({
622
+ attemptId: request.attemptId,
623
+ threadId: request.threadId,
624
+ requestedModel: request.model,
625
+ allowPartitionFallback: false,
626
+ signal: controller.signal
627
+ });
628
+ } catch (error) {
629
+ await recordPreDispatchFailure(request, error);
630
+ throw error;
631
+ }
632
+ const wrappedControls = {
633
+ ...controls,
634
+ signal: controller.signal,
635
+ requestApproval: async disclosure => {
636
+ slot?.markDisclosed?.([disclosure?.providerId, typeof disclosure?.recipientIdentity === 'string' ? disclosure.recipientIdentity : null].filter(Boolean));
637
+ return controls.requestApproval(disclosure);
638
+ }
639
+ };
640
+ const requester = createFleetRequester(requesterPorts({
641
+ participant: { generate: execute, recoverGeneration: textParticipant.recoverGeneration }
642
+ }));
643
+ const result = await requester.generate(request, wrappedControls);
644
+ return {
645
+ ...result,
646
+ execution: {
647
+ ...result.execution,
648
+ ...(slot.queuedMs > 0 ? { admission: { ...(result.execution?.admission || {}), queuedMs: slot.queuedMs } } : {}),
649
+ serviceCredit: result.serviceCredit
650
+ }
651
+ };
652
+ } finally {
653
+ slot?.release?.();
654
+ controls.signal.removeEventListener('abort', abort);
655
+ requests.delete(request.attemptId);
656
+ await publishDemand().catch(error => onChange({ warning: error.message }));
657
+ }
658
+ },
659
+ async recoverAccountedGeneration(input, controls, recoverExecution) {
660
+ assert(participant && !closed && !preparing, 'Requester unavailable');
661
+ assert(credits, 'Service credits are not configured');
662
+ const requester = createFleetRequester(requesterPorts({ participant: textParticipant }));
663
+ return requester.recover(input, controls, recoverExecution);
664
+ },
665
+ async recover(input, signal = AbortSignal.timeout(30000)) {
666
+ assert(participant && !closed && !preparing, 'Requester unavailable');
667
+ assert(credits, 'Service credits are not configured');
668
+ const requester = createFleetRequester(requesterPorts({ participant }));
669
+ return requester.recover(input, { signal });
670
+ },
671
+ async browserState() {
672
+ assert(credits, 'Service credits are not configured');
673
+ const network = await credits.call('computers', {});
674
+ const poolIdentity = 'sha256:' + createHash('sha256').update(JSON.stringify(network.computers.map(row =>
675
+ [row.id, row.participantIds]))).digest('hex');
676
+ const available = participant?.getState()?.models || [];
677
+ const models = [...new Map(config.models.map(entry => [entry.model.id + entry.model.identity, entry.model])).values()].map(model => {
678
+ const routes = available.filter(row => row.id === model.id && row.identity === model.identity && !row.adapters?.length);
679
+ const status = routes.some(row => row.availability === 'ready') ? 'ready'
680
+ : routes.some(row => row.availability === 'busy') ? 'busy' : routes.some(row => ['loading', 'preparing'].includes(row.availability)) ? 'loading' : 'unavailable';
681
+ return { id: model.id, identity: model.identity, name: model.name || model.id, provider: 'doppler',
682
+ contextLength: model.contextLength, generation: model.generation, availability: status };
683
+ });
684
+ return { schema: 'reploid.fleet-browser/v1', poolIdentity, requester: { id: config.computerId, name: config.name, participantId: identity.peerId },
685
+ models, computers: includeRequesterPresence(network.computers, state(), network.at), at: network.at };
686
+ },
687
+ async receipt(id) { assert(credits, 'Service credits are not configured'); return credits.call('request', { id }); },
688
+ async balances() { assert(credits, 'Service credits are not configured'); await reconcileCredits(); return credits.call('snapshot', {}); },
689
+ async generate(request, onDelta = () => {}, onRoute = () => {}, onAdmission = () => {}) {
690
+ assert(participant && !closed && !preparing && !requests.has(request.attemptId), 'Requester unavailable or attempt already active');
691
+ request = structuredClone(request);
692
+ const configured = config.models.find(entry => entry.model.id === request.model.id && entry.model.identity === request.model.identity);
693
+ assert(configured, 'Model is outside owner preferences');
694
+ request.model = { ...structuredClone(configured.model), ...(request.model.partition ? { partition: request.model.partition } : {}) };
695
+ const controller = new AbortController(); requests.set(request.attemptId, { controller, request, since: Date.now() });
696
+ let slot = null;
697
+ try {
698
+ await publishDemand();
699
+ try {
700
+ slot = await acquireDispatchSlot({
701
+ attemptId: request.attemptId,
702
+ threadId: request.threadId,
703
+ requestedModel: request.model,
704
+ allowPartitionFallback: true,
705
+ signal: controller.signal,
706
+ onAdmission
707
+ });
708
+ if (slot.chosen) request.model = structuredClone(slot.chosen);
709
+ } catch (error) {
710
+ await recordPreDispatchFailure(request, error);
711
+ throw error;
712
+ }
713
+ const controls = { signal: controller.signal, onDelta,
714
+ requestApproval: async disclosure => {
715
+ assert(same(disclosure.input?.messages, request.messages), 'Changed prompt disclosure');
716
+ slot?.markDisclosed?.([disclosure?.providerId, typeof disclosure?.recipientIdentity === 'string' ? disclosure.recipientIdentity : null].filter(Boolean));
717
+ onRoute(disclosure);
718
+ return true; // Explicit generate command is the local owner's disclosure instruction.
719
+ } };
720
+ const requester = credits ? createFleetRequester(requesterPorts({ participant })) : participant;
721
+ const result = await requester.generate(request, controls);
722
+ return slot.queuedMs > 0
723
+ ? { ...result, execution: { ...result.execution, admission: { ...(result.execution?.admission || {}), queuedMs: slot.queuedMs } } }
724
+ : result;
725
+ } finally {
726
+ slot?.release?.();
727
+ requests.delete(request.attemptId);
728
+ await publishDemand().catch(error => onChange({ warning: error.message }));
729
+ }
730
+ },
731
+ cancel(attemptId) { requests.get(attemptId)?.controller.abort(new DOMException('Stopped by owner', 'AbortError')); },
732
+ async close() {
733
+ closed = true;
734
+ laneScheduler.close();
735
+ for (const { controller } of requests.values()) controller.abort();
736
+ try { await participant?.close(); } finally { await owner?.close(); await gpu.close(); }
737
+ }
738
+ });
739
+ }
740
+
741
+ export async function runFleetNativeCommands({ host, config, input = process.stdin, signals = process }) {
742
+ const pending = new Set(); let lines = null;
743
+ let timer = null, browserBridge = null, textApi = null;
744
+ if (config.auto === true) {
745
+ assert(Number.isSafeInteger(config.pollMs) && config.pollMs >= 1000, 'Explicit bounded placement interval required');
746
+ let busy = false;
747
+ timer = setInterval(async () => {
748
+ if (busy) return; busy = true;
749
+ try { await host.reconcile(); } catch (error) { process.stderr.write(JSON.stringify({ event: 'fleet-error', error: error.message }) + '\n'); }
750
+ finally { busy = false; }
751
+ }, config.pollMs);
752
+ }
753
+ let stopped = false, endDaemon;
754
+ const daemonStopped = new Promise(resolve => { endDaemon = resolve; });
755
+ const stop = () => { stopped = true; lines?.close(); endDaemon(); };
756
+ signals.once('SIGTERM', stop); signals.once('SIGINT', stop);
757
+ try {
758
+ if (Number.isSafeInteger(config.initialModel)) await host.select(config.initialModel, config.initialContribution === true);
759
+ if (typeof host.reconcileCredits === 'function') {
760
+ await host.reconcileCredits().catch(error => process.stderr.write(JSON.stringify({ event: 'fleet-error', error: error.message }) + '\n'));
761
+ }
762
+ const browserBridgeToken = config.browserBridge ? (await readOwnFile(config.browserBridge.tokenFile)).trim() : null;
763
+ if (config.browserBridge) browserBridge = await startFleetBrowserBridge({ host, ...config.browserBridge,
764
+ token: browserBridgeToken });
765
+ if (config.textApi) {
766
+ const tls = config.textApi.tls ? (assert(path.isAbsolute(config.textApi.tls.certFile), 'Absolute TLS certificate path required'),
767
+ { key: await readOwnFile(config.textApi.tls.keyFile), cert: await fs.readFile(config.textApi.tls.certFile) }) : null;
768
+ const mountedBridge = config.browserBridge ? {
769
+ origins: config.browserBridge.origins,
770
+ token: browserBridgeToken,
771
+ basePath: config.browserBridge.tunnelPath ?? '/bridge',
772
+ ...(config.browserBridge.maxBodyBytes !== undefined ? { maxBodyBytes: config.browserBridge.maxBodyBytes } : {}),
773
+ ...(config.browserBridge.maxConcurrent !== undefined ? { maxConcurrent: config.browserBridge.maxConcurrent } : {})
774
+ } : null;
775
+ textApi = await startFleetTextApi({ host, ...config.textApi, tls,
776
+ ...(mountedBridge ? { browserBridge: mountedBridge } : {}),
777
+ token: (await readOwnFile(config.textApi.tokenFile)).trim() });
778
+ }
779
+ // Readline begins consuming immediately: create it only when its iterator can
780
+ // subscribe, after asynchronous model/bridge setup, or EOF can be lost.
781
+ if (!stopped) lines = createInterface({ input });
782
+ for await (const line of lines || []) {
783
+ const operation = (async () => {
784
+ let command;
785
+ try {
786
+ command = JSON.parse(line);
787
+ let result;
788
+ if (command.op === 'select') result = await host.select(command.index, command.contribute === true);
789
+ else if (command.op === 'state') result = host.state();
790
+ else if (command.op === 'reconcile') result = await host.reconcile();
791
+ else if (command.op === 'balances') result = await host.balances();
792
+ else if (command.op === 'generate') result = await host.generate(command.request,
793
+ delta => console.log(JSON.stringify({ id: command.id, delta })));
794
+ else if (command.op === 'cancel') result = host.cancel(command.attemptId) ?? { cancelled: true };
795
+ else if (command.op === 'outcome') result = await host.inspectOutcome(command.targetId);
796
+ else if (command.op === 'recover') result = await host.recover(command.input);
797
+ else throw Error('Unknown native fleet command');
798
+ console.log(JSON.stringify({ id: command.id, result }));
799
+ } catch (error) { console.log(JSON.stringify({ id: command?.id, error: error.message })); }
800
+ })();
801
+ pending.add(operation); operation.finally(() => pending.delete(operation));
802
+ }
803
+ if (config.daemon === true && !stopped) await daemonStopped;
804
+ } finally {
805
+ signals.removeListener('SIGTERM', stop); signals.removeListener('SIGINT', stop);
806
+ clearInterval(timer); await textApi?.close(); await browserBridge?.close(); await host.close(); await Promise.allSettled(pending);
807
+ }
808
+ }
809
+
810
+ // npm starts a package command through a symbolic link, so real paths are compared.
811
+ const entered = (() => { try { return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); } catch { return false; } })();
812
+ if (entered) {
813
+ const config = JSON.parse(await fs.readFile(process.argv[2], 'utf8'));
814
+ const host = await openFleetNativeHost({ configuration: config, onChange: value => process.stderr.write(JSON.stringify({ event: 'fleet-state', ...value }) + '\n') });
815
+ console.log(JSON.stringify({ ready: true, computerId: config.computerId, participantId: host.identity.peerId, publicJwk: host.identity.publicJwk }));
816
+ await runFleetNativeCommands({ host, config });
817
+ }