reploid 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +534 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +83 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalize } from '../../packages/reploid/src/artifacts/canonical-json.js';
|
|
3
|
+
import { assert, same } from './contract.js';
|
|
4
|
+
import { verifyCreditEvidence, verifyPartitionComponents } from './credit-evidence.js';
|
|
5
|
+
|
|
6
|
+
/** Durable admission fences and independently verified proof components. No timeout asserts
|
|
7
|
+
* remote cleanup. Deadline expiry only closes admission; an admitted owner must still sign. */
|
|
8
|
+
export function createCreditRecovery({ db, transaction, read, settle, enrollment, policy, now }) {
|
|
9
|
+
db.exec(`CREATE TABLE IF NOT EXISTS fleet_admissions (request_id TEXT NOT NULL, stage INTEGER NOT NULL, PRIMARY KEY(request_id,stage));
|
|
10
|
+
CREATE TABLE IF NOT EXISTS fleet_components (request_id TEXT NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(request_id,kind));`);
|
|
11
|
+
const admitted = id => db.prepare('SELECT stage FROM fleet_admissions WHERE request_id=?').all(id).map(row => row.stage);
|
|
12
|
+
const pieces = id => Object.fromEntries(db.prepare('SELECT kind,payload FROM fleet_components WHERE request_id=?').all(id)
|
|
13
|
+
.map(row => [row.kind, JSON.parse(row.payload)]));
|
|
14
|
+
const store = (id, key, value) => db.prepare('INSERT OR REPLACE INTO fleet_components VALUES (?,?,?)').run(id, key, JSON.stringify(value));
|
|
15
|
+
const expiry = () => transaction(() => {
|
|
16
|
+
for (const row of db.prepare("SELECT id FROM fleet_requests WHERE state IN ('reserved','dispatched')").all()) {
|
|
17
|
+
const request = read(row.id);
|
|
18
|
+
if (now() < request.binding.deadline) continue;
|
|
19
|
+
if (request.state === 'reserved') {
|
|
20
|
+
db.prepare('UPDATE fleet_accounts SET held=held-? WHERE id=?').run(request.binding.maximum, request.binding.computerId);
|
|
21
|
+
db.prepare("UPDATE fleet_requests SET state='abandoned' WHERE id=?").run(row.id);
|
|
22
|
+
} else db.prepare("UPDATE fleet_requests SET state='closing' WHERE id=?").run(row.id);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
expire: expiry,
|
|
27
|
+
unadmitted(id) {
|
|
28
|
+
const row = read(id);
|
|
29
|
+
// Old requests have no complete admission journal. Never infer zero work for those.
|
|
30
|
+
return ['closing', 'settled'].includes(row?.state) && row.binding.admissionVersion === 1
|
|
31
|
+
? row.binding.providers.map((_, index) => index).filter(index => !admitted(id).includes(index)) : [];
|
|
32
|
+
},
|
|
33
|
+
admit(id, stage) {
|
|
34
|
+
return transaction(() => {
|
|
35
|
+
const row = read(id);
|
|
36
|
+
assert(row?.state === 'dispatched' && now() <= row.binding.deadline, 'Credit admission is closed');
|
|
37
|
+
db.prepare('INSERT OR IGNORE INTO fleet_admissions VALUES (?,?)').run(id, stage);
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
async collect(id, evidence) {
|
|
41
|
+
const row = read(id); assert(row, 'Unknown credit reservation');
|
|
42
|
+
if (row.binding.mode === 'whole-model') {
|
|
43
|
+
assert(evidence?.terminal, 'Whole-model terminal cleanup required');
|
|
44
|
+
await verifyCreditEvidence(row.binding, evidence.terminal, enrollment, now(), policy.maxClockSkewMs);
|
|
45
|
+
if (row.state === 'settled') return settle(id, evidence.terminal);
|
|
46
|
+
transaction(() => {
|
|
47
|
+
const prior = pieces(id).terminal;
|
|
48
|
+
assert(!prior || prior.status === evidence.terminal.status, 'Conflicting terminal cleanup');
|
|
49
|
+
if (!prior) store(id, 'terminal', evidence.terminal);
|
|
50
|
+
db.prepare("UPDATE fleet_requests SET state='closing' WHERE id=? AND state='dispatched'").run(id);
|
|
51
|
+
});
|
|
52
|
+
return this.recover(id);
|
|
53
|
+
}
|
|
54
|
+
const proof = await verifyPartitionComponents(row.binding, evidence, enrollment, now(), policy.maxClockSkewMs);
|
|
55
|
+
if (proof.acknowledgment?.count > 0 && !proof.acknowledgedReceipt) {
|
|
56
|
+
proof.acknowledgedReceipt = proof.receipts.find(receipt => receipt.partitionIndex === row.binding.providers.length - 1
|
|
57
|
+
&& receipt.output?.count === proof.acknowledgment.count && receipt.output.chain === proof.acknowledgment.chain);
|
|
58
|
+
assert(proof.acknowledgedReceipt, 'Acknowledgment needs its signed output prefix');
|
|
59
|
+
}
|
|
60
|
+
if (row.state === 'settled') {
|
|
61
|
+
const ack = proof.acknowledgment;
|
|
62
|
+
assert(!ack || ack.count <= row.outcome.outputTokens, 'Conflicting completed evidence');
|
|
63
|
+
if (ack?.count === row.outcome.outputTokens && row.evidence?.acknowledgment) {
|
|
64
|
+
assert(ack.chain === row.evidence.acknowledgment.chain, 'Conflicting completed evidence');
|
|
65
|
+
}
|
|
66
|
+
return row.outcome;
|
|
67
|
+
}
|
|
68
|
+
transaction(() => {
|
|
69
|
+
const retained = pieces(id);
|
|
70
|
+
for (const receipt of proof.receipts) {
|
|
71
|
+
const key = `stage:${receipt.partitionIndex}`, prior = retained[key];
|
|
72
|
+
if (prior && prior.sequence > receipt.sequence) continue;
|
|
73
|
+
if (prior && prior.status !== 'executing') {
|
|
74
|
+
const semantic = ({ signature: _signature, publicJwk: _key, signedAt: _at, ...body }) => body;
|
|
75
|
+
assert(same(semantic(prior), semantic(receipt)), 'Conflicting stage closure');
|
|
76
|
+
}
|
|
77
|
+
store(id, key, receipt);
|
|
78
|
+
}
|
|
79
|
+
const ack = proof.acknowledgment, prior = retained.ack?.acknowledgment;
|
|
80
|
+
if (ack && (!prior || ack.count >= prior.count)) {
|
|
81
|
+
assert(!prior || ack.count !== prior.count || ack.chain === prior.chain, 'Conflicting acknowledged output');
|
|
82
|
+
store(id, 'ack', { acknowledgment: ack, acknowledgedReceipt: proof.acknowledgedReceipt });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return this.recover(id);
|
|
86
|
+
},
|
|
87
|
+
fence(id, consumer) {
|
|
88
|
+
transaction(() => {
|
|
89
|
+
const row = read(id); assert(row?.binding.consumer === consumer, 'Reservation consumer differs');
|
|
90
|
+
if (row.state === 'dispatched') db.prepare("UPDATE fleet_requests SET state='closing' WHERE id=?").run(id);
|
|
91
|
+
});
|
|
92
|
+
return this.recover(id);
|
|
93
|
+
},
|
|
94
|
+
async recoverFor(participant) {
|
|
95
|
+
const ids = db.prepare("SELECT id FROM fleet_requests WHERE state IN ('reserved','dispatched','closing')").all()
|
|
96
|
+
.filter(row => { const request = read(row.id).binding; return request.consumer === participant
|
|
97
|
+
|| request.providers.some(provider => provider.participantId === participant); });
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const { id } of ids) { const result = await this.recover(id); if (result.status !== 'pending') results.push(result); }
|
|
100
|
+
return results;
|
|
101
|
+
},
|
|
102
|
+
providerOpenRequests(participant) {
|
|
103
|
+
expiry();
|
|
104
|
+
return db.prepare("SELECT id FROM fleet_requests WHERE state IN ('dispatched','closing')").all()
|
|
105
|
+
.map(row => read(row.id))
|
|
106
|
+
.filter(row => row?.binding.mode === 'whole-model'
|
|
107
|
+
&& row.binding.providers[0]?.participantId === participant
|
|
108
|
+
&& !pieces(row.id).terminal)
|
|
109
|
+
.map(row => ({ id: row.id, binding: row.binding }));
|
|
110
|
+
},
|
|
111
|
+
async reconcileConsumer(consumer, activeIds = []) {
|
|
112
|
+
assert(Array.isArray(activeIds) && activeIds.length <= 1024 && activeIds.every(id => typeof id === 'string' && id.length <= 512),
|
|
113
|
+
'Active reservation list must be bounded text IDs');
|
|
114
|
+
expiry();
|
|
115
|
+
const activeSet = new Set(activeIds);
|
|
116
|
+
const abandonedResults = [];
|
|
117
|
+
const toRecover = transaction(() => {
|
|
118
|
+
const rows = db.prepare("SELECT id FROM fleet_requests WHERE state IN ('reserved','dispatched','closing')").all();
|
|
119
|
+
const pendingIds = [];
|
|
120
|
+
for (const { id } of rows) {
|
|
121
|
+
if (activeSet.has(id)) continue;
|
|
122
|
+
const current = read(id);
|
|
123
|
+
if (!current || current.binding.consumer !== consumer) continue;
|
|
124
|
+
if (current.state === 'reserved') {
|
|
125
|
+
db.prepare('UPDATE fleet_accounts SET held=held-? WHERE id=?').run(current.binding.maximum, current.binding.computerId);
|
|
126
|
+
db.prepare("UPDATE fleet_requests SET state='abandoned' WHERE id=?").run(id);
|
|
127
|
+
abandonedResults.push({ id, status: 'abandoned', charge: 0, outputTokens: 0, settledAt: now() });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (current.state === 'dispatched') {
|
|
131
|
+
db.prepare("UPDATE fleet_requests SET state='closing' WHERE id=?").run(id);
|
|
132
|
+
}
|
|
133
|
+
pendingIds.push(id);
|
|
134
|
+
}
|
|
135
|
+
return pendingIds;
|
|
136
|
+
});
|
|
137
|
+
const results = [...abandonedResults];
|
|
138
|
+
for (const id of toRecover) {
|
|
139
|
+
const result = await this.recover(id);
|
|
140
|
+
if (result.status !== 'pending') results.push(result);
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
},
|
|
144
|
+
async recover(id) {
|
|
145
|
+
expiry();
|
|
146
|
+
const row = read(id); assert(row, 'Unknown credit reservation');
|
|
147
|
+
if (row.state === 'settled') return row.outcome;
|
|
148
|
+
if (row.state === 'abandoned') return { id, status: 'abandoned' };
|
|
149
|
+
if (row.state !== 'closing') return { id, status: 'pending' };
|
|
150
|
+
const data = pieces(id), missing = this.unadmitted(id), request = row.binding;
|
|
151
|
+
if (request.mode === 'whole-model') {
|
|
152
|
+
if (data.terminal && request.terminalCharge != null) return settle(id, data.terminal);
|
|
153
|
+
if (missing.length !== request.providers.length) return { id, status: 'pending' };
|
|
154
|
+
// No provider could have started: admission was fenced in the same SQLite journal.
|
|
155
|
+
return transaction(() => {
|
|
156
|
+
const current = read(id); if (current.state === 'settled') return current.outcome;
|
|
157
|
+
const outcome = { id, unit: policy.unit, charge: 0, ownComputeUnits: 0, outputTokens: 0, credits: [],
|
|
158
|
+
evidenceHash: createHash('sha256').update(canonicalize({ id, unadmitted: missing })).digest('hex'), settledAt: now() };
|
|
159
|
+
db.prepare('UPDATE fleet_accounts SET held=held-? WHERE id=?').run(request.maximum, request.computerId);
|
|
160
|
+
db.prepare("UPDATE fleet_requests SET state='settled',outcome=? WHERE id=?").run(JSON.stringify(outcome), id);
|
|
161
|
+
return outcome;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!data.ack) return { id, status: 'pending' };
|
|
165
|
+
const receipts = request.providers.map((_, index) => data[`stage:${index}`]).filter(Boolean);
|
|
166
|
+
for (let index = 0; index < request.providers.length; index++) {
|
|
167
|
+
const receipt = receipts.find(value => value.partitionIndex === index);
|
|
168
|
+
if (!receipt && missing.includes(index) && data.ack.acknowledgment.count === 0) continue;
|
|
169
|
+
if (!receipt || receipt.status === 'executing' || receipt.timing.closedAt === null
|
|
170
|
+
|| receipt.steps < data.ack.acknowledgment.count) return { id, status: 'pending' };
|
|
171
|
+
}
|
|
172
|
+
return settle(id, { identity: request.route, receipts, ...data.ack });
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { canonicalize } from '../../packages/reploid/src/artifacts/canonical-json.js';
|
|
6
|
+
import { canonicalRouteAttemptBinding } from '../../packages/reploid/src/mesh/partitions/partition-contract.js';
|
|
7
|
+
import { assert, creditPolicy, digest, enrollComputers, integer, same, text } from './contract.js';
|
|
8
|
+
import { createCreditRecovery } from './credit-recovery.js';
|
|
9
|
+
import { verifyCreditEvidence } from './credit-evidence.js';
|
|
10
|
+
|
|
11
|
+
/** Non-redeemable mutual service credit. No purchased lots, issuance, or payout path. */
|
|
12
|
+
export async function openFleetCredits({ file, computers, policy: supplied, now = Date.now }) {
|
|
13
|
+
const policy = creditPolicy(supplied), enrollment = await enrollComputers(computers);
|
|
14
|
+
assert(path.isAbsolute(file), 'Absolute service-credit database path required');
|
|
15
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
16
|
+
const handle = await fs.open(file, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
17
|
+
try { assert((await handle.stat()).isFile(), 'Credit database must be a regular file'); } finally { await handle.close(); }
|
|
18
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
19
|
+
const db = new DatabaseSync(file);
|
|
20
|
+
db.exec(`PRAGMA synchronous=FULL; PRAGMA journal_mode=WAL;
|
|
21
|
+
CREATE TABLE IF NOT EXISTS fleet_accounts (id TEXT PRIMARY KEY, balance INTEGER NOT NULL DEFAULT 0, held INTEGER NOT NULL DEFAULT 0);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS fleet_requests (id TEXT PRIMARY KEY, binding TEXT NOT NULL, state TEXT NOT NULL, outcome TEXT);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS fleet_work (id TEXT PRIMARY KEY, request_id TEXT NOT NULL);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS fleet_evidence (request_id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS fleet_journal (sequence INTEGER PRIMARY KEY, request_id TEXT NOT NULL, account_id TEXT NOT NULL, delta INTEGER NOT NULL, at INTEGER NOT NULL);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS fleet_metadata (id TEXT PRIMARY KEY, value TEXT NOT NULL);`);
|
|
27
|
+
const transaction = work => {
|
|
28
|
+
db.exec('BEGIN IMMEDIATE');
|
|
29
|
+
try { const result = work(); db.exec('COMMIT'); return result; }
|
|
30
|
+
catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
transaction(() => {
|
|
34
|
+
// Existing balances cannot be reinterpreted using another unit, rate or enrolled owner.
|
|
35
|
+
for (const [id, value] of [['policy', policy], ['enrollment', computers]]) {
|
|
36
|
+
const encoded = canonicalize(value), prior = db.prepare('SELECT value FROM fleet_metadata WHERE id=?').get(id);
|
|
37
|
+
assert(!prior || prior.value === encoded, 'Credit policy or enrollment changed; explicit migration required');
|
|
38
|
+
db.prepare('INSERT OR IGNORE INTO fleet_metadata VALUES (?,?)').run(id, encoded);
|
|
39
|
+
}
|
|
40
|
+
for (const id of enrollment.byId.keys()) db.prepare('INSERT OR IGNORE INTO fleet_accounts (id) VALUES (?)').run(id);
|
|
41
|
+
});
|
|
42
|
+
} catch (error) { db.close(); throw error; }
|
|
43
|
+
const accountFor = peer => { const account = enrollment.byPeer.get(peer); assert(account, 'Unenrolled participant'); return account; };
|
|
44
|
+
const read = id => {
|
|
45
|
+
const row = db.prepare('SELECT * FROM fleet_requests WHERE id=?').get(id);
|
|
46
|
+
const evidence = row && db.prepare('SELECT payload FROM fleet_evidence WHERE request_id=?').get(id);
|
|
47
|
+
return row ? { ...row, binding: JSON.parse(row.binding), outcome: row.outcome && JSON.parse(row.outcome),
|
|
48
|
+
evidence: evidence ? JSON.parse(evidence.payload) : null } : null;
|
|
49
|
+
};
|
|
50
|
+
let ledger;
|
|
51
|
+
const recovery = createCreditRecovery({ db, transaction, read, settle: (id, evidence) => ledger.settle(id, evidence), enrollment, policy, now });
|
|
52
|
+
ledger = Object.freeze({
|
|
53
|
+
enrollment, policy, ...recovery,
|
|
54
|
+
reserve(input) {
|
|
55
|
+
recovery.expire();
|
|
56
|
+
const value = structuredClone(input), consumer = accountFor(value.consumer);
|
|
57
|
+
assert(text(value.id) && text(value.executionId) && ['whole-model', 'partition'].includes(value.mode)
|
|
58
|
+
&& text(value.model?.id) && digest(value.model.identity) && Array.isArray(value.model.adapterIdentities)
|
|
59
|
+
&& value.model.adapterIdentities.every(digest) && digest(value.inputDigest)
|
|
60
|
+
&& integer(value.maxOutputTokens) && value.maxOutputTokens > 0 && value.maxOutputTokens <= policy.maxOutputTokens
|
|
61
|
+
&& Array.isArray(value.providers) && value.providers.length > 0 && value.providers.length <= 128,
|
|
62
|
+
'Invalid credit reservation');
|
|
63
|
+
let weight = 0;
|
|
64
|
+
for (const provider of value.providers) {
|
|
65
|
+
const owner = accountFor(provider.participantId);
|
|
66
|
+
assert(integer(provider.weight) && provider.weight > 0 && (value.mode === 'partition' || owner.computerId !== consumer.computerId)
|
|
67
|
+
&& (policy.allowSameOwner || owner.ownerId !== consumer.ownerId), 'Ineligible credited provider');
|
|
68
|
+
weight += provider.weight;
|
|
69
|
+
}
|
|
70
|
+
assert(Number.isSafeInteger(weight), 'Invalid total stage weight');
|
|
71
|
+
if (value.mode === 'whole-model') assert(value.providers.length === 1 && value.route === undefined, 'Whole-model credit needs one provider');
|
|
72
|
+
else {
|
|
73
|
+
value.route = canonicalRouteAttemptBinding(value.route);
|
|
74
|
+
assert(value.route.attemptId === value.executionId, 'Partition attempt differs');
|
|
75
|
+
// Canonical route validation and exact provider ordering happen before disclosure.
|
|
76
|
+
const recipients = value.route.route.stages.map(stage => stage.participantId);
|
|
77
|
+
assert(Array.isArray(recipients) && same(recipients, value.providers.map(row => row.participantId)), 'Partition credit recipients differ');
|
|
78
|
+
assert(value.route.requesterId === value.consumer && value.route.route.modelId === value.model.id && value.route.route.modelIdentity === value.model.identity,
|
|
79
|
+
'Partition model differs');
|
|
80
|
+
}
|
|
81
|
+
const requestHash = createHash('sha256').update(canonicalize(value)).digest('hex');
|
|
82
|
+
return transaction(() => {
|
|
83
|
+
const prior = read(value.id);
|
|
84
|
+
if (prior) { assert(prior.binding.requestHash === requestHash, 'Reservation identity reused with different terms'); return prior.binding; }
|
|
85
|
+
const account = db.prepare('SELECT * FROM fleet_accounts WHERE id=?').get(consumer.computerId);
|
|
86
|
+
const pending = db.prepare("SELECT COUNT(*) AS count FROM fleet_requests WHERE state IN ('reserved','dispatched','closing') AND json_extract(binding,'$.computerId')=?").get(consumer.computerId).count;
|
|
87
|
+
const maximum = value.maxOutputTokens * policy.creditsPerOutputToken;
|
|
88
|
+
assert(pending < policy.maxPendingPerComputer && account.balance - account.held - maximum >= -policy.maxDebt,
|
|
89
|
+
'Service-credit admission limit reached');
|
|
90
|
+
const createdAt = now();
|
|
91
|
+
const binding = { ...value, requestHash, computerId: consumer.computerId, maximum,
|
|
92
|
+
policyVersion: policy.version, terminalCharge: value.mode === 'whole-model' ? policy.wholeModelTerminalCharge ?? null : null,
|
|
93
|
+
admissionVersion: 1, createdAt, deadline: createdAt + policy.maxRequestMs };
|
|
94
|
+
db.prepare("INSERT INTO fleet_requests VALUES (?,?,'reserved',NULL)").run(value.id, JSON.stringify(binding));
|
|
95
|
+
db.prepare('UPDATE fleet_accounts SET held=held+? WHERE id=?').run(maximum, consumer.computerId);
|
|
96
|
+
return binding;
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
async settle(id, evidence) {
|
|
100
|
+
evidence = structuredClone(evidence);
|
|
101
|
+
const row = read(id); assert(row, 'Unknown credit reservation');
|
|
102
|
+
const observed = await verifyCreditEvidence(row.binding, evidence, enrollment, now(), policy.maxClockSkewMs, { unadmitted: recovery.unadmitted(id) });
|
|
103
|
+
assert(!observed.terminalStatus || integer(observed.fixedCharge), 'Accepted fixed terminal terms required');
|
|
104
|
+
const economicHash = createHash('sha256').update(canonicalize(observed)).digest('hex');
|
|
105
|
+
const previousEconomicHash = row.outcome?.economicHash ?? (row.evidence ? createHash('sha256').update(canonicalize(
|
|
106
|
+
await verifyCreditEvidence(row.binding, row.evidence, enrollment, now(), policy.maxClockSkewMs))).digest('hex') : null);
|
|
107
|
+
const evidenceHash = createHash('sha256').update(canonicalize(evidence)).digest('hex');
|
|
108
|
+
return transaction(() => {
|
|
109
|
+
const current = read(id);
|
|
110
|
+
if (current.state === 'settled') {
|
|
111
|
+
assert(current.outcome.evidenceHash === evidenceHash || current.outcome.economicHash === economicHash || previousEconomicHash === economicHash, 'Conflicting completed evidence');
|
|
112
|
+
db.prepare('INSERT OR IGNORE INTO fleet_evidence VALUES (?,?)').run(id, JSON.stringify(evidence));
|
|
113
|
+
return current.outcome;
|
|
114
|
+
}
|
|
115
|
+
assert(['dispatched', 'closing'].includes(current.state), 'Request no longer admits settlement');
|
|
116
|
+
const request = current.binding, totalUnits = observed.fixedCharge ?? observed.outputTokens * policy.creditsPerOutputToken;
|
|
117
|
+
const totalWeight = request.providers.reduce((sum, row) => sum + row.weight, 0);
|
|
118
|
+
let remaining = totalUnits, ownComputeUnits = 0;
|
|
119
|
+
const credits = request.providers.map((provider, index) => {
|
|
120
|
+
// Integer arithmetic, stable stage-order remainder; every credited unit has a matching debit.
|
|
121
|
+
const amount = index === request.providers.length - 1 ? remaining
|
|
122
|
+
: Number(BigInt(totalUnits) * BigInt(provider.weight) / BigInt(totalWeight));
|
|
123
|
+
remaining -= amount;
|
|
124
|
+
const computerId = accountFor(provider.participantId).computerId;
|
|
125
|
+
if (computerId === request.computerId) ownComputeUnits += amount;
|
|
126
|
+
return { participantId: provider.participantId, computerId, amount };
|
|
127
|
+
}).filter(row => row.computerId !== request.computerId);
|
|
128
|
+
const charge = totalUnits - ownComputeUnits;
|
|
129
|
+
for (const work of observed.workIds) db.prepare('INSERT INTO fleet_work VALUES (?,?)').run(work, id);
|
|
130
|
+
const deltas = new Map([[request.computerId, -charge]]);
|
|
131
|
+
for (const credit of credits) deltas.set(credit.computerId, (deltas.get(credit.computerId) || 0) + credit.amount);
|
|
132
|
+
for (const [computerId, delta] of deltas) {
|
|
133
|
+
const account = db.prepare('SELECT balance FROM fleet_accounts WHERE id=?').get(computerId);
|
|
134
|
+
assert(Number.isSafeInteger(account.balance + delta), 'Credit balance exceeds integer precision');
|
|
135
|
+
db.prepare('UPDATE fleet_accounts SET balance=balance+? WHERE id=?').run(delta, computerId);
|
|
136
|
+
db.prepare('INSERT INTO fleet_journal (request_id,account_id,delta,at) VALUES (?,?,?,?)').run(id, computerId, delta, now());
|
|
137
|
+
}
|
|
138
|
+
db.prepare('UPDATE fleet_accounts SET held=held-? WHERE id=?').run(request.maximum, request.computerId);
|
|
139
|
+
const outcome = { id, unit: policy.unit, charge, ownComputeUnits, outputTokens: observed.outputTokens,
|
|
140
|
+
...(observed.terminalStatus ? { status: observed.terminalStatus, usage: null } : {}),
|
|
141
|
+
credits, evidenceHash, economicHash, settledAt: now() };
|
|
142
|
+
db.prepare('INSERT INTO fleet_evidence VALUES (?,?)').run(id, JSON.stringify(evidence));
|
|
143
|
+
db.prepare("UPDATE fleet_requests SET state='settled',outcome=? WHERE id=?").run(JSON.stringify(outcome), id);
|
|
144
|
+
return outcome;
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
dispatch(id, consumer) {
|
|
148
|
+
return transaction(() => {
|
|
149
|
+
const row = read(id);
|
|
150
|
+
assert(row?.binding.consumer === consumer && ['reserved', 'dispatched'].includes(row.state)
|
|
151
|
+
&& (row.state === 'dispatched' || now() < row.binding.deadline),
|
|
152
|
+
'Request cannot dispatch');
|
|
153
|
+
db.prepare("UPDATE fleet_requests SET state='dispatched' WHERE id=?").run(id);
|
|
154
|
+
return row.binding;
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
/** Only a request that never left the requester can release without execution evidence. */
|
|
158
|
+
abandonBeforeDispatch(id, consumer) {
|
|
159
|
+
return transaction(() => {
|
|
160
|
+
const row = read(id); assert(row?.binding.consumer === consumer, 'Reservation consumer differs');
|
|
161
|
+
assert(row.state === 'reserved', 'Reservation already closed');
|
|
162
|
+
db.prepare('UPDATE fleet_accounts SET held=held-? WHERE id=?').run(row.binding.maximum, row.binding.computerId);
|
|
163
|
+
db.prepare("UPDATE fleet_requests SET state='abandoned' WHERE id=?").run(id);
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
request: read,
|
|
167
|
+
snapshot() {
|
|
168
|
+
recovery.expire();
|
|
169
|
+
const accounts = db.prepare('SELECT * FROM fleet_accounts ORDER BY id').all().map(row => ({ ...row,
|
|
170
|
+
name: enrollment.byId.get(row.id).name, available: row.balance - row.held }));
|
|
171
|
+
const journal = db.prepare('SELECT * FROM fleet_journal ORDER BY sequence').all();
|
|
172
|
+
assert(accounts.reduce((sum, row) => sum + BigInt(row.balance), 0n) === 0n, 'Service credit conservation failed');
|
|
173
|
+
return { unit: policy.unit, policyVersion: policy.version, accounts, journal };
|
|
174
|
+
},
|
|
175
|
+
close() { db.close(); }
|
|
176
|
+
});
|
|
177
|
+
return ledger;
|
|
178
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { hashJson } from '../../packages/reploid/src/artifacts/signatures.js';
|
|
2
|
+
import { partitionAdapterIdentities } from '../../packages/reploid/src/mesh/partitions/partition-contract.js';
|
|
3
|
+
import { assert } from './contract.js';
|
|
4
|
+
|
|
5
|
+
/** Provider-side enforcement: skipping credit admission cannot obtain credited service. */
|
|
6
|
+
export function createFleetExecutionAdmission({ credits, model }) {
|
|
7
|
+
const exact = { id: model.id, identity: model.identity, adapterIdentities: partitionAdapterIdentities(model) };
|
|
8
|
+
const outputLimit = model.generation?.maxTokens;
|
|
9
|
+
assert(Number.isSafeInteger(outputLimit) && outputLimit > 0, 'Credited providers need an explicit output bound');
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
async whole({ request, consumer }) {
|
|
12
|
+
assert(request.consumer === consumer, 'Execution consumer differs from authenticated peer');
|
|
13
|
+
await credits.call('admit', { id: `service:${consumer}:${request.requestId}`, executionId: request.requestId,
|
|
14
|
+
consumer, mode: 'whole-model', model: exact, inputDigest: await hashJson(request.messages), outputLimit });
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
17
|
+
async partition(request) {
|
|
18
|
+
assert(request.charge?.reservationId && request.identity?.requesterId, 'Credited stage requires signed admission');
|
|
19
|
+
await credits.call('admit', { id: request.charge.reservationId, executionId: request.identity.attemptId,
|
|
20
|
+
consumer: request.identity.requesterId, mode: 'partition', model: exact, route: request.identity,
|
|
21
|
+
partitionIndex: request.partitionIndex, outputLimit: request.maxTokens });
|
|
22
|
+
},
|
|
23
|
+
partitionReceipt(bundle) { return credits.collect(bundle.reservationId, bundle); },
|
|
24
|
+
terminal(terminal) {
|
|
25
|
+
return credits.collect(`service:${terminal.consumer}:${terminal.requestId}`, { terminal });
|
|
26
|
+
},
|
|
27
|
+
async receipt(receipt) {
|
|
28
|
+
assert(receipt.jobHash?.startsWith('request:'), 'Whole-model request receipt required');
|
|
29
|
+
return credits.settle(`service:${receipt.consumer}:${receipt.jobHash.slice(8)}`, receipt);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|