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,209 @@
|
|
|
1
|
+
import defaults from './defaults.json' with { type: 'json' };
|
|
2
|
+
import requestPolicy from './request-overrides.json' with { type: 'json' };
|
|
3
|
+
import routing from '../rules/routing.rules.json' with { type: 'json' };
|
|
4
|
+
import { sha256Hex } from '../hashing.js';
|
|
5
|
+
|
|
6
|
+
const resolvedConfigurations = new WeakSet();
|
|
7
|
+
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
8
|
+
|
|
9
|
+
export class ConfigurationError extends TypeError {
|
|
10
|
+
constructor(path, message) {
|
|
11
|
+
super(`Reploid configuration ${path}: ${message}`);
|
|
12
|
+
this.name = 'ConfigurationError';
|
|
13
|
+
this.path = path;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function snapshotJson(value, path = '$') {
|
|
18
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
19
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
20
|
+
if (Array.isArray(value)) return value.map((entry, index) => snapshotJson(entry, `${path}[${index}]`));
|
|
21
|
+
if (value && Object.getPrototypeOf(value) === Object.prototype) {
|
|
22
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => {
|
|
23
|
+
if (forbiddenKeys.has(key)) throw new ConfigurationError(path, 'unsafe object key');
|
|
24
|
+
return [key, snapshotJson(value[key], `${path}.${key}`)];
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
throw new ConfigurationError(path, 'expected JSON data; inject functions, resources and credentials as ports');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function freezeJson(value) {
|
|
31
|
+
if (value && typeof value === 'object') {
|
|
32
|
+
Object.values(value).forEach(freezeJson);
|
|
33
|
+
Object.freeze(value);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function merge(target, patch, source, provenance, path = '') {
|
|
39
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
40
|
+
const field = path ? `${path}.${key}` : key;
|
|
41
|
+
if (!Object.hasOwn(target, key)) throw new ConfigurationError(field, 'unknown field');
|
|
42
|
+
const old = target[key];
|
|
43
|
+
if (old && typeof old === 'object' && !Array.isArray(old) && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
44
|
+
merge(old, value, source, provenance, field);
|
|
45
|
+
} else {
|
|
46
|
+
target[key] = value;
|
|
47
|
+
provenance[field] = source;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function recordSources(value, sources, path = '') {
|
|
53
|
+
for (const [key, item] of Object.entries(value)) {
|
|
54
|
+
const field = path ? `${path}.${key}` : key;
|
|
55
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) recordSources(item, sources, field);
|
|
56
|
+
else sources[field] = 'schema-defaults';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requireType(value, expected, path) {
|
|
61
|
+
if (typeof value !== expected) throw new ConfigurationError(path, `expected ${expected}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validate(config) {
|
|
65
|
+
if (config.schema !== defaults.schema) throw new ConfigurationError('schema', 'unsupported schema');
|
|
66
|
+
for (const section of Object.keys(defaults).filter(key => key !== 'schema')) {
|
|
67
|
+
if (!config[section] || typeof config[section] !== 'object' || Array.isArray(config[section])) {
|
|
68
|
+
throw new ConfigurationError(section, 'expected object');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const section of ['agent', 'legacyAgent', 'memory', 'mesh', 'webrtc', 'artifacts']) {
|
|
72
|
+
for (const [key, value] of Object.entries(defaults[section])) {
|
|
73
|
+
if (typeof value === 'number' && (!Number.isSafeInteger(config[section][key]) || config[section][key] < 1)) {
|
|
74
|
+
throw new ConfigurationError(`${section}.${key}`, 'expected positive safe integer');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (config.legacyAgent.settings !== null) {
|
|
79
|
+
const settings = config.legacyAgent.settings;
|
|
80
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)
|
|
81
|
+
|| Object.keys(settings).some(key => !['providerThrottle', 'cycleIntervalMs', 'functionGemma'].includes(key))
|
|
82
|
+
|| !Number.isSafeInteger(settings.cycleIntervalMs) || settings.cycleIntervalMs < 0) {
|
|
83
|
+
throw new ConfigurationError('legacyAgent.settings', 'invalid attempt settings');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const key of ['enabled', 'executeJobs', 'supplyArtifacts', 'shareCandidates']) requireType(config.mesh[key], 'boolean', `mesh.${key}`);
|
|
87
|
+
requireType(config.tools.allowDynamic, 'boolean', 'tools.allowDynamic');
|
|
88
|
+
requireType(config.webrtc.dataChannelOptions?.ordered, 'boolean', 'webrtc.dataChannelOptions.ordered');
|
|
89
|
+
const retransmits = config.webrtc.dataChannelOptions?.maxRetransmits;
|
|
90
|
+
if (retransmits !== null && (!Number.isSafeInteger(retransmits) || retransmits < 0 || retransmits > 65535)) {
|
|
91
|
+
throw new ConfigurationError('webrtc.dataChannelOptions.maxRetransmits', 'expected uint16 or explicit null');
|
|
92
|
+
}
|
|
93
|
+
requireType(config.webrtc.dataChannelLabel, 'string', 'webrtc.dataChannelLabel');
|
|
94
|
+
if (!config.webrtc.dataChannelLabel) throw new ConfigurationError('webrtc.dataChannelLabel', 'must not be empty');
|
|
95
|
+
for (const [key, value] of Object.entries(config.webrtc.signaling)) {
|
|
96
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new ConfigurationError(`webrtc.signaling.${key}`, 'expected positive safe integer');
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(config.webrtc.signaling).length !== Object.keys(defaults.webrtc.signaling).length) {
|
|
99
|
+
throw new ConfigurationError('webrtc.signaling', 'missing required policy');
|
|
100
|
+
}
|
|
101
|
+
requireType(config.improvement.enabled, 'boolean', 'improvement.enabled');
|
|
102
|
+
requireType(config.observation.enabled, 'boolean', 'observation.enabled');
|
|
103
|
+
for (const field of ['allowed', 'parallelSafe', 'ordered', 'exclusive']) {
|
|
104
|
+
if (!Array.isArray(config.tools[field]) || config.tools[field].some(value => typeof value !== 'string' || !value.trim())
|
|
105
|
+
|| new Set(config.tools[field]).size !== config.tools[field].length) {
|
|
106
|
+
throw new ConfigurationError(`tools.${field}`, 'expected unique non-empty string array');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const [section, key] of [['tools','loaderId'],['memory','storeId'],['models','providerId'],
|
|
110
|
+
['mesh','roomId'],['webrtc','signalingUrl'],['webrtc','sessionId'],['webrtc','broadcastRoomId'],
|
|
111
|
+
['improvement','evaluatorId'],['improvement','approvalId'],['improvement','isolationId']]) {
|
|
112
|
+
const value = config[section][key];
|
|
113
|
+
if (value !== null && (typeof value !== 'string' || !value.trim())) {
|
|
114
|
+
throw new ConfigurationError(`${section}.${key}`, 'expected non-empty identifier or explicit null');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (config.models.contract !== null && (!config.models.contract || typeof config.models.contract !== 'object' || Array.isArray(config.models.contract))) {
|
|
118
|
+
throw new ConfigurationError('models.contract', 'expected exact model contract or null');
|
|
119
|
+
}
|
|
120
|
+
if (!routing.rules.some(rule => rule.id === config.mesh.routing)) throw new ConfigurationError('mesh.routing', 'unknown routing rule');
|
|
121
|
+
if (!['assignment/v1', 'swarm/v1'].includes(config.webrtc.protocol)) throw new ConfigurationError('webrtc.protocol', 'unknown wire protocol');
|
|
122
|
+
if (!Array.isArray(config.webrtc.transportOrder) || !config.webrtc.transportOrder.length
|
|
123
|
+
|| config.webrtc.transportOrder.some(type => !['webrtc', 'broadcast'].includes(type))) {
|
|
124
|
+
throw new ConfigurationError('webrtc.transportOrder', 'expected explicit transport order');
|
|
125
|
+
}
|
|
126
|
+
if (config.mesh.enabled && config.webrtc.transportOrder.includes('broadcast') && config.webrtc.broadcastRoomId === null) {
|
|
127
|
+
throw new ConfigurationError('webrtc.broadcastRoomId', 'BroadcastChannel requires its explicit protocol room');
|
|
128
|
+
}
|
|
129
|
+
const rtc = config.webrtc.rtcConfig;
|
|
130
|
+
if (!rtc || !Array.isArray(rtc.iceServers) || !['all', 'relay'].includes(rtc.iceTransportPolicy)) {
|
|
131
|
+
throw new ConfigurationError('webrtc.rtcConfig', 'ICE servers and transport policy are required');
|
|
132
|
+
}
|
|
133
|
+
for (const server of rtc.iceServers) {
|
|
134
|
+
if (!server || typeof server !== 'object' || Object.keys(server).some(key => key !== 'urls')) {
|
|
135
|
+
throw new ConfigurationError('webrtc.rtcConfig.iceServers', 'JSON carries URLs only; inject TURN credentials through RTC ports');
|
|
136
|
+
}
|
|
137
|
+
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
138
|
+
if (!urls.length || urls.some(url => typeof url !== 'string' || !/^(stun|stuns|turn|turns):/.test(url))) {
|
|
139
|
+
throw new ConfigurationError('webrtc.rtcConfig.iceServers.urls', 'invalid ICE URL');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (config.mesh.enabled && config.mesh.roomId === null) throw new ConfigurationError('mesh.roomId', 'enabled networking requires an explicit room');
|
|
143
|
+
if (config.improvement.enabled && ['evaluatorId', 'approvalId', 'isolationId'].some(key => config.improvement[key] === null)) {
|
|
144
|
+
throw new ConfigurationError('improvement', 'enabled improvement requires evaluator, approval and isolation ports');
|
|
145
|
+
}
|
|
146
|
+
for (const [section,key] of [['artifacts','root'],['memory','checkpointPrefix']]) {
|
|
147
|
+
const value = config[section][key];
|
|
148
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value.split('/').includes('..')) {
|
|
149
|
+
throw new ConfigurationError(`${section}.${key}`, 'expected absolute logical path without traversal');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const secrets = new Set(['apiKey', 'accessToken', 'refreshToken', 'authorization', 'privateKey', 'privateJwk', 'password', 'credential']);
|
|
153
|
+
const rejectSecrets = (value, path) => {
|
|
154
|
+
if (!value || typeof value !== 'object') return;
|
|
155
|
+
for (const [key, item] of Object.entries(value)) {
|
|
156
|
+
if (secrets.has(key)) throw new ConfigurationError(`${path}.${key}`, 'credentials belong in host ports');
|
|
157
|
+
rejectSecrets(item, `${path}.${key}`);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
rejectSecrets(config, '$');
|
|
161
|
+
if (config.webrtc.signalingUrl !== null) {
|
|
162
|
+
let url;
|
|
163
|
+
try { url = new URL(config.webrtc.signalingUrl); } catch { throw new ConfigurationError('webrtc.signalingUrl', 'expected absolute WebSocket URL'); }
|
|
164
|
+
if (!['ws:', 'wss:'].includes(url.protocol) || url.username || url.password
|
|
165
|
+
|| [...url.searchParams.keys()].some(key => secrets.has(key) || key === 'token')) {
|
|
166
|
+
throw new ConfigurationError('webrtc.signalingUrl', 'use a credential-free WebSocket URL and host credential ports');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return config;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function resolveConfig({ chain = [], profile = null, overrides = {}, request = {} } = {}) {
|
|
173
|
+
if (!Array.isArray(chain)) throw new ConfigurationError('chain', 'expected array');
|
|
174
|
+
const value = snapshotJson(defaults);
|
|
175
|
+
const provenance = {};
|
|
176
|
+
recordSources(value, provenance);
|
|
177
|
+
chain.forEach((layer, index) => merge(value, snapshotJson(layer), `chain[${index}]`, provenance));
|
|
178
|
+
if (profile !== null) {
|
|
179
|
+
const selected = snapshotJson(profile);
|
|
180
|
+
if (selected.schema !== 'reploid.profile/v1' || typeof selected.id !== 'string' || !selected.config) {
|
|
181
|
+
throw new ConfigurationError('profile', 'expected versioned profile');
|
|
182
|
+
}
|
|
183
|
+
merge(value, selected.config, `profile:${selected.id}`, provenance);
|
|
184
|
+
}
|
|
185
|
+
merge(value, snapshotJson(overrides), 'application-overrides', provenance);
|
|
186
|
+
const requestLayer = snapshotJson(request);
|
|
187
|
+
const paths = {};
|
|
188
|
+
recordSources(requestLayer, paths);
|
|
189
|
+
for (const field of Object.keys(paths)) {
|
|
190
|
+
if (!requestPolicy.paths.includes(field)) throw new ConfigurationError(field, 'request override is not allowlisted');
|
|
191
|
+
}
|
|
192
|
+
merge(value, requestLayer, 'request-overrides', provenance);
|
|
193
|
+
validate(value);
|
|
194
|
+
// Canonical JSON is the full identity, not an unverified short checksum.
|
|
195
|
+
const identity = `reploid.config/v1:${JSON.stringify(snapshotJson(value))}`;
|
|
196
|
+
const resolved = freezeJson({ value, provenance, identity });
|
|
197
|
+
resolvedConfigurations.add(resolved);
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function requireResolvedConfig(config) {
|
|
202
|
+
if (!resolvedConfigurations.has(config)) throw new ConfigurationError('$', 'call resolveConfig() before construction');
|
|
203
|
+
return config.value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function hashConfiguration(config, cryptoApi = globalThis.crypto) {
|
|
207
|
+
requireResolvedConfig(config);
|
|
208
|
+
return sha256Hex(config.identity, cryptoApi);
|
|
209
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The library's one browser SHA-256 over Web Crypto. It lives at the library
|
|
3
|
+
* root, outside artifacts/, so modules any surface ships (including the Zero source
|
|
4
|
+
* image, which never exports artifacts paths) can import it.
|
|
5
|
+
*/
|
|
6
|
+
const textEncoder = new TextEncoder();
|
|
7
|
+
const bytesToHex = (bytes) => Array.from(bytes)
|
|
8
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
9
|
+
.join('');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The library's one SHA-256 over Web Crypto, as the raw ArrayBuffer digest: a
|
|
13
|
+
* Uint8Array is hashed as is, any other value as the UTF-8 bytes of
|
|
14
|
+
* String(value). Hosted services use their node:crypto twin, server/hashing.js.
|
|
15
|
+
*/
|
|
16
|
+
export async function sha256Bytes(value, cryptoApi = globalThis.crypto) {
|
|
17
|
+
const subtle = (cryptoApi || globalThis.crypto)?.subtle;
|
|
18
|
+
if (!subtle) throw new Error('WebCrypto unavailable');
|
|
19
|
+
return subtle.digest('SHA-256', value instanceof Uint8Array ? value : textEncoder.encode(String(value)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Lowercase hex form of sha256Bytes, with no prefix. */
|
|
23
|
+
export async function sha256HexUnprefixed(value, cryptoApi = globalThis.crypto) {
|
|
24
|
+
return bytesToHex(new Uint8Array(await sha256Bytes(value, cryptoApi)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `sha256:`-prefixed form of sha256HexUnprefixed. */
|
|
28
|
+
export async function sha256Hex(value, cryptoApi = globalThis.crypto) {
|
|
29
|
+
return `sha256:${await sha256HexUnprefixed(value, cryptoApi)}`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/** Capacity-weighted readiness utilization over an interval, from exported records only.
|
|
2
|
+
*
|
|
3
|
+
* Maintained: time a contributor stage was prepared and offered (`ready` readiness spans) times the capacity it
|
|
4
|
+
* admitted (attempt slots or admitted bytes, as the span declares). Consumed: each admission record's held slot
|
|
5
|
+
* (reservedAt to settledAt) times its admitted amount, classified by the requester attempt's outcome. Useful: only
|
|
6
|
+
* completed attempts a quality decision marked satisfactory. Paid: readiness inside an accepted readiness contract
|
|
7
|
+
* for that exact stage. Time no span covers, undeclared capacity and undecided outcomes stay unknown, never zero,
|
|
8
|
+
* and make the affected ratio null with its reasons. No prices: capacity-seconds only. */
|
|
9
|
+
|
|
10
|
+
export const CAPACITY_UTILIZATION_SCHEMA = 'reploid.capacity-utilization/v1';
|
|
11
|
+
export const READINESS_SPAN_SCHEMA = 'reploid.readiness-span/v1';
|
|
12
|
+
/** Capacity unit -> the admission record's `resources` field holding each admission's amount in that unit. */
|
|
13
|
+
export const CAPACITY_UNITS = Object.freeze({ 'attempt-slots': 'attemptSlots', bytes: 'attemptBytes' });
|
|
14
|
+
export const ADMISSION_OUTCOMES = Object.freeze(['satisfactory', 'unsatisfactory', 'unassessed', 'failed', 'cancelled',
|
|
15
|
+
'interrupted', 'active', 'unmatched']);
|
|
16
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
|
|
17
|
+
const ACTIVE = new Set(['queued', 'loading', 'executing', 'approval', 'cancelling']);
|
|
18
|
+
const SUPPLY_LABELS = ['local', 'mesh', 'hosted', 'mixed', 'none', 'unknown'];
|
|
19
|
+
|
|
20
|
+
const fail = message => { throw new Error(`Capacity utilization: ${message}`); };
|
|
21
|
+
const time = value => typeof value === 'number' && Number.isFinite(value);
|
|
22
|
+
const text = value => typeof value === 'string' && value.length > 0;
|
|
23
|
+
const keyOf = (...parts) => JSON.stringify(parts);
|
|
24
|
+
const seconds = ms => ms / 1000;
|
|
25
|
+
const overlap = (a, b, from, to) => Math.max(0, Math.min(b, to) - Math.max(a, from));
|
|
26
|
+
|
|
27
|
+
function stageOf(input, where) {
|
|
28
|
+
if (!text(input.participantId) || !text(input.modelIdentity) || !text(input.planId)
|
|
29
|
+
|| !Number.isSafeInteger(input.stageIndex) || input.stageIndex < 0) fail(`${where} needs participantId, modelIdentity, planId and stageIndex`);
|
|
30
|
+
return { participantId: input.participantId, modelIdentity: input.modelIdentity, planId: input.planId, stageIndex: input.stageIndex };
|
|
31
|
+
}
|
|
32
|
+
const stageKey = stage => keyOf(stage.participantId, stage.modelIdentity, stage.planId, stage.stageIndex);
|
|
33
|
+
|
|
34
|
+
function readinessSpan(span) {
|
|
35
|
+
const stage = stageOf(span, 'A readiness span');
|
|
36
|
+
if (!time(span.from) || !time(span.to) || span.to <= span.from || !['ready', 'unready'].includes(span.state)) {
|
|
37
|
+
fail('a readiness span needs from < to and state ready or unready');
|
|
38
|
+
}
|
|
39
|
+
const capacity = span.capacity ?? null;
|
|
40
|
+
if (capacity !== null && (!Object.hasOwn(CAPACITY_UNITS, capacity.unit) || !time(capacity.amount) || capacity.amount < 0)) {
|
|
41
|
+
fail('readiness capacity needs a known unit and a non-negative amount');
|
|
42
|
+
}
|
|
43
|
+
return { ...stage, from: span.from, to: span.to, state: span.state, capacity: capacity && { unit: capacity.unit, amount: capacity.amount } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The admission's stage, attempt and route from its own reservation identity (ordered route or legacy pair). */
|
|
47
|
+
function admissionRecord(record) {
|
|
48
|
+
const identity = record?.identity, route = identity?.route;
|
|
49
|
+
if (!text(identity?.attemptId) || !text(record.participantId) || !Number.isSafeInteger(record.index)) {
|
|
50
|
+
fail('an admission record needs identity.attemptId, participantId and index');
|
|
51
|
+
}
|
|
52
|
+
const stage = stageOf({ participantId: record.participantId, stageIndex: record.index,
|
|
53
|
+
modelIdentity: route?.modelIdentity ?? identity.modelIdentity, planId: route?.planId ?? identity.planId }, 'An admission record');
|
|
54
|
+
const participants = Array.isArray(route?.stages) ? route.stages.map(row => row?.participantId)
|
|
55
|
+
: [identity.participantA, identity.participantB];
|
|
56
|
+
if (!participants.every(text)) fail('an admission record needs its route participants');
|
|
57
|
+
if (record.reservedAt !== null && !time(record.reservedAt) || record.settledAt != null && !time(record.settledAt)
|
|
58
|
+
|| record.observedUntil != null && !time(record.observedUntil) || record.releasedBy != null
|
|
59
|
+
&& (!time(record.releasedBy) || record.observedUntil == null || record.releasedBy < record.observedUntil)) {
|
|
60
|
+
fail('admission times must be epoch milliseconds or null');
|
|
61
|
+
}
|
|
62
|
+
// An unsettled admission was observed held until observedUntil; after it, whether it was held is unknown until
|
|
63
|
+
// releasedBy (its host session had ended by then), or indefinitely without one.
|
|
64
|
+
return { ...stage, attemptId: identity.attemptId, reservedAt: record.reservedAt ?? null, settledAt: record.settledAt ?? null,
|
|
65
|
+
observedUntil: record.observedUntil ?? null, releasedBy: record.releasedBy ?? null,
|
|
66
|
+
failed: record.failure != null || record.phase === 'failed', resources: record.resources ?? null,
|
|
67
|
+
route: { modelIdentity: stage.modelIdentity, planId: stage.planId, participants } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Repeated exports of one admission merge to the most settled copy; contradictory copies are refused. */
|
|
71
|
+
function mergeAdmissions(records) {
|
|
72
|
+
const rows = new Map();
|
|
73
|
+
for (const row of records.map(admissionRecord)) {
|
|
74
|
+
const key = keyOf(stageKey(row), row.attemptId), previous = rows.get(key);
|
|
75
|
+
if (!previous) { rows.set(key, row); continue; }
|
|
76
|
+
if (previous.reservedAt !== row.reservedAt || previous.settledAt !== null && row.settledAt !== null && previous.settledAt !== row.settledAt) {
|
|
77
|
+
fail(`conflicting admission records for attempt ${row.attemptId}`);
|
|
78
|
+
}
|
|
79
|
+
// A copy without its own horizon was observed at export, which bounds it no earlier than any recorded copy.
|
|
80
|
+
const latest = previous.observedUntil === null || row.observedUntil === null ? null
|
|
81
|
+
: row.observedUntil > previous.observedUntil ? row : previous;
|
|
82
|
+
rows.set(key, { ...previous, settledAt: previous.settledAt ?? row.settledAt, failed: previous.failed || row.failed,
|
|
83
|
+
observedUntil: latest?.observedUntil ?? null, releasedBy: latest?.releasedBy ?? null });
|
|
84
|
+
}
|
|
85
|
+
return [...rows.values()];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function contractRecord(contract) {
|
|
89
|
+
const stage = stageOf(contract, 'A readiness contract');
|
|
90
|
+
if (!text(contract.offerId) || !time(contract.from) || !time(contract.to) || contract.to <= contract.from) {
|
|
91
|
+
fail('a readiness contract needs offerId and from < to');
|
|
92
|
+
}
|
|
93
|
+
return { ...stage, offerId: contract.offerId, from: contract.from, to: contract.to, accepted: contract.status === 'accepted' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Piecewise stage state over the interval: ready/unready/unknown, capacity, and whether a contract pays for it. */
|
|
97
|
+
function stageTimeline(spans, contracts, from, to) {
|
|
98
|
+
const clamp = value => Math.min(to, Math.max(from, value));
|
|
99
|
+
const points = [...new Set([from, to, ...spans.flatMap(row => [clamp(row.from), clamp(row.to)]),
|
|
100
|
+
...contracts.flatMap(row => [clamp(row.from), clamp(row.to)])])].sort((a, b) => a - b);
|
|
101
|
+
const segments = [];
|
|
102
|
+
let next = 0;
|
|
103
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
104
|
+
const a = points[i], b = points[i + 1];
|
|
105
|
+
while (next < spans.length && spans[next].to <= a) next++;
|
|
106
|
+
const span = next < spans.length && spans[next].from <= a ? spans[next] : null;
|
|
107
|
+
segments.push({ from: a, to: b, state: span ? span.state : 'unknown',
|
|
108
|
+
capacity: span?.state === 'ready' ? span.capacity?.amount ?? null : null,
|
|
109
|
+
paid: contracts.some(row => row.from <= a && row.to >= b) });
|
|
110
|
+
}
|
|
111
|
+
return segments;
|
|
112
|
+
}
|
|
113
|
+
const segmentAt = (segments, at) => segments.find(row => row.from <= at && at < row.to);
|
|
114
|
+
|
|
115
|
+
function emptyMaintained() {
|
|
116
|
+
return { capacitySeconds: null, knownCapacitySeconds: 0, paidCapacitySeconds: 0, unpaidCapacitySeconds: 0,
|
|
117
|
+
readyMs: 0, unreadyMs: 0, unknownMs: 0, undeclaredCapacityMs: 0, paidUnknownMs: 0 };
|
|
118
|
+
}
|
|
119
|
+
/** Adds one piece of readiness: its state, total capacity (null when undeclared) and paid share of that capacity. */
|
|
120
|
+
function addReadiness(total, { state, capacity, paidCapacity, paidAny }, ms) {
|
|
121
|
+
if (state === 'unknown') { total.unknownMs += ms; if (paidAny) total.paidUnknownMs += ms; return; }
|
|
122
|
+
if (state === 'unready') { total.unreadyMs += ms; return; }
|
|
123
|
+
total.readyMs += ms;
|
|
124
|
+
if (capacity === null) { total.undeclaredCapacityMs += ms; if (paidAny) total.paidUnknownMs += ms; return; }
|
|
125
|
+
total.knownCapacitySeconds += capacity * seconds(ms);
|
|
126
|
+
total.paidCapacitySeconds += paidCapacity * seconds(ms);
|
|
127
|
+
total.unpaidCapacitySeconds += (capacity - paidCapacity) * seconds(ms);
|
|
128
|
+
}
|
|
129
|
+
const closeMaintained = total => ({ ...total,
|
|
130
|
+
capacitySeconds: total.unknownMs || total.undeclaredCapacityMs ? null : total.knownCapacitySeconds });
|
|
131
|
+
|
|
132
|
+
function emptyConsumed() {
|
|
133
|
+
return { capacitySeconds: 0, admissions: 0, open: 0, unknownAmount: 0,
|
|
134
|
+
byOutcome: Object.fromEntries(ADMISSION_OUTCOMES.map(name => [name, { admissions: 0, capacitySeconds: 0, paidCapacitySeconds: 0 }])) };
|
|
135
|
+
}
|
|
136
|
+
function addConsumed(total, row) {
|
|
137
|
+
total.admissions++; if (row.open) total.open++;
|
|
138
|
+
const bucket = total.byOutcome[row.outcome];
|
|
139
|
+
bucket.admissions++;
|
|
140
|
+
if (row.amount === null) { total.unknownAmount++; return; }
|
|
141
|
+
total.capacitySeconds += row.capacitySeconds; bucket.capacitySeconds += row.capacitySeconds;
|
|
142
|
+
bucket.paidCapacitySeconds += row.paidCapacitySeconds;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function outcomeOf(admission, attempt) {
|
|
146
|
+
if (admission.failed) return 'failed';
|
|
147
|
+
if (!attempt) return 'unmatched';
|
|
148
|
+
if (attempt.status === 'completed') return attempt.satisfactory === true ? 'satisfactory' : attempt.satisfactory === false ? 'unsatisfactory' : 'unassessed';
|
|
149
|
+
if (TERMINAL.has(attempt.status)) return attempt.status;
|
|
150
|
+
return ACTIVE.has(attempt.status) ? 'active' : 'unmatched';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Ratios of useful and occupied capacity-seconds to maintained; null with reasons when any input is unknown. */
|
|
154
|
+
function ratios(maintained, consumed, incomplete) {
|
|
155
|
+
const reasons = [];
|
|
156
|
+
if (maintained.unknownMs) reasons.push('readiness-gaps');
|
|
157
|
+
if (maintained.undeclaredCapacityMs) reasons.push('capacity-not-declared');
|
|
158
|
+
if (consumed.unknownAmount) reasons.push('unknown-consumption-amount');
|
|
159
|
+
if (incomplete) reasons.push('open-after-observation');
|
|
160
|
+
const outcomes = consumed.byOutcome;
|
|
161
|
+
if (outcomes.unassessed.admissions) reasons.push('unassessed-completions');
|
|
162
|
+
if (outcomes.unmatched.admissions) reasons.push('unmatched-admissions');
|
|
163
|
+
if (outcomes.active.admissions) reasons.push('active-attempts');
|
|
164
|
+
const occupiedKnown = !maintained.unknownMs && !maintained.undeclaredCapacityMs && !consumed.unknownAmount && !incomplete;
|
|
165
|
+
const usefulKnown = occupiedKnown && !outcomes.unassessed.admissions && !outcomes.unmatched.admissions && !outcomes.active.admissions;
|
|
166
|
+
const divide = (value, by, known) => known && by > 0 ? value / by : null;
|
|
167
|
+
return { reasons,
|
|
168
|
+
usefulToMaintained: divide(outcomes.satisfactory.capacitySeconds, maintained.knownCapacitySeconds, usefulKnown),
|
|
169
|
+
usefulToPaid: divide(outcomes.satisfactory.paidCapacitySeconds, maintained.paidCapacitySeconds, usefulKnown && !maintained.paidUnknownMs),
|
|
170
|
+
occupiedToMaintained: divide(consumed.capacitySeconds, maintained.knownCapacitySeconds, occupiedKnown) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const unitOf = spans => {
|
|
174
|
+
const units = [...new Set(spans.filter(row => row.capacity).map(row => row.capacity.unit))];
|
|
175
|
+
if (units.length > 1) fail(`one stage declares several capacity units: ${units.join(', ')}`);
|
|
176
|
+
return units[0] ?? 'attempt-slots';
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {object} input
|
|
181
|
+
* @param {{from:number,to:number}} input.interval epoch milliseconds
|
|
182
|
+
* @param {number} [input.observedUntil] when the admission and attempt records were exported; defaults to interval.to
|
|
183
|
+
* @param {object[]} [input.readiness] `reploid.readiness-span/v1` spans per contributor stage
|
|
184
|
+
* @param {object[]} [input.admissions] contributor reservation records (resident `getState().reservations.records`)
|
|
185
|
+
* @param {object[]} [input.attempts] requester outcomes `{ attemptId, status, satisfactory?, supply? }`
|
|
186
|
+
* @param {object[]} [input.contracts] readiness contracts `{ offerId, participantId, modelIdentity, planId, stageIndex, from, to, status }`
|
|
187
|
+
*/
|
|
188
|
+
export function summarizeCapacityUtilization({ interval, observedUntil, readiness = [], admissions = [], attempts = [], contracts = [] } = {}) {
|
|
189
|
+
if (!time(interval?.from) || !time(interval?.to) || interval.to <= interval.from) fail('interval needs from < to');
|
|
190
|
+
const { from, to } = interval, until = observedUntil ?? to;
|
|
191
|
+
if (!time(until)) fail('observedUntil must be epoch milliseconds');
|
|
192
|
+
const outcomes = new Map();
|
|
193
|
+
for (const row of attempts) {
|
|
194
|
+
if (!text(row?.attemptId) || !text(row.status)) fail('an attempt outcome needs attemptId and status');
|
|
195
|
+
const previous = outcomes.get(row.attemptId);
|
|
196
|
+
if (previous && (previous.status !== row.status || (previous.satisfactory ?? null) !== (row.satisfactory ?? null))) {
|
|
197
|
+
fail(`conflicting outcomes for attempt ${row.attemptId}`);
|
|
198
|
+
}
|
|
199
|
+
outcomes.set(row.attemptId, row);
|
|
200
|
+
}
|
|
201
|
+
const stages = new Map(), stage = record => {
|
|
202
|
+
const key = stageKey(record);
|
|
203
|
+
if (!stages.has(key)) stages.set(key, { stage: stageOf(record, 'A stage'), spans: [], contracts: [], admissions: [] });
|
|
204
|
+
return stages.get(key);
|
|
205
|
+
};
|
|
206
|
+
for (const span of readiness.map(readinessSpan)) stage(span).spans.push(span);
|
|
207
|
+
for (const contract of contracts.map(contractRecord)) if (contract.accepted) stage(contract).contracts.push(contract);
|
|
208
|
+
const routes = new Map();
|
|
209
|
+
for (const admission of mergeAdmissions(admissions)) {
|
|
210
|
+
stage(admission).admissions.push(admission);
|
|
211
|
+
const key = keyOf(admission.route.modelIdentity, admission.route.planId, ...admission.route.participants);
|
|
212
|
+
if (!routes.has(key)) routes.set(key, { route: admission.route, admissions: [] });
|
|
213
|
+
routes.get(key).admissions.push(admission);
|
|
214
|
+
}
|
|
215
|
+
const rows = new Map();
|
|
216
|
+
for (const [key, entry] of stages) {
|
|
217
|
+
entry.spans.sort((a, b) => a.from - b.from);
|
|
218
|
+
entry.spans.forEach((row, index) => { if (index && entry.spans[index - 1].to > row.from) fail(`readiness spans overlap for ${key}`); });
|
|
219
|
+
const unit = unitOf(entry.spans), segments = stageTimeline(entry.spans, entry.contracts, from, to);
|
|
220
|
+
const maintained = emptyMaintained();
|
|
221
|
+
for (const row of segments) addReadiness(maintained, { state: row.state, capacity: row.capacity,
|
|
222
|
+
paidCapacity: row.paid ? row.capacity ?? 0 : 0, paidAny: row.paid }, row.to - row.from);
|
|
223
|
+
const consumed = emptyConsumed();
|
|
224
|
+
// An admission holds its slot from reservation until cleanup settles; one still open when the records were
|
|
225
|
+
// exported (or at its own last observation, when earlier) is counted to that time, and the rest of the interval
|
|
226
|
+
// is unknown until it is known released. Tombstones never held a slot.
|
|
227
|
+
const measured = entry.admissions.flatMap(admission => {
|
|
228
|
+
if (admission.reservedAt === null) return [];
|
|
229
|
+
const open = admission.settledAt === null;
|
|
230
|
+
const horizon = open && admission.observedUntil !== null ? Math.min(until, admission.observedUntil) : until;
|
|
231
|
+
const released = open ? admission.releasedBy ?? Infinity : admission.settledAt;
|
|
232
|
+
const end = open ? Math.min(to, horizon) : admission.settledAt;
|
|
233
|
+
if (admission.reservedAt >= to || released <= from) return [];
|
|
234
|
+
const field = CAPACITY_UNITS[unit], amount = time(admission.resources?.[field]) ? admission.resources[field] : null;
|
|
235
|
+
const heldMs = overlap(admission.reservedAt, end, from, to);
|
|
236
|
+
const paidMs = segments.filter(row => row.paid).reduce((sum, row) => sum + overlap(admission.reservedAt, end, row.from, row.to), 0);
|
|
237
|
+
return [{ admission, outcome: outcomeOf(admission, outcomes.get(admission.attemptId)), amount, open,
|
|
238
|
+
incomplete: open && Math.max(from, horizon) < Math.min(to, released),
|
|
239
|
+
capacitySeconds: amount === null ? null : amount * seconds(heldMs), paidCapacitySeconds: amount === null ? null : amount * seconds(paidMs) }];
|
|
240
|
+
});
|
|
241
|
+
for (const row of measured) addConsumed(consumed, row);
|
|
242
|
+
const incomplete = measured.some(row => row.incomplete);
|
|
243
|
+
const closed = closeMaintained(maintained);
|
|
244
|
+
rows.set(key, { stage: entry.stage, unit, segments, measured, incomplete,
|
|
245
|
+
summary: { ...entry.stage, unit, contracts: entry.contracts.map(row => row.offerId), maintained: closed, consumed,
|
|
246
|
+
ratios: ratios(closed, consumed, incomplete) } });
|
|
247
|
+
}
|
|
248
|
+
const aggregate = list => {
|
|
249
|
+
const units = [...new Set(list.map(row => row.unit))];
|
|
250
|
+
const maintained = emptyMaintained(), consumed = emptyConsumed();
|
|
251
|
+
for (const row of list) {
|
|
252
|
+
for (const name of Object.keys(maintained)) if (name !== 'capacitySeconds') maintained[name] += row.summary.maintained[name];
|
|
253
|
+
for (const name of ['capacitySeconds', 'admissions', 'open', 'unknownAmount']) consumed[name] += row.summary.consumed[name];
|
|
254
|
+
for (const name of ADMISSION_OUTCOMES) for (const field of ['admissions', 'capacitySeconds', 'paidCapacitySeconds']) {
|
|
255
|
+
consumed.byOutcome[name][field] += row.summary.consumed.byOutcome[name][field];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const closed = closeMaintained(maintained);
|
|
259
|
+
return units.length > 1 ? { unit: null, reason: 'mixed-units' }
|
|
260
|
+
: { unit: units[0] ?? 'attempt-slots', maintained: closed, consumed, ratios: ratios(closed, consumed, list.some(row => row.incomplete)) };
|
|
261
|
+
};
|
|
262
|
+
const contributors = [...new Set([...rows.values()].map(row => row.stage.participantId))].sort().map(participantId => {
|
|
263
|
+
const list = [...rows.values()].filter(row => row.stage.participantId === participantId);
|
|
264
|
+
return { participantId, stages: list.map(row => row.summary), total: aggregate(list) };
|
|
265
|
+
});
|
|
266
|
+
return { schema: CAPACITY_UTILIZATION_SCHEMA, interval: { from, to, durationMs: to - from }, observedUntil: until,
|
|
267
|
+
contributors, routes: [...routes.values()].map(entry => routeSummary(entry, rows, from, to)),
|
|
268
|
+
attempts: attemptSupply(outcomes) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A route is ready while every stage is ready; its capacity is the sum of its stages', in their shared unit. */
|
|
272
|
+
function routeSummary({ route, admissions }, rows, from, to) {
|
|
273
|
+
const stages = route.participants.map((participantId, stageIndex) => rows.get(stageKey({ participantId, stageIndex,
|
|
274
|
+
modelIdentity: route.modelIdentity, planId: route.planId })));
|
|
275
|
+
const units = [...new Set(stages.map(row => row?.unit ?? 'attempt-slots'))];
|
|
276
|
+
const base = { route: structuredClone(route) };
|
|
277
|
+
if (units.length > 1) return { ...base, unit: null, reason: 'mixed-units' };
|
|
278
|
+
const points = [...new Set([from, to, ...stages.flatMap(row => row ? row.segments.flatMap(segment => [segment.from, segment.to]) : [])])]
|
|
279
|
+
.sort((a, b) => a - b);
|
|
280
|
+
const maintained = emptyMaintained();
|
|
281
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
282
|
+
const parts = stages.map(row => row && segmentAt(row.segments, points[i]));
|
|
283
|
+
const state = parts.some(part => !part || part.state === 'unknown') ? 'unknown' : parts.every(part => part.state === 'ready') ? 'ready' : 'unready';
|
|
284
|
+
const capacity = state !== 'ready' || parts.some(part => part.capacity === null) ? null : parts.reduce((sum, part) => sum + part.capacity, 0);
|
|
285
|
+
const paidCapacity = capacity === null ? 0 : parts.reduce((sum, part) => sum + (part.paid ? part.capacity : 0), 0);
|
|
286
|
+
addReadiness(maintained, { state, capacity, paidCapacity, paidAny: parts.some(part => part?.paid) }, points[i + 1] - points[i]);
|
|
287
|
+
}
|
|
288
|
+
const consumed = emptyConsumed(), ids = new Set(admissions.map(row => row.attemptId));
|
|
289
|
+
let incomplete = false;
|
|
290
|
+
for (const row of stages) for (const entry of row?.measured ?? []) {
|
|
291
|
+
if (!ids.has(entry.admission.attemptId)) continue;
|
|
292
|
+
addConsumed(consumed, entry); incomplete ||= entry.incomplete;
|
|
293
|
+
}
|
|
294
|
+
const closed = closeMaintained(maintained);
|
|
295
|
+
return { ...base, unit: units[0], maintained: closed, consumed, ratios: ratios(closed, consumed, incomplete) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function attemptSupply(outcomes) {
|
|
299
|
+
const bySupply = Object.fromEntries([...SUPPLY_LABELS, 'unclassified'].map(name => [name, 0]));
|
|
300
|
+
const byStatus = {};
|
|
301
|
+
for (const row of outcomes.values()) {
|
|
302
|
+
bySupply[SUPPLY_LABELS.includes(row.supply) ? row.supply : 'unclassified']++;
|
|
303
|
+
byStatus[row.status] = (byStatus[row.status] ?? 0) + 1;
|
|
304
|
+
}
|
|
305
|
+
return { total: outcomes.size, bySupply, byStatus };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Requester outcomes from an exported `reploid.chat-workspace/v1`, with each attempt's recorded answer supply. */
|
|
309
|
+
export function attemptOutcomesFromWorkspace(workspace) {
|
|
310
|
+
if (workspace?.schema !== 'reploid.chat-workspace/v1' || !Array.isArray(workspace.threads)) fail('expected a reploid.chat-workspace/v1 export');
|
|
311
|
+
return workspace.threads.flatMap(thread => thread.attempts.map(attempt => ({ attemptId: attempt.id, status: attempt.status,
|
|
312
|
+
supply: attempt.provenance?.schema === 'reploid.answer-provenance/v1' ? attempt.provenance.supply : null })));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Readiness contracts from `reploid.contribution-offer/v1` prepared-readiness offers; only accepted ones pay. */
|
|
316
|
+
export function readinessContractsFromOffers(offers) {
|
|
317
|
+
return offers.filter(offer => offer?.schema === 'reploid.contribution-offer/v1' && offer.category === 'prepared_readiness')
|
|
318
|
+
.map(offer => ({ offerId: offer.offerId, participantId: offer.work?.participantId, modelIdentity: offer.work?.stage?.model?.modelIdentity,
|
|
319
|
+
planId: offer.work?.stage?.model?.planId, stageIndex: offer.work?.stage?.stageIndex,
|
|
320
|
+
from: Date.parse(offer.duration?.startsAt), to: Date.parse(offer.duration?.endsAt), status: offer.status ?? null }));
|
|
321
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CAPACITY_UTILIZATION_SCHEMA, READINESS_SPAN_SCHEMA, CAPACITY_UNITS, ADMISSION_OUTCOMES, summarizeCapacityUtilization,
|
|
2
|
+
attemptOutcomesFromWorkspace, readinessContractsFromOffers } from './capacity-utilization.js';
|
|
3
|
+
export { STAGE_ADMISSION_SCHEMA, UTILIZATION_INPUT_SCHEMA, READINESS_CHECKPOINT_MS, READINESS_MAX_OBSERVATION_GAP_MS,
|
|
4
|
+
READINESS_STORE_MAX_RECORDS, READINESS_HOSTS, createReadinessRecorder, readinessInputFromRecords,
|
|
5
|
+
createReadinessRecordStore } from './readiness-records.js';
|