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,162 @@
|
|
|
1
|
+
/** Owner admission gate for the native participant. It decides whether new stage
|
|
2
|
+
* work, contribution preparation and piece supply may start; it never cancels,
|
|
3
|
+
* aborts or closes work that was already admitted. With no owner policy the
|
|
4
|
+
* participant never creates it and behaves exactly as before. */
|
|
5
|
+
import failureMessages from '../../packages/reploid/src/transport/partition-errors.json' with { type: 'json' };
|
|
6
|
+
import { readNativeOwnerPolicy, validateNativeOwnerPolicy, nativeOwnerPolicyDigest, decideNativeOwnerAdmission, custodySupplyTransfers } from './policy.js';
|
|
7
|
+
import { detectNativeOwnerSupport, createNativeOwnerMonitor, runNativeTool } from './monitor.js';
|
|
8
|
+
import { createByteRateLimiter, createTransferMeter, paceCustodyChannel } from './rate.js';
|
|
9
|
+
|
|
10
|
+
export const NO_OWNER_POLICY = Object.freeze({ status: 'no owner policy' });
|
|
11
|
+
|
|
12
|
+
/** Compact admission state for each partition-state status line. */
|
|
13
|
+
export function nativeOwnerStatus(owner) {
|
|
14
|
+
if (!owner) return NO_OWNER_POLICY;
|
|
15
|
+
const { action, message } = owner.decide();
|
|
16
|
+
return { status: 'owner policy', digest: owner.digest, action, message };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `file` (absolute owner policy path) or an already parsed `policy`. `custody` is the
|
|
20
|
+
* application's custody policy (self/config/chat-files.json). */
|
|
21
|
+
export async function openNativeOwnerControls({ file, policy: parsed, custody, platform = process.platform, sysfsRoot = '/sys',
|
|
22
|
+
run = runNativeTool, now = Date.now, readGpuBuffers = null, onChange = () => {} }) {
|
|
23
|
+
const loaded = file !== undefined ? await readNativeOwnerPolicy(file, { custody })
|
|
24
|
+
: (policy => ({ policy, digest: nativeOwnerPolicyDigest(policy) }))(validateNativeOwnerPolicy(parsed, { custody }));
|
|
25
|
+
const { policy, digest } = loaded;
|
|
26
|
+
const support = await detectNativeOwnerSupport({ policy, platform, sysfsRoot, run });
|
|
27
|
+
const monitor = createNativeOwnerMonitor({ policy, support, platform, run, now, readGpuBuffers });
|
|
28
|
+
const limiter = key => policy.bandwidth?.[key] === undefined ? null
|
|
29
|
+
: createByteRateLimiter({ bytesPerSecond: policy.bandwidth[key], capacityBytes: custody.channel.maxChunkBytes, now });
|
|
30
|
+
const upload = limiter('uploadBytesPerSecond'), download = limiter('downloadBytesPerSecond'), meter = createTransferMeter();
|
|
31
|
+
// Paced supply admits only as many transfers as the rate can finish within custody deadlines.
|
|
32
|
+
const supplyTransfers = upload ? custodySupplyTransfers(custody, upload.bytesPerSecond) : custody.maxTransfers;
|
|
33
|
+
const startedAt = now(), cpuStart = process.cpuUsage(), waiters = new Map();
|
|
34
|
+
const counts = { stageAdmitted: 0, stageRefused: 0, supplyAdmitted: 0, supplyRefused: 0 };
|
|
35
|
+
const actionMs = { admit: 0, reduce: 0, defer: 0, reject: 0 };
|
|
36
|
+
let readings = null, last = null, closed = false, sampling = null, peakResidentBytes = 0, maxCelsius = null;
|
|
37
|
+
let previousTransfer = { at: startedAt, ...meter.getState() }, measuredRates = { suppliedBytesPerSecond: null, receivedBytesPerSecond: null };
|
|
38
|
+
const decide = () => decideNativeOwnerAdmission({ policy, support, readings, at: now() });
|
|
39
|
+
const stageAllowance = decision => decision.action === 'admit' ? Infinity
|
|
40
|
+
: decision.action === 'reduce' ? decision.limits.maxConcurrentAttempts : 0;
|
|
41
|
+
const supplyAllowance = decision => decision.action === 'admit' ? supplyTransfers
|
|
42
|
+
: decision.action === 'reduce' ? Math.min(supplyTransfers, decision.limits.maxSupplyTransfers) : 0;
|
|
43
|
+
const decisionRecord = decision => ({ action: decision.action, message: decision.message, at: new Date(decision.at).toISOString(),
|
|
44
|
+
reasons: decision.reasons.map(reason => reason.message), ...(decision.limits ? { limits: { ...decision.limits } } : {}) });
|
|
45
|
+
|
|
46
|
+
function summary() {
|
|
47
|
+
const decision = decide(), transfer = meter.getState(), cpu = process.cpuUsage(cpuStart);
|
|
48
|
+
return { status: 'owner policy', digest, decision: decisionRecord(decision),
|
|
49
|
+
waiting: [...new Set(waiters.values())],
|
|
50
|
+
readings: readings && { at: new Date(readings.at).toISOString(), processResidentBytes: readings.processResidentBytes,
|
|
51
|
+
gpuBuffers: readings.gpuBuffers, temperatureCelsius: readings.temperature?.maxCelsius ?? null,
|
|
52
|
+
gpuPowerWatts: readings.gpuPower?.watts ?? null, externalPower: readings.externalPower,
|
|
53
|
+
inputIdleSeconds: readings.inputIdleSeconds, foregroundSignal: readings.foregroundSignal },
|
|
54
|
+
transfer: { upload: { limitBytesPerSecond: upload?.bytesPerSecond ?? null, maxSupplyTransfers: supplyAllowance(decision),
|
|
55
|
+
suppliedBytes: transfer.suppliedBytes, measuredBytesPerSecond: measuredRates.suppliedBytesPerSecond, queuedBytes: upload?.getState().queuedBytes ?? 0 },
|
|
56
|
+
download: { limitBytesPerSecond: download?.bytesPerSecond ?? null, receivedBytes: transfer.receivedBytes,
|
|
57
|
+
measuredBytesPerSecond: measuredRates.receivedBytesPerSecond, queuedBytes: download?.getState().queuedBytes ?? 0 } },
|
|
58
|
+
measurements: { since: new Date(startedAt).toISOString(), elapsedMs: now() - startedAt,
|
|
59
|
+
cpuUserMicros: cpu.user, cpuSystemMicros: cpu.system, cpuScope: 'whole native process',
|
|
60
|
+
peakProcessResidentBytes: peakResidentBytes, maxTemperatureCelsius: maxCelsius, gpuEnergy: monitor.getEnergy(),
|
|
61
|
+
actionMs: { ...actionMs }, ...counts } };
|
|
62
|
+
}
|
|
63
|
+
function refresh() {
|
|
64
|
+
const decision = decide();
|
|
65
|
+
if (last) actionMs[last.action] += Math.max(0, decision.at - last.at);
|
|
66
|
+
const changed = !last || last.message !== decision.message;
|
|
67
|
+
last = decision;
|
|
68
|
+
for (const wake of [...waiters.keys()]) wake(decision);
|
|
69
|
+
if (changed && !closed) onChange(summary());
|
|
70
|
+
return decision;
|
|
71
|
+
}
|
|
72
|
+
function sample() {
|
|
73
|
+
return sampling ||= (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const next = await monitor.sample();
|
|
76
|
+
readings = next;
|
|
77
|
+
peakResidentBytes = Math.max(peakResidentBytes, next.processResidentBytes);
|
|
78
|
+
const celsius = next.temperature?.maxCelsius;
|
|
79
|
+
if (typeof celsius === 'number') maxCelsius = maxCelsius === null ? celsius : Math.max(maxCelsius, celsius);
|
|
80
|
+
const transfer = meter.getState(), elapsed = (next.at - previousTransfer.at) / 1000;
|
|
81
|
+
if (elapsed > 0) measuredRates = { suppliedBytesPerSecond: (transfer.suppliedBytes - previousTransfer.suppliedBytes) / elapsed,
|
|
82
|
+
receivedBytesPerSecond: (transfer.receivedBytes - previousTransfer.receivedBytes) / elapsed };
|
|
83
|
+
previousTransfer = { at: next.at, ...transfer };
|
|
84
|
+
} catch { /* Previous readings age out and defer admission; they are never assumed safe. */ }
|
|
85
|
+
return refresh();
|
|
86
|
+
})().finally(() => { sampling = null; });
|
|
87
|
+
}
|
|
88
|
+
await sample();
|
|
89
|
+
const timer = setInterval(() => { if (!closed) sample(); }, policy.sampleIntervalMs);
|
|
90
|
+
timer.unref?.();
|
|
91
|
+
|
|
92
|
+
function admitStage(active) {
|
|
93
|
+
const allowed = active < stageAllowance(decide());
|
|
94
|
+
counts[allowed ? 'stageAdmitted' : 'stageRefused']++;
|
|
95
|
+
return allowed;
|
|
96
|
+
}
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
policy, digest, support,
|
|
99
|
+
decide, sample, summary,
|
|
100
|
+
declaration: () => ({ status: 'owner policy', digest, policy, platform: support.platform, controls: structuredClone(support.controls) }),
|
|
101
|
+
/** Resolves when new work may start; rejects at once if the policy can never be enforced here. */
|
|
102
|
+
waitForAdmission({ kind, signal } = {}) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const done = error => {
|
|
105
|
+
if (!waiters.delete(check)) return;
|
|
106
|
+
signal?.removeEventListener('abort', abort);
|
|
107
|
+
if (error) reject(error); else resolve(decide());
|
|
108
|
+
};
|
|
109
|
+
const check = decision => {
|
|
110
|
+
if (closed) done(new Error('Owner controls closed'));
|
|
111
|
+
else if (decision.action === 'reject') done(new Error('Owner policy ' + decision.message));
|
|
112
|
+
else if (decision.action !== 'defer') done(null);
|
|
113
|
+
};
|
|
114
|
+
const abort = () => done(signal.reason);
|
|
115
|
+
waiters.set(check, kind || 'work');
|
|
116
|
+
if (signal?.aborted) { abort(); return; }
|
|
117
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
118
|
+
check(decide());
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
/** Supply port for the custody exchange: `advertise` asks whether any new supply is admissible. */
|
|
122
|
+
admitSupply({ active, advertise = false }) {
|
|
123
|
+
const allowed = active < supplyAllowance(decide());
|
|
124
|
+
if (!advertise) counts[allowed ? 'supplyAdmitted' : 'supplyRefused']++;
|
|
125
|
+
return allowed;
|
|
126
|
+
},
|
|
127
|
+
paceChannel: createChannel => paceCustodyChannel(createChannel, { upload, download, meter }),
|
|
128
|
+
/** A paced download needs time for its bytes on top of the unchanged stall deadline. */
|
|
129
|
+
pieceTimeoutMs: (baseMs, maxBytes) => download ? baseMs + Math.ceil(maxBytes * 1000 / download.bytesPerSecond) : baseMs,
|
|
130
|
+
gpuBufferBytes: maxBytes => policy.memory?.maxGpuBufferBytes === undefined ? maxBytes : Math.min(maxBytes, policy.memory.maxGpuBufferBytes),
|
|
131
|
+
/** Gates only new attempts. An attempt this resident already holds keeps executing and settles normally. */
|
|
132
|
+
gateResident(resident) {
|
|
133
|
+
const ordered = typeof resident.executeStage === 'function' && typeof resident.transportId === 'string';
|
|
134
|
+
const holds = attemptId => typeof attemptId === 'string' && (resident.getState().reservations?.records || [])
|
|
135
|
+
.some(record => record.identity?.attemptId === attemptId && record.settledAt === null);
|
|
136
|
+
const active = () => resident.getState().reservations?.active ?? 0;
|
|
137
|
+
const gated = state => {
|
|
138
|
+
const reservations = state?.reservations;
|
|
139
|
+
if (!reservations) return state;
|
|
140
|
+
const allowed = Math.max(0, stageAllowance(decide()) - reservations.active);
|
|
141
|
+
return allowed >= reservations.availableSlots ? state : { ...state, reservations: { ...reservations, availableSlots: allowed } };
|
|
142
|
+
};
|
|
143
|
+
const admit = identity => {
|
|
144
|
+
if (holds(identity?.attemptId) || admitStage(active())) return;
|
|
145
|
+
// The same coded refusal the resident's own capacity limit produces on each path.
|
|
146
|
+
throw Object.assign(new Error(failureMessages.CAPACITY_UNAVAILABLE), { code: 'CAPACITY_UNAVAILABLE' });
|
|
147
|
+
};
|
|
148
|
+
return Object.freeze({ ...resident,
|
|
149
|
+
getState: () => gated(resident.getState()),
|
|
150
|
+
subscribe: listener => resident.subscribe(state => listener(gated(state))),
|
|
151
|
+
canAccept: identity => resident.canAccept(identity)
|
|
152
|
+
&& (holds(identity?.attemptId) || stageAllowance(decide()) - active() > 0),
|
|
153
|
+
reserve: ordered ? async request => { admit(request?.identity); return resident.reserve(request); }
|
|
154
|
+
: identity => { admit(identity); return resident.reserve(identity); } });
|
|
155
|
+
},
|
|
156
|
+
close() {
|
|
157
|
+
if (closed) return;
|
|
158
|
+
closed = true; clearInterval(timer); upload?.close(); download?.close();
|
|
159
|
+
for (const wake of [...waiters.keys()]) wake(decide());
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/** Detects which owner controls this host can enforce and samples their readings.
|
|
2
|
+
* A control is supported only when a reading source was found and actually read;
|
|
3
|
+
* a configured setting alone never counts as enforcement. */
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const readText = async file => (await fs.readFile(file, 'utf8')).trim();
|
|
9
|
+
const readNumber = async file => { const value = Number(await readText(file)); if (!Number.isFinite(value)) throw new Error('Not numeric: ' + file); return value; };
|
|
10
|
+
|
|
11
|
+
/** Runs a fixed system tool with no shell and a bounded wait; resolves its stdout. */
|
|
12
|
+
export function runNativeTool(file, args, { timeoutMs }) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
execFile(file, args, { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true },
|
|
15
|
+
(error, stdout) => error ? reject(error) : resolve(String(stdout)));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Linux hwmon devices whose inputs were readable without elevated privileges. */
|
|
20
|
+
export async function probeLinuxHwmon(sysfsRoot) {
|
|
21
|
+
const base = path.join(sysfsRoot, 'class', 'hwmon');
|
|
22
|
+
let entries;
|
|
23
|
+
try { entries = await fs.readdir(base); } catch { return []; }
|
|
24
|
+
const devices = [];
|
|
25
|
+
for (const id of entries.sort()) {
|
|
26
|
+
const directory = path.join(base, id);
|
|
27
|
+
let name, files;
|
|
28
|
+
try { name = await readText(path.join(directory, 'name')); files = await fs.readdir(directory); } catch { continue; }
|
|
29
|
+
const readable = async pattern => {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const file of files.filter(item => pattern.test(item)).sort()) {
|
|
32
|
+
try { await readNumber(path.join(directory, file)); found.push(file); } catch { /* unreadable input is not a source */ }
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
};
|
|
36
|
+
const powers = await readable(/^power\d+_(average|input)$/);
|
|
37
|
+
// One power input per channel; the driver's averaged value is preferred.
|
|
38
|
+
const channels = new Map();
|
|
39
|
+
for (const file of powers) { const channel = file.split('_')[0]; if (!channels.has(channel) || file.endsWith('_average')) channels.set(channel, file); }
|
|
40
|
+
devices.push({ id, name, directory, temperatures: await readable(/^temp\d+_input$/),
|
|
41
|
+
powers: [...channels.values()], energies: await readable(/^energy\d+_input$/) });
|
|
42
|
+
}
|
|
43
|
+
return devices;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** System power supplies, excluding peripheral (scope=Device) batteries. */
|
|
47
|
+
export async function probeLinuxPowerSupply(sysfsRoot) {
|
|
48
|
+
const base = path.join(sysfsRoot, 'class', 'power_supply');
|
|
49
|
+
let entries;
|
|
50
|
+
try { entries = await fs.readdir(base); } catch { return null; }
|
|
51
|
+
const supplies = [];
|
|
52
|
+
for (const id of entries.sort()) {
|
|
53
|
+
const directory = path.join(base, id);
|
|
54
|
+
let type, scope = null;
|
|
55
|
+
try { type = await readText(path.join(directory, 'type')); } catch { continue; }
|
|
56
|
+
try { scope = await readText(path.join(directory, 'scope')); } catch { /* optional */ }
|
|
57
|
+
if (scope === 'Device') continue;
|
|
58
|
+
supplies.push({ id, type, directory });
|
|
59
|
+
}
|
|
60
|
+
return supplies;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** true: external power; false: running on a system battery; null: unknown. */
|
|
64
|
+
async function readLinuxExternalPower(supplies) {
|
|
65
|
+
const batteries = supplies.filter(row => row.type === 'Battery');
|
|
66
|
+
const external = supplies.filter(row => row.type !== 'Battery');
|
|
67
|
+
let unreadable = false;
|
|
68
|
+
for (const supply of external) {
|
|
69
|
+
try { if (await readNumber(path.join(supply.directory, 'online')) === 1) return true; } catch { unreadable = true; }
|
|
70
|
+
}
|
|
71
|
+
if (!batteries.length) return true; // No system battery: the host runs on external power.
|
|
72
|
+
if (unreadable) return null;
|
|
73
|
+
if (external.length) return false;
|
|
74
|
+
// Only batteries are exposed: their charge status says whether external power is connected.
|
|
75
|
+
try {
|
|
76
|
+
const statuses = await Promise.all(batteries.map(row => readText(path.join(row.directory, 'status'))));
|
|
77
|
+
return !statuses.includes('Discharging');
|
|
78
|
+
} catch { return null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parseIdleSeconds = output => {
|
|
82
|
+
const match = /"HIDIdleTime"\s*=\s*(\d+)/.exec(output);
|
|
83
|
+
return match ? Number(match[1]) / 1e9 : null;
|
|
84
|
+
};
|
|
85
|
+
const parseExternalPower = output => {
|
|
86
|
+
const match = /Now drawing from '([^']+)'/.exec(output);
|
|
87
|
+
return match ? match[1] === 'AC Power' : null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
async function readSignal(file) {
|
|
91
|
+
try { await fs.lstat(file); return true; }
|
|
92
|
+
catch (error) { return error.code === 'ENOENT' ? false : null; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MAC_TOOLS = { idle: ['/usr/sbin/ioreg', ['-c', 'IOHIDSystem', '-d', '4']], power: ['/usr/bin/pmset', ['-g', 'batt']] };
|
|
96
|
+
|
|
97
|
+
/** Declares every owner control for this platform. Supported controls name their
|
|
98
|
+
* source; unsupported controls name why. `requested` marks controls the policy uses. */
|
|
99
|
+
export async function detectNativeOwnerSupport({ policy, platform, sysfsRoot, run }) {
|
|
100
|
+
const timeoutMs = Math.floor(policy.sampleIntervalMs / 2);
|
|
101
|
+
const supported = (source, extra = {}) => ({ supported: true, source, ...extra });
|
|
102
|
+
const unsupported = reason => ({ supported: false, reason });
|
|
103
|
+
const controls = {
|
|
104
|
+
schedule: supported('host clock with policy IANA time zone'),
|
|
105
|
+
processMemory: supported('process resident set size'),
|
|
106
|
+
gpuBuffers: supported('Doppler device GPU buffer budget', { scope: 'application GPU buffer allocations, not physical VRAM' }),
|
|
107
|
+
uploadRate: supported('paced custody chunk supply', { scope: 'custody piece and file bytes; execution activations are not paced' }),
|
|
108
|
+
downloadRate: supported('paced custody chunk requests', { scope: 'custody piece and file bytes; execution activations are not paced' }),
|
|
109
|
+
temperature: unsupported('no non-root temperature source on ' + platform),
|
|
110
|
+
gpuPower: unsupported('no non-root GPU power source on ' + platform),
|
|
111
|
+
externalPower: unsupported('no power source reading on ' + platform),
|
|
112
|
+
inputIdle: unsupported('no input-idle source on ' + platform),
|
|
113
|
+
ownerSignal: supported('owner foreground signal file'),
|
|
114
|
+
wallPower: unsupported('wall power requires an external meter')
|
|
115
|
+
};
|
|
116
|
+
const hardware = { hwmon: [], supplies: null };
|
|
117
|
+
if (platform === 'linux') {
|
|
118
|
+
hardware.hwmon = await probeLinuxHwmon(sysfsRoot);
|
|
119
|
+
const names = devices => [...new Set(devices.map(device => device.name))];
|
|
120
|
+
const thermal = hardware.hwmon.filter(device => device.temperatures.length);
|
|
121
|
+
const electrical = hardware.hwmon.filter(device => device.powers.length || device.energies.length);
|
|
122
|
+
controls.temperature = thermal.length ? supported('linux hwmon', { sensors: names(thermal) })
|
|
123
|
+
: unsupported('no readable hwmon temperature input');
|
|
124
|
+
controls.gpuPower = electrical.length ? supported('linux hwmon', { sensors: names(electrical),
|
|
125
|
+
scope: 'component power reading, not wall power' }) : unsupported('no readable hwmon power or energy input');
|
|
126
|
+
hardware.supplies = await probeLinuxPowerSupply(sysfsRoot);
|
|
127
|
+
if (hardware.supplies) {
|
|
128
|
+
const external = await readLinuxExternalPower(hardware.supplies);
|
|
129
|
+
controls.externalPower = external === null ? unsupported('power_supply online state unreadable')
|
|
130
|
+
: supported('linux power_supply');
|
|
131
|
+
} else controls.externalPower = unsupported('no readable power_supply class');
|
|
132
|
+
controls.inputIdle = unsupported('no reliable non-root input-idle source; use foreground source "owner-signal"');
|
|
133
|
+
} else if (platform === 'darwin') {
|
|
134
|
+
controls.temperature = unsupported('no non-root temperature interface (powermetrics requires root)');
|
|
135
|
+
controls.gpuPower = unsupported('no non-root GPU power interface (powermetrics requires root)');
|
|
136
|
+
try {
|
|
137
|
+
controls.externalPower = parseExternalPower(await run(...MAC_TOOLS.power, { timeoutMs })) === null
|
|
138
|
+
? unsupported('pmset reported no power source') : supported('pmset -g batt');
|
|
139
|
+
} catch (error) { controls.externalPower = unsupported('pmset unavailable: ' + error.message); }
|
|
140
|
+
try {
|
|
141
|
+
controls.inputIdle = parseIdleSeconds(await run(...MAC_TOOLS.idle, { timeoutMs })) === null
|
|
142
|
+
? unsupported('ioreg reported no HIDIdleTime') : supported('ioreg IOHIDSystem HIDIdleTime');
|
|
143
|
+
} catch (error) { controls.inputIdle = unsupported('ioreg unavailable: ' + error.message); }
|
|
144
|
+
} else if (platform === 'win32') {
|
|
145
|
+
controls.temperature = unsupported('no non-administrator temperature interface implemented on Windows');
|
|
146
|
+
controls.gpuPower = unsupported('no GPU power interface implemented on Windows');
|
|
147
|
+
controls.externalPower = unsupported('no power source interface implemented on Windows');
|
|
148
|
+
controls.inputIdle = unsupported('input idle requires the interactive session; not implemented on Windows');
|
|
149
|
+
}
|
|
150
|
+
// A named sensor must exist; a sensor class alone does not enforce the owner's choice.
|
|
151
|
+
const requireSensors = (control, names) => {
|
|
152
|
+
const entry = controls[control];
|
|
153
|
+
if (!entry.supported) return;
|
|
154
|
+
const missing = names.filter(name => !entry.sensors.includes(name));
|
|
155
|
+
if (missing.length) controls[control] = unsupported('no readable sensor named ' + missing.join(', '));
|
|
156
|
+
else controls[control] = { ...entry, sensors: [...names] };
|
|
157
|
+
};
|
|
158
|
+
if (policy.temperature) requireSensors('temperature', policy.temperature.sensors);
|
|
159
|
+
if (policy.power?.gpuSensors) requireSensors('gpuPower', policy.power.gpuSensors);
|
|
160
|
+
if (policy.foreground?.source === 'owner-signal') {
|
|
161
|
+
const parent = path.dirname(policy.foreground.signalFile);
|
|
162
|
+
try { if (!(await fs.stat(parent)).isDirectory()) throw new Error('not a directory'); }
|
|
163
|
+
catch { controls.ownerSignal = unsupported('signal file directory is unavailable: ' + parent); }
|
|
164
|
+
}
|
|
165
|
+
const requested = {
|
|
166
|
+
schedule: !!policy.schedule, processMemory: policy.memory?.maxProcessResidentBytes !== undefined,
|
|
167
|
+
gpuBuffers: policy.memory?.maxGpuBufferBytes !== undefined, uploadRate: policy.bandwidth?.uploadBytesPerSecond !== undefined,
|
|
168
|
+
downloadRate: policy.bandwidth?.downloadBytesPerSecond !== undefined, temperature: !!policy.temperature,
|
|
169
|
+
// Measurement-only GPU sensors are declared but limit nothing, so they are not requested controls.
|
|
170
|
+
gpuPower: policy.power?.reduceAboveWatts !== undefined || policy.power?.deferAboveWatts !== undefined,
|
|
171
|
+
externalPower: !!policy.power?.requireExternalPower,
|
|
172
|
+
inputIdle: policy.foreground?.source === 'input-idle', ownerSignal: policy.foreground?.source === 'owner-signal', wallPower: false };
|
|
173
|
+
for (const [name, entry] of Object.entries(controls)) controls[name] = { requested: requested[name], ...entry };
|
|
174
|
+
return { platform, controls, hardware };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Samples readings for supported controls. Unsupported sources are never read and
|
|
178
|
+
* remain null. GPU energy uses energy counters where present, otherwise the
|
|
179
|
+
* integral of sampled power: both are component estimates, not wall power. */
|
|
180
|
+
export function createNativeOwnerMonitor({ policy, support, platform, run, now, readGpuBuffers = null }) {
|
|
181
|
+
const timeoutMs = Math.floor(policy.sampleIntervalMs / 2);
|
|
182
|
+
const controls = support.controls, { hwmon, supplies } = support.hardware;
|
|
183
|
+
const named = (control, names) => controls[control].supported
|
|
184
|
+
? hwmon.filter(device => (names || controls[control].sensors).includes(device.name)) : [];
|
|
185
|
+
// Only sensors the owner named are read.
|
|
186
|
+
const thermal = policy.temperature ? named('temperature', policy.temperature.sensors).filter(device => device.temperatures.length) : [];
|
|
187
|
+
const electrical = policy.power?.gpuSensors ? named('gpuPower', policy.power.gpuSensors)
|
|
188
|
+
.filter(device => device.powers.length || device.energies.length) : [];
|
|
189
|
+
const signalFile = policy.foreground?.source === 'owner-signal' ? policy.foreground.signalFile : null;
|
|
190
|
+
const counters = new Map(), lastPower = new Map(), methods = new Set();
|
|
191
|
+
let joules = 0;
|
|
192
|
+
async function temperature() {
|
|
193
|
+
const sensors = [];
|
|
194
|
+
for (const device of thermal) for (const file of device.temperatures) {
|
|
195
|
+
try { sensors.push({ name: device.name, input: file, celsius: await readNumber(path.join(device.directory, file)) / 1000 }); }
|
|
196
|
+
catch { return null; } // A configured sensor that stops reading must not pass as cool.
|
|
197
|
+
}
|
|
198
|
+
return sensors.length ? { maxCelsius: Math.max(...sensors.map(row => row.celsius)), sensors } : null;
|
|
199
|
+
}
|
|
200
|
+
async function gpuPower(at) {
|
|
201
|
+
let watts = 0, complete = true;
|
|
202
|
+
for (const device of electrical) {
|
|
203
|
+
if (device.powers.length) {
|
|
204
|
+
let deviceWatts = 0;
|
|
205
|
+
for (const file of device.powers) deviceWatts += await readNumber(path.join(device.directory, file)) / 1e6;
|
|
206
|
+
const last = lastPower.get(device.directory);
|
|
207
|
+
if (last && at > last.at) { joules += (last.watts + deviceWatts) / 2 * (at - last.at) / 1000; methods.add('integrated hwmon power samples'); }
|
|
208
|
+
lastPower.set(device.directory, { watts: deviceWatts, at });
|
|
209
|
+
watts += deviceWatts;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
for (const file of device.energies) {
|
|
213
|
+
const key = path.join(device.directory, file), microjoules = await readNumber(key);
|
|
214
|
+
const last = counters.get(key); counters.set(key, { microjoules, at });
|
|
215
|
+
// The first sample or a counter reset has no interval to measure.
|
|
216
|
+
if (!last || at <= last.at || microjoules < last.microjoules) { complete = false; continue; }
|
|
217
|
+
watts += (microjoules - last.microjoules) / (at - last.at) / 1000;
|
|
218
|
+
joules += (microjoules - last.microjoules) / 1e6; methods.add('hwmon energy counters');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return complete ? { watts } : null;
|
|
222
|
+
}
|
|
223
|
+
async function sample() {
|
|
224
|
+
const at = now(), memory = process.memoryUsage(), cpu = process.cpuUsage();
|
|
225
|
+
const readings = { at, processResidentBytes: memory.rss, cpuUserMicros: cpu.user, cpuSystemMicros: cpu.system,
|
|
226
|
+
gpuBuffers: null, temperature: null, gpuPower: null, externalPower: null, inputIdleSeconds: null, foregroundSignal: null };
|
|
227
|
+
if (readGpuBuffers) {
|
|
228
|
+
try { const value = await readGpuBuffers(); readings.gpuBuffers = value && { liveBytes: value.liveBytes, peakBytes: value.peakBytes, maxBytes: value.maxBytes }; }
|
|
229
|
+
catch { readings.gpuBuffers = null; }
|
|
230
|
+
}
|
|
231
|
+
if (thermal.length) readings.temperature = await temperature();
|
|
232
|
+
if (electrical.length) {
|
|
233
|
+
// A failed read leaves a gap; power is not integrated across it.
|
|
234
|
+
try { readings.gpuPower = await gpuPower(at); } catch { readings.gpuPower = null; lastPower.clear(); }
|
|
235
|
+
}
|
|
236
|
+
if (controls.externalPower.supported && policy.power?.requireExternalPower) {
|
|
237
|
+
try {
|
|
238
|
+
readings.externalPower = platform === 'linux' ? await readLinuxExternalPower(supplies)
|
|
239
|
+
: parseExternalPower(await run(...MAC_TOOLS.power, { timeoutMs }));
|
|
240
|
+
} catch { readings.externalPower = null; }
|
|
241
|
+
}
|
|
242
|
+
if (controls.inputIdle.supported && policy.foreground?.source === 'input-idle') {
|
|
243
|
+
try { readings.inputIdleSeconds = parseIdleSeconds(await run(...MAC_TOOLS.idle, { timeoutMs })); }
|
|
244
|
+
catch { readings.inputIdleSeconds = null; }
|
|
245
|
+
}
|
|
246
|
+
if (controls.ownerSignal.supported && signalFile) readings.foregroundSignal = await readSignal(signalFile);
|
|
247
|
+
return readings;
|
|
248
|
+
}
|
|
249
|
+
return Object.freeze({ sample,
|
|
250
|
+
getEnergy: () => methods.size ? { gpuJoules: joules, method: [...methods].join(' + '), scope: 'GPU component estimate, not wall power' } : null });
|
|
251
|
+
}
|