pi-daddy 0.22.0 → 0.23.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/CHANGELOG.md +30 -0
- package/contracts/daily-view/v1/README.md +114 -0
- package/contracts/daily-view/v1/fixtures/view.json +525 -0
- package/contracts/daily-view/v1/fixtures/view.txt +75 -0
- package/contracts/daily-view/v1/fixtures/work.jsonl +17 -0
- package/contracts/daily-view/v1/p03/README.md +26 -0
- package/contracts/daily-view/v1/p03/fixtures/retained-executions.json +241 -0
- package/contracts/daily-view/v1/p03/projection.schema.json +194 -0
- package/contracts/daily-view/v1/p03/provenance.json +21 -0
- package/contracts/dashboard-host/v1/README.md +176 -0
- package/contracts/debrief/v1/README.md +146 -0
- package/contracts/debrief/v1/intervention/README.md +32 -0
- package/contracts/debrief/v1/intervention/fixtures/artifact-3ee11136beb864f518252e5024dae9197421fbf97b6e981d3243c782b5c44bd0.txt +1 -0
- package/contracts/debrief/v1/intervention/fixtures/artifact-58905d872dd3c54a9705f67b05fa82edbad7af6476ae8993faf1a7b9fadfb905.txt +1 -0
- package/contracts/debrief/v1/intervention/fixtures/assessment.json +29 -0
- package/contracts/debrief/v1/intervention/fixtures/blind-view.json +20 -0
- package/contracts/debrief/v1/intervention/fixtures/choice.json +7 -0
- package/contracts/debrief/v1/intervention/fixtures/manifest.json +57 -0
- package/contracts/debrief/v1/intervention/fixtures/reveal.json +39 -0
- package/contracts/debrief/v1/provenance.json +89 -0
- package/contracts/debrief/v1/work-capture/README.md +38 -0
- package/contracts/debrief/v1/work-capture/fixtures/batch.json +17 -0
- package/contracts/debrief/v1/work-capture/fixtures/cases.json +184 -0
- package/contracts/debrief/v1/work-capture/fixtures/review.json +184 -0
- package/contracts/debrief/v1/work-capture/review-request.schema.json +41 -0
- package/contracts/debrief/v1/work-capture/work-case.schema.json +190 -0
- package/contracts/debrief/v2/README.md +71 -0
- package/contracts/debrief/v2/input-pin.json +66 -0
- package/contracts/debrief/v2/intervention-upstream.md +40 -0
- package/contracts/debrief/v2/work-signals-upstream.md +22 -0
- package/contracts/dispatch-control/v1/README.md +109 -0
- package/contracts/dispatch-control/v1/request.schema.json +19 -0
- package/contracts/effect-profile/v1/README.md +124 -0
- package/contracts/execution-retention/v1/README.md +109 -0
- package/contracts/execution-retention/v2/README.md +148 -0
- package/contracts/execution-retention/v2/fixtures/interrupted-native-bytes.json +91 -0
- package/contracts/execution-retention/v2/fixtures/missing-native-session.json +91 -0
- package/contracts/execution-retention/v2/fixtures/native-file-unknown-branch.json +91 -0
- package/contracts/execution-retention/v2/fixtures/native-id-without-bytes.json +91 -0
- package/contracts/execution-retention/v2/fixtures/native-live-branch.json +90 -0
- package/contracts/execution-retention/v2/fixtures/replaced-native-session.json +92 -0
- package/contracts/execution-retention/v2/fixtures/result-82c620cdfeb8ca6b858cf653d7f5e2eedca5315d6244216d60e8b36f03223d6a.bin +1 -0
- package/contracts/execution-retention/v2/fixtures/session-2ec1f134bc739984434f7fca689e1c3d1c39fce2dcadae709e9b6faedfcc4ac7.bin +3 -0
- package/contracts/execution-retention/v2/fixtures/session-9bdb4bd16a901da31580acf056bda525225259e15ec409e0410480109011772f.bin +3 -0
- package/contracts/execution-retention/v2/manifest.schema.json +858 -0
- package/contracts/experiment/v1/README.md +164 -0
- package/contracts/factory-order/v1/README.md +151 -0
- package/contracts/factory-order/v1/adoption-pin.json +8 -0
- package/contracts/intent-control/v1/README.md +135 -0
- package/contracts/intent-control/v1/request.schema.json +1096 -0
- package/contracts/intent-control/v2/README.md +34 -0
- package/contracts/intent-control/v2/request.schema.json +1070 -0
- package/contracts/ledger/v4/README.md +241 -0
- package/contracts/ledger/v4/fixtures/layout-options.json +428 -0
- package/contracts/ledger/v4/fixtures/work-acceptance.json +57 -0
- package/contracts/ledger/v4/fixtures/work-occurrence.json +44 -0
- package/contracts/ledger/v4/fixtures/work-revision.json +26 -0
- package/contracts/ledger/v4/fixtures/work-snapshot.json +73 -0
- package/contracts/ledger/v4/ledger-event.schema.json +162 -0
- package/contracts/ordinary-control/v1/README.md +59 -0
- package/contracts/producer-ipc/v1/README.md +137 -0
- package/dist/check-runner.d.ts +5 -0
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +16 -2
- package/dist/check-runner.js.map +1 -1
- package/dist/control-journal.d.ts +20 -0
- package/dist/control-journal.d.ts.map +1 -0
- package/dist/control-journal.js +73 -0
- package/dist/control-journal.js.map +1 -0
- package/dist/daily-view-input.d.ts +44 -0
- package/dist/daily-view-input.d.ts.map +1 -0
- package/dist/daily-view-input.js +68 -0
- package/dist/daily-view-input.js.map +1 -0
- package/dist/daily-view-render.d.ts +4 -0
- package/dist/daily-view-render.d.ts.map +1 -0
- package/dist/daily-view-render.js +41 -0
- package/dist/daily-view-render.js.map +1 -0
- package/dist/daily-view.d.ts +74 -0
- package/dist/daily-view.d.ts.map +1 -0
- package/dist/daily-view.js +170 -0
- package/dist/daily-view.js.map +1 -0
- package/dist/dashboard-cli.d.ts +26 -1
- package/dist/dashboard-cli.d.ts.map +1 -1
- package/dist/dashboard-cli.js +121 -3
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-harness.d.ts +19 -0
- package/dist/dashboard-harness.d.ts.map +1 -0
- package/dist/dashboard-harness.js +75 -0
- package/dist/dashboard-harness.js.map +1 -0
- package/dist/dashboard-herdr.d.ts.map +1 -1
- package/dist/dashboard-herdr.js +7 -2
- package/dist/dashboard-herdr.js.map +1 -1
- package/dist/dashboard-host-contract.d.ts +111 -0
- package/dist/dashboard-host-contract.d.ts.map +1 -0
- package/dist/dashboard-host-contract.js +14 -0
- package/dist/dashboard-host-contract.js.map +1 -0
- package/dist/dashboard-host-transport.d.ts +15 -0
- package/dist/dashboard-host-transport.d.ts.map +1 -0
- package/dist/dashboard-host-transport.js +122 -0
- package/dist/dashboard-host-transport.js.map +1 -0
- package/dist/dashboard-host.d.ts +149 -0
- package/dist/dashboard-host.d.ts.map +1 -0
- package/dist/dashboard-host.js +329 -0
- package/dist/dashboard-host.js.map +1 -0
- package/dist/dashboard-observation.d.ts +28 -0
- package/dist/dashboard-observation.d.ts.map +1 -0
- package/dist/dashboard-observation.js +86 -0
- package/dist/dashboard-observation.js.map +1 -0
- package/dist/debrief-contract.d.ts +65 -0
- package/dist/debrief-contract.d.ts.map +1 -0
- package/dist/debrief-contract.js +151 -0
- package/dist/debrief-contract.js.map +1 -0
- package/dist/debrief-fixture.d.ts +16 -0
- package/dist/debrief-fixture.d.ts.map +1 -0
- package/dist/debrief-fixture.js +41 -0
- package/dist/debrief-fixture.js.map +1 -0
- package/dist/debrief-host.d.ts +59 -0
- package/dist/debrief-host.d.ts.map +1 -0
- package/dist/debrief-host.js +49 -0
- package/dist/debrief-host.js.map +1 -0
- package/dist/debrief-render.d.ts +5 -0
- package/dist/debrief-render.d.ts.map +1 -0
- package/dist/debrief-render.js +65 -0
- package/dist/debrief-render.js.map +1 -0
- package/dist/debrief.d.ts +90 -0
- package/dist/debrief.d.ts.map +1 -0
- package/dist/debrief.js +263 -0
- package/dist/debrief.js.map +1 -0
- package/dist/delegate-types.d.ts +2 -0
- package/dist/delegate-types.d.ts.map +1 -1
- package/dist/delegate-types.js.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +1 -0
- package/dist/delegate.js.map +1 -1
- package/dist/dispatch-control.d.ts +45 -0
- package/dist/dispatch-control.d.ts.map +1 -0
- package/dist/dispatch-control.js +76 -0
- package/dist/dispatch-control.js.map +1 -0
- package/dist/effect-profile-runtime.d.ts +40 -0
- package/dist/effect-profile-runtime.d.ts.map +1 -0
- package/dist/effect-profile-runtime.js +51 -0
- package/dist/effect-profile-runtime.js.map +1 -0
- package/dist/effect-profile.d.ts +40 -0
- package/dist/effect-profile.d.ts.map +1 -0
- package/dist/effect-profile.js +152 -0
- package/dist/effect-profile.js.map +1 -0
- package/dist/execution-retention.d.ts +90 -0
- package/dist/execution-retention.d.ts.map +1 -0
- package/dist/execution-retention.js +247 -0
- package/dist/execution-retention.js.map +1 -0
- package/dist/experiment-contract.d.ts +56 -0
- package/dist/experiment-contract.d.ts.map +1 -0
- package/dist/experiment-contract.js +80 -0
- package/dist/experiment-contract.js.map +1 -0
- package/dist/experiment-state.d.ts +22 -0
- package/dist/experiment-state.d.ts.map +1 -0
- package/dist/experiment-state.js +79 -0
- package/dist/experiment-state.js.map +1 -0
- package/dist/experiment-store.d.ts +28 -0
- package/dist/experiment-store.d.ts.map +1 -0
- package/dist/experiment-store.js +130 -0
- package/dist/experiment-store.js.map +1 -0
- package/dist/experiment.d.ts +65 -0
- package/dist/experiment.d.ts.map +1 -0
- package/dist/experiment.js +357 -0
- package/dist/experiment.js.map +1 -0
- package/dist/factory-contract.d.ts +79 -0
- package/dist/factory-contract.d.ts.map +1 -0
- package/dist/factory-contract.js +89 -0
- package/dist/factory-contract.js.map +1 -0
- package/dist/factory-migration.d.ts +20 -0
- package/dist/factory-migration.d.ts.map +1 -0
- package/dist/factory-migration.js +55 -0
- package/dist/factory-migration.js.map +1 -0
- package/dist/factory-order.d.ts +848 -0
- package/dist/factory-order.d.ts.map +1 -0
- package/dist/factory-order.js +83 -0
- package/dist/factory-order.js.map +1 -0
- package/dist/factory-registry.d.ts +121 -0
- package/dist/factory-registry.d.ts.map +1 -0
- package/dist/factory-registry.js +133 -0
- package/dist/factory-registry.js.map +1 -0
- package/dist/file-lock.d.ts +8 -2
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +78 -19
- package/dist/file-lock.js.map +1 -1
- package/dist/herdr-poll.d.ts +8 -0
- package/dist/herdr-poll.d.ts.map +1 -1
- package/dist/herdr-poll.js +23 -0
- package/dist/herdr-poll.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/dist/intent-application.d.ts +17 -0
- package/dist/intent-application.d.ts.map +1 -0
- package/dist/intent-application.js +113 -0
- package/dist/intent-application.js.map +1 -0
- package/dist/intent-control.d.ts +201 -0
- package/dist/intent-control.d.ts.map +1 -0
- package/dist/intent-control.js +110 -0
- package/dist/intent-control.js.map +1 -0
- package/dist/intent-scheduling.d.ts +9 -0
- package/dist/intent-scheduling.d.ts.map +1 -0
- package/dist/intent-scheduling.js +15 -0
- package/dist/intent-scheduling.js.map +1 -0
- package/dist/ledger-append.d.ts +15 -0
- package/dist/ledger-append.d.ts.map +1 -0
- package/dist/ledger-append.js +94 -0
- package/dist/ledger-append.js.map +1 -0
- package/dist/ledger.d.ts +11 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +3 -7
- package/dist/ledger.js.map +1 -1
- package/dist/native-session-target.d.ts +9 -0
- package/dist/native-session-target.d.ts.map +1 -0
- package/dist/native-session-target.js +29 -0
- package/dist/native-session-target.js.map +1 -0
- package/dist/native-session.d.ts +59 -0
- package/dist/native-session.d.ts.map +1 -0
- package/dist/native-session.js +154 -0
- package/dist/native-session.js.map +1 -0
- package/dist/order-schedule.d.ts +37 -0
- package/dist/order-schedule.d.ts.map +1 -0
- package/dist/order-schedule.js +81 -0
- package/dist/order-schedule.js.map +1 -0
- package/dist/ordinary-children.d.ts +52 -0
- package/dist/ordinary-children.d.ts.map +1 -0
- package/dist/ordinary-children.js +93 -0
- package/dist/ordinary-children.js.map +1 -0
- package/dist/producer-ipc-contract.d.ts +29 -0
- package/dist/producer-ipc-contract.d.ts.map +1 -0
- package/dist/producer-ipc-contract.js +29 -0
- package/dist/producer-ipc-contract.js.map +1 -0
- package/dist/producer-ipc.d.ts +55 -0
- package/dist/producer-ipc.d.ts.map +1 -0
- package/dist/producer-ipc.js +143 -0
- package/dist/producer-ipc.js.map +1 -0
- package/dist/resource-budget.d.ts +334 -0
- package/dist/resource-budget.d.ts.map +1 -0
- package/dist/resource-budget.js +415 -0
- package/dist/resource-budget.js.map +1 -0
- package/dist/retention-contract.d.ts +73 -0
- package/dist/retention-contract.d.ts.map +1 -0
- package/dist/retention-contract.js +104 -0
- package/dist/retention-contract.js.map +1 -0
- package/dist/retention-json.d.ts +3 -0
- package/dist/retention-json.d.ts.map +1 -0
- package/dist/retention-json.js +32 -0
- package/dist/retention-json.js.map +1 -0
- package/dist/run-child.d.ts +4 -0
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +16 -2
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts +6 -0
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +19 -6
- package/dist/run-herdr.js.map +1 -1
- package/dist/vendor/adoption.d.ts +129 -0
- package/dist/vendor/adoption.d.ts.map +1 -0
- package/dist/vendor/adoption.js +102 -0
- package/dist/vendor/adoption.js.map +1 -0
- package/dist/work-ledger-destination.d.ts +20 -0
- package/dist/work-ledger-destination.d.ts.map +1 -0
- package/dist/work-ledger-destination.js +131 -0
- package/dist/work-ledger-destination.js.map +1 -0
- package/dist/work-ledger-json.d.ts +25 -0
- package/dist/work-ledger-json.d.ts.map +1 -0
- package/dist/work-ledger-json.js +267 -0
- package/dist/work-ledger-json.js.map +1 -0
- package/dist/work-ledger-occurrences.d.ts +227 -0
- package/dist/work-ledger-occurrences.d.ts.map +1 -0
- package/dist/work-ledger-occurrences.js +115 -0
- package/dist/work-ledger-occurrences.js.map +1 -0
- package/dist/work-ledger-projection.d.ts +295 -0
- package/dist/work-ledger-projection.d.ts.map +1 -0
- package/dist/work-ledger-projection.js +237 -0
- package/dist/work-ledger-projection.js.map +1 -0
- package/dist/work-ledger-snapshot.d.ts +29 -0
- package/dist/work-ledger-snapshot.d.ts.map +1 -0
- package/dist/work-ledger-snapshot.js +267 -0
- package/dist/work-ledger-snapshot.js.map +1 -0
- package/dist/work-ledger-types.d.ts +261 -0
- package/dist/work-ledger-types.d.ts.map +1 -0
- package/dist/work-ledger-types.js +13 -0
- package/dist/work-ledger-types.js.map +1 -0
- package/dist/work-ledger-validation.d.ts +14 -0
- package/dist/work-ledger-validation.d.ts.map +1 -0
- package/dist/work-ledger-validation.js +226 -0
- package/dist/work-ledger-validation.js.map +1 -0
- package/dist/work-ledger.d.ts +43 -0
- package/dist/work-ledger.d.ts.map +1 -0
- package/dist/work-ledger.js +225 -0
- package/dist/work-ledger.js.map +1 -0
- package/extensions/delegate-chain.ts +1 -0
- package/extensions/delegation-native.ts +12 -0
- package/extensions/delegation.ts +4 -3
- package/extensions/execute-child.ts +34 -4
- package/extensions/grants.ts +2 -0
- package/extensions/ordinary-runtime.ts +14 -0
- package/extensions/run-delegation.ts +7 -7
- package/extensions/session.ts +3 -1
- package/package.json +78 -4
- package/src/check-runner.ts +20 -2
- package/src/control-journal.ts +26 -0
- package/src/daily-view-input.ts +59 -0
- package/src/daily-view-render.ts +44 -0
- package/src/daily-view.ts +152 -0
- package/src/dashboard-cli.ts +103 -4
- package/src/dashboard-harness.ts +28 -0
- package/src/dashboard-herdr.ts +7 -2
- package/src/dashboard-host-contract.ts +42 -0
- package/src/dashboard-host-transport.ts +38 -0
- package/src/dashboard-host.ts +168 -0
- package/src/dashboard-observation.ts +44 -0
- package/src/debrief-contract.ts +127 -0
- package/src/debrief-fixture.ts +29 -0
- package/src/debrief-host.ts +53 -0
- package/src/debrief-render.ts +49 -0
- package/src/debrief.ts +198 -0
- package/src/delegate-types.ts +2 -0
- package/src/delegate.ts +1 -0
- package/src/dispatch-control.ts +81 -0
- package/src/effect-profile-runtime.ts +46 -0
- package/src/effect-profile.ts +129 -0
- package/src/execution-retention.ts +230 -0
- package/src/experiment-contract.ts +66 -0
- package/src/experiment-state.ts +44 -0
- package/src/experiment-store.ts +90 -0
- package/src/experiment.ts +231 -0
- package/src/factory-contract.ts +47 -0
- package/src/factory-migration.ts +29 -0
- package/src/factory-order.ts +56 -0
- package/src/factory-registry.ts +60 -0
- package/src/file-lock.ts +47 -11
- package/src/herdr-poll.ts +18 -0
- package/src/index.ts +32 -0
- package/src/intent-application.ts +90 -0
- package/src/intent-control.ts +107 -0
- package/src/intent-scheduling.ts +14 -0
- package/src/ledger-append.ts +72 -0
- package/src/ledger.ts +3 -9
- package/src/native-session-target.ts +23 -0
- package/src/native-session.ts +139 -0
- package/src/order-schedule.ts +42 -0
- package/src/ordinary-children.ts +59 -0
- package/src/producer-ipc-contract.ts +26 -0
- package/src/producer-ipc.ts +107 -0
- package/src/resource-budget.ts +349 -0
- package/src/retention-contract.ts +84 -0
- package/src/retention-json.ts +20 -0
- package/src/run-child.ts +13 -2
- package/src/run-herdr.ts +11 -7
- package/src/vendor/adoption.ts +109 -0
- package/src/work-ledger-destination.ts +113 -0
- package/src/work-ledger-json.ts +228 -0
- package/src/work-ledger-occurrences.ts +99 -0
- package/src/work-ledger-projection.ts +262 -0
- package/src/work-ledger-snapshot.ts +246 -0
- package/src/work-ledger-types.ts +229 -0
- package/src/work-ledger-validation.ts +174 -0
- package/src/work-ledger.ts +186 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { closed, cloneExperiment, experimentHash } from "./experiment-contract.ts";
|
|
2
|
+
import type { VariantRecord } from "./experiment-state.ts";
|
|
3
|
+
export interface OrderNode { nodeId: string; dependencies: readonly string[]; executions: readonly string[]; expectedDigest: string; decision: { decisionId: string; authorityId: string } | null }
|
|
4
|
+
export interface OrderSchedule { version: "order-schedule-v1"; policyDigest: string; nodes: readonly OrderNode[] }
|
|
5
|
+
export interface FactoryDecision { version: "factory-decision-v1"; requestId: string; bindingDigest: string; nodeId: string; evidenceDigest: string; authorityId: string; choice: "approve" | "reject" }
|
|
6
|
+
export const factoryDecisionDigest = (d: FactoryDecision) => experimentHash(factoryDecision(d));
|
|
7
|
+
export function factoryDecision(value: FactoryDecision): FactoryDecision {
|
|
8
|
+
const d = cloneExperiment(value); closed(d,["version","requestId","bindingDigest","nodeId","evidenceDigest","authorityId","choice"]);
|
|
9
|
+
if(d.version!=="factory-decision-v1" || ![d.requestId,d.nodeId,d.authorityId].every(x=>typeof x==="string"&&/^[a-zA-Z0-9:_-]{1,128}$/.test(x)) || ![d.bindingDigest,d.evidenceDigest].every(x=>typeof x==="string"&&/^[a-f0-9]{64}$/.test(x)) || !["approve","reject"].includes(d.choice))throw new Error("invalid reserved decision");return d;
|
|
10
|
+
}
|
|
11
|
+
export function orderSchedule(input: OrderSchedule, executions: readonly string[]): OrderSchedule {
|
|
12
|
+
const s=cloneExperiment(input);closed(s,["version","policyDigest","nodes"]);
|
|
13
|
+
if(s.version!=="order-schedule-v1"|| !/^[a-f0-9]{64}$/.test(s.policyDigest)||!Array.isArray(s.nodes)||!s.nodes.length||s.nodes.length>16)throw new Error("bounded order schedule required");
|
|
14
|
+
const ids=new Set<string>(),all:string[]=[];
|
|
15
|
+
for(const n of s.nodes as readonly OrderNode[]){closed(n,["nodeId","dependencies","executions","expectedDigest","decision"]);
|
|
16
|
+
if(!/^[a-zA-Z0-9:_-]{1,128}$/.test(n.nodeId)||ids.has(n.nodeId)||!Array.isArray(n.dependencies)||n.dependencies.length>16||new Set(n.dependencies).size!==n.dependencies.length||!Array.isArray(n.executions)||n.executions.length<1||n.executions.length>3||!/^[a-f0-9]{64}$/.test(n.expectedDigest))throw new Error("invalid node/dependency/recovery policy");
|
|
17
|
+
if(n.decision!==null){closed(n.decision,["decisionId","authorityId"]);if(![n.decision.decisionId,n.decision.authorityId].every(x=>/^[a-zA-Z0-9:_-]{1,128}$/.test(x)))throw new Error("invalid decision owner");}
|
|
18
|
+
ids.add(n.nodeId);all.push(...n.executions);
|
|
19
|
+
}
|
|
20
|
+
if(new Set(all).size!==all.length||all.length!==executions.length||all.some(x=>!executions.includes(x)))throw new Error("schedule must own every reserved execution exactly once");
|
|
21
|
+
const visiting=new Set<string>(),done=new Set<string>();
|
|
22
|
+
const visit=(id:string)=>{if(done.has(id))return;if(visiting.has(id))throw new Error("dependency cycle");const n=s.nodes.find(n=>n.nodeId===id);if(!n)throw new Error("missing dependency");visiting.add(id);n.dependencies.forEach(visit);visiting.delete(id);done.add(id);};s.nodes.forEach(n=>visit(n.nodeId));return s;
|
|
23
|
+
}
|
|
24
|
+
export interface OrderNodeView {nodeId:string;state:"eligible"|"running"|"unknown"|"exhausted"|"decision-required"|"decision-rejected"|"dependency-blocked"|"satisfied";action:"dispatch"|"decide"|"wait"|"stakeholder"|"none";executionId:string|null;evidenceDigest:string|null}
|
|
25
|
+
export function evaluateOrder(s:OrderSchedule,variants:readonly VariantRecord[],digests:ReadonlyMap<string,string|null>,decisions:readonly FactoryDecision[]):OrderNodeView[]{
|
|
26
|
+
const rows=new Map<string,OrderNodeView>();
|
|
27
|
+
function evaluate(n:OrderNode):OrderNodeView{
|
|
28
|
+
if(rows.has(n.nodeId))return rows.get(n.nodeId)!;
|
|
29
|
+
const row:OrderNodeView={nodeId:n.nodeId,state:"exhausted",action:"stakeholder",executionId:null,evidenceDigest:null};
|
|
30
|
+
if(n.dependencies.some(id=>evaluate(s.nodes.find(n=>n.nodeId===id)!).state!=="satisfied")){row.state="dependency-blocked";row.action="wait";rows.set(n.nodeId,row);return row;}
|
|
31
|
+
for(const id of n.executions){const v=variants.find(v=>v.executionId===id)!;row.executionId=id;
|
|
32
|
+
if(v.state==="unknown"){row.state="unknown";row.action="stakeholder";break;}
|
|
33
|
+
if(v.state==="completed"&&digests.get(id)===n.expectedDigest){row.evidenceDigest=experimentHash({nodeId:n.nodeId,policyDigest:s.policyDigest,executionId:id,artifactDigest:v.artifactDigest,expectedDigest:n.expectedDigest});
|
|
34
|
+
const d=decisions.find(d=>d.nodeId===n.nodeId&&d.evidenceDigest===row.evidenceDigest);
|
|
35
|
+
row.state=n.decision?d?d.choice==="approve"?"satisfied":"decision-rejected":"decision-required":"satisfied";row.action=row.state==="decision-required"?"decide":row.state==="decision-rejected"?"stakeholder":"none";break;}
|
|
36
|
+
if(["unstarted","queued"].includes(v.state)){row.state="eligible";row.action="dispatch";break;}
|
|
37
|
+
if(["dispatching","running"].includes(v.state)){row.state="running";row.action="wait";break;}
|
|
38
|
+
}
|
|
39
|
+
rows.set(n.nodeId,row);return row;
|
|
40
|
+
}
|
|
41
|
+
return s.nodes.map(evaluate);
|
|
42
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isExecutionId } from "./execution-id.ts";
|
|
3
|
+
import { dataDigest, detached, freeze, sha } from "./debrief-contract.ts";
|
|
4
|
+
import { controlShape } from "./dispatch-control.ts";
|
|
5
|
+
export interface OrdinaryTarget { executionId:string; parentExecutionId:string|null; toolCallId:string|null }
|
|
6
|
+
export interface OrdinaryCancellation { version:"ordinary-cancel-v1"; requestId:string; bindingDigest:string; expectedRevision:number; target:OrdinaryTarget }
|
|
7
|
+
export interface OrdinaryAuthority { bindingDigest:string; requestDigests:readonly string[] }
|
|
8
|
+
type ControlState="not-assessed"|"failed"|"unknown";
|
|
9
|
+
interface Row { target:OrdinaryTarget; state:"active"|"settled"|"unknown"; abortRequested:boolean; control:ControlState; boundary:ControlState; outcome:unknown }
|
|
10
|
+
interface State { holds:Set<string>; enabled:boolean; coverageGap:boolean; bindingDigest:string; revision:number; rows:Map<string,Row>; handles:Map<string,AbortController>; requests:Map<string,{digest:string;result:unknown}> }
|
|
11
|
+
const sessions=new WeakMap<object,OrdinaryChildren>(),ports=new WeakMap<object,State>();
|
|
12
|
+
const id=(v:unknown)=>typeof v==="string"&&/^[a-zA-Z0-9:_-]{1,128}$/.test(v);
|
|
13
|
+
function target(value:OrdinaryTarget){const t=detached(value);controlShape(t,["executionId","parentExecutionId","toolCallId"]);if(!isExecutionId(t.executionId)||!(t.parentExecutionId===null||isExecutionId(t.parentExecutionId))||!(t.toolCallId===null||typeof t.toolCallId==="string"&&t.toolCallId.length>0&&t.toolCallId.length<=256))throw Error("exact original occurrence identity required");return t;}
|
|
14
|
+
export function ordinaryCancellation(input:OrdinaryCancellation){const r=detached(input);controlShape(r,["version","requestId","bindingDigest","expectedRevision","target"]);if(r.version!=="ordinary-cancel-v1"||!id(r.requestId)||!sha(r.bindingDigest)||!Number.isSafeInteger(r.expectedRevision)||r.expectedRevision<0)throw Error("invalid ordinary cancellation");return freeze({...r,target:target(r.target)});}
|
|
15
|
+
export const ordinaryCancellationDigest=(r:OrdinaryCancellation)=>dataDigest(ordinaryCancellation(r));
|
|
16
|
+
export interface OrdinaryChildren { readonly bindingDigest:string; inspect():unknown; quiescent():boolean; cancel(request:OrdinaryCancellation,authority:OrdinaryAuthority|null):unknown }
|
|
17
|
+
export const isOrdinaryChildren=(p:unknown):p is OrdinaryChildren=>typeof p==="object"&&p!==null&&ports.has(p);
|
|
18
|
+
/** Extension-internal constructor. No disk discovery/recovery and no identity inferred from labels. */
|
|
19
|
+
function initializeOrdinaryChildren(session:object):void {
|
|
20
|
+
if(sessions.has(session))throw Error("original session already registered");
|
|
21
|
+
const s:State={holds:new Set(),enabled:false,coverageGap:false,bindingDigest:dataDigest({occurrence:randomUUID()}),revision:0,rows:new Map(),handles:new Map(),requests:new Map()};
|
|
22
|
+
const port:OrdinaryChildren=Object.freeze({bindingDigest:s.bindingDigest,
|
|
23
|
+
quiescent:()=>!s.coverageGap&&[...s.rows.values()].every(r=>r.state==="settled"&&r.boundary==="not-assessed"),
|
|
24
|
+
inspect:()=>freeze(detached({bindingDigest:s.bindingDigest,revision:s.revision,children:[...s.rows.values()],admission:s.holds.size?"held-by-original-owner":"open",coverage:s.coverageGap?"unretained-before-opt-in":"original-registered-lifetimes-only",freshness:"snapshot-unknown",recovery:"unavailable",acceptance:"not-assessed"})),
|
|
25
|
+
cancel(input:OrdinaryCancellation,authority:OrdinaryAuthority|null){
|
|
26
|
+
const r=ordinaryCancellation(input),digest=ordinaryCancellationDigest(input),old=s.requests.get(r.requestId);
|
|
27
|
+
if(old){if(old.digest!==digest)throw Error("immutable ordinary cancellation ID");return freeze(detached(old.result));}
|
|
28
|
+
if(r.bindingDigest!==s.bindingDigest||r.expectedRevision!==s.revision)throw Error("stale original ordinary controller CAS");
|
|
29
|
+
if(!authority||authority.bindingDigest!==s.bindingDigest||!authority.requestDigests.includes(digest))throw Error("independent exact ordinary cancellation authority required");
|
|
30
|
+
const row=s.rows.get(r.target.executionId),handle=s.handles.get(r.target.executionId);
|
|
31
|
+
if(!row||dataDigest(row.target)!==dataDigest(r.target)||row.state!=="active"||!handle)throw Error("original live child handle unavailable; no recovery");
|
|
32
|
+
if(s.requests.size>=1024)throw Error("ordinary cancellation capacity exhausted");
|
|
33
|
+
// Synchronous original-handle operation: CAS cannot yield between checking and requesting abort.
|
|
34
|
+
const result={state:"abort-requested",target:r.target,revision:++s.revision,completion:"pending-original-caller",durability:"process-local"};
|
|
35
|
+
s.requests.set(r.requestId,{digest,result});row.abortRequested=true;handle.abort();return freeze(detached(result));
|
|
36
|
+
}});
|
|
37
|
+
sessions.set(session,port);ports.set(port,s);
|
|
38
|
+
}
|
|
39
|
+
/** Only the original session object can retrieve its retained port; spreading/copying cannot. */
|
|
40
|
+
export function ordinaryChildrenFor(session:object):OrdinaryChildren {const p=sessions.get(session);if(!p)throw Error("original grants session required");ports.get(p)!.enabled=true;return p;}
|
|
41
|
+
/** Associate the actual extension API object at factory creation, never a label or path. */
|
|
42
|
+
export function associateOrdinaryHost(host:object,session:object){initializeOrdinaryChildren(session);const p=sessions.get(session);if(!p||sessions.has(host))throw Error("original unbound extension host required");sessions.set(host,p);}
|
|
43
|
+
export function retainSessionChild(session:object,input:OrdinaryTarget,caller?:AbortSignal){const p=sessions.get(session);if(!p)return undefined;const s=ports.get(p)!;if(!s.enabled){s.coverageGap=true;return undefined;}return retainOrdinaryChild(p,input,caller);}
|
|
44
|
+
/** Internal host control seam, not a model tool or recovered capability. Reads never call it. */
|
|
45
|
+
export function holdOrdinaryDispatch(port:OrdinaryChildren,key:string){
|
|
46
|
+
const s=ports.get(port);if(!s||!sha(key))throw Error("original ordinary boundary required");
|
|
47
|
+
if(s.coverageGap||[...s.rows.values()].some(r=>r.state!=="active"&&r.boundary!=="not-assessed"))throw Error("ordinary boundary unavailable; no recovery");
|
|
48
|
+
if(!s.holds.has(key)){if(s.holds.size>=32)throw Error("ordinary boundary capacity exhausted");s.holds.add(key);s.revision++;}
|
|
49
|
+
return Object.freeze({ready:()=>port.quiescent(),release:()=>{if(s.holds.delete(key))s.revision++;}});
|
|
50
|
+
}
|
|
51
|
+
/** Extension executor seam; never exported through the package root or model tools. */
|
|
52
|
+
export function retainOrdinaryChild(port:OrdinaryChildren,input:OrdinaryTarget,caller?:AbortSignal){
|
|
53
|
+
const s=ports.get(port);if(!s)throw Error("original ordinary controller required");const t=target(input);
|
|
54
|
+
if(s.holds.size)throw Error("ordinary dispatch held at original control boundary");
|
|
55
|
+
if(s.rows.has(t.executionId)||s.rows.size>=1024)throw Error("ordinary occurrence reused or capacity exhausted");
|
|
56
|
+
const abort=new AbortController(),row:Row={target:t,state:"active",abortRequested:false,control:"not-assessed",boundary:"not-assessed",outcome:null};s.rows.set(t.executionId,row);s.handles.set(t.executionId,abort);s.revision++;
|
|
57
|
+
let done=false;return {signal:caller?AbortSignal.any([caller,abort.signal]):abort.signal,
|
|
58
|
+
settle(outcome:unknown,control:ControlState,boundary:ControlState=control){if(done)throw Error("original child already settled");done=true;row.outcome=detached(outcome);row.control=control;row.boundary=boundary;row.state=boundary==="unknown"?"unknown":"settled";s.handles.delete(t.executionId);s.revision++;}};
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isExecutionId } from "./execution-id.ts";
|
|
3
|
+
import type { AttemptDemand } from "./resource-budget.ts";
|
|
4
|
+
export const PRODUCER_IPC_LIMITS = Object.freeze({ frameBytes:1024, referenceBytes:128, childMs:3000, graceMs:50, maxElapsedMs:30000 });
|
|
5
|
+
export interface ProducerIpcBinding { version:"producer-ipc-v1"; budgetDigest:string; orderId:string; experimentId:string; executionId:string; charterSha256:string; invocationId:string }
|
|
6
|
+
export interface ProducerIpcReferences { claimRef:string; responseRef:string }
|
|
7
|
+
const id = (v:unknown):v is string => typeof v === "string" && /^[a-zA-Z0-9:_-]{1,128}$/.test(v);
|
|
8
|
+
const hash = (v:unknown):v is string => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
9
|
+
export function ipcShape(value:unknown, keys:readonly string[]): asserts value is Record<string,unknown> {
|
|
10
|
+
if (!value || typeof value !== "object" || ![Object.prototype,null].includes(Object.getPrototypeOf(value)) || Reflect.ownKeys(value).length !== keys.length || keys.some(k=>{const d=Object.getOwnPropertyDescriptor(value,k);return !d||!d.enumerable||!Object.hasOwn(d,"value");})) throw new TypeError("closed producer IPC data required");
|
|
11
|
+
}
|
|
12
|
+
export function producerIpcBinding(value:ProducerIpcBinding):Readonly<ProducerIpcBinding> {
|
|
13
|
+
ipcShape(value,["version","budgetDigest","orderId","experimentId","executionId","charterSha256","invocationId"]);
|
|
14
|
+
if (value.version!=="producer-ipc-v1" || !hash(value.budgetDigest) || !hash(value.charterSha256) || !isExecutionId(value.executionId) || ![value.orderId,value.experimentId,value.invocationId].every(id)) throw new TypeError("exact producer IPC binding required");
|
|
15
|
+
return Object.freeze({version:value.version,budgetDigest:value.budgetDigest,orderId:value.orderId,experimentId:value.experimentId,executionId:value.executionId,charterSha256:value.charterSha256,invocationId:value.invocationId});
|
|
16
|
+
}
|
|
17
|
+
export const producerIpcBindingDigest = (value:ProducerIpcBinding) => createHash("sha256").update(JSON.stringify(producerIpcBinding(value))).digest("hex");
|
|
18
|
+
export const producerIpcFrame = (value:ProducerIpcBinding):Buffer => Buffer.from(JSON.stringify({id:producerIpcBinding(value).invocationId,sequence:1})+"\n");
|
|
19
|
+
/** Existing v4 reservation schema, not a new model/effort/skills experiment operation. */
|
|
20
|
+
export function producerIpcDemand(value:ProducerIpcBinding):AttemptDemand {
|
|
21
|
+
const b=producerIpcBinding(value);return {attemptId:b.executionId,orderId:b.orderId,experimentId:b.experimentId,kind:"primary",parentAttemptId:null,inputBytes:Buffer.byteLength(JSON.stringify(b)),inputDigest:producerIpcBindingDigest(b)};
|
|
22
|
+
}
|
|
23
|
+
export function ipcReferences(value:ProducerIpcReferences):Readonly<ProducerIpcReferences> {
|
|
24
|
+
ipcShape(value,["claimRef","responseRef"]);if(!id(value.claimRef)||!id(value.responseRef))throw new TypeError("bounded evidence references required");
|
|
25
|
+
return Object.freeze({claimRef:value.claimRef,responseRef:value.responseRef});
|
|
26
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
4
|
+
import { claimResourcePermit, type ResourcePermit, type openResourceBudget } from "./resource-budget.ts";
|
|
5
|
+
import { digestRuntime, digestNamespaceArgs } from "./effect-profile-runtime.ts";
|
|
6
|
+
import { runChild, type ChildRunResult } from "./run-child.ts";
|
|
7
|
+
import { ipcShape, ipcReferences, producerIpcBinding, producerIpcBindingDigest, producerIpcDemand, producerIpcFrame, PRODUCER_IPC_LIMITS as LIMITS, type ProducerIpcBinding, type ProducerIpcReferences } from "./producer-ipc-contract.ts";
|
|
8
|
+
export { producerIpcBinding, producerIpcBindingDigest, producerIpcDemand, PRODUCER_IPC_LIMITS, type ProducerIpcBinding, type ProducerIpcReferences } from "./producer-ipc-contract.ts";
|
|
9
|
+
type Owner = ReturnType<typeof openResourceBudget>;
|
|
10
|
+
export interface ProducerIpcContext { readonly binding:Readonly<ProducerIpcBinding>; readonly signal:AbortSignal }
|
|
11
|
+
export interface ProducerIpcHost { readonly binding:Readonly<ProducerIpcBinding> }
|
|
12
|
+
interface HostState { owner:Owner; exchange:(frames:Readable,context:ProducerIpcContext)=>Promise<ProducerIpcReferences>; used:boolean }
|
|
13
|
+
const hosts = new WeakMap<ProducerIpcHost,HostState>();
|
|
14
|
+
/** Trusted host-only capability constructor, NOT approval/entitlement discovery. */
|
|
15
|
+
export function createProducerIpcHost(input:{owner:Owner;binding:ProducerIpcBinding;exchange:HostState["exchange"]}):ProducerIpcHost {
|
|
16
|
+
ipcShape(input,["owner","binding","exchange"]);
|
|
17
|
+
if(typeof input.exchange!=="function")throw new TypeError("original host exchange required");
|
|
18
|
+
const port=Object.freeze({binding:producerIpcBinding(input.binding)});
|
|
19
|
+
hosts.set(port,{owner:input.owner,exchange:input.exchange,used:false});return port;
|
|
20
|
+
}
|
|
21
|
+
export interface ProducerIpcSnapshot {
|
|
22
|
+
readonly binding:Readonly<ProducerIpcBinding>;
|
|
23
|
+
readonly outcome:"pending"|"completed"|"failed"|"cancelled"|"timed-out";
|
|
24
|
+
readonly reason:null|"ADMISSION_FAILED"|"RUNTIME_FAILED"|"CHILD_FAILED"|"FRAME_INVALID"|"HOST_FAILED"|"CANCELLED"|"DEADLINE";
|
|
25
|
+
readonly child:Readonly<ChildRunResult>|null;
|
|
26
|
+
readonly childState:"pending"|"not-spawned"|"settled"|"unknown";
|
|
27
|
+
readonly hostState:"not-called"|"pending"|"acknowledged"|"failed";
|
|
28
|
+
readonly frameSha256:string|null;
|
|
29
|
+
readonly references:Readonly<ProducerIpcReferences>|null;
|
|
30
|
+
readonly settlement:"pending"|"acknowledged"|"failed-or-unknown";
|
|
31
|
+
readonly acceptance:"not-assessed";
|
|
32
|
+
}
|
|
33
|
+
export interface ProducerIpcRun {
|
|
34
|
+
readonly started:Promise<"spawned"|"not-spawned">;
|
|
35
|
+
readonly readiness:Promise<"frame-ready"|"not-ready">;
|
|
36
|
+
readonly child:Promise<ChildRunResult|null>;
|
|
37
|
+
/** Bounded effect observation, NOT a resource-release/child-settlement receipt. */
|
|
38
|
+
readonly result:Promise<ProducerIpcSnapshot>;
|
|
39
|
+
/** May remain pending if original child, host or required accounting does not acknowledge. */
|
|
40
|
+
readonly completion:Promise<ProducerIpcSnapshot>;
|
|
41
|
+
inspect():ProducerIpcSnapshot;
|
|
42
|
+
}
|
|
43
|
+
// This is a separate fixed model-free emitter, not a new operation on linux-bwrap-digest-v1.
|
|
44
|
+
const WORKER = "/* producer-ipc-v1 */process.stdout.write(Buffer.from(process.argv[1],'base64'));";
|
|
45
|
+
function deferred<T>() { let resolve!:(value:T)=>void;const promise=new Promise<T>(r=>{resolve=r;});return {promise,resolve}; }
|
|
46
|
+
export async function startProducerIpc(input:{owner:Owner;permit:ResourcePermit;binding:ProducerIpcBinding;host:ProducerIpcHost;signal:AbortSignal;timeoutMs:number}):Promise<ProducerIpcRun> {
|
|
47
|
+
ipcShape(input,["owner","permit","binding","host","signal","timeoutMs"]);
|
|
48
|
+
const binding=producerIpcBinding(input.binding), host=hosts.get(input.host);
|
|
49
|
+
if(!host || host.used || host.owner!==input.owner || producerIpcBindingDigest(input.host.binding)!==producerIpcBindingDigest(binding))throw Error("original unused host with exact owner/charter/invocation required");
|
|
50
|
+
if(!(input.signal instanceof AbortSignal) || !Number.isSafeInteger(input.timeoutMs) || input.timeoutMs<50 || input.timeoutMs>LIMITS.maxElapsedMs)throw new TypeError("bounded original cancellation/deadline required");
|
|
51
|
+
const permit=claimResourcePermit(input.owner,input.permit,binding.budgetDigest,producerIpcDemand(binding));host.used=true;
|
|
52
|
+
const caller=input.signal, deadline=performance.now()+input.timeoutMs, abort=new AbortController(), signal=AbortSignal.any([caller,abort.signal]);
|
|
53
|
+
const started=deferred<"spawned"|"not-spawned">(), readiness=deferred<"frame-ready"|"not-ready">(), result=deferred<ProducerIpcSnapshot>();
|
|
54
|
+
let outcome:ProducerIpcSnapshot["outcome"]="pending", reason:ProducerIpcSnapshot["reason"]=null, child:ChildRunResult|null=null;
|
|
55
|
+
let childState:ProducerIpcSnapshot["childState"]="pending",hostState:ProducerIpcSnapshot["hostState"]="not-called",settlement:ProducerIpcSnapshot["settlement"]="pending";
|
|
56
|
+
let references:Readonly<ProducerIpcReferences>|null=null,frameSha256:string|null=null,frames:Readable|undefined,timer:NodeJS.Timeout|undefined;
|
|
57
|
+
const inspect=():ProducerIpcSnapshot=>Object.freeze({binding,outcome,reason,child:child?Object.freeze({...child}):null,childState,hostState,frameSha256,references,settlement,acceptance:"not-assessed"});
|
|
58
|
+
const latch=(next:Exclude<ProducerIpcSnapshot["outcome"],"pending">,why:ProducerIpcSnapshot["reason"])=>{
|
|
59
|
+
if(outcome!=="pending")return;outcome=next;reason=why;clearTimeout(timer);caller.removeEventListener("abort",cancel);result.resolve(inspect());
|
|
60
|
+
};
|
|
61
|
+
const cancel=()=>{latch("cancelled","CANCELLED");abort.abort();frames?.destroy();};
|
|
62
|
+
const expire=()=>{latch("timed-out","DEADLINE");abort.abort();frames?.destroy();};
|
|
63
|
+
const current=()=>{if(outcome!=="pending")return false;if(caller.aborted)cancel();else if(performance.now()>=deadline)expire();return outcome==="pending";};
|
|
64
|
+
timer=setTimeout(expire,input.timeoutMs);caller.addEventListener("abort",cancel,{once:true});if(caller.aborted)cancel();
|
|
65
|
+
let stdout=Buffer.alloc(0),overflow=false,eof=false,spawned=false;
|
|
66
|
+
const childPromise=(async()=>{
|
|
67
|
+
try {await permit.admission;} catch {latch("failed","ADMISSION_FAILED");return null;}
|
|
68
|
+
if(!current())return null;
|
|
69
|
+
try {
|
|
70
|
+
const runtime=await digestRuntime();
|
|
71
|
+
if(!current())return null;
|
|
72
|
+
return Object.freeze(await runChild({command:"/usr/bin/bwrap",args:digestNamespaceArgs(runtime,WORKER,[producerIpcFrame(binding).toString("base64")]),env:{},cwd:"/",signal,
|
|
73
|
+
timeoutMs:LIMITS.childMs,killGraceMs:LIMITS.graceMs,hardDeadlineAt:Date.now()+LIMITS.childMs+500,maxOutputBytes:LIMITS.frameBytes,
|
|
74
|
+
onSpawn:()=>{spawned=true;started.resolve("spawned");},onStreamEnd:s=>{if(s==="stdout")eof=true;},
|
|
75
|
+
onObservation:(s,b)=>{if(s!=="stdout"||overflow)return;if(stdout.length+b.length>LIMITS.frameBytes){overflow=true;return;}stdout=Buffer.concat([stdout,Buffer.from(b)]);}}));
|
|
76
|
+
} catch {latch("failed","RUNTIME_FAILED");return null;}
|
|
77
|
+
})();
|
|
78
|
+
const completion=(async()=>{
|
|
79
|
+
child=await childPromise;childState=spawned?(child?"settled":"unknown"):"not-spawned";started.resolve("not-spawned");
|
|
80
|
+
if(childState==="unknown"){readiness.resolve("not-ready");settlement="failed-or-unknown";return inspect();}
|
|
81
|
+
if(current()) {
|
|
82
|
+
if(!child || child.code!==0 || child.aborted || child.timedOut || child.truncated || child.spawnError || child.signal)latch("failed","CHILD_FAILED");
|
|
83
|
+
else if(!eof || overflow || !stdout.equals(producerIpcFrame(binding)))latch("failed","FRAME_INVALID");
|
|
84
|
+
else {
|
|
85
|
+
frameSha256=createHash("sha256").update(stdout).digest("hex");readiness.resolve("frame-ready");
|
|
86
|
+
// Replay ONLY exact bytes from the original completed pipe; never reconstruct a host-authored frame.
|
|
87
|
+
frames=Readable.from([Buffer.from(stdout)]);
|
|
88
|
+
try {
|
|
89
|
+
if(current()) {
|
|
90
|
+
hostState="pending";references=ipcReferences(await host.exchange(frames,Object.freeze({binding,signal})));
|
|
91
|
+
if(!frames.readableEnded)throw Error("original host frame not consumed");
|
|
92
|
+
hostState="acknowledged";if(current())latch("completed",null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {hostState="failed";if(current())latch("failed","HOST_FAILED");}
|
|
96
|
+
finally {frames.destroy();}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
readiness.resolve("not-ready");
|
|
100
|
+
// Observation timeout never reaches here until the ORIGINAL child and invoked host have acknowledged.
|
|
101
|
+
const terminal=inspect().outcome;
|
|
102
|
+
try {await permit.settle(terminal==="completed"?"completed":terminal==="cancelled"?"cancelled":"failed");settlement="acknowledged";}
|
|
103
|
+
catch {settlement="failed-or-unknown";}
|
|
104
|
+
clearTimeout(timer);caller.removeEventListener("abort",cancel);return inspect();
|
|
105
|
+
})();
|
|
106
|
+
return Object.freeze({started:started.promise,readiness:readiness.promise,child:childPromise,result:result.promise,completion,inspect});
|
|
107
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open, realpath } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { withFileLock } from "./file-lock.ts";
|
|
6
|
+
import { parseWorkJson, freezeWork } from "./work-ledger-json.ts";
|
|
7
|
+
import { runWithFinalizers } from "./finalization.ts";
|
|
8
|
+
import { workIntentBinding, readIntentWork, resolveIntent, validateIntentApplication, applyIntentApplication } from "./intent-application.ts";
|
|
9
|
+
import { intentKey, intentRequest, intentRequestDigest, intentReceipt, matchesIntentReceipt, intentDecision, replayIntent, intentSnapshot,
|
|
10
|
+
type WorkIntentBinding, type IntentState, type IntentRequest, type IntentAdmission } from "./intent-control.ts";
|
|
11
|
+
import { intentAdmission, nextIntent } from "./intent-scheduling.ts";
|
|
12
|
+
import { dispatchAuthority, dispatchRequest, dispatchRequestDigest, dispatchDecision, replayDispatch, freezeDispatch,
|
|
13
|
+
type DispatchAuthority, type DispatchRequest, type DispatchState } from "./dispatch-control.ts";
|
|
14
|
+
|
|
15
|
+
export interface ResourceLimits { maxAttempts: number; maxInputBytes: number; maxConcurrent: number }
|
|
16
|
+
export interface BudgetBinding<V extends "1.0" | "2.0" | "3.0" | "4.0" = "1.0"> {
|
|
17
|
+
version: V; directory: string; device: string; inode: string;
|
|
18
|
+
journalDevice: string; journalInode: string; authorityDigest: string; limits: ResourceLimits; intent?: V extends "3.0" ? WorkIntentBinding : never;
|
|
19
|
+
}
|
|
20
|
+
export type DispatchBudgetBinding = BudgetBinding<"2.0">;
|
|
21
|
+
export type ExperimentBudgetBinding = BudgetBinding<"4.0">;
|
|
22
|
+
export type GovernedBudgetBinding = BudgetBinding<"1.0" | "2.0" | "3.0" | "4.0">;
|
|
23
|
+
export type IntentBudgetBinding = BudgetBinding<"3.0"> & { intent: WorkIntentBinding };
|
|
24
|
+
export interface AttemptDemand {
|
|
25
|
+
attemptId: string; orderId: string; experimentId: string;
|
|
26
|
+
kind: "primary" | "retry" | "shadow" | "descendant";
|
|
27
|
+
parentAttemptId: string | null; inputBytes: number; inputDigest: string;
|
|
28
|
+
}
|
|
29
|
+
interface Reservation extends AttemptDemand { intent?: IntentAdmission; owner: string; state: "reserved" | "settled"; outcome: string | null }
|
|
30
|
+
export interface BudgetSnapshot { attempts: number; inputBytes: number; active: number; reservations: readonly Readonly<Reservation>[] }
|
|
31
|
+
export interface ResourcePermit { readonly attemptId: string; settle(outcome: "completed" | "failed" | "cancelled"): Promise<void> }
|
|
32
|
+
interface PermitControl { owner:object; bindingDigest:string; version:string; demand:AttemptDemand; claimed:boolean; settling:number; settled:boolean; verify():Promise<void>; finish:ResourcePermit["settle"] }
|
|
33
|
+
const originalPermits = new WeakMap<ResourcePermit,PermitControl>();
|
|
34
|
+
/** Exclusive original-permit handoff for the separate opt-in IPC route. No mint/recovery API. */
|
|
35
|
+
export function claimResourcePermit(owner:object, permit:ResourcePermit, bindingDigest:string, expected:AttemptDemand) {
|
|
36
|
+
const d=demand(expected), c=originalPermits.get(permit);
|
|
37
|
+
if(!c || c.owner!==owner || c.version!=="4.0" || c.bindingDigest!==bindingDigest || JSON.stringify(c.demand)!==JSON.stringify(d) || c.claimed || c.settling || c.settled) fail("OWNERSHIP_LOST","original unclaimed experiment permit required");
|
|
38
|
+
c.claimed=true;
|
|
39
|
+
// Admission failure is consumed by the original recipient, which must still settle the charged attempt.
|
|
40
|
+
return Object.freeze({admission:c.verify(),settle:c.finish});
|
|
41
|
+
}
|
|
42
|
+
export class ResourceAdmissionError extends Error {
|
|
43
|
+
readonly code: "INVALID" | "AUTHORITY_CHANGED" | "DUPLICATE" | "EXHAUSTED" | "OWNERSHIP_LOST" | "DISPATCH_BLOCKED";
|
|
44
|
+
constructor(code: ResourceAdmissionError["code"], message: string) {
|
|
45
|
+
super(message); this.name = "ResourceAdmissionError"; this.code = code;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function fail(code: ResourceAdmissionError["code"], text: string): never { throw new ResourceAdmissionError(code, text); }
|
|
49
|
+
const hash = (text: string) => createHash("sha256").update(text).digest("hex");
|
|
50
|
+
const digest = (x: unknown): x is string => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
|
|
51
|
+
const id = (x: unknown): x is string => typeof x === "string" && /^[a-zA-Z0-9:_-]{1,128}$/.test(x);
|
|
52
|
+
const integer = (x: unknown, max: number): x is number => Number.isSafeInteger(x) && Number(x) >= 0 && Number(x) <= max;
|
|
53
|
+
function shape(x: unknown, keys: string[]): asserts x is Record<string, unknown> {
|
|
54
|
+
if (!x || typeof x !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(x)) ||
|
|
55
|
+
Reflect.ownKeys(x).length !== keys.length || keys.some(k => { const d = Object.getOwnPropertyDescriptor(x, k); return !d || !d.enumerable || !Object.hasOwn(d, "value"); })) fail("INVALID", "closed data object required");
|
|
56
|
+
}
|
|
57
|
+
function limits(x: ResourceLimits): ResourceLimits {
|
|
58
|
+
shape(x, ["maxAttempts", "maxInputBytes", "maxConcurrent"]);
|
|
59
|
+
if (!integer(x.maxAttempts, 1024) || x.maxAttempts < 1 || !integer(x.maxInputBytes, 16 * 1024 * 1024) ||
|
|
60
|
+
!integer(x.maxConcurrent, 32) || x.maxConcurrent < 1 || x.maxConcurrent > x.maxAttempts) fail("INVALID", "unsupported resource limits");
|
|
61
|
+
return Object.freeze({ maxAttempts: x.maxAttempts, maxInputBytes: x.maxInputBytes, maxConcurrent: x.maxConcurrent });
|
|
62
|
+
}
|
|
63
|
+
function binding<T extends GovernedBudgetBinding>(x: T): Readonly<T> {
|
|
64
|
+
shape(x, ["version", "directory", "device", "inode", "journalDevice", "journalInode", "authorityDigest", "limits", ...(Object.getOwnPropertyDescriptor(x ?? {}, "version")?.value === "3.0" ? ["intent"] : [])]);
|
|
65
|
+
if (!["1.0", "2.0", "3.0", "4.0"].includes(x.version) || typeof x.directory !== "string" || !isAbsolute(x.directory) || resolve(x.directory) !== x.directory ||
|
|
66
|
+
x.directory.length > 1024 || x.directory.split("/").includes(".pi") || ![x.device, x.inode, x.journalDevice, x.journalInode].every(v => typeof v === "string" && /^\d+$/.test(v)) || !digest(x.authorityDigest)) fail("INVALID", "invalid independent budget binding");
|
|
67
|
+
return Object.freeze({ version: x.version, directory: x.directory, device: x.device, inode: x.inode, journalDevice: x.journalDevice, journalInode: x.journalInode, authorityDigest: x.authorityDigest, limits: limits(x.limits), ...(x.version === "3.0" ? { intent: freezeWork(workIntentBinding(x.intent!)) } : {}) }) as Readonly<T>;
|
|
68
|
+
}
|
|
69
|
+
function demand(x: AttemptDemand): AttemptDemand {
|
|
70
|
+
shape(x, ["attemptId", "orderId", "experimentId", "kind", "parentAttemptId", "inputBytes", "inputDigest"]);
|
|
71
|
+
if (![x.attemptId, x.orderId, x.experimentId].every(id) || !["primary", "retry", "shadow", "descendant"].includes(x.kind) ||
|
|
72
|
+
!(x.parentAttemptId === null || id(x.parentAttemptId)) || (x.kind !== "primary" && x.parentAttemptId === null) ||
|
|
73
|
+
!integer(x.inputBytes, 16384) || !digest(x.inputDigest)) fail("INVALID", "invalid attempt demand");
|
|
74
|
+
return { attemptId: x.attemptId, orderId: x.orderId, experimentId: x.experimentId, kind: x.kind,
|
|
75
|
+
parentAttemptId: x.parentAttemptId, inputBytes: x.inputBytes, inputDigest: x.inputDigest };
|
|
76
|
+
}
|
|
77
|
+
async function privateDirectory(path: string) {
|
|
78
|
+
const st = await lstat(path, { bigint: true });
|
|
79
|
+
if (!st.isDirectory() || st.isSymbolicLink() || (st.mode & 0o077n) !== 0n || st.uid !== BigInt(process.getuid!()) || await realpath(path) !== path) fail("INVALID", "a canonical owner-private directory is required");
|
|
80
|
+
return st;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Explicit one-time creation, never implicit reset/recovery. Keep this binding outside the mutable journal. */
|
|
84
|
+
type BudgetCreation = { directory: string; authorityDigest: string; limits: ResourceLimits };
|
|
85
|
+
export function createResourceBudget(input: BudgetCreation): Promise<Readonly<BudgetBinding>> { return createBudget(input, "1.0"); }
|
|
86
|
+
/** Explicit opt-in v2 journal; old readers refuse rather than ignore control events. */
|
|
87
|
+
export function createDispatchBudget(input: BudgetCreation): Promise<Readonly<DispatchBudgetBinding>> { return createBudget(input, "2.0"); }
|
|
88
|
+
export async function createIntentBudget(input: BudgetCreation, intent: Parameters<typeof readIntentWork>[0]): Promise<Readonly<IntentBudgetBinding>> {
|
|
89
|
+
return createBudget(input, "3.0", JSON.parse(intentKey(intent)));
|
|
90
|
+
}
|
|
91
|
+
/** Explicit batch semantics. All queued slots remain conservatively active until original settlement. */
|
|
92
|
+
export function createExperimentBudget(input: BudgetCreation): Promise<Readonly<ExperimentBudgetBinding>> { return createBudget(input, "4.0"); }
|
|
93
|
+
function createBudget(input: BudgetCreation, version: "4.0"): Promise<Readonly<ExperimentBudgetBinding>>;
|
|
94
|
+
function createBudget(input: BudgetCreation, version: "1.0"): Promise<Readonly<BudgetBinding>>;
|
|
95
|
+
function createBudget(input: BudgetCreation, version: "2.0"): Promise<Readonly<DispatchBudgetBinding>>;
|
|
96
|
+
function createBudget(input: BudgetCreation, version: "3.0", intent: WorkIntentBinding): Promise<Readonly<IntentBudgetBinding>>;
|
|
97
|
+
async function createBudget(input: BudgetCreation, version: GovernedBudgetBinding["version"], intent?: WorkIntentBinding): Promise<Readonly<GovernedBudgetBinding>> {
|
|
98
|
+
const initialIntent = version === "3.0" ? workIntentBinding(intent!) : undefined;
|
|
99
|
+
shape(input, ["directory", "authorityDigest", "limits"]);
|
|
100
|
+
const policy = limits(input.limits), directoryPath = input.directory, authorityDigest = input.authorityDigest;
|
|
101
|
+
if (typeof directoryPath !== "string" || !isAbsolute(directoryPath) || resolve(directoryPath) !== directoryPath ||
|
|
102
|
+
directoryPath.split("/").includes(".pi") || !digest(authorityDigest)) fail("INVALID", "invalid budget creation");
|
|
103
|
+
if (initialIntent) resolveIntent(await readIntentWork(initialIntent), initialIntent.selection, initialIntent.priorities);
|
|
104
|
+
await privateDirectory(dirname(directoryPath));
|
|
105
|
+
await mkdir(directoryPath, { mode: 0o700 }); // EEXIST refuses even if its journal was deleted.
|
|
106
|
+
const st = await privateDirectory(directoryPath);
|
|
107
|
+
const file = await open(join(directoryPath, "budget.jsonl"), "wx", 0o600);
|
|
108
|
+
const b = await runWithFinalizers(async () => {
|
|
109
|
+
const journal = await file.stat({ bigint: true });
|
|
110
|
+
const result = binding({ version, directory: directoryPath, device: String(st.dev), inode: String(st.ino),
|
|
111
|
+
journalDevice: String(journal.dev), journalInode: String(journal.ino), authorityDigest, limits: policy, ...(initialIntent ? { intent: initialIntent } : {}) });
|
|
112
|
+
await file.writeFile(JSON.stringify(result) + "\n"); await file.sync(); return result;
|
|
113
|
+
}, [{ label: "budget creation close failed", run: () => file.close() }]);
|
|
114
|
+
const directory = await open(b.directory, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
115
|
+
await runWithFinalizers(() => directory.sync(), [{ label: "budget directory close failed", run: () => directory.close() }]);
|
|
116
|
+
return b;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Host-side arithmetic for this exact independently retained binding. Not a money/CPU/memory cap. */
|
|
120
|
+
export function openResourceBudget<T extends GovernedBudgetBinding>(input: T) {
|
|
121
|
+
const b = binding(input), path = join(b.directory, "budget.jsonl"), owner = randomUUID();
|
|
122
|
+
let pinnedFile: string | undefined;
|
|
123
|
+
async function transaction<T>(work: (records: Reservation[], append: (event: unknown) => Promise<void>, control: DispatchState, intent: IntentState | null) => Promise<T>, readOnly = false): Promise<T> {
|
|
124
|
+
const checkRoot = async () => {
|
|
125
|
+
const st = await privateDirectory(b.directory);
|
|
126
|
+
if (String(st.dev) !== b.device || String(st.ino) !== b.inode) fail("AUTHORITY_CHANGED", "budget root identity changed");
|
|
127
|
+
};
|
|
128
|
+
await checkRoot();
|
|
129
|
+
const execute = async () => {
|
|
130
|
+
await checkRoot();
|
|
131
|
+
const file = await open(path, (readOnly ? constants.O_RDONLY : constants.O_RDWR | constants.O_APPEND) | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
132
|
+
return runWithFinalizers(async () => {
|
|
133
|
+
const st = await file.stat({ bigint: true }), key = `${st.dev}:${st.ino}`;
|
|
134
|
+
if (!st.isFile() || st.nlink !== 1n || st.uid !== BigInt(process.getuid!()) || (st.mode & 0o077n) !== 0n || st.size > 2n * 1024n * 1024n) fail("INVALID", "unsafe or oversized budget journal");
|
|
135
|
+
if (key !== `${b.journalDevice}:${b.journalInode}` || (pinnedFile && pinnedFile !== key)) fail("AUTHORITY_CHANGED", "budget journal replaced");
|
|
136
|
+
pinnedFile = key;
|
|
137
|
+
const buffer = Buffer.alloc(Number(st.size));
|
|
138
|
+
let offset = 0;
|
|
139
|
+
while (offset < buffer.length) { const n = (await file.read(buffer, offset, buffer.length - offset, offset)).bytesRead; if (!n) fail("INVALID", "short budget read"); offset += n; }
|
|
140
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
141
|
+
if (!text.endsWith("\n")) fail("INVALID", "incomplete budget journal; no automatic recovery");
|
|
142
|
+
const lines = text.slice(0, -1).split("\n");
|
|
143
|
+
if (lines.length > (b.version === "3.0" ? 2369 : b.version !== "1.0" ? 2305 : 2049) || JSON.stringify(binding(parseWorkJson(lines[0]) as unknown as GovernedBudgetBinding)) !== JSON.stringify(b)) fail("AUTHORITY_CHANGED", "budget header does not match independent authority");
|
|
144
|
+
const records: Reservation[] = [], control: DispatchState = { revision: 0, paused: false, records: [] };
|
|
145
|
+
const intent: IntentState | null = b.intent ? { revision: 0, selection: b.intent.selection, priorities: b.intent.priorities, records: [] } : null;
|
|
146
|
+
for (const line of lines.slice(1)) {
|
|
147
|
+
const event = parseWorkJson(line) as Record<string, unknown>;
|
|
148
|
+
if (event.type === "intent-request" || event.type === "intent-apply") {
|
|
149
|
+
if (!intent) fail("INVALID", "intent controls require explicit v3 binding");
|
|
150
|
+
replayIntent(intent, event, records.filter(r => r.state === "reserved").length, control.records.some(r => r.application === "pending"));
|
|
151
|
+
} else if (event.type === "control-request" || event.type === "control-apply") {
|
|
152
|
+
if (b.version === "1.0") fail("INVALID", "controls require explicit v2/v3 binding");
|
|
153
|
+
replayDispatch(control, event, resourceBindingDigest(b), records.filter(r => r.state === "reserved").length, Boolean(intent?.records.some(r => r.application === "pending-or-unknown")));
|
|
154
|
+
} else if (event.type === "reserve-batch") {
|
|
155
|
+
if (b.version !== "4.0") fail("INVALID", "batch requires explicit v4 budget");
|
|
156
|
+
shape(event, ["type", "owner", "demands"]);
|
|
157
|
+
if (!id(event.owner) || !Array.isArray(event.demands) || !event.demands.length || event.demands.length > 32 || control.paused || control.records.some(r => r.application === "pending")) fail("INVALID", "invalid batch");
|
|
158
|
+
for (const value of event.demands) {
|
|
159
|
+
const d = demand(value as AttemptDemand);
|
|
160
|
+
if (records.some(r => r.attemptId === d.attemptId) || d.parentAttemptId !== null && !records.some(r => r.attemptId === d.parentAttemptId)) fail("INVALID", "invalid batch sequence");
|
|
161
|
+
records.push({ ...d, owner: event.owner, state: "reserved", outcome: null });
|
|
162
|
+
}
|
|
163
|
+
} else if (event.type === "reserve") {
|
|
164
|
+
if (control.paused || control.records.some(r => r.application === "pending") || intent?.records.some(r => r.application === "pending-or-unknown")) fail("INVALID", "reservation crossed dispatch barrier");
|
|
165
|
+
shape(event, ["type", "owner", "demand", ...(intent ? ["intent"] : [])]);
|
|
166
|
+
const d = demand(event.demand as AttemptDemand);
|
|
167
|
+
if (!id(event.owner) || records.some(r => r.attemptId === d.attemptId) || (d.parentAttemptId !== null && !records.some(r => r.attemptId === d.parentAttemptId))) fail("INVALID", "invalid reservation sequence");
|
|
168
|
+
if (intent && (d.kind !== "primary" || d.parentAttemptId !== null)) fail("INVALID", "v3 schedules primary dispatch only");
|
|
169
|
+
const assigned = intent ? intentAdmission(intent, records, event.intent as IntentAdmission) : undefined;
|
|
170
|
+
records.push({ ...d, ...(assigned ? { intent: assigned } : {}), owner: event.owner, state: "reserved", outcome: null });
|
|
171
|
+
} else {
|
|
172
|
+
shape(event, ["type", "owner", "attemptId", "outcome"]);
|
|
173
|
+
const r = records.find(r => r.attemptId === event.attemptId);
|
|
174
|
+
if (event.type !== "settle" || !r || r.state !== "reserved" || event.owner !== r.owner || !["completed", "failed", "cancelled"].includes(String(event.outcome))) fail("INVALID", "invalid settlement sequence");
|
|
175
|
+
r.state = "settled"; r.outcome = String(event.outcome);
|
|
176
|
+
}
|
|
177
|
+
if (records.length > b.limits.maxAttempts || records.reduce((n, r) => n + r.inputBytes, 0) > b.limits.maxInputBytes || records.filter(r => r.state === "reserved").length > b.limits.maxConcurrent) fail("INVALID", "journal exceeds independently fixed limits");
|
|
178
|
+
}
|
|
179
|
+
let expectedSize = st.size;
|
|
180
|
+
const unchanged = async () => {
|
|
181
|
+
await checkRoot(); const current = await lstat(path, { bigint: true });
|
|
182
|
+
if (`${current.dev}:${current.ino}` !== key || current.size !== expectedSize || (await file.stat({ bigint: true })).size !== expectedSize ||
|
|
183
|
+
current.nlink !== 1n || (current.mode & 0o077n) !== 0n) fail("AUTHORITY_CHANGED", "budget journal changed under lock");
|
|
184
|
+
};
|
|
185
|
+
await unchanged();
|
|
186
|
+
return await work(records, async event => {
|
|
187
|
+
if (readOnly) fail("INVALID", "inspection cannot write");
|
|
188
|
+
const line = JSON.stringify(event) + "\n";
|
|
189
|
+
if (expectedSize + BigInt(Buffer.byteLength(line)) > 2n * 1024n * 1024n) fail("EXHAUSTED", "journal capacity reached");
|
|
190
|
+
await unchanged(); await file.writeFile(line); expectedSize += BigInt(Buffer.byteLength(line)); await file.sync(); await unchanged();
|
|
191
|
+
}, control, intent);
|
|
192
|
+
}, [{ label: "budget journal close failed", run: () => file.close() }]);
|
|
193
|
+
};
|
|
194
|
+
return readOnly ? execute() : withFileLock(path, "resource budget", execute, { staleRecovery: "disabled" });
|
|
195
|
+
}
|
|
196
|
+
const permit = (d:AttemptDemand): Readonly<ResourcePermit> => {
|
|
197
|
+
const attemptId=d.attemptId;
|
|
198
|
+
const c:PermitControl={owner:api,bindingDigest:resourceBindingDigest(b),version:b.version,demand:d,claimed:false,settling:0,settled:false,
|
|
199
|
+
verify:()=>transaction(async(records,_append,control,intent)=>{
|
|
200
|
+
const r=records.find(r=>r.attemptId===attemptId);
|
|
201
|
+
if(!r || r.owner!==owner || r.state!=="reserved" || Object.keys(d).some(k=>r[k as keyof AttemptDemand]!==d[k as keyof AttemptDemand])) fail("OWNERSHIP_LOST","original reservation is not held");
|
|
202
|
+
if(control.paused || control.records.some(r=>r.application==="pending") || intent?.records.some(r=>r.application==="pending-or-unknown")) fail("DISPATCH_BLOCKED","dispatch barrier");
|
|
203
|
+
}),
|
|
204
|
+
async finish(outcome) {
|
|
205
|
+
if(!["completed","failed","cancelled"].includes(outcome)) fail("INVALID","invalid outcome");
|
|
206
|
+
c.settling++;
|
|
207
|
+
try {
|
|
208
|
+
await transaction(async(records,append)=>{
|
|
209
|
+
const r=records.find(r=>r.attemptId===attemptId);
|
|
210
|
+
if(!r || r.owner!==owner) fail("OWNERSHIP_LOST","only original live host may settle");
|
|
211
|
+
if(r.state==="settled"){if(r.outcome!==outcome)fail("INVALID","contradictory settlement");return;}
|
|
212
|
+
await append({type:"settle",owner,attemptId,outcome});
|
|
213
|
+
}); c.settled=true;
|
|
214
|
+
} finally {c.settling--;}
|
|
215
|
+
}};
|
|
216
|
+
const result=Object.freeze({attemptId,async settle(outcome:Parameters<ResourcePermit["settle"]>[0]) {
|
|
217
|
+
if(c.claimed) fail("OWNERSHIP_LOST","original permit handed to IPC lifetime");
|
|
218
|
+
return c.finish(outcome);
|
|
219
|
+
}}); originalPermits.set(result,c); return result;
|
|
220
|
+
};
|
|
221
|
+
const api = Object.freeze({
|
|
222
|
+
binding: b,
|
|
223
|
+
async reserveBatch(inputs: readonly AttemptDemand[]): Promise<readonly Readonly<ResourcePermit>[]> {
|
|
224
|
+
if (b.version !== "4.0" || !Array.isArray(inputs) || !inputs.length || inputs.length > 32 || Reflect.ownKeys(inputs).length !== inputs.length + 1 || Array.from({ length: inputs.length }, (_, i) => Object.getOwnPropertyDescriptor(inputs, String(i))).some(d => !d?.enumerable || !Object.hasOwn(d, "value"))) fail("INVALID", "explicit v4 dense bounded batch required");
|
|
225
|
+
const demands = Array.from(inputs, demand);
|
|
226
|
+
await transaction(async (records, append, control) => {
|
|
227
|
+
if (control.paused || control.records.some(r => r.application === "pending")) fail("DISPATCH_BLOCKED", "dispatch barrier");
|
|
228
|
+
const ids = new Set(records.map(r => r.attemptId));
|
|
229
|
+
for (const d of demands) {
|
|
230
|
+
if (ids.has(d.attemptId)) fail("DUPLICATE", "batch identity already charged");
|
|
231
|
+
if (d.parentAttemptId !== null && !ids.has(d.parentAttemptId)) fail("INVALID", "batch parent missing");
|
|
232
|
+
ids.add(d.attemptId);
|
|
233
|
+
}
|
|
234
|
+
if (records.length + demands.length > b.limits.maxAttempts || records.reduce((n,r) => n+r.inputBytes,0) + demands.reduce((n,d) => n+d.inputBytes,0) > b.limits.maxInputBytes || records.filter(r => r.state === "reserved").length + demands.length > b.limits.maxConcurrent) fail("EXHAUSTED", "whole experiment exceeds aggregate allowance");
|
|
235
|
+
await append({ type: "reserve-batch", owner, demands });
|
|
236
|
+
});
|
|
237
|
+
return Object.freeze(demands.map(d => permit(d)));
|
|
238
|
+
},
|
|
239
|
+
intentControls(input: DispatchAuthority | null) {
|
|
240
|
+
if (!b.intent) fail("INVALID", "intent controls require explicit v3 budget creation");
|
|
241
|
+
const work = b.intent, authority = dispatchAuthority(input), scope = resourceBindingDigest(b);
|
|
242
|
+
const authorized = (digest: string) => authority?.authorityDigest === b.authorityDigest && authority.requestDigests.includes(digest);
|
|
243
|
+
const apply = async (request: ReturnType<typeof intentRequest>, records: Reservation[], append: (event: unknown) => Promise<void>, control: DispatchState, state: IntentState) => {
|
|
244
|
+
const pending = state.records.find(r => r.requestId === request.requestId);
|
|
245
|
+
if (!pending || pending.digest !== intentRequestDigest(request)) fail("DUPLICATE", "exact original intent request required");
|
|
246
|
+
if (!matchesIntentReceipt(pending, request)) fail("INVALID", "intent receipt projection does not match original request");
|
|
247
|
+
if (pending.application !== "pending-or-unknown" || !authorized(pending.digest) || records.some(r => r.state === "reserved") || control.records.some(r => r.application === "pending")) return;
|
|
248
|
+
await applyIntentApplication(work, state, request);
|
|
249
|
+
const event = { type: "intent-apply", requestId: request.requestId };
|
|
250
|
+
await append(event); replayIntent(state, event, 0, false);
|
|
251
|
+
};
|
|
252
|
+
return Object.freeze({
|
|
253
|
+
inspect: () => transaction(async (records, _append, control, state) => {
|
|
254
|
+
resolveIntent(await readIntentWork(work), state!.selection, state!.priorities);
|
|
255
|
+
const blocked = control.paused || control.records.some(r => r.application === "pending") || state!.records.some(r => r.application === "pending-or-unknown");
|
|
256
|
+
return freezeWork({ ...intentSnapshot(state!, scope), dispatchPaused: control.paused, nextObligation: blocked ? null : nextIntent(state!, records) });
|
|
257
|
+
}, true),
|
|
258
|
+
async request(input: IntentRequest) {
|
|
259
|
+
const request = intentRequest(input), digest = intentRequestDigest(request);
|
|
260
|
+
if (request.bindingDigest !== scope) fail("AUTHORITY_CHANGED", "intent request is for another binding");
|
|
261
|
+
return transaction(async (records, append, control, state) => {
|
|
262
|
+
const prior = state!.records.find(r => r.requestId === request.requestId);
|
|
263
|
+
if (prior) {
|
|
264
|
+
if (prior.digest !== digest) fail("DUPLICATE", "immutable intent request identity");
|
|
265
|
+
if (!matchesIntentReceipt(prior, request)) fail("INVALID", "intent receipt projection does not match original request");
|
|
266
|
+
return intentSnapshot(state!, scope);
|
|
267
|
+
}
|
|
268
|
+
if (state!.records.length >= 32) fail("EXHAUSTED", "intent request capacity reached");
|
|
269
|
+
const decision = intentDecision(state!, request, Boolean(authorized(digest)), control.records.some(r => r.application === "pending"));
|
|
270
|
+
if (decision === "approved") await validateIntentApplication(work, state!, request);
|
|
271
|
+
const event = { type: "intent-request", receipt: intentReceipt(request, decision) };
|
|
272
|
+
await append(event); replayIntent(state!, event, records.filter(r => r.state === "reserved").length, control.records.some(r => r.application === "pending"));
|
|
273
|
+
if (decision === "approved") await apply(request, records, append, control, state!);
|
|
274
|
+
return intentSnapshot(state!, scope);
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
async reconcile(input: IntentRequest) {
|
|
278
|
+
const request = intentRequest(input);
|
|
279
|
+
if (request.bindingDigest !== scope) fail("AUTHORITY_CHANGED", "intent request is for another binding");
|
|
280
|
+
return transaction(async (records, append, control, state) => { await apply(request, records, append, control, state!); return intentSnapshot(state!, scope); });
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
controls(input: DispatchAuthority | null) {
|
|
285
|
+
if (b.version === "1.0") fail("INVALID", "controls require explicit v2/v3 budget creation");
|
|
286
|
+
const authority = dispatchAuthority(input), scope = resourceBindingDigest(b);
|
|
287
|
+
const authorized = (digest: string) => authority?.authorityDigest === b.authorityDigest && authority.requestDigests.includes(digest);
|
|
288
|
+
const apply = async (requestId: string, records: Reservation[], append: (event: unknown) => Promise<void>, control: DispatchState) => {
|
|
289
|
+
const pending = control.records.find(r => r.request.requestId === requestId && r.application === "pending");
|
|
290
|
+
if (!pending || !authorized(pending.digest) || records.some(r => r.state === "reserved")) return;
|
|
291
|
+
const event = { type: "control-apply", requestId: pending.request.requestId };
|
|
292
|
+
await append(event); replayDispatch(control, event, scope, 0);
|
|
293
|
+
};
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
inspect: () => transaction(async (_records, _append, control, intent) => freezeDispatch(control, scope, Boolean(intent?.records.some(r => r.application === "pending-or-unknown"))), true),
|
|
296
|
+
async reconcile(requestId: string) {
|
|
297
|
+
if (typeof requestId !== "string" || !/^[a-zA-Z0-9:_-]{1,128}$/.test(requestId)) fail("INVALID", "exact request ID required for reconciliation");
|
|
298
|
+
return transaction(async (records, append, control, intent) => { await apply(requestId, records, append, control); return freezeDispatch(control, scope, Boolean(intent?.records.some(r => r.application === "pending-or-unknown"))); });
|
|
299
|
+
},
|
|
300
|
+
async request(input: DispatchRequest) {
|
|
301
|
+
const request = dispatchRequest(input), digest = dispatchRequestDigest(request);
|
|
302
|
+
if (request.bindingDigest !== scope) fail("AUTHORITY_CHANGED", "request is for another binding");
|
|
303
|
+
return transaction(async (records, append, control, intent) => {
|
|
304
|
+
const previous = control.records.find(r => r.request.requestId === request.requestId);
|
|
305
|
+
if (previous) {
|
|
306
|
+
if (previous.digest !== digest) fail("DUPLICATE", "request ID cannot name another decision");
|
|
307
|
+
// Redelivery is readback only, never a second effect or implicit reconciliation.
|
|
308
|
+
return freezeDispatch(control, scope, Boolean(intent?.records.some(r => r.application === "pending-or-unknown")));
|
|
309
|
+
}
|
|
310
|
+
if (control.records.length >= 128) fail("EXHAUSTED", "control request capacity reached");
|
|
311
|
+
const event = { type: "control-request", request, decision: dispatchDecision(control, request, Boolean(authorized(digest)), Boolean(intent?.records.some(r => r.application === "pending-or-unknown"))) };
|
|
312
|
+
await append(event); replayDispatch(control, event, scope, records.filter(r => r.state === "reserved").length, Boolean(intent?.records.some(r => r.application === "pending-or-unknown")));
|
|
313
|
+
if (event.decision === "approved") await apply(request.requestId, records, append, control);
|
|
314
|
+
return freezeDispatch(control, scope, Boolean(intent?.records.some(r => r.application === "pending-or-unknown")));
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
/** Explicit control-path synchronization; unlike inspect, this takes the existing journal lock. */
|
|
320
|
+
async controlSnapshot(): Promise<Readonly<BudgetSnapshot>> {
|
|
321
|
+
return transaction(async records => Object.freeze({ attempts: records.length, inputBytes: records.reduce((n,r)=>n+r.inputBytes,0), active: records.filter(r=>r.state==="reserved").length, reservations:Object.freeze(records.map(r=>freezeWork({...r}))) }));
|
|
322
|
+
},
|
|
323
|
+
async inspect(): Promise<Readonly<BudgetSnapshot>> {
|
|
324
|
+
return transaction(async records => Object.freeze({ attempts: records.length, inputBytes: records.reduce((n, r) => n + r.inputBytes, 0),
|
|
325
|
+
active: records.filter(r => r.state === "reserved").length, reservations: Object.freeze(records.map(r => freezeWork({ ...r }))) }), true);
|
|
326
|
+
},
|
|
327
|
+
async reserve(input: AttemptDemand, selection?: IntentAdmission): Promise<Readonly<ResourcePermit>> {
|
|
328
|
+
const d = demand(input), selected: IntentAdmission | null = selection === undefined ? null : JSON.parse(intentKey(selection));
|
|
329
|
+
if (Boolean(b.intent) !== Boolean(selected)) fail("INVALID", "v3 reservations require an exact scheduled intent; older bindings do not accept it");
|
|
330
|
+
await transaction(async (records, append, control, intent) => {
|
|
331
|
+
if (control.paused || control.records.some(r => r.application === "pending") || intent?.records.some(r => r.application === "pending-or-unknown")) fail("DISPATCH_BLOCKED", "dispatch paused or awaiting verified boundary");
|
|
332
|
+
if (records.some(r => r.attemptId === d.attemptId)) fail("DUPLICATE", "attempt identity already charged; never launch a redelivery");
|
|
333
|
+
if (d.parentAttemptId !== null && !records.some(r => r.attemptId === d.parentAttemptId)) fail("INVALID", "parent attempt is not in this budget");
|
|
334
|
+
if (records.length >= b.limits.maxAttempts || records.reduce((n, r) => n + r.inputBytes, 0) + d.inputBytes > b.limits.maxInputBytes ||
|
|
335
|
+
records.filter(r => r.state === "reserved").length >= b.limits.maxConcurrent) fail("EXHAUSTED", "aggregate allowance exhausted");
|
|
336
|
+
if (intent) {
|
|
337
|
+
if (d.kind !== "primary" || d.parentAttemptId !== null) fail("INVALID", "v3 schedules primary dispatch only");
|
|
338
|
+
resolveIntent(await readIntentWork(b.intent!), intent.selection, intent.priorities);
|
|
339
|
+
const assigned = intentAdmission(intent, records, selected!);
|
|
340
|
+
await append({ type: "reserve", owner, demand: d, intent: assigned });
|
|
341
|
+
} else await append({ type: "reserve", owner, demand: d });
|
|
342
|
+
});
|
|
343
|
+
return permit(d);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
return api;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function resourceBindingDigest(b: GovernedBudgetBinding): string { return hash(JSON.stringify(binding(b))); }
|