reploid 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +542 -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 +85 -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,310 @@
|
|
|
1
|
+
/** Readiness spans and stage admissions as a contributor host records them, and the summary inputs read back from them.
|
|
2
|
+
*
|
|
3
|
+
* A host gives the recorder each automatic-partitions state it observes (`observe`) and each resident it prepares
|
|
4
|
+
* (`watchResident`). A stage is `ready` while the host offers it, its phase is `ready`, Doppler reports the resident
|
|
5
|
+
* ready and it holds or admits at least one attempt slot; its capacity is those slots (`active + availableSlots`, after
|
|
6
|
+
* any owner gate). Every other observed state of a stage this session has seen is `unready`, and a stage first seen
|
|
7
|
+
* at time t was unready on this host from the session start to t. A span is written when it opens, at each checkpoint
|
|
8
|
+
* and when it closes; every copy is complete, so a bounded store may keep only the latest. Admissions are written when
|
|
9
|
+
* reserved, at checkpoints while held and when settled.
|
|
10
|
+
*
|
|
11
|
+
* Records carry identities, times and slot counts only: never conversation text, thread or requester identities, or
|
|
12
|
+
* error text. Unknown stays unknown: before a session first sees a stage, after an open span's last observation (a
|
|
13
|
+
* crashed host, or one still running when its records were exported), and over an observation gap longer than
|
|
14
|
+
* `maxObservationGapMs` while ready (a suspended machine is not serving) no span covers the time. */
|
|
15
|
+
import { READINESS_SPAN_SCHEMA, CAPACITY_UNITS } from './capacity-utilization.js';
|
|
16
|
+
|
|
17
|
+
export const STAGE_ADMISSION_SCHEMA = 'reploid.stage-admission/v1';
|
|
18
|
+
export const UTILIZATION_INPUT_SCHEMA = 'reploid.capacity-utilization-input/v1';
|
|
19
|
+
export const READINESS_CHECKPOINT_MS = 60_000;
|
|
20
|
+
export const READINESS_MAX_OBSERVATION_GAP_MS = 60_000;
|
|
21
|
+
export const READINESS_STORE_MAX_RECORDS = 500;
|
|
22
|
+
export const READINESS_HOSTS = Object.freeze(['node', 'browser']);
|
|
23
|
+
|
|
24
|
+
const fail = message => { throw new Error(`Readiness records: ${message}`); };
|
|
25
|
+
const time = value => typeof value === 'number' && Number.isFinite(value);
|
|
26
|
+
const text = value => typeof value === 'string' && value.length > 0;
|
|
27
|
+
const count = value => Number.isSafeInteger(value) && value >= 0;
|
|
28
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
29
|
+
const keyOf = (...parts) => JSON.stringify(parts);
|
|
30
|
+
const stageKey = stage => keyOf(stage.participantId, stage.modelIdentity, stage.planId, stage.stageIndex);
|
|
31
|
+
|
|
32
|
+
/** The stage an automatic-partitions state holds, whether it is offered ready, and the slots it holds or admits. */
|
|
33
|
+
function observedStage(state, participantId) {
|
|
34
|
+
const descriptor = state?.descriptor;
|
|
35
|
+
if (!descriptor || !text(descriptor.modelIdentity) || !text(descriptor.planId) || !count(descriptor.index)) return null;
|
|
36
|
+
const reservations = state.reservations;
|
|
37
|
+
const amount = reservations && reservations.closed !== true && count(reservations.active) && count(reservations.availableSlots)
|
|
38
|
+
? reservations.active + reservations.availableSlots : 0;
|
|
39
|
+
const ready = state.offering === true && state.phase === 'ready' && descriptor.ready === true && amount > 0;
|
|
40
|
+
return { key: null, ready, capacity: ready ? { unit: 'attempt-slots', amount } : null,
|
|
41
|
+
stage: { participantId, modelIdentity: descriptor.modelIdentity, planId: descriptor.planId, stageIndex: descriptor.index } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Why a ready span ended, from the state that ended it. */
|
|
45
|
+
function readyEnd(state, key, current) {
|
|
46
|
+
if (current?.ready && current.key === key) return 'capacity-changed';
|
|
47
|
+
if (state?.phase === 'failed') return 'failed';
|
|
48
|
+
if (state?.offering !== true) return 'stopped';
|
|
49
|
+
if (current?.key === key) return 'not-admitting';
|
|
50
|
+
return current ? 'stage-changed' : 'unprepared';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The fields the summary reads from a resident reservation record; tombstones never held a slot. */
|
|
54
|
+
function admissionOf(resident, record) {
|
|
55
|
+
const identity = record?.identity, route = identity?.route;
|
|
56
|
+
if (!time(record?.reservedAt) || !text(identity?.attemptId)) return null;
|
|
57
|
+
const participantId = record.participantId ?? resident.participantId, index = record.index ?? resident.index;
|
|
58
|
+
if (!text(participantId) || !count(index)) return null;
|
|
59
|
+
const amounts = record.resources && Object.fromEntries(['attemptSlots', 'attemptBytes']
|
|
60
|
+
.filter(name => count(record.resources[name])).map(name => [name, record.resources[name]]));
|
|
61
|
+
return { participantId, index, phase: text(record.phase) ? record.phase : null,
|
|
62
|
+
reservedAt: record.reservedAt, settlingAt: time(record.settlingAt) ? record.settlingAt : null,
|
|
63
|
+
settledAt: time(record.settledAt) ? record.settledAt : null, failure: record.failure == null ? null : 'failed',
|
|
64
|
+
identity: route ? { attemptId: identity.attemptId, route: { modelIdentity: route.modelIdentity, planId: route.planId,
|
|
65
|
+
stages: (Array.isArray(route.stages) ? route.stages : []).map(row => ({ index: row?.index, participantId: row?.participantId })) } }
|
|
66
|
+
: { attemptId: identity.attemptId, modelIdentity: identity.modelIdentity, planId: identity.planId,
|
|
67
|
+
participantA: identity.participantA, participantB: identity.participantB },
|
|
68
|
+
resources: amounts && Object.keys(amounts).length ? amounts : null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {object} options
|
|
73
|
+
* @param {string} options.participantId this host's participant identity (the one its residents admit as)
|
|
74
|
+
* @param {'node'|'browser'} options.host
|
|
75
|
+
* @param {(record: object) => void} options.write sink for each record copy; it owns persistence and its own failures
|
|
76
|
+
*/
|
|
77
|
+
export function createReadinessRecorder({ participantId, host, write, now = Date.now, sessionId = crypto.randomUUID(),
|
|
78
|
+
checkpointMs = READINESS_CHECKPOINT_MS, maxObservationGapMs = READINESS_MAX_OBSERVATION_GAP_MS }) {
|
|
79
|
+
if (!text(participantId) || !READINESS_HOSTS.includes(host) || typeof write !== 'function' || typeof now !== 'function'
|
|
80
|
+
|| !text(sessionId) || !positive(checkpointMs) || !positive(maxObservationGapMs)) {
|
|
81
|
+
fail('a recorder needs participantId, host node or browser, a write sink and positive checkpoint and gap bounds');
|
|
82
|
+
}
|
|
83
|
+
let latest = now();
|
|
84
|
+
const startedAt = latest;
|
|
85
|
+
let observedAt = startedAt, checkpointAt = startedAt, ended = false, closed = false, failures = 0;
|
|
86
|
+
const stages = new Map(), residents = new Set();
|
|
87
|
+
// Times never run backwards within a session, so its own spans cannot overlap.
|
|
88
|
+
const clock = () => { const value = now(); if (time(value) && value > latest) latest = value; return latest; };
|
|
89
|
+
const emit = record => { try { write(record); } catch { failures++; } };
|
|
90
|
+
const session = { sessionId, sessionStartedAt: startedAt, host };
|
|
91
|
+
const spanCopy = (span, observed, to = null, end = null) => ({ schema: READINESS_SPAN_SCHEMA, spanId: span.spanId, ...session,
|
|
92
|
+
...span.stage, state: span.state, capacity: span.capacity && { ...span.capacity }, from: span.from, to, observedAt: observed, end });
|
|
93
|
+
const admissionCopy = (record, observed) => ({ schema: STAGE_ADMISSION_SCHEMA, ...session, observedAt: observed, ...structuredClone(record) });
|
|
94
|
+
function open(entry, state, capacity, from, at) {
|
|
95
|
+
entry.span = { spanId: crypto.randomUUID(), stage: entry.stage, state, capacity, from };
|
|
96
|
+
emit(spanCopy(entry.span, at));
|
|
97
|
+
}
|
|
98
|
+
function close(entry, to, end) {
|
|
99
|
+
if (!entry.span) return;
|
|
100
|
+
emit(spanCopy(entry.span, to, to, end)); entry.span = null;
|
|
101
|
+
}
|
|
102
|
+
// Time since the last observation is unobserved: a ready claim cannot cover it. Unready stays unready.
|
|
103
|
+
function closeGap(at) {
|
|
104
|
+
if (at - observedAt <= maxObservationGapMs) return;
|
|
105
|
+
for (const entry of stages.values()) if (entry.span?.state === 'ready') close(entry, observedAt, 'observation-gap');
|
|
106
|
+
}
|
|
107
|
+
function checkpoint(at) {
|
|
108
|
+
checkpointAt = at;
|
|
109
|
+
for (const entry of stages.values()) if (entry.span) emit(spanCopy(entry.span, at));
|
|
110
|
+
for (const watch of residents) for (const record of watch.held()) emit(admissionCopy(record, at));
|
|
111
|
+
}
|
|
112
|
+
const guarded = fn => (...args) => { try { return fn(...args); } catch { failures++; } };
|
|
113
|
+
const observe = guarded(state => {
|
|
114
|
+
if (ended) return;
|
|
115
|
+
const at = clock();
|
|
116
|
+
closeGap(at);
|
|
117
|
+
const current = observedStage(state, participantId);
|
|
118
|
+
if (current) {
|
|
119
|
+
current.key = stageKey(current.stage);
|
|
120
|
+
if (!stages.has(current.key)) {
|
|
121
|
+
const entry = { stage: current.stage, span: null };
|
|
122
|
+
stages.set(current.key, entry);
|
|
123
|
+
// This session observed every state since it started: the stage was not ready here before now.
|
|
124
|
+
if (startedAt < at) open(entry, 'unready', null, startedAt, at);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [key, entry] of stages) {
|
|
128
|
+
const ready = !!current?.ready && current.key === key, capacity = ready ? current.capacity : null;
|
|
129
|
+
if (entry.span && entry.span.state === (ready ? 'ready' : 'unready') && entry.span.capacity?.amount === capacity?.amount) continue;
|
|
130
|
+
if (entry.span) close(entry, at, entry.span.state === 'ready' ? readyEnd(state, key, current) : 'ready');
|
|
131
|
+
open(entry, ready ? 'ready' : 'unready', capacity, at, at);
|
|
132
|
+
}
|
|
133
|
+
observedAt = at;
|
|
134
|
+
if (at - checkpointAt >= checkpointMs) checkpoint(at);
|
|
135
|
+
});
|
|
136
|
+
/** Records this resident's admissions from its own state until it closes; the resident is borrowed, never closed. */
|
|
137
|
+
const watchResident = guarded(resident => {
|
|
138
|
+
if (closed || typeof resident?.subscribe !== 'function') return;
|
|
139
|
+
const written = new Map();
|
|
140
|
+
let held = [], done = false, unsubscribe = null;
|
|
141
|
+
const watch = { held: () => held, stop() { done = true; residents.delete(watch); unsubscribe?.(); } };
|
|
142
|
+
residents.add(watch);
|
|
143
|
+
const listener = guarded(state => {
|
|
144
|
+
if (done || closed) return;
|
|
145
|
+
const at = clock(), records = Array.isArray(state?.reservations?.records) ? state.reservations.records : [];
|
|
146
|
+
const rows = records.map(record => admissionOf(state, record)).filter(Boolean);
|
|
147
|
+
held = rows.filter(row => row.settledAt === null);
|
|
148
|
+
for (const row of rows) {
|
|
149
|
+
const settled = row.settledAt !== null, previous = written.get(row.identity.attemptId);
|
|
150
|
+
if (previous === true || previous === false && !settled) continue;
|
|
151
|
+
written.set(row.identity.attemptId, settled); emit(admissionCopy(row, at));
|
|
152
|
+
}
|
|
153
|
+
if (state?.phase === 'closed') watch.stop();
|
|
154
|
+
});
|
|
155
|
+
unsubscribe = resident.subscribe(listener);
|
|
156
|
+
if (done) unsubscribe?.();
|
|
157
|
+
});
|
|
158
|
+
/** Stops readiness: every open span closes now with `reason`. Admissions keep recording until `close`. */
|
|
159
|
+
const end = guarded((reason = 'closed') => {
|
|
160
|
+
if (ended) return;
|
|
161
|
+
const at = clock();
|
|
162
|
+
closeGap(at);
|
|
163
|
+
for (const entry of stages.values()) close(entry, at, reason);
|
|
164
|
+
ended = true;
|
|
165
|
+
});
|
|
166
|
+
return Object.freeze({ sessionId, observe, watchResident, end,
|
|
167
|
+
/** Ends readiness, writes each still-held admission's last observation and stops watching residents. */
|
|
168
|
+
close: guarded((reason = 'closed') => {
|
|
169
|
+
end(reason);
|
|
170
|
+
if (closed) return;
|
|
171
|
+
const at = clock();
|
|
172
|
+
for (const watch of [...residents]) { for (const record of watch.held()) emit(admissionCopy(record, at)); watch.stop(); }
|
|
173
|
+
closed = true;
|
|
174
|
+
}),
|
|
175
|
+
getState: () => ({ sessionId, host, participantId, startedAt, observedAt, ended, closed, failures,
|
|
176
|
+
stages: stages.size, open: [...stages.values()].filter(entry => entry.span).length, residents: residents.size })
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
181
|
+
|
|
182
|
+
const sessionOf = record => text(record.sessionId) && time(record.sessionStartedAt) && READINESS_HOSTS.includes(record.host);
|
|
183
|
+
|
|
184
|
+
function spanRecord(record) {
|
|
185
|
+
const capacity = record.capacity ?? null;
|
|
186
|
+
if (!text(record.spanId) || !sessionOf(record) || !text(record.participantId) || !text(record.modelIdentity)
|
|
187
|
+
|| !text(record.planId) || !count(record.stageIndex) || !['ready', 'unready'].includes(record.state) || !time(record.from)
|
|
188
|
+
|| !time(record.observedAt) || record.observedAt < record.from || record.to != null && (!time(record.to) || record.to < record.from)
|
|
189
|
+
|| capacity !== null && (record.state !== 'ready' || !Object.hasOwn(CAPACITY_UNITS, capacity.unit) || !time(capacity.amount) || capacity.amount < 0)) {
|
|
190
|
+
fail(`malformed readiness span ${String(record.spanId)}`);
|
|
191
|
+
}
|
|
192
|
+
return record;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function admissionRecord(record) {
|
|
196
|
+
if (!sessionOf(record) || !text(record.participantId) || !count(record.index) || !text(record.identity?.attemptId)
|
|
197
|
+
|| !time(record.reservedAt) || !time(record.observedAt) || record.settledAt != null && !time(record.settledAt)) {
|
|
198
|
+
fail(`malformed stage admission ${String(record.identity?.attemptId)}`);
|
|
199
|
+
}
|
|
200
|
+
return record;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Spans and admissions for summarizeCapacityUtilization from recorded copies. A closed span keeps its recorded end; an
|
|
205
|
+
* open span counts only through its last observed copy and is listed in `unterminated`. An admission never seen settled
|
|
206
|
+
* carries `observedUntil` (its last observation), after which the summary treats it as unknown; its resident cannot
|
|
207
|
+
* outlive its session, so the next session of the same participant and host kind to start after that observation
|
|
208
|
+
* bounds it as `releasedBy`. Conflicting copies are refused.
|
|
209
|
+
*/
|
|
210
|
+
export function readinessInputFromRecords(records) {
|
|
211
|
+
if (!Array.isArray(records)) fail('expected an array of records');
|
|
212
|
+
const spans = new Map(), admissions = new Map(), sessions = new Map();
|
|
213
|
+
for (const record of records) {
|
|
214
|
+
if (record?.schema === READINESS_SPAN_SCHEMA || record?.schema === STAGE_ADMISSION_SCHEMA) {
|
|
215
|
+
if (!sessionOf(record)) fail(`record without its session: ${String(record.spanId ?? record.identity?.attemptId)}`);
|
|
216
|
+
const session = [record.participantId, record.host, record.sessionStartedAt], known = sessions.get(record.sessionId);
|
|
217
|
+
if (known && !sameJson(known, session)) fail(`conflicting session ${record.sessionId}`);
|
|
218
|
+
sessions.set(record.sessionId, session);
|
|
219
|
+
}
|
|
220
|
+
if (record?.schema === READINESS_SPAN_SCHEMA) {
|
|
221
|
+
const row = spanRecord(record), entry = spans.get(row.spanId);
|
|
222
|
+
const fixed = [row.sessionId, row.participantId, row.modelIdentity, row.planId, row.stageIndex, row.state, row.capacity ?? null, row.from];
|
|
223
|
+
if (!entry) { spans.set(row.spanId, { fixed, to: row.to ?? null, end: row.end ?? null, observedAt: row.observedAt, row }); continue; }
|
|
224
|
+
if (!sameJson(entry.fixed, fixed) || entry.to !== null && row.to != null && entry.to !== row.to) fail(`conflicting copies of span ${row.spanId}`);
|
|
225
|
+
entry.to ??= row.to ?? null; entry.end ??= row.end ?? null; entry.observedAt = Math.max(entry.observedAt, row.observedAt);
|
|
226
|
+
} else if (record?.schema === STAGE_ADMISSION_SCHEMA) {
|
|
227
|
+
const row = admissionRecord(record), key = keyOf(row.sessionId, row.participantId, row.index, row.identity.attemptId);
|
|
228
|
+
const entry = admissions.get(key);
|
|
229
|
+
if (!entry) { admissions.set(key, { row, observedAt: row.observedAt }); continue; }
|
|
230
|
+
if (entry.row.reservedAt !== row.reservedAt || entry.row.settledAt != null && row.settledAt != null && entry.row.settledAt !== row.settledAt) {
|
|
231
|
+
fail(`conflicting copies of admission ${row.identity.attemptId}`);
|
|
232
|
+
}
|
|
233
|
+
if (entry.row.settledAt == null && (row.settledAt != null || row.observedAt >= entry.row.observedAt)) entry.row = row;
|
|
234
|
+
entry.observedAt = Math.max(entry.observedAt, row.observedAt);
|
|
235
|
+
} else fail(`unknown record schema ${String(record?.schema)}`);
|
|
236
|
+
}
|
|
237
|
+
const readiness = [], unterminated = [];
|
|
238
|
+
let zeroLength = 0;
|
|
239
|
+
for (const [spanId, { to, end, observedAt, row }] of spans) {
|
|
240
|
+
const until = to ?? observedAt;
|
|
241
|
+
if (to === null) unterminated.push({ spanId, sessionId: row.sessionId, participantId: row.participantId, modelIdentity: row.modelIdentity,
|
|
242
|
+
planId: row.planId, stageIndex: row.stageIndex, state: row.state, from: row.from, lastObservedAt: observedAt });
|
|
243
|
+
if (until > row.from) readiness.push({ schema: READINESS_SPAN_SCHEMA, participantId: row.participantId, modelIdentity: row.modelIdentity,
|
|
244
|
+
planId: row.planId, stageIndex: row.stageIndex, state: row.state, capacity: row.capacity ?? null, from: row.from, to: until,
|
|
245
|
+
...(end ? { end } : {}) });
|
|
246
|
+
else if (to !== null) zeroLength++;
|
|
247
|
+
}
|
|
248
|
+
const releasedBy = (row, observedAt) => {
|
|
249
|
+
const starts = [...sessions].filter(([id, [participantId, host, startedAt]]) => id !== row.sessionId
|
|
250
|
+
&& participantId === row.participantId && host === row.host && startedAt >= observedAt).map(([, session]) => session[2]);
|
|
251
|
+
return starts.length ? Math.min(...starts) : null;
|
|
252
|
+
};
|
|
253
|
+
return { readiness, unterminated, zeroLength,
|
|
254
|
+
admissions: [...admissions.values()].map(({ row, observedAt }) => {
|
|
255
|
+
const { schema: _schema, sessionId: _session, sessionStartedAt: _started, host: _host, observedAt: _observed, ...admission } = row;
|
|
256
|
+
return row.settledAt == null ? { ...admission, observedUntil: observedAt, releasedBy: releasedBy(row, observedAt) } : admission;
|
|
257
|
+
}) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A bounded store over a `getItem`/`setItem` storage port whose stored value is itself a
|
|
262
|
+
* `reploid.capacity-utilization-input/v1` document (`readinessRecords[]`, `readinessDropped`), readable by the CLI.
|
|
263
|
+
* Only the latest copy of each span or admission is kept; past `maxRecords` the oldest observed are dropped and counted.
|
|
264
|
+
* Storage failures keep the batch for the next write; nothing is thrown to the host.
|
|
265
|
+
*/
|
|
266
|
+
export function createReadinessRecordStore({ storage, key, maxRecords = READINESS_STORE_MAX_RECORDS }) {
|
|
267
|
+
if (typeof storage?.getItem !== 'function' || typeof storage.setItem !== 'function' || !text(key) || !positive(maxRecords)) {
|
|
268
|
+
fail('a record store needs a storage port, a key and a positive record bound');
|
|
269
|
+
}
|
|
270
|
+
let pending = [], scheduled = false, failures = 0;
|
|
271
|
+
const identityOf = record => record.schema === READINESS_SPAN_SCHEMA ? keyOf('span', record.spanId)
|
|
272
|
+
: keyOf('admission', record.sessionId, record.participantId, record.index, record.identity?.attemptId);
|
|
273
|
+
const final = record => record.schema === READINESS_SPAN_SCHEMA ? record.to != null : record.settledAt != null;
|
|
274
|
+
const supersedes = (next, previous) => final(next) !== final(previous) ? final(next) : !final(previous) && next.observedAt >= previous.observedAt;
|
|
275
|
+
function read() {
|
|
276
|
+
try {
|
|
277
|
+
const doc = JSON.parse(storage.getItem(key) ?? 'null');
|
|
278
|
+
if (doc?.schema === UTILIZATION_INPUT_SCHEMA && Array.isArray(doc.readinessRecords)) {
|
|
279
|
+
return { schema: UTILIZATION_INPUT_SCHEMA, readinessRecords: doc.readinessRecords, readinessDropped: count(doc.readinessDropped) ? doc.readinessDropped : 0 };
|
|
280
|
+
}
|
|
281
|
+
} catch { /* An unreadable stored value starts a new document. */ }
|
|
282
|
+
return { schema: UTILIZATION_INPUT_SCHEMA, readinessRecords: [], readinessDropped: 0 };
|
|
283
|
+
}
|
|
284
|
+
function merged() {
|
|
285
|
+
const doc = read(), rows = new Map(doc.readinessRecords.map(record => [identityOf(record), record]));
|
|
286
|
+
for (const record of pending) {
|
|
287
|
+
const id = identityOf(record), previous = rows.get(id);
|
|
288
|
+
if (!previous || supersedes(record, previous)) rows.set(id, record);
|
|
289
|
+
}
|
|
290
|
+
const list = [...rows.values()].sort((a, b) => a.observedAt - b.observedAt), dropped = Math.max(0, list.length - maxRecords);
|
|
291
|
+
return { schema: UTILIZATION_INPUT_SCHEMA, readinessRecords: list.slice(dropped), readinessDropped: doc.readinessDropped + dropped };
|
|
292
|
+
}
|
|
293
|
+
function flush() {
|
|
294
|
+
scheduled = false;
|
|
295
|
+
if (!pending.length) return;
|
|
296
|
+
const doc = merged();
|
|
297
|
+
try { storage.setItem(key, JSON.stringify(doc)); pending = []; }
|
|
298
|
+
catch { failures++; pending = pending.slice(-maxRecords); }
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
write(record) {
|
|
302
|
+
pending.push(structuredClone(record));
|
|
303
|
+
if (!scheduled) { scheduled = true; queueMicrotask(flush); }
|
|
304
|
+
},
|
|
305
|
+
flush,
|
|
306
|
+
/** The stored document with any unwritten records merged in; the CLI reads it as an input file. */
|
|
307
|
+
export() { flush(); return structuredClone(pending.length ? merged() : read()); },
|
|
308
|
+
getState: () => ({ pending: pending.length, failures })
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import policy from './computer-identity.policy.json' with { type: 'json' };
|
|
2
|
+
import { publicProviderHardware } from './provider-hardware.js';
|
|
3
|
+
|
|
4
|
+
/** Stable display names identify a key, never a unique physical computer. */
|
|
5
|
+
export function participantLabel(id) {
|
|
6
|
+
if (typeof id !== 'string' || !id) return 'Remote device';
|
|
7
|
+
let hash = 0x811c9dc5;
|
|
8
|
+
const input = policy.labelVersion + id;
|
|
9
|
+
for (let i = 0; i < input.length; i++) hash = Math.imul(hash ^ input.charCodeAt(i), 0x01000193) >>> 0;
|
|
10
|
+
return `${policy.colors[hash & 31]} ${policy.animals[(hash >>> 5) & 31]} · ${(hash >>> 16).toString(16).padStart(4, '0').toUpperCase()}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const labelText = value => {
|
|
14
|
+
if (typeof value !== 'string' || value.length > policy.maxLabelLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
15
|
+
throw new TypeError('Computer label must be plain text of at most 64 characters');
|
|
16
|
+
}
|
|
17
|
+
return value.trim();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The host supplies its existing channel-bound verifier and optional local label store.
|
|
21
|
+
* No GPU work, network discovery, or global storage access occurs implicitly.
|
|
22
|
+
*/
|
|
23
|
+
export async function identifyComputer(connectionId, { verifyPeer, storage, ownerLabel, hardware, signal } = {}) {
|
|
24
|
+
if (typeof verifyPeer !== 'function') throw new TypeError('A channel-bound peer verifier is required');
|
|
25
|
+
const suppliedLabel = ownerLabel === undefined ? undefined : labelText(ownerLabel);
|
|
26
|
+
const reportedHardware = publicProviderHardware(hardware);
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
const participantId = await verifyPeer(connectionId, signal);
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
if (typeof participantId !== 'string' || !/^peer:[a-f0-9]{24}$/.test(participantId)) {
|
|
31
|
+
throw new Error('Computer identity could not be verified');
|
|
32
|
+
}
|
|
33
|
+
const key = policy.storagePrefix + participantId;
|
|
34
|
+
let label = suppliedLabel, persistence = storage ? 'available' : 'unavailable';
|
|
35
|
+
try {
|
|
36
|
+
if (label === undefined && storage) {
|
|
37
|
+
const saved = await storage.getItem(key);
|
|
38
|
+
if (saved !== null && saved !== undefined) label = labelText(saved);
|
|
39
|
+
}
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
if (suppliedLabel !== undefined && storage) await storage.setItem(key, suppliedLabel);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
persistence = 'unavailable';
|
|
45
|
+
}
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
return { participantId, connectionId, label: label || participantLabel(participantId),
|
|
48
|
+
labelSource: label ? 'local-name' : 'participant-key', persistence,
|
|
49
|
+
identityBasis: 'signing-key', hardware: reportedHardware };
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"labelVersion": "reploid-label-v1:",
|
|
3
|
+
"storagePrefix": "reploid.computer-label:v1:",
|
|
4
|
+
"maxLabelLength": 64,
|
|
5
|
+
"maxBackgroundProofs": 1,
|
|
6
|
+
"colors": ["Amber", "Azure", "Bronze", "Cobalt", "Copper", "Coral", "Crimson", "Emerald", "Garnet", "Golden", "Indigo", "Ivory", "Jade", "Lavender", "Lilac", "Lunar", "Mint", "Moss", "Onyx", "Opal", "Pearl", "Plum", "Quartz", "Ruby", "Saffron", "Sapphire", "Scarlet", "Silver", "Slate", "Teal", "Violet", "Viridian"],
|
|
7
|
+
"animals": ["Badger", "Crane", "Dolphin", "Falcon", "Finch", "Fox", "Gecko", "Heron", "Ibis", "Jaguar", "Kestrel", "Kingfisher", "Lark", "Lynx", "Marten", "Moth", "Otter", "Owl", "Panda", "Puffin", "Raven", "Robin", "Seal", "Sparrow", "Starling", "Swift", "Tiger", "Tern", "Turtle", "Whale", "Wolf", "Wren"]
|
|
8
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure scoring and scheduling rules for swarm contribution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import policy from '../rules/contribution.rules.json' with { type: 'json' };
|
|
6
|
+
export const DEFAULT_REWARD_POLICY = Object.freeze(policy);
|
|
7
|
+
|
|
8
|
+
const roundScore = (value) => Math.round(Number(value || 0) * 1000) / 1000;
|
|
9
|
+
|
|
10
|
+
export function createContributionSummary(input = {}) {
|
|
11
|
+
input = input || {};
|
|
12
|
+
return {
|
|
13
|
+
score: roundScore(input.score || 0),
|
|
14
|
+
providedInputTokens: Math.max(0, Number(input.providedInputTokens || 0)),
|
|
15
|
+
providedOutputTokens: Math.max(0, Number(input.providedOutputTokens || 0)),
|
|
16
|
+
receiptsServed: Math.max(0, Number(input.receiptsServed || 0)),
|
|
17
|
+
receiptsConsumed: Math.max(0, Number(input.receiptsConsumed || 0)),
|
|
18
|
+
uniquePeers: Array.from(new Set(Array.isArray(input.uniquePeers) ? input.uniquePeers.filter(Boolean) : [])),
|
|
19
|
+
updatedAt: input.updatedAt || null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deriveContributionDelta(receipt, policy = DEFAULT_REWARD_POLICY) {
|
|
24
|
+
const inputTokens = Math.max(0, Number(receipt?.inputTokens || 0));
|
|
25
|
+
const outputTokens = Math.max(0, Number(receipt?.outputTokens || 0));
|
|
26
|
+
const totalTokens = inputTokens + outputTokens;
|
|
27
|
+
|
|
28
|
+
if (totalTokens < policy.minCountedTokens) {
|
|
29
|
+
return {
|
|
30
|
+
eligible: false,
|
|
31
|
+
inputTokens,
|
|
32
|
+
outputTokens,
|
|
33
|
+
scoreDelta: 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const outputScore = (outputTokens / 1000) * Number(policy.outputPointsPer1k || 0);
|
|
38
|
+
const inputScore = (inputTokens / 1000) * Number(policy.inputPointsPer1k || 0);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
eligible: true,
|
|
42
|
+
inputTokens,
|
|
43
|
+
outputTokens,
|
|
44
|
+
scoreDelta: roundScore(outputScore + inputScore)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getPairReceiptCount(receiptHistory = [], providerId = '', consumerId = '') {
|
|
49
|
+
return (Array.isArray(receiptHistory) ? receiptHistory : []).filter((receipt) => (
|
|
50
|
+
receipt?.provider === providerId && receipt?.consumer === consumerId
|
|
51
|
+
)).length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isReceiptEligible(receipt, receiptHistory = [], policy = DEFAULT_REWARD_POLICY) {
|
|
55
|
+
if (!receipt || receipt.status === 'failed' || receipt.status === 'cancelled') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const delta = deriveContributionDelta(receipt, policy);
|
|
60
|
+
if (!delta.eligible) return false;
|
|
61
|
+
|
|
62
|
+
const pairCount = getPairReceiptCount(receiptHistory, receipt.provider, receipt.consumer);
|
|
63
|
+
return pairCount < Number(policy.repeatedPairCap || 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function applyReceiptToContribution(contribution, receipt, receiptHistory = [], policy = DEFAULT_REWARD_POLICY) {
|
|
67
|
+
const next = createContributionSummary(contribution);
|
|
68
|
+
if (!isReceiptEligible(receipt, receiptHistory, policy)) {
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const delta = deriveContributionDelta(receipt, policy);
|
|
73
|
+
const uniquePeers = new Set(next.uniquePeers);
|
|
74
|
+
const otherPeerId = String(receipt?.consumer || '').trim();
|
|
75
|
+
const isNewPeer = !!otherPeerId && !uniquePeers.has(otherPeerId);
|
|
76
|
+
if (isNewPeer) {
|
|
77
|
+
uniquePeers.add(otherPeerId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
next.score = roundScore(
|
|
81
|
+
next.score
|
|
82
|
+
+ delta.scoreDelta
|
|
83
|
+
+ (isNewPeer ? Number(policy.diversityBonus || 0) : 0)
|
|
84
|
+
);
|
|
85
|
+
next.providedInputTokens += delta.inputTokens;
|
|
86
|
+
next.providedOutputTokens += delta.outputTokens;
|
|
87
|
+
next.receiptsServed += 1;
|
|
88
|
+
next.uniquePeers = Array.from(uniquePeers);
|
|
89
|
+
next.updatedAt = receipt?.timestamp || Date.now();
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function decayContributionScore(contribution, now = Date.now(), policy = DEFAULT_REWARD_POLICY) {
|
|
94
|
+
const summary = createContributionSummary(contribution);
|
|
95
|
+
if (!summary.score || !summary.updatedAt) {
|
|
96
|
+
return summary.score;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const halfLife = Math.max(1, Number(policy.scoreHalfLifeMs || DEFAULT_REWARD_POLICY.scoreHalfLifeMs));
|
|
100
|
+
const age = Math.max(0, Number(now) - Number(summary.updatedAt));
|
|
101
|
+
if (age === 0) return summary.score;
|
|
102
|
+
|
|
103
|
+
const decayed = summary.score * Math.pow(0.5, age / halfLife);
|
|
104
|
+
return roundScore(decayed);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function rankProviderPeers(peers = [], options = {}) {
|
|
108
|
+
const now = Number(options.now || Date.now());
|
|
109
|
+
const policy = options.policy || DEFAULT_REWARD_POLICY;
|
|
110
|
+
|
|
111
|
+
return [...(Array.isArray(peers) ? peers : [])]
|
|
112
|
+
.filter((peer) => peer?.role === 'provider')
|
|
113
|
+
.map((peer) => ({
|
|
114
|
+
...peer,
|
|
115
|
+
decayedScore: decayContributionScore(peer?.contribution, now, policy)
|
|
116
|
+
}))
|
|
117
|
+
.sort((left, right) => {
|
|
118
|
+
if (right.decayedScore !== left.decayedScore) {
|
|
119
|
+
return right.decayedScore - left.decayedScore;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rightUnique = Array.isArray(right?.contribution?.uniquePeers) ? right.contribution.uniquePeers.length : 0;
|
|
123
|
+
const leftUnique = Array.isArray(left?.contribution?.uniquePeers) ? left.contribution.uniquePeers.length : 0;
|
|
124
|
+
if (rightUnique !== leftUnique) {
|
|
125
|
+
return rightUnique - leftUnique;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return String(left?.peerId || '').localeCompare(String(right?.peerId || ''));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default {
|
|
133
|
+
DEFAULT_REWARD_POLICY,
|
|
134
|
+
applyReceiptToContribution,
|
|
135
|
+
createContributionSummary,
|
|
136
|
+
decayContributionScore,
|
|
137
|
+
deriveContributionDelta,
|
|
138
|
+
getPairReceiptCount,
|
|
139
|
+
isReceiptEligible,
|
|
140
|
+
rankProviderPeers
|
|
141
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Recovery queries never execute a prompt. Unknown or pruned work stays unresolved. */
|
|
2
|
+
import { createRemoteGenerationRequests } from './remote-generation-requests.js';
|
|
3
|
+
|
|
4
|
+
export function createMemoryGenerationJournal(maxResults) {
|
|
5
|
+
const records = new Map(), results = [];
|
|
6
|
+
const key = binding => JSON.stringify([binding.provider, binding.consumer, binding.requestId]);
|
|
7
|
+
return {
|
|
8
|
+
async get(binding) { return structuredClone(records.get(key(binding)) || null); },
|
|
9
|
+
async claim(binding) {
|
|
10
|
+
const id = key(binding);
|
|
11
|
+
if (records.has(id)) return false;
|
|
12
|
+
records.set(id, { binding: structuredClone(binding), outcome: null }); return true;
|
|
13
|
+
},
|
|
14
|
+
async finish(binding, outcome) {
|
|
15
|
+
const id = key(binding), record = records.get(id);
|
|
16
|
+
if (!record) throw Error('Generation admission missing');
|
|
17
|
+
if (record.finished) throw Error('Generation already finished');
|
|
18
|
+
record.outcome = structuredClone(outcome); record.finished = true; results.push(id);
|
|
19
|
+
while (results.length > maxResults) records.get(results.shift()).outcome = null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createGenerationRecovery({ transport, getIdentity, verifyPeer, journal, timeoutMs, maxPending }) {
|
|
25
|
+
const pending = createRemoteGenerationRequests({ timeoutMs, maxPending, sendCancel() {} });
|
|
26
|
+
const handlers = new Set(); let closed = false;
|
|
27
|
+
async function answer(peerId, input) {
|
|
28
|
+
if (closed || handlers.size >= maxPending) return;
|
|
29
|
+
const query = structuredClone(input);
|
|
30
|
+
if (!query || typeof query.requestId !== 'string' || query.requestId.length > 512
|
|
31
|
+
|| typeof query.queryId !== 'string' || !/^[a-f0-9-]{36}$/.test(query.queryId)) return;
|
|
32
|
+
const identity = getIdentity(), consumer = await verifyPeer(peerId);
|
|
33
|
+
if (!consumer || closed) return;
|
|
34
|
+
const record = await journal.get({ provider: identity.peerId, consumer, requestId: query.requestId });
|
|
35
|
+
if (!closed) transport.sendToPeer(peerId, 'reploid:generation-recovery', {
|
|
36
|
+
queryId: query.queryId, requestId: query.requestId, outcome: record?.outcome || null
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
transport.onMessage('reploid:generation-recover', (peer, query) => {
|
|
40
|
+
const job = answer(peer, query); handlers.add(job);
|
|
41
|
+
job.finally(() => handlers.delete(job)).catch(() => {});
|
|
42
|
+
return job;
|
|
43
|
+
});
|
|
44
|
+
transport.onMessage('reploid:generation-recovery', (peer, input) => {
|
|
45
|
+
const job = pending.get(input?.queryId, peer);
|
|
46
|
+
if (job && input?.requestId === job.originalRequestId) pending.settle(job, { response: structuredClone(input.outcome) });
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
async read(reference, signal) {
|
|
50
|
+
if (closed) throw Error('Generation recovery closed');
|
|
51
|
+
if (await verifyPeer(reference.providerId, signal) !== reference.providerIdentity) throw Error('Recovery provider identity changed');
|
|
52
|
+
const queryId = crypto.randomUUID();
|
|
53
|
+
return pending.start({ requestId: queryId, originalRequestId: reference.requestId, providerPeerId: reference.providerId, signal },
|
|
54
|
+
() => transport.sendToPeer(reference.providerId, 'reploid:generation-recover', { queryId, requestId: reference.requestId }));
|
|
55
|
+
},
|
|
56
|
+
async close() { closed = true; pending.close(); await Promise.allSettled([...handlers]); }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import policy from './gpu-signature.policy.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
// Atomic tickets record relative completion order, without a GPU timestamp query.
|
|
4
|
+
// No inter-workgroup waiting: every workgroup completes independently.
|
|
5
|
+
export const gpuSignatureShader = `
|
|
6
|
+
@group(0) @binding(0) var<uniform> params: vec4u;
|
|
7
|
+
@group(0) @binding(1) var<storage, read> data: array<u32>;
|
|
8
|
+
@group(0) @binding(2) var<storage, read_write> result: array<atomic<u32>>;
|
|
9
|
+
var<workgroup> laneValues: array<u32, ${policy.lanes}>;
|
|
10
|
+
@compute @workgroup_size(${policy.lanes})
|
|
11
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
12
|
+
var value = params.x ^ (group.x * 747796405u) ^ lane;
|
|
13
|
+
for (var i = 0u; i < ${policy.iterations}u; i++) {
|
|
14
|
+
value = value * 1664525u + 1013904223u;
|
|
15
|
+
if (params.y == 1u) { value = value ^ data[value & ${policy.dataWords - 1}u]; }
|
|
16
|
+
value = value ^ (value >> 16u);
|
|
17
|
+
}
|
|
18
|
+
laneValues[lane] = value;
|
|
19
|
+
workgroupBarrier();
|
|
20
|
+
if (lane == 0u) {
|
|
21
|
+
var checksum = 0u;
|
|
22
|
+
for (var i = 0u; i < ${policy.lanes}u; i++) { checksum = checksum ^ laneValues[i]; }
|
|
23
|
+
let ticket = atomicAdd(&result[0], 1u);
|
|
24
|
+
atomicStore(&result[1u + group.x], ticket);
|
|
25
|
+
atomicStore(&result[1u + ${policy.workgroups}u + group.x], checksum);
|
|
26
|
+
}
|
|
27
|
+
}`;
|