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,30 @@
1
+ /** Append-only JSONL sink for the native host's readiness records (reploid/measurement readiness-records.js).
2
+ * Off unless the operator names an absolute path; there is no default location. Each record is one line, written in
3
+ * order. A line cut short by a crash is ended before the next write so it cannot join a later record; the CLI reports
4
+ * such lines as unreadable. Write failures go to `onError` and never stop contribution. */
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+
8
+ export async function openNativeReadinessLog({ file, onError = () => {} }) {
9
+ if (typeof file !== 'string' || !path.isAbsolute(file)) throw new Error('--readiness-log requires an absolute file path');
10
+ const handle = await fs.open(file, 'a+', 0o600);
11
+ try {
12
+ const stat = await handle.stat();
13
+ if (stat.isFile() && stat.size > 0) {
14
+ const last = Buffer.alloc(1);
15
+ await handle.read(last, 0, 1, stat.size - 1);
16
+ if (last[0] !== 0x0a) await handle.write('\n');
17
+ }
18
+ } catch (error) { await handle.close(); throw error; }
19
+ let tail = Promise.resolve(), closing = null;
20
+ return Object.freeze({
21
+ file,
22
+ write(record) {
23
+ if (closing) { onError(new Error('Readiness log is closed')); return; }
24
+ const line = JSON.stringify(record) + '\n';
25
+ tail = tail.then(() => handle.write(line)).catch(error => onError(error));
26
+ },
27
+ /** Waits for every accepted record, then closes the file. */
28
+ close: () => closing ||= tail.then(() => handle.close())
29
+ });
30
+ }
@@ -0,0 +1,64 @@
1
+ /** Native persistence for the application's existing monotonic release contract. */
2
+ import fs from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ const requireValue = (ok, message) => { if (!ok) throw new Error(`Native release storage: ${message}`); };
7
+ const valid = value => Number.isSafeInteger(value?.sequence) && value.sequence >= 0
8
+ && (value.sequence === 0 ? value.digest === null : /^sha256:[a-f0-9]{64}$/.test(value.digest));
9
+ const same = (a, b) => a.sequence === b.sequence && a.digest === b.digest;
10
+
11
+ export async function openNativeReleaseCheckpoints({ directory }) {
12
+ requireValue(typeof directory === 'string' && directory.length > 0, 'explicit directory required');
13
+ // Lazy native-only dependency: unsupported Node versions fail before signed
14
+ // opening; ordinary manifest participation does not import SQLite.
15
+ const { DatabaseSync } = await import('node:sqlite');
16
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
17
+ const filename = path.join(await fs.realpath(directory), 'release-checkpoints.sqlite');
18
+ const handle = await fs.open(filename, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
19
+ try { requireValue((await handle.stat()).isFile(), 'checkpoint store must be a regular file'); }
20
+ finally { await handle.close(); }
21
+ const db = new DatabaseSync(filename);
22
+ let closed = false;
23
+ try {
24
+ db.prepare('PRAGMA busy_timeout=1000').run();
25
+ db.prepare('PRAGMA synchronous=FULL').run();
26
+ db.prepare('CREATE TABLE IF NOT EXISTS checkpoints (namespace TEXT PRIMARY KEY, sequence INTEGER NOT NULL, digest TEXT)').run();
27
+ const read = db.prepare('SELECT sequence, digest FROM checkpoints WHERE namespace = ?');
28
+ const save = db.prepare('INSERT INTO checkpoints(namespace, sequence, digest) VALUES (?, ?, ?) ON CONFLICT(namespace) DO UPDATE SET sequence=excluded.sequence, digest=excluded.digest');
29
+ const transact = (namespace, update) => {
30
+ requireValue(!closed, 'store closed');
31
+ requireValue(/^sha256:[a-f0-9]{64}$/.test(namespace), 'bounded namespace digest required');
32
+ db.prepare('BEGIN IMMEDIATE').run();
33
+ try {
34
+ const row = read.get(namespace);
35
+ const current = row ? { sequence: row.sequence, digest: row.digest } : { sequence: 0, digest: null };
36
+ requireValue(valid(current), 'stored checkpoint is invalid');
37
+ const result = update(current);
38
+ save.run(namespace, result.sequence, result.digest);
39
+ db.prepare('COMMIT').run(); return result;
40
+ } catch (error) {
41
+ try { db.prepare('ROLLBACK').run(); }
42
+ catch (rollback) { throw new AggregateError([error, rollback], 'Native checkpoint transaction and rollback failed'); }
43
+ throw error;
44
+ }
45
+ };
46
+ return Object.freeze({
47
+ async read(namespace) { return transact(namespace, current => current); },
48
+ async advance(namespace, expected, next) {
49
+ requireValue(valid(expected) && valid(next) && next.sequence > 0, 'valid checkpoints required');
50
+ const prior = structuredClone(expected), observed = structuredClone(next);
51
+ return transact(namespace, current => {
52
+ if (same(current, observed)) return current;
53
+ requireValue(same(current, prior), 'checkpoint changed during verification');
54
+ requireValue(observed.sequence > current.sequence, 'rollback or fork rejected');
55
+ return observed;
56
+ });
57
+ },
58
+ close() { if (!closed) { db.close(); closed = true; } }
59
+ });
60
+ } catch (error) {
61
+ try { db.close(); } catch (cleanup) { throw new AggregateError([error, cleanup], 'Native checkpoint opening and cleanup failed'); }
62
+ throw error;
63
+ }
64
+ }
@@ -0,0 +1,369 @@
1
+ /** Sealed Mesa shader cache for native GPU contribution.
2
+ *
3
+ * Mesa Vulkan drivers persist compiled pipelines under MESA_SHADER_CACHE_DIR. Mesa ANV 24.2.8 stores a
4
+ * shader's NIR before its backend compile and the binary after it, so a process stopped mid-compile leaves
5
+ * NIR without a binary; every later process that needs that pipeline reloads the NIR and can crash
6
+ * (artifacts/native-crash-20261014). This protocol never lets a process inherit what an unfinished
7
+ * preparation wrote:
8
+ *
9
+ * - The cache is keyed by everything that determines the compiled pipelines: the installed Doppler runtime
10
+ * and its archive pin, the WebGPU provider, the Vulkan driver files, render devices and driver environment,
11
+ * and the model, Capsule, catalog entry, policy and stage.
12
+ * - Without a sealed cache for its key, a process compiles into its own new staging directory.
13
+ * - After the stage reports complete pipeline preparation, and once Mesa's cache writer threads are idle and
14
+ * the directory has stopped changing, a hashed snapshot of the staging directory is made read-only and
15
+ * renamed atomically to sealed/<key>. The process keeps its open handles on its own staging directory.
16
+ * - With a sealed cache, a process verifies every file against the seal record and works on a private copy.
17
+ * Mesa is never pointed at a sealed directory.
18
+ * - Every start removes sessions and promotions whose process has exited (an interrupted, killed or crashed
19
+ * preparation), so their contents are never reused.
20
+ *
21
+ * Linux only: process liveness and writer idleness come from /proc. The variable is set before the Vulkan
22
+ * driver initializes; the caller binds this before loading any native module that starts threads.
23
+ */
24
+ import fs from 'node:fs/promises';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { randomUUID } from 'node:crypto';
28
+ import { canonicalJson, sha256Hex as sha256 } from './hashing.js';
29
+
30
+ export const NATIVE_SHADER_CACHE_SCHEMA = 'reploid.native-shader-cache/v1';
31
+ const SEAL_SCHEMA = 'reploid.native-shader-cache-seal/v1';
32
+ /** Sealing requires `samples` consecutive idle-writer, unchanged-directory observations `intervalMs` apart. */
33
+ export const SHADER_CACHE_QUIESCENCE = Object.freeze({ intervalMs: 250, samples: 4, timeoutMs: 120000 });
34
+ const MANAGED = 'MESA_SHADER_CACHE_DIR';
35
+ const CONFLICTING = [MANAGED, 'MESA_SHADER_CACHE_DISABLE', 'MESA_GLSL_CACHE_DISABLE'];
36
+ const DRIVER_ENVIRONMENT = /^(MESA|INTEL|ANV|RADV|ACO|NIR|LP|GALLIUM|VK)_/;
37
+ const SESSION = /^([0-9a-f]{12})-(\d+)-(\d+)-([0-9a-f]{8})(?:\.rejected)?$/;
38
+ const PROMOTION = /^\.promote-([0-9a-f]{12}-\d+-\d+-[0-9a-f]{8})$/;
39
+
40
+ const requireValue = (condition, message) => { if (!condition) throw new Error(message); };
41
+ const missing = error => error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
42
+ const present = target => fs.lstat(target).then(() => true, error => { if (missing(error)) return false; throw error; });
43
+
44
+ export { canonicalJson };
45
+
46
+ /** Sorted relative paths of regular files (and symbolic links when `links` is true) below `root`. */
47
+ async function listTree(root, { links = false } = {}, relative = '') {
48
+ const rows = [];
49
+ const entries = await fs.readdir(path.join(root, relative), { withFileTypes: true });
50
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
51
+ for (const entry of entries) {
52
+ const name = relative ? relative + '/' + entry.name : entry.name;
53
+ if (entry.isDirectory()) rows.push(...await listTree(root, { links }, name));
54
+ else if (entry.isFile() || links && entry.isSymbolicLink()) rows.push(name);
55
+ else throw new Error(`Unexpected non-regular entry in ${root}: ${name}`);
56
+ }
57
+ return rows;
58
+ }
59
+
60
+ /** Content digest of an installed package tree: every file's path and bytes, links by target. */
61
+ export async function digestTree(root) {
62
+ const lines = [];
63
+ for (const name of await listTree(root, { links: true })) {
64
+ const file = path.join(root, name), stat = await fs.lstat(file);
65
+ lines.push(name + '\0' + (stat.isSymbolicLink() ? 'link:' + await fs.readlink(file) : sha256(await fs.readFile(file))));
66
+ }
67
+ return sha256(lines.join('\n'));
68
+ }
69
+
70
+ /** Nearest ancestor directory of `start` holding a package.json, optionally with the given package name. */
71
+ export async function installedPackageDirectory(start, name = null) {
72
+ let directory = path.resolve(start);
73
+ for (;;) {
74
+ const manifest = await fs.readFile(path.join(directory, 'package.json'), 'utf8').catch(error => {
75
+ if (missing(error)) return null; throw error;
76
+ });
77
+ if (manifest !== null && (name === null || JSON.parse(manifest).name === name)) return directory;
78
+ const parent = path.dirname(directory);
79
+ requireValue(parent !== directory, `No installed package${name ? ' ' + name : ''} contains ${start}`);
80
+ directory = parent;
81
+ }
82
+ }
83
+
84
+ /** The Vulkan loader's driver manifest search on Linux, including its override variables. */
85
+ function driverSearchPaths(env, home) {
86
+ const list = value => (value || '').split(':').filter(Boolean);
87
+ const explicit = env.VK_DRIVER_FILES || env.VK_ICD_FILENAMES;
88
+ if (explicit) return [...list(explicit), ...list(env.VK_ADD_DRIVER_FILES)];
89
+ const config = env.XDG_CONFIG_HOME || path.join(home, '.config'), data = env.XDG_DATA_HOME || path.join(home, '.local/share');
90
+ return [config, ...list(env.XDG_CONFIG_DIRS || '/etc/xdg'), '/etc', data, ...list(env.XDG_DATA_DIRS || '/usr/local/share:/usr/share')]
91
+ .map(base => path.join(base, 'vulkan/icd.d')).concat(list(env.VK_ADD_DRIVER_FILES));
92
+ }
93
+
94
+ async function vulkanDrivers(env, home) {
95
+ const manifests = [];
96
+ for (const entry of driverSearchPaths(env, home)) {
97
+ const stat = await fs.stat(entry).catch(error => { if (missing(error)) return null; throw error; });
98
+ if (stat?.isDirectory()) {
99
+ for (const name of (await fs.readdir(entry)).sort()) if (name.endsWith('.json')) manifests.push(path.join(entry, name));
100
+ } else if (stat?.isFile()) manifests.push(entry);
101
+ }
102
+ return Promise.all([...new Set(manifests)].map(async manifest => {
103
+ const bytes = await fs.readFile(manifest);
104
+ let library = null;
105
+ try { library = JSON.parse(bytes).ICD?.library_path ?? null; } catch { library = null; }
106
+ // Bare library names are resolved by the dynamic linker, not by a path the protocol can hash.
107
+ const file = typeof library === 'string' && library.includes('/') ? path.resolve(path.dirname(manifest), library) : null;
108
+ const contents = file && await fs.readFile(file).catch(error => { if (missing(error)) return null; throw error; });
109
+ return { manifest, manifestSha256: sha256(bytes), library, librarySha256: contents ? sha256(contents) : null };
110
+ }));
111
+ }
112
+
113
+ async function renderDevices(drmRoot) {
114
+ const names = await fs.readdir(drmRoot).catch(error => { if (missing(error)) return []; throw error; });
115
+ return Promise.all(names.filter(name => /^renderD\d+$/.test(name)).sort().map(async node => {
116
+ const read = file => fs.readFile(path.join(drmRoot, node, 'device', file), 'utf8').then(text => text.trim(), () => null);
117
+ const driver = await fs.readlink(path.join(drmRoot, node, 'device/driver')).then(link => path.basename(link), () => null);
118
+ return { node, vendor: await read('vendor'), device: await read('device'), revision: await read('revision'), driver };
119
+ }));
120
+ }
121
+
122
+ /** The cache key: runtime, provider, driver and program identity. Any change selects a different sealed cache. */
123
+ export async function nativeShaderCacheIdentity({ runtime, provider, program, env = process.env, home = os.homedir(),
124
+ drmRoot = '/sys/class/drm' }) {
125
+ requireValue(typeof runtime?.directory === 'string' && typeof runtime.archiveIntegrity === 'string' && runtime.archiveIntegrity,
126
+ 'Shader cache identity requires the installed Doppler runtime directory and its pinned archive integrity');
127
+ requireValue(typeof provider?.directory === 'string' && typeof provider.module === 'string',
128
+ 'Shader cache identity requires the selected WebGPU provider module and its installed directory');
129
+ const { model, policy, stage = null, canonical = false } = program || {};
130
+ requireValue(typeof model?.id === 'string' && typeof model.identity === 'string' && policy && typeof policy === 'object'
131
+ && (stage === null || Number.isSafeInteger(stage) && stage >= 0) && typeof canonical === 'boolean',
132
+ 'Shader cache identity requires the catalog model, partition policy, stage index or null, and canonical flag');
133
+ const manifest = async directory => JSON.parse(await fs.readFile(path.join(directory, 'package.json'), 'utf8'));
134
+ const [runtimePackage, providerPackage] = await Promise.all([manifest(runtime.directory), manifest(provider.directory)]);
135
+ const environment = Object.fromEntries(Object.keys(env).filter(name => DRIVER_ENVIRONMENT.test(name) && name !== MANAGED)
136
+ .sort().map(name => [name, env[name]]));
137
+ const identity = { schema: NATIVE_SHADER_CACHE_SCHEMA,
138
+ runtime: { name: runtimePackage.name, version: runtimePackage.version, archiveIntegrity: runtime.archiveIntegrity,
139
+ installedSha256: await digestTree(runtime.directory) },
140
+ provider: { module: provider.module, name: providerPackage.name, version: providerPackage.version,
141
+ installedSha256: await digestTree(provider.directory) },
142
+ driver: { icds: await vulkanDrivers(env, home), devices: await renderDevices(drmRoot), environment },
143
+ program: { modelId: model.id, modelIdentity: model.identity,
144
+ capsule: model.capsule ? model.capsule.semanticRoot ?? model.capsule.capsuleId ?? null : null,
145
+ catalogEntrySha256: sha256(canonicalJson(model)), policySha256: sha256(canonicalJson(policy)), stage, canonical } };
146
+ return { key: sha256(canonicalJson(identity)), identity };
147
+ }
148
+
149
+ const processStart = async (proc, pid) => {
150
+ const stat = await fs.readFile(path.join(proc, String(pid), 'stat'), 'utf8');
151
+ return stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19];
152
+ };
153
+
154
+ /** Mesa names its disk cache writer threads `<process>:disk$<n>`; an idle writer sleeps on its queue. */
155
+ export async function observeMesaCacheWriters(proc = '/proc') {
156
+ const tasks = path.join(proc, 'self/task');
157
+ let writers = 0, busy = 0;
158
+ for (const tid of await fs.readdir(tasks)) {
159
+ const comm = await fs.readFile(path.join(tasks, tid, 'comm'), 'utf8').then(text => text.trim(), () => '');
160
+ if (!/disk\$\d*$/.test(comm)) continue;
161
+ const stat = await fs.readFile(path.join(tasks, tid, 'stat'), 'utf8').catch(() => null);
162
+ if (stat === null) continue;
163
+ writers++;
164
+ if (stat[stat.lastIndexOf(')') + 2] !== 'S') busy++;
165
+ }
166
+ return { writers, busy };
167
+ }
168
+
169
+ async function treeSignature(root) {
170
+ const rows = [];
171
+ for (const name of await listTree(root)) {
172
+ const stat = await fs.stat(path.join(root, name));
173
+ rows.push([name, stat.size, stat.mtimeMs]);
174
+ }
175
+ return canonicalJson(rows);
176
+ }
177
+
178
+ async function writePrivate(file, bytes) {
179
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
180
+ const handle = await fs.open(file, 'wx', 0o600);
181
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
182
+ }
183
+
184
+ async function syncDirectory(directory) {
185
+ const handle = await fs.open(directory, 'r');
186
+ try { await handle.sync(); } finally { await handle.close(); }
187
+ }
188
+
189
+ /** Removes a tree, including sealed read-only directories. */
190
+ async function removeTree(target) {
191
+ const open = async directory => {
192
+ await fs.chmod(directory, 0o700);
193
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
194
+ if (entry.isDirectory()) await open(path.join(directory, entry.name));
195
+ }
196
+ };
197
+ try { await open(target); } catch (error) { if (missing(error)) return; throw error; }
198
+ await fs.rm(target, { recursive: true, force: true });
199
+ }
200
+
201
+ async function sealReadOnly(directory) {
202
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
203
+ const target = path.join(directory, entry.name);
204
+ if (entry.isDirectory()) await sealReadOnly(target); else await fs.chmod(target, 0o444);
205
+ }
206
+ await fs.chmod(directory, 0o555);
207
+ }
208
+
209
+ /** Verifies a sealed cache against its record and copies it into a private writable directory. */
210
+ async function copySealed(sealed, key, identity, target) {
211
+ const record = JSON.parse(await fs.readFile(path.join(sealed, 'seal.json'), 'utf8'));
212
+ requireValue(record.schema === SEAL_SCHEMA && record.key === key && canonicalJson(record.identity) === canonicalJson(identity),
213
+ 'seal record does not match this identity');
214
+ const names = await listTree(path.join(sealed, 'cache'));
215
+ requireValue(Array.isArray(record.files) && canonicalJson(names) === canonicalJson(record.files.map(file => file.path))
216
+ && sha256(canonicalJson(record.files)) === record.contentSha256, 'sealed files differ from the seal record');
217
+ for (const file of record.files) {
218
+ const bytes = await fs.readFile(path.join(sealed, 'cache', file.path));
219
+ requireValue(bytes.length === file.bytes && sha256(bytes) === file.sha256, `sealed file changed: ${file.path}`);
220
+ await writePrivate(path.join(target, file.path), bytes);
221
+ }
222
+ return { files: record.files.length, bytes: record.files.reduce((total, file) => total + file.bytes, 0), sealedAt: record.sealedAt };
223
+ }
224
+
225
+ /** Binds this process to its sealed cache, or to a new staging directory, and sets MESA_SHADER_CACHE_DIR. */
226
+ export async function openNativeShaderCache({ root, key, identity, env = process.env, platform = process.platform, proc = '/proc',
227
+ pid = process.pid, observeWriters = () => observeMesaCacheWriters(proc), quiescence = SHADER_CACHE_QUIESCENCE,
228
+ now = () => performance.now(), sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), clock = () => new Date(),
229
+ onEvent = () => {} }) {
230
+ requireValue(platform === 'linux', `The sealed shader cache protocol requires Linux (process liveness and Mesa writer state come from /proc); this host is ${platform}. Native GPU contribution is qualified on Linux/Vulkan only.`);
231
+ requireValue(typeof root === 'string' && path.isAbsolute(root), 'The shader cache root must be an absolute directory (--shader-cache-root)');
232
+ for (const name of CONFLICTING) {
233
+ requireValue(env[name] === undefined, `${name} is set to ${JSON.stringify(env[name])}, but the native participant manages the Mesa shader cache under ${root} with the sealed-cache protocol. Unset ${name}` + (name === MANAGED ? '; choose the location with --shader-cache-root.' : '.'));
234
+ }
235
+ requireValue(/^[0-9a-f]{64}$/.test(key) && identity && typeof identity === 'object', 'Shader cache binding requires an identity and its key');
236
+ const base = path.join(root, 'v1'), sealedRoot = path.join(base, 'sealed'), sessionsRoot = path.join(base, 'sessions');
237
+ let boot, start;
238
+ try {
239
+ await fs.mkdir(sealedRoot, { recursive: true, mode: 0o700 }); await fs.mkdir(sessionsRoot, { recursive: true, mode: 0o700 });
240
+ await fs.access(base, fs.constants.W_OK);
241
+ } catch (error) { throw new Error(`Shader cache root ${root} is not a writable directory (${error.code || error.message}); choose a private directory with --shader-cache-root`); }
242
+ try {
243
+ boot = (await fs.readFile(path.join(proc, 'sys/kernel/random/boot_id'), 'utf8')).trim().replaceAll('-', '').slice(0, 12);
244
+ start = await processStart(proc, pid);
245
+ requireValue(/^[0-9a-f]{12}$/.test(boot) && /^\d+$/.test(start), 'unrecognized /proc identity');
246
+ } catch (error) { throw new Error(`The sealed shader cache protocol could not read this process's identity from ${proc}: ${error.message}`); }
247
+ const alive = async owner => owner[1] === boot && await processStart(proc, owner[2]).then(value => value === owner[3], () => false);
248
+ // Sweep: anything a dead process left behind is discarded, never inherited.
249
+ const discarded = [];
250
+ for (const name of await fs.readdir(sessionsRoot)) {
251
+ const owner = SESSION.exec(name);
252
+ if (owner && await alive(owner)) continue;
253
+ await removeTree(path.join(sessionsRoot, name));
254
+ discarded.push({ name, reason: owner ? 'owner process exited before sealing' : 'unrecognized session entry' });
255
+ }
256
+ for (const name of await fs.readdir(sealedRoot)) {
257
+ const promotion = PROMOTION.exec(name), owner = promotion && SESSION.exec(promotion[1]);
258
+ if (!promotion || owner && await alive(owner)) continue;
259
+ await removeTree(path.join(sealedRoot, name));
260
+ discarded.push({ name, reason: 'promotion interrupted' });
261
+ }
262
+ const session = `${boot}-${pid}-${start}-${randomUUID().replaceAll('-', '').slice(0, 8)}`;
263
+ const sessionDirectory = path.join(sessionsRoot, session), directory = path.join(sessionDirectory, 'cache');
264
+ const sealed = path.join(sealedRoot, key);
265
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
266
+ let mode = 'cold', loaded = null, rejected = null;
267
+ if (await present(sealed)) {
268
+ try { loaded = await copySealed(sealed, key, identity, directory); mode = 'warm'; }
269
+ catch (error) {
270
+ rejected = error.message;
271
+ // Move the rejected seal out of lookup atomically (a directory needs write permission to change
272
+ // parent), then remove it; this start compiles cold.
273
+ const aside = path.join(sessionsRoot, session + '.rejected');
274
+ await fs.chmod(sealed, 0o700).then(() => fs.rename(sealed, aside)).then(() => removeTree(aside),
275
+ cause => { if (!missing(cause)) throw cause; });
276
+ await removeTree(directory); await fs.mkdir(directory, { recursive: true, mode: 0o700 });
277
+ }
278
+ }
279
+ env[MANAGED] = directory;
280
+ let seal = null, sealing = null, closing = null, cancelled = false;
281
+ const status = () => ({ schema: NATIVE_SHADER_CACHE_SCHEMA, root, key, mode, session, directory,
282
+ sealed: mode === 'warm' || seal?.status === 'sealed' || seal?.status === 'already-sealed', loaded, discarded: structuredClone(discarded), rejected, seal: structuredClone(seal) });
283
+ onEvent({ phase: 'bound', ...status() });
284
+
285
+ async function waitForQuiescence(started) {
286
+ let previous = null, stable = 0;
287
+ while (!cancelled && now() - started <= quiescence.timeoutMs) {
288
+ const writers = await observeWriters(), signature = await treeSignature(directory);
289
+ if (writers.writers === 0) return { quiescent: false, reason: 'no Mesa shader cache writer threads in this process; the driver does not use MESA_SHADER_CACHE_DIR' };
290
+ stable = writers.busy === 0 && signature === previous ? stable + 1 : 0;
291
+ previous = signature;
292
+ if (stable >= quiescence.samples) return { quiescent: true, signature };
293
+ await sleep(quiescence.intervalMs);
294
+ }
295
+ return { quiescent: false, reason: cancelled ? 'cancelled by shutdown'
296
+ : `Mesa shader cache writers were not idle with an unchanged directory for ${quiescence.samples} samples within ${quiescence.timeoutMs} ms` };
297
+ }
298
+
299
+ async function promote(preparation) {
300
+ const started = now();
301
+ if (!(preparation?.complete === true && Number.isSafeInteger(preparation.count) && preparation.count > 0)) {
302
+ return { status: 'refused', reason: 'stage did not report complete pipeline preparation' };
303
+ }
304
+ const program = { count: preparation.count, durationMs: preparation.durationMs ?? null, planId: preparation.planId ?? null,
305
+ index: preparation.index ?? null, modelIdentity: preparation.modelIdentity ?? null,
306
+ pipelinesSha256: sha256(canonicalJson((preparation.pipelines || []).map(row => row?.key ?? row))) };
307
+ const promotion = path.join(sealedRoot, '.promote-' + session);
308
+ for (;;) {
309
+ const idle = await waitForQuiescence(started);
310
+ if (!idle.quiescent) return { status: 'refused', reason: idle.reason };
311
+ if ((await listTree(directory)).length === 0) return { status: 'refused', reason: 'the driver wrote nothing to MESA_SHADER_CACHE_DIR' };
312
+ await removeTree(promotion);
313
+ const files = [];
314
+ for (const name of await listTree(directory)) {
315
+ const bytes = await fs.readFile(path.join(directory, name));
316
+ await writePrivate(path.join(promotion, 'cache', name), bytes);
317
+ files.push({ path: name, bytes: bytes.length, sha256: sha256(bytes) });
318
+ }
319
+ // A write during the copy invalidates this snapshot; wait for quiescence again.
320
+ const after = await observeWriters();
321
+ if (cancelled || after.busy !== 0 || await treeSignature(directory) !== idle.signature) {
322
+ await removeTree(promotion);
323
+ if (cancelled) return { status: 'refused', reason: 'cancelled by shutdown' };
324
+ continue;
325
+ }
326
+ const record = { schema: SEAL_SCHEMA, key, identity, sealedAt: clock().toISOString(), session, program, files,
327
+ contentSha256: sha256(canonicalJson(files)) };
328
+ await writePrivate(path.join(promotion, 'seal.json'), JSON.stringify(record, null, 1) + '\n');
329
+ await sealReadOnly(promotion); await syncDirectory(promotion);
330
+ // rename(2) never replaces a non-empty directory, so a seal another process completed first stays.
331
+ const raced = { status: 'already-sealed', reason: 'another process sealed this identity first' };
332
+ if (await present(sealed)) { await removeTree(promotion); return raced; }
333
+ try { await fs.rename(promotion, sealed); }
334
+ catch (error) {
335
+ await removeTree(promotion);
336
+ if (await present(sealed)) return raced;
337
+ throw error;
338
+ }
339
+ await syncDirectory(sealedRoot);
340
+ return { status: 'sealed', files: files.length, bytes: files.reduce((total, file) => total + file.bytes, 0),
341
+ waitedMs: Math.round(now() - started), program };
342
+ }
343
+ }
344
+
345
+ return Object.freeze({
346
+ status,
347
+ /** Seals at most once, after a complete stage preparation; a refused or failed attempt leaves a later
348
+ * preparation free to try. Never rejects; every outcome is reported. */
349
+ seal(preparation) {
350
+ if (mode === 'warm') return Promise.resolve({ status: 'sealed-before-start' });
351
+ if (closing) return Promise.resolve({ status: 'refused', reason: 'closed' });
352
+ if (sealing) return sealing;
353
+ return sealing = promote(preparation).catch(error => ({ status: 'failed', reason: error.message })).then(outcome => {
354
+ seal = outcome; onEvent({ phase: 'seal', key, session, ...outcome });
355
+ if (outcome.status !== 'sealed' && outcome.status !== 'already-sealed') sealing = null;
356
+ return outcome;
357
+ });
358
+ },
359
+ /** Removes this process's private cache after its GPU provider is released; a pending seal is cancelled.
360
+ * Never rejects: a directory left behind belongs to an exited process and the next start removes it. */
361
+ close() {
362
+ return closing ||= (async () => {
363
+ cancelled = true; await sealing;
364
+ try { await removeTree(sessionDirectory); onEvent({ phase: 'closed', key, session }); }
365
+ catch (error) { onEvent({ phase: 'close-failed', key, session, reason: error.message }); }
366
+ })();
367
+ },
368
+ });
369
+ }
@@ -0,0 +1,66 @@
1
+ /** Process-safe cumulative supply allowance. A successful reservation is never refunded. */
2
+ import fs from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { custodyBudgetError, reserveCustodyBudget } from '../packages/reploid/src/artifacts/custody/supply-budget.js';
6
+
7
+ export async function openNativeSupplyBudget(options) {
8
+ try { return await openStore(options); }
9
+ catch (cause) {
10
+ if (cause.code?.startsWith('CUSTODY_BUDGET_')) throw cause;
11
+ throw custodyBudgetError('STORAGE', 'File contribution ledger unavailable; serving is blocked', cause);
12
+ }
13
+ }
14
+ async function openStore({ directory, maxBytes }) {
15
+ const { DatabaseSync } = await import('node:sqlite');
16
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
17
+ const root = await fs.realpath(directory), filename = path.join(root, 'custody-budget.sqlite');
18
+ const entries = await fs.readdir(root);
19
+ const legacy = entries.some(name => !name.startsWith('custody-budget.sqlite'));
20
+ const handle = await fs.open(filename, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
21
+ let existed;
22
+ try { const stat = await handle.stat(); if (!stat.isFile()) throw custodyBudgetError('STORAGE', 'Budget store must be a regular file'); existed = stat.size > 0; }
23
+ finally { await handle.close(); }
24
+ const db = new DatabaseSync(filename); let closed = false, state;
25
+ const transaction = bytes => {
26
+ if (closed) throw custodyBudgetError('STORAGE', 'File contribution budget closed');
27
+ db.prepare('BEGIN IMMEDIATE').run();
28
+ try {
29
+ const row = db.prepare('SELECT record FROM allowance WHERE id = 1').get();
30
+ let record;
31
+ try { record = row && JSON.parse(row.record); }
32
+ catch (cause) { throw custodyBudgetError('CORRUPT', 'File contribution ledger is corrupt', cause); }
33
+ const next = reserveCustodyBudget(record, bytes, maxBytes);
34
+ db.prepare('UPDATE allowance SET record = ? WHERE id = 1').run(JSON.stringify(next));
35
+ db.prepare('COMMIT').run(); state = next; return { ...state };
36
+ } catch (error) {
37
+ try { db.prepare('ROLLBACK').run(); }
38
+ catch (rollback) { throw new AggregateError([error, rollback], 'Budget transaction and rollback failed'); }
39
+ throw error;
40
+ }
41
+ };
42
+ try {
43
+ db.prepare('PRAGMA busy_timeout=1000').run();
44
+ db.prepare('PRAGMA synchronous=FULL').run();
45
+ db.prepare('BEGIN IMMEDIATE').run();
46
+ try {
47
+ db.prepare('CREATE TABLE IF NOT EXISTS allowance (id INTEGER PRIMARY KEY CHECK(id=1), record TEXT NOT NULL)').run();
48
+ if (!db.prepare('SELECT id FROM allowance WHERE id=1').get()) {
49
+ if (existed || legacy) throw custodyBudgetError('MISSING', 'File contribution ledger missing for existing cache; allowance cannot be renewed');
50
+ const initial = { schema: 'reploid.custody-supply-budget/v1', maxBytes, reservedBytes: 0 };
51
+ reserveCustodyBudget(initial, 0, maxBytes);
52
+ db.prepare('INSERT INTO allowance VALUES (1, ?)').run(JSON.stringify(initial));
53
+ }
54
+ db.prepare('COMMIT').run();
55
+ } catch (error) { db.prepare('ROLLBACK').run(); throw error; }
56
+ transaction(0);
57
+ return Object.freeze({ getState: () => ({ ...state }), reserve: async bytes => {
58
+ try { return transaction(bytes); }
59
+ catch (cause) {
60
+ if (cause.code?.startsWith('CUSTODY_BUDGET_')) throw cause;
61
+ throw custodyBudgetError('STORAGE', 'File contribution reservation not confirmed', cause);
62
+ }
63
+ },
64
+ close() { if (!closed) { db.close(); closed = true; } } });
65
+ } catch (error) { db.close(); throw error; }
66
+ }
@@ -0,0 +1,67 @@
1
+ /** Durable whole-model contribution references. The service owns validation,
2
+ * settlement and issuance; this file retains no prompts or generated text. */
3
+ import { verifyReceipt } from '../packages/reploid/src/artifacts/receipt.js';
4
+ import { sha256Hex } from './hashing.js';
5
+
6
+ const STORE = 'whole-model-work';
7
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
8
+ const text = value => typeof value === 'string' && value.length > 0 && value.length <= 512;
9
+ const assert = (ok, message) => { if (!ok) throw Error(message); };
10
+
11
+ export function createNativeWholeEarning({ storage, participantId, ownerPolicyDigest, findOffer, contributionFor, now }) {
12
+ let queue = Promise.resolve();
13
+ const serial = operation => {
14
+ const running = queue.then(operation); queue = running.catch(() => {}); return running;
15
+ };
16
+ const read = async () => await storage.get(STORE) || [];
17
+ return Object.freeze({
18
+ async prepare({ model, requestId, consumer }) {
19
+ assert(model.executionMode === 'whole-model' && text(requestId) && text(consumer), 'Whole-model admission identity required');
20
+ model = structuredClone(model);
21
+ const offer = await findOffer(model);
22
+ assert(offer, 'Whole-model offer must be accepted before admitting requests');
23
+ return serial(async () => {
24
+ const rows = await read(), jobHash = `request:${requestId}`;
25
+ const admissionKey = 'whole-admission:' + sha256Hex(JSON.stringify([participantId, consumer, requestId]));
26
+ const prior = await storage.get(admissionKey);
27
+ assert(!prior, 'Whole-model admission already recorded; do not repeat execution');
28
+ const record = { jobHash, consumer, offerId: offer.offerId, model, startedAt: now(), receiptId: null, ownerPolicyDigest };
29
+ // Keep the admission tombstone after the pending claim is retired, so a
30
+ // replayed request cannot execute again following a contributor restart.
31
+ await storage.set(admissionKey, record);
32
+ await storage.set(STORE, [...rows, record]);
33
+ return record;
34
+ });
35
+ },
36
+ record(receipt) {
37
+ receipt = structuredClone(receipt);
38
+ return serial(async () => {
39
+ if (receipt.version !== 2 || receipt.provider !== participantId || receipt.ownerPolicyDigest !== ownerPolicyDigest) return false;
40
+ const verified = await verifyReceipt(receipt);
41
+ assert(verified.providerValid && verified.consumerValid, 'Countersigned whole-model receipt required');
42
+ const rows = await read(), record = rows.find(row => row.jobHash === receipt.jobHash && row.consumer === receipt.consumer);
43
+ if (!record) return false; // No accepted offer was bound before this execution.
44
+ assert(record.ownerPolicyDigest === receipt.ownerPolicyDigest && record.model.modelId === receipt.model
45
+ && record.model.modelIdentity === receipt.modelIdentity && same(record.model.adapterIdentities, receipt.adapterIdentities),
46
+ 'Whole-model receipt changed the admitted model or owner policy');
47
+ assert(!record.receiptId || record.receiptId === receipt.receiptId, 'Whole-model receipt identity changed');
48
+ record.receiptId = receipt.receiptId;
49
+ await storage.set(STORE, rows);
50
+ return true;
51
+ });
52
+ },
53
+ flush() {
54
+ return serial(async () => {
55
+ const rows = await read(), results = [], pending = [];
56
+ for (const record of rows) {
57
+ if (!record.receiptId) { pending.push(record); continue; }
58
+ const [result] = await contributionFor(record.offerId).claimWholeModels([record]);
59
+ results.push(result);
60
+ if (result?.status !== 'done') pending.push(record);
61
+ }
62
+ await storage.set(STORE, pending);
63
+ return results;
64
+ });
65
+ }
66
+ });
67
+ }