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,90 @@
|
|
|
1
|
+
/** Durable native process epochs and the existing resident owner's unsettled marker. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
const assert = (ok, message) => { if (!ok) throw Error('Native canonical epochs: ' + message); };
|
|
7
|
+
const epoch = value => typeof value === 'string' && /^[a-f0-9]{32}$/.test(value);
|
|
8
|
+
const text = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
9
|
+
const fresh = () => randomBytes(16).toString('hex');
|
|
10
|
+
|
|
11
|
+
export async function openNativeCanonicalEpochs({ directory, participantId, meshId }) {
|
|
12
|
+
assert(text(directory) && text(participantId) && text(meshId), 'explicit directory and authenticated scope required');
|
|
13
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
14
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
15
|
+
const filename = path.join(await fs.realpath(directory), 'canonical-epochs.sqlite');
|
|
16
|
+
const file = await fs.open(filename, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
17
|
+
let existed;
|
|
18
|
+
try { const stat = await file.stat(); assert(stat.isFile(), 'store must be a regular file'); existed = stat.size > 0; }
|
|
19
|
+
finally { await file.close(); }
|
|
20
|
+
const db = new DatabaseSync(filename);
|
|
21
|
+
let closed = false, claimed;
|
|
22
|
+
const validate = value => {
|
|
23
|
+
assert(value?.schema === 'reploid.native-canonical-epochs/v1' && value.participantId === participantId && value.meshId === meshId
|
|
24
|
+
&& Number.isSafeInteger(value.revision) && value.revision > 0 && epoch(value.participantEpoch) && epoch(value.coordinatorEpoch)
|
|
25
|
+
&& (value.pendingOwner === null || epoch(value.pendingOwner))
|
|
26
|
+
&& (value.settledOwner === null || epoch(value.settledOwner)), 'stored epoch record is corrupt or belongs to another scope');
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
const read = () => {
|
|
30
|
+
const row = db.prepare('SELECT record FROM epochs WHERE id = 1').get();
|
|
31
|
+
assert(row, 'epoch record missing'); return validate(JSON.parse(row.record));
|
|
32
|
+
};
|
|
33
|
+
const current = () => {
|
|
34
|
+
assert(!closed, 'owner closed'); const value = read();
|
|
35
|
+
assert(value.revision === claimed.revision && value.participantEpoch === claimed.participantEpoch
|
|
36
|
+
&& value.coordinatorEpoch === claimed.coordinatorEpoch, 'process epoch replaced'); return value;
|
|
37
|
+
};
|
|
38
|
+
const transact = update => {
|
|
39
|
+
assert(!closed, 'owner closed'); db.prepare('BEGIN IMMEDIATE').run();
|
|
40
|
+
try {
|
|
41
|
+
const result = update(); db.prepare('COMMIT').run(); return result;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
try { db.prepare('ROLLBACK').run(); }
|
|
44
|
+
catch (rollback) { throw new AggregateError([error, rollback], 'Native epoch transaction outcome unknown'); }
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const save = value => db.prepare('INSERT INTO epochs VALUES(1, ?) ON CONFLICT(id) DO UPDATE SET record=excluded.record').run(JSON.stringify(validate(value)));
|
|
49
|
+
try {
|
|
50
|
+
db.prepare('PRAGMA busy_timeout=1000').run(); db.prepare('PRAGMA synchronous=FULL').run();
|
|
51
|
+
claimed = transact(() => {
|
|
52
|
+
db.prepare('CREATE TABLE IF NOT EXISTS epochs (id INTEGER PRIMARY KEY CHECK(id=1), record TEXT NOT NULL)').run();
|
|
53
|
+
const row = db.prepare('SELECT record FROM epochs WHERE id = 1').get();
|
|
54
|
+
assert(row || !existed, 'existing store has no epoch record');
|
|
55
|
+
const prior = row ? validate(JSON.parse(row.record)) : null;
|
|
56
|
+
assert(!prior?.pendingOwner, 'prior resident ownership is unsettled; new admission refused');
|
|
57
|
+
const value = { schema: 'reploid.native-canonical-epochs/v1', participantId, meshId,
|
|
58
|
+
revision: (prior?.revision || 0) + 1, participantEpoch: fresh(), coordinatorEpoch: fresh(), pendingOwner: null, settledOwner: null };
|
|
59
|
+
save(value); return value;
|
|
60
|
+
});
|
|
61
|
+
current();
|
|
62
|
+
return Object.freeze({ participantEpoch: claimed.participantEpoch, coordinatorEpoch: claimed.coordinatorEpoch,
|
|
63
|
+
assertCurrent() { current(); },
|
|
64
|
+
begin(owner) {
|
|
65
|
+
assert(epoch(owner), 'invalid resident owner');
|
|
66
|
+
transact(() => { const value = current(); assert(value.pendingOwner === null, 'resident ownership already outstanding'); save({ ...value, pendingOwner: owner, settledOwner: null }); });
|
|
67
|
+
},
|
|
68
|
+
settle(owner) {
|
|
69
|
+
assert(epoch(owner), 'invalid resident owner');
|
|
70
|
+
const value = current();
|
|
71
|
+
if (value.pendingOwner === null && value.settledOwner === owner) {
|
|
72
|
+
// A failed COMMIT acknowledgement may have left the exact settlement
|
|
73
|
+
// durable. Confirm through a separate reader, never an open transaction.
|
|
74
|
+
const reader = new DatabaseSync(filename, { readOnly: true });
|
|
75
|
+
try {
|
|
76
|
+
const committed = validate(JSON.parse(reader.prepare('SELECT record FROM epochs WHERE id = 1').get()?.record));
|
|
77
|
+
assert(JSON.stringify(committed) === JSON.stringify(value), 'settlement outcome remains unknown');
|
|
78
|
+
} finally { reader.close(); }
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
transact(() => { const latest = current(); assert(latest.pendingOwner === owner, 'resident settlement owner differs');
|
|
82
|
+
save({ ...latest, pendingOwner: null, settledOwner: owner }); });
|
|
83
|
+
},
|
|
84
|
+
close() { if (!closed) { db.close(); closed = true; } }
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
try { db.close(); } catch (cleanup) { throw new AggregateError([error, cleanup], 'Native epoch opening and cleanup failed'); }
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change-only status lines for the native participant. A record is written when its
|
|
3
|
+
* content differs from the last one written for the same kind, or when `heartbeatMs` has
|
|
4
|
+
* passed since that kind was last written (then marked `heartbeat: true`). Time-like fields
|
|
5
|
+
* (`at`, `*At`, `lastSeen*`, `timestamp`, `*elapsedMs`, `*uptimeMs`) are ignored when deciding
|
|
6
|
+
* whether content changed, so an idle host writes only heartbeats.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const VOLATILE_KEY = /^(at|timestamp)$|At$|^lastSeen|elapsedMs$|uptimeMs$/;
|
|
10
|
+
|
|
11
|
+
/** Stable comparison key: sorted keys, volatile fields removed at every depth. */
|
|
12
|
+
export function statusContentKey(value) {
|
|
13
|
+
const strip = item => {
|
|
14
|
+
if (Array.isArray(item)) return item.map(strip);
|
|
15
|
+
if (!item || typeof item !== 'object') return item;
|
|
16
|
+
return Object.fromEntries(Object.keys(item).sort().filter(key => !VOLATILE_KEY.test(key)).map(key => [key, strip(item[key])]));
|
|
17
|
+
};
|
|
18
|
+
return JSON.stringify(strip(value));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} options
|
|
23
|
+
* @param {number} options.heartbeatMs positive interval after which an unchanged kind is written again
|
|
24
|
+
* @param {(line: string) => void} options.write sink for one JSON line
|
|
25
|
+
* @param {() => number} [options.now]
|
|
26
|
+
* @returns {(kind: string, record: object) => boolean} writes when changed or due; returns whether it wrote
|
|
27
|
+
*/
|
|
28
|
+
export function createChangeOnlyLog({ heartbeatMs, write, now = Date.now }) {
|
|
29
|
+
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs <= 0) throw new Error('statusHeartbeatMs must be a positive integer in the partition policy');
|
|
30
|
+
if (typeof write !== 'function') throw new Error('A status log sink is required');
|
|
31
|
+
const last = new Map();
|
|
32
|
+
return (kind, record) => {
|
|
33
|
+
const key = statusContentKey(record), previous = last.get(kind), at = now();
|
|
34
|
+
const changed = !previous || previous.key !== key;
|
|
35
|
+
if (!changed && at - previous.writtenAt < heartbeatMs) return false;
|
|
36
|
+
write(JSON.stringify(changed ? record : { ...record, heartbeat: true }));
|
|
37
|
+
last.set(kind, { key, writtenAt: at });
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native contributor earning: the composition a native participant passes as
|
|
3
|
+
* its `earning` port (native-participant.js). From an explicit
|
|
4
|
+
* reploid.native-earning/v1 file it signs this host in through the service's
|
|
5
|
+
* sign-in flow, registers the host's persistent participant identity as a
|
|
6
|
+
* native device, records the owner policy in force, asks the service for a
|
|
7
|
+
* stage-share or exact whole-model variant offer for the capacity this host
|
|
8
|
+
* serves, accepts it, and claims settled work under the offer that covered it.
|
|
9
|
+
* Durable state (session, outbox, claims and accepted offers) lives in one
|
|
10
|
+
* file beside the identity, so a restarted host neither loses nor doubles a
|
|
11
|
+
* claim. Whole-model execution records admission before dispatch and retains
|
|
12
|
+
* countersigned receipt references until the service settles and claims them.
|
|
13
|
+
* Off unless the file is given; the repository ships none.
|
|
14
|
+
*
|
|
15
|
+
* File: { schema: 'reploid.native-earning/v1',
|
|
16
|
+
* client: { schema: 'reploid.earning-client/v1', enabled: true, baseUrl, providerId },
|
|
17
|
+
* signIn?: { subject } } // only for the service's local test flow
|
|
18
|
+
*
|
|
19
|
+
* Native sign-in supports the local test flow only: Supabase email links
|
|
20
|
+
* complete in a browser, and a native link flow is not implemented.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { sha256Hex } from './hashing.js';
|
|
26
|
+
import { createNativeWholeEarning } from './native-whole-earning.js';
|
|
27
|
+
|
|
28
|
+
import { hashDopplerEvidence } from '../shared/pool/executable-pack.js';
|
|
29
|
+
import { EARNING_CLIENT_SCHEMA, createEarningClient, readEarningClientConfig } from '../apps/poolday/host/earning/client.js';
|
|
30
|
+
import { createEarningContribution } from '../apps/poolday/host/earning/contribution.js';
|
|
31
|
+
|
|
32
|
+
export const NATIVE_EARNING_SCHEMA = 'reploid.native-earning/v1';
|
|
33
|
+
const STAGE_SHARE = 'demanded_inference';
|
|
34
|
+
// An offer is renewed once this share of its duration has passed, so the next one is accepted before it ends.
|
|
35
|
+
const RENEW_AT = 0.9;
|
|
36
|
+
const SUBJECT = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
37
|
+
|
|
38
|
+
const fail = (message) => { throw new Error(message); };
|
|
39
|
+
const toMs = (value) => (typeof value === 'number' ? value : Date.parse(value));
|
|
40
|
+
const selectedModel = model => model.executionMode === 'whole-model'
|
|
41
|
+
? { modelId: model.modelId, modelIdentity: model.modelIdentity, executionMode: 'whole-model', adapterIdentities: [...model.adapterIdentities] }
|
|
42
|
+
: { modelId: model.modelId, modelIdentity: model.modelIdentity, planId: model.planId };
|
|
43
|
+
const modelKey = model => model.executionMode === 'whole-model' ? JSON.stringify(selectedModel(model))
|
|
44
|
+
: `${model.modelId}|${model.modelIdentity}|${model.planId}`;
|
|
45
|
+
|
|
46
|
+
/** Read and check the native earning file: an absolute path to a regular JSON file. */
|
|
47
|
+
export async function readNativeEarningFile(file) {
|
|
48
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) fail('--earning-config requires an absolute path');
|
|
49
|
+
const stat = await fs.stat(file).catch(() => null);
|
|
50
|
+
if (!stat?.isFile()) fail(`--earning-config must name a regular file: ${file}`);
|
|
51
|
+
let value;
|
|
52
|
+
try { value = JSON.parse(await fs.readFile(file, 'utf8')); } catch { fail(`--earning-config is not valid JSON: ${file}`); }
|
|
53
|
+
const keys = Object.keys(value ?? {});
|
|
54
|
+
if (value?.schema !== NATIVE_EARNING_SCHEMA || keys.some((key) => !['schema', 'client', 'signIn'].includes(key))) {
|
|
55
|
+
fail(`--earning-config must be ${NATIVE_EARNING_SCHEMA} with client and an optional signIn`);
|
|
56
|
+
}
|
|
57
|
+
if (value.client?.schema !== EARNING_CLIENT_SCHEMA || value.client.enabled !== true) fail(`--earning-config client must be an enabled ${EARNING_CLIENT_SCHEMA} configuration`);
|
|
58
|
+
const client = readEarningClientConfig(value.client);
|
|
59
|
+
if (value.signIn !== undefined && (Object.keys(value.signIn ?? {}).join() !== 'subject' || !SUBJECT.test(value.signIn.subject ?? ''))) {
|
|
60
|
+
fail('--earning-config signIn must be { subject } with a short lowercase name');
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze({ client, signIn: value.signIn ? { subject: value.signIn.subject } : null });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Durable { get, set, delete } over one JSON file, written whole and renamed into place. */
|
|
66
|
+
export function createNativeEarningStorage(file) {
|
|
67
|
+
let queue = Promise.resolve();
|
|
68
|
+
const load = async () => {
|
|
69
|
+
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return {}; throw error; }
|
|
70
|
+
};
|
|
71
|
+
const write = (change) => {
|
|
72
|
+
const run = queue.then(async () => {
|
|
73
|
+
const state = await load();
|
|
74
|
+
change(state);
|
|
75
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
76
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
77
|
+
await fs.writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
|
|
78
|
+
await fs.rename(temporary, file);
|
|
79
|
+
});
|
|
80
|
+
queue = run.catch(() => {});
|
|
81
|
+
return run;
|
|
82
|
+
};
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
async get(key) { await queue; const state = await load(); return key in state ? structuredClone(state[key]) : null; },
|
|
85
|
+
set: (key, value) => write((state) => { state[key] = structuredClone(value); }),
|
|
86
|
+
delete: (key) => write((state) => { delete state[key]; })
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The sign-in port for a native host: the service's local test flow for the configured subject. */
|
|
91
|
+
export function createNativeSignIn({ config, subject, fetch = globalThis.fetch }) {
|
|
92
|
+
return async ({ providerId = config.providerId } = {}) => {
|
|
93
|
+
const settings = await fetch(`${config.baseUrl}/sign-in/${encodeURIComponent(providerId)}`).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
94
|
+
if (settings?.flow !== 'local_test_subject') {
|
|
95
|
+
fail(`Native sign-in supports the service's local test flow only; ${providerId} offers ${settings?.flow ?? 'no flow this host can use'}`);
|
|
96
|
+
}
|
|
97
|
+
if (!subject) fail('The local test flow needs signIn.subject in the native earning file');
|
|
98
|
+
const response = await fetch(`${config.baseUrl}/sign-in/${encodeURIComponent(providerId)}/assertions`, {
|
|
99
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ subject })
|
|
100
|
+
});
|
|
101
|
+
const body = await response.json().catch(() => null);
|
|
102
|
+
if (!response.ok || typeof body?.assertion !== 'string') fail(body?.message || `The sign-in service answered ${response.status}`);
|
|
103
|
+
return body.assertion;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {object} options
|
|
109
|
+
* @param {object} options.settings readNativeEarningFile(...) result
|
|
110
|
+
* @param {object} options.identity this host's persistent participant identity
|
|
111
|
+
* @param {object} options.ownerAuthorization { policy, digest } of the owner policy in force
|
|
112
|
+
* @param {object} options.storage createNativeEarningStorage(...) or an equivalent durable store
|
|
113
|
+
*/
|
|
114
|
+
export async function openNativeEarning({ settings, identity, ownerAuthorization, storage, fetch = globalThis.fetch,
|
|
115
|
+
hashEvidence = hashDopplerEvidence, now = Date.now, signIn = null }) {
|
|
116
|
+
if (!ownerAuthorization?.digest || !ownerAuthorization.policy) fail('Native earning requires the owner policy in force');
|
|
117
|
+
const client = createEarningClient({ config: settings.client, identity, host: 'native', storage, fetch,
|
|
118
|
+
signIn: signIn ?? createNativeSignIn({ config: settings.client, subject: settings.signIn?.subject, fetch }) });
|
|
119
|
+
const session = await client.ensureSession();
|
|
120
|
+
const ownerPolicyDigest = ownerAuthorization.digest;
|
|
121
|
+
const base = createEarningContribution({ client, offers: {}, ownerPolicyDigest, hashEvidence });
|
|
122
|
+
await base.recordOwnerAuthorization(ownerAuthorization);
|
|
123
|
+
const contributions = new Map();
|
|
124
|
+
const contributionFor = (offerId) => {
|
|
125
|
+
if (!contributions.has(offerId)) contributions.set(offerId, createEarningContribution({ client, offers: { stageShare: offerId, wholeModel: offerId }, ownerPolicyDigest, hashEvidence }));
|
|
126
|
+
return contributions.get(offerId);
|
|
127
|
+
};
|
|
128
|
+
const readOffers = async () => (await storage.get('offers')) ?? [];
|
|
129
|
+
let offering = Promise.resolve();
|
|
130
|
+
|
|
131
|
+
/** An accepted, current stage-share offer for `model`, requested and accepted when none covers now. */
|
|
132
|
+
function ensureOffer(model) {
|
|
133
|
+
const run = offering.then(async () => {
|
|
134
|
+
const offers = await readOffers();
|
|
135
|
+
const current = offers.find((offer) => offer.key === modelKey(model)
|
|
136
|
+
&& (model.executionMode !== 'whole-model' || offer.ownerPolicyDigest === ownerPolicyDigest && now() >= toMs(offer.startsAt))
|
|
137
|
+
&& now() < toMs(offer.startsAt) + RENEW_AT * (toMs(offer.endsAt) - toMs(offer.startsAt)));
|
|
138
|
+
if (current) return current;
|
|
139
|
+
const offered = await client.send({ opId: `offer:${sha256Hex(modelKey(model))}:${now()}`, path: '/offers', durable: false,
|
|
140
|
+
body: { category: STAGE_SHARE, model: selectedModel(model) } });
|
|
141
|
+
if (model.executionMode === 'whole-model' && (offered.work?.participantId !== identity.peerId
|
|
142
|
+
|| modelKey(offered.work.model) !== modelKey(model))) fail('Offered whole-model variant differs from this contributor');
|
|
143
|
+
const accepted = await base.acceptOffer(offered.offerId, ownerAuthorization);
|
|
144
|
+
const record = { offerId: offered.offerId, key: modelKey(model), acceptedAt: accepted.acceptedAt ?? new Date(now()).toISOString(),
|
|
145
|
+
startsAt: offered.duration.startsAt, endsAt: offered.duration.endsAt, ownerPolicyDigest };
|
|
146
|
+
if (model.executionMode === 'whole-model' && !(now() >= toMs(record.startsAt) && now() < toMs(record.endsAt))) {
|
|
147
|
+
fail('Accepted whole-model offer is outside its execution window');
|
|
148
|
+
}
|
|
149
|
+
await storage.set('offers', [...offers, record]);
|
|
150
|
+
return record;
|
|
151
|
+
});
|
|
152
|
+
offering = run.catch(() => {});
|
|
153
|
+
return run;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const whole = createNativeWholeEarning({ storage, participantId: identity.peerId, ownerPolicyDigest, contributionFor, now,
|
|
157
|
+
findOffer: async model => (await readOffers()).find(offer => offer.key === modelKey(model) && offer.ownerPolicyDigest === ownerPolicyDigest
|
|
158
|
+
&& toMs(offer.acceptedAt) <= now() && toMs(offer.startsAt) <= now() && now() < toMs(offer.endsAt)) });
|
|
159
|
+
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
client,
|
|
162
|
+
session,
|
|
163
|
+
participantId: identity.peerId,
|
|
164
|
+
ensureOffer,
|
|
165
|
+
prepareWholeModel: whole.prepare,
|
|
166
|
+
recordWholeModelReceipt: whole.record,
|
|
167
|
+
offers: readOffers,
|
|
168
|
+
/**
|
|
169
|
+
* The participant's earning port. `descriptor`, when the host passes its prepared stage, names the
|
|
170
|
+
* model and plan to be offered for before any route admits it; admission records also name their
|
|
171
|
+
* route's model, so a host that passes none still earns from later work.
|
|
172
|
+
*/
|
|
173
|
+
async observe({ admissions = [], deliveries = [], descriptor = null, wholeModels = [] } = {}) {
|
|
174
|
+
// Previously accepted work can still claim even if renewing supply fails.
|
|
175
|
+
const claimedWholeModels = await whole.flush();
|
|
176
|
+
const served = descriptor?.planId ? [descriptor, ...wholeModels] : [...wholeModels];
|
|
177
|
+
for (const record of admissions) if (record?.identity?.route?.planId) served.push(record.identity.route);
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
for (const model of served) {
|
|
180
|
+
if (seen.has(modelKey(model))) continue;
|
|
181
|
+
seen.add(modelKey(model));
|
|
182
|
+
await ensureOffer(model);
|
|
183
|
+
}
|
|
184
|
+
const offers = await readOffers();
|
|
185
|
+
// Each record is claimed under the accepted offer whose window covered its admission.
|
|
186
|
+
const groups = new Map();
|
|
187
|
+
for (const record of admissions) {
|
|
188
|
+
const at = toMs(record?.reservedAt);
|
|
189
|
+
const offer = offers.find((row) => row.key === modelKey(record?.identity?.route ?? {}) && toMs(row.acceptedAt) <= at && at <= toMs(row.endsAt));
|
|
190
|
+
if (!offer) continue;
|
|
191
|
+
groups.set(offer.offerId, [...(groups.get(offer.offerId) ?? []), record]);
|
|
192
|
+
}
|
|
193
|
+
// Signed receipts of every closed charged attempt are submitted, offer or not: they settle the requester's charge.
|
|
194
|
+
const { evidence, deliveries: delivered } = await base.observe({ admissions, deliveries });
|
|
195
|
+
const results = { evidence, stages: [], deliveries: delivered, wholeModels: claimedWholeModels };
|
|
196
|
+
for (const [offerId, records] of groups) results.stages.push(...await contributionFor(offerId).claimStageShares(records));
|
|
197
|
+
return results;
|
|
198
|
+
},
|
|
199
|
+
reconcile: () => client.reconcile()
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default openNativeEarning;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Durable admission fences and bounded result replay. No prompt text is stored. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
export async function openNativeGenerationJournal({ file, maxResults, maxResultBytes }) {
|
|
7
|
+
if (!path.isAbsolute(file) || !Number.isSafeInteger(maxResults) || maxResults < 1
|
|
8
|
+
|| !Number.isSafeInteger(maxResultBytes) || maxResultBytes < 1) throw Error('Explicit generation journal limits required');
|
|
9
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
10
|
+
const handle = await fs.open(file, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
11
|
+
try { if (!(await handle.stat()).isFile()) throw Error('Generation journal must be a regular file'); }
|
|
12
|
+
finally { await handle.close(); }
|
|
13
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
14
|
+
const db = new DatabaseSync(file);
|
|
15
|
+
db.exec('PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS generation (id TEXT PRIMARY KEY, binding TEXT NOT NULL, outcome TEXT, finished INTEGER NOT NULL DEFAULT 0)');
|
|
16
|
+
let closed = false;
|
|
17
|
+
const key = binding => JSON.stringify([binding.provider, binding.consumer, binding.requestId]);
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
async get(binding) {
|
|
20
|
+
const row = db.prepare('SELECT binding, outcome FROM generation WHERE id = ?').get(key(binding));
|
|
21
|
+
return row ? { binding: JSON.parse(row.binding), outcome: row.outcome ? JSON.parse(row.outcome) : null } : null;
|
|
22
|
+
},
|
|
23
|
+
async unfinished() {
|
|
24
|
+
return db.prepare('SELECT binding FROM generation WHERE finished = 0 ORDER BY rowid ASC').all()
|
|
25
|
+
.map(row => JSON.parse(row.binding));
|
|
26
|
+
},
|
|
27
|
+
async claim(binding) {
|
|
28
|
+
return db.prepare('INSERT OR IGNORE INTO generation (id, binding) VALUES (?, ?)')
|
|
29
|
+
.run(key(binding), JSON.stringify(binding)).changes === 1;
|
|
30
|
+
},
|
|
31
|
+
async finish(binding, outcome) {
|
|
32
|
+
const serialized = JSON.stringify(outcome);
|
|
33
|
+
if (Buffer.byteLength(serialized) > maxResultBytes) throw Error('Generation recovery result exceeds retained byte limit');
|
|
34
|
+
db.exec('BEGIN IMMEDIATE');
|
|
35
|
+
try {
|
|
36
|
+
const changed = db.prepare('UPDATE generation SET outcome = ?, finished = 1 WHERE id = ? AND finished = 0').run(serialized, key(binding));
|
|
37
|
+
if (changed.changes !== 1) throw Error('Generation missing or already finished');
|
|
38
|
+
// Keep admission fences when pruning result bodies. A replay cannot execute again.
|
|
39
|
+
db.prepare('UPDATE generation SET outcome = NULL WHERE outcome IS NOT NULL AND rowid NOT IN '
|
|
40
|
+
+ '(SELECT rowid FROM generation WHERE outcome IS NOT NULL ORDER BY rowid DESC LIMIT ?)').run(maxResults);
|
|
41
|
+
db.exec('COMMIT');
|
|
42
|
+
} catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
43
|
+
},
|
|
44
|
+
close() { if (!closed) { closed = true; db.close(); } }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Operator-local model configuration; never public catalog or release authority. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { sha256Digest } from './hashing.js';
|
|
5
|
+
import { resolveGenerationOptions } from '../shared/config/doppler-generation-contract.js';
|
|
6
|
+
|
|
7
|
+
const requireValue = (condition, message) => { if (!condition) throw new Error(message); };
|
|
8
|
+
|
|
9
|
+
export async function readNativeModelConfiguration(file, { modelId, modelDirectory, models = [],
|
|
10
|
+
loadRuntime = () => import('doppler-gpu/partitions') }) {
|
|
11
|
+
requireValue(path.isAbsolute(file), 'Native model configuration requires an absolute JSON path');
|
|
12
|
+
const configuration = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
13
|
+
requireValue(configuration?.schema === 'reploid.native-model/v1'
|
|
14
|
+
&& Object.keys(configuration).every(key => ['schema', 'model', 'partition', 'partitionRuntime'].includes(key)),
|
|
15
|
+
'Unknown native model configuration schema or field');
|
|
16
|
+
const catalogReference = typeof configuration.model === 'string';
|
|
17
|
+
const model = structuredClone(catalogReference ? models.find(row => row.id === configuration.model) : configuration.model);
|
|
18
|
+
requireValue(model?.id === modelId && model.provider === 'doppler' && typeof model.name === 'string'
|
|
19
|
+
&& /^sha256:[a-f0-9]{64}$/.test(model.identity), 'Native model configuration requires an exact Doppler model identity');
|
|
20
|
+
model.generation = resolveGenerationOptions(model.generation);
|
|
21
|
+
const root = await fs.realpath(modelDirectory);
|
|
22
|
+
const manifestPath = await fs.realpath(path.join(root, 'manifest.json'));
|
|
23
|
+
requireValue(path.dirname(manifestPath) === root, 'Native model manifest escapes the approved directory');
|
|
24
|
+
const bytes = await fs.readFile(manifestPath);
|
|
25
|
+
requireValue(sha256Digest(bytes) === model.identity,
|
|
26
|
+
'Native model configuration differs from the actual manifest');
|
|
27
|
+
const manifest = JSON.parse(bytes);
|
|
28
|
+
requireValue(manifest.modelId === model.id, 'Native model configuration differs from the actual model id');
|
|
29
|
+
if (configuration.partitionRuntime !== undefined) model.partitionRuntime = structuredClone(configuration.partitionRuntime);
|
|
30
|
+
if (model.partitionRuntime?.runtimeConfig === 'model') {
|
|
31
|
+
requireValue(model.loadOptions?.runtimeConfig && typeof model.loadOptions.runtimeConfig === 'object',
|
|
32
|
+
'The model runtime reference requires explicit model.loadOptions.runtimeConfig');
|
|
33
|
+
model.partitionRuntime.runtimeConfig = structuredClone(model.loadOptions.runtimeConfig);
|
|
34
|
+
}
|
|
35
|
+
if (configuration.partition !== undefined) {
|
|
36
|
+
requireValue(catalogReference || model.partitionPlan === undefined && model.partitionPlanId === undefined,
|
|
37
|
+
'Choose one partition request rather than competing prepared plans');
|
|
38
|
+
requireValue(catalogReference || model.executionModes?.includes('partition'), 'Native model must explicitly allow partition execution');
|
|
39
|
+
const runtime = await loadRuntime();
|
|
40
|
+
requireValue(typeof runtime.createOrderedLayerPartitionPlanFromManifest === 'function'
|
|
41
|
+
&& typeof runtime.hashLayerPartitionPlan === 'function',
|
|
42
|
+
'Installed Doppler lacks manifest-driven partition preparation; install the matching runtime');
|
|
43
|
+
model.partitionPlan = runtime.createOrderedLayerPartitionPlanFromManifest(manifest, configuration.partition);
|
|
44
|
+
model.partitionPlanId = runtime.hashLayerPartitionPlan(model.partitionPlan);
|
|
45
|
+
// An explicit ordered plan replaces the catalog's legacy two-stage cut.
|
|
46
|
+
// Keeping both makes downstream selection reject the otherwise valid plan.
|
|
47
|
+
delete model.partitionSplitLayer;
|
|
48
|
+
if (catalogReference) model.executionModes = [...new Set([...(model.executionModes || []), 'partition'])];
|
|
49
|
+
const budgets = model.partitionRuntime?.maxGpuBufferBytes;
|
|
50
|
+
requireValue(Array.isArray(budgets) && budgets.length === model.partitionPlan.stageCount
|
|
51
|
+
&& budgets.every(value => Number.isSafeInteger(value) && value > 0),
|
|
52
|
+
'Declare one positive GPU buffer budget per partition stage');
|
|
53
|
+
requireValue(model.partitionRuntime.runtimeConfig && typeof model.partitionRuntime.runtimeConfig === 'object',
|
|
54
|
+
'Declare partition runtime configuration explicitly');
|
|
55
|
+
}
|
|
56
|
+
return model;
|
|
57
|
+
}
|