codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.3
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 +15 -0
- package/README.md +27 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +155 -1
- package/contracts/v1/parity-suite.json +96 -1
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
- package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/downloads.d.ts +2 -0
- package/dist/src/client.d.ts +38 -0
- package/dist/src/client.js +1222 -59
- package/dist/src/commands/configuration.js +13 -7
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +26 -3
- package/dist/src/commands/experience.js +35 -0
- package/dist/src/commands/files.js +4 -27
- package/dist/src/commands/modes.js +49 -17
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +627 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +7 -2
- package/dist/src/commands/work.js +31 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +161 -0
- package/dist/src/operations/browser-adapter.js +2023 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1208 -0
- package/dist/src/operations/browser-target.d.ts +103 -0
- package/dist/src/operations/browser-target.js +489 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1150 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1143 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +22 -0
- package/dist/src/operations/index.js +22 -0
- package/dist/src/operations/journal.d.ts +142 -0
- package/dist/src/operations/journal.js +1931 -0
- package/dist/src/operations/production-attachments.d.ts +116 -0
- package/dist/src/operations/production-attachments.js +1018 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1363 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +1073 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +118 -0
- package/dist/src/operations/runtime-adapter.js +786 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +281 -0
- package/dist/src/operations/service.js +2810 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +39 -0
- package/dist/src/runtime/coordinated-page.js +634 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
- package/dist/src/scripts/live-smoke/scenarios.js +110 -18
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +1 -1
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +73 -0
- package/references/python-parity.md +72 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,1498 @@
|
|
|
1
|
+
import { OPERATION_CONTROL_RECEIPT_SCHEMA_VERSION, OPERATION_CONTROL_REQUEST_SCHEMA_VERSION, OPERATION_HANDLE_SCHEMA_VERSION, OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION, OPERATION_SCHEMA_VERSION } from "./types.js";
|
|
2
|
+
import { assertOperationStateShape, assertOwnershipBaselineShape } from "./state-machine.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adapter-free coordinator for operation-bound Stop and Work steer.
|
|
5
|
+
*
|
|
6
|
+
* The coordinator deliberately has no browser or journal dependency. A
|
|
7
|
+
* caller supplies a keyed control request digest and ports for reloading the
|
|
8
|
+
* authenticated parent state, recording an action intent/receipt, observing
|
|
9
|
+
* the exact assistant turn, and executing one bounded browser transaction.
|
|
10
|
+
* Stop retains its generic one-shot port. Work steer has a stricter,
|
|
11
|
+
* four-phase port: prepare (read only), atomically persist the child intent
|
|
12
|
+
* with its complete per-action baseline, executePrepared once, then verify or
|
|
13
|
+
* recover observation-only. The steer prompt is request-local and never
|
|
14
|
+
* crosses any port in a durable shape.
|
|
15
|
+
*/
|
|
16
|
+
export const CONTROL_COORDINATOR_SCHEMA_VERSION = "chatgpt.browser_control.operation_control_coordinator.v1";
|
|
17
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
18
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
19
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
|
|
20
|
+
const CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
21
|
+
const INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
22
|
+
const BASELINE_EPOCH = "1970-01-01T00:00:00.000Z";
|
|
23
|
+
const MAX_STEER_PROMPT_BYTES = 8 * 1024 * 1024;
|
|
24
|
+
const MAX_TIMEOUT_MS = 86_400_000;
|
|
25
|
+
const MAX_DEADLINE_AT = Date.UTC(2100, 0, 1);
|
|
26
|
+
const MAX_POSTCONDITION_RETRY_ATTEMPTS = 64;
|
|
27
|
+
const MAX_POSTCONDITION_RETRY_INTERVAL_MS = 1_000;
|
|
28
|
+
const MAX_POSTCONDITION_RETRY_WINDOW_MS = 15_000;
|
|
29
|
+
// These values are intentionally not valid UUIDs or digests. They are only
|
|
30
|
+
// used on the fail-closed input-error path, where returning a result-shaped
|
|
31
|
+
// blocker still requires the public identity fields. In particular, never
|
|
32
|
+
// turn absent or malformed evidence into a cryptographically valid sentinel.
|
|
33
|
+
const INVALID_OPERATION_ID = "invalid-operation";
|
|
34
|
+
const INVALID_ACTION_ID = "invalid-control";
|
|
35
|
+
const INVALID_DIGEST = "invalid-digest";
|
|
36
|
+
const INVALID_TURN_ID = "invalid-turn";
|
|
37
|
+
const BOUNDARY_RANK = {
|
|
38
|
+
none: 0,
|
|
39
|
+
handoff_may_have_occurred: 1,
|
|
40
|
+
send_may_have_occurred: 2,
|
|
41
|
+
control_may_have_occurred: 3
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Versioned, prompt-free material for the keyed prepared-action digest.
|
|
45
|
+
*
|
|
46
|
+
* The journal stores `preparedDigest`, not this material, but a restart must
|
|
47
|
+
* be able to reconstruct exactly what was authenticated from the durable
|
|
48
|
+
* action-prepared event. The browser primitive and its service adapter should
|
|
49
|
+
* pass this material (canonicalized by the adapter's digest authority) to the
|
|
50
|
+
* same keyed `work-steer-prepared` digest domain. Keeping the material here
|
|
51
|
+
* makes that contract explicit without making the coordinator depend on a
|
|
52
|
+
* key, browser, or journal implementation.
|
|
53
|
+
*/
|
|
54
|
+
export const CONTROL_STEER_PREPARED_MATERIAL_SCHEMA_VERSION =
|
|
55
|
+
// Keep this value aligned with the production primitive's digest material
|
|
56
|
+
// schema. The coordinator owns the adapter-neutral contract; the shared
|
|
57
|
+
// version lets service recovery validate the existing primitive digest
|
|
58
|
+
// without persisting another opaque material record.
|
|
59
|
+
"chatgpt.browser_control.production_work_steer.v1";
|
|
60
|
+
/**
|
|
61
|
+
* Build the exact redacted digest material from either a prepared result or
|
|
62
|
+
* an atomic persistence request. This clones and freezes caller data so a
|
|
63
|
+
* later mutation/accessor cannot change what a service hashes or journals.
|
|
64
|
+
*/
|
|
65
|
+
export function controlSteerPreparedDigestMaterial(value) {
|
|
66
|
+
try {
|
|
67
|
+
const cloned = clonePlainData(value);
|
|
68
|
+
if (!isRecord(cloned))
|
|
69
|
+
throw new Error("invalid material");
|
|
70
|
+
assertUuid(cloned.parentOperationId, "prepared material operationId");
|
|
71
|
+
assertDigest(cloned.parentRequestDigest, "prepared material parentRequestDigest");
|
|
72
|
+
assertDigest(cloned.parentTargetBindingDigest, "prepared material targetBindingDigest");
|
|
73
|
+
assertUuid(cloned.controlActionId, "prepared material controlActionId");
|
|
74
|
+
assertIdentifier(cloned.expectedAssistantTurnId, "prepared material expectedAssistantTurnId");
|
|
75
|
+
assertIdentifier(cloned.assistantBranchId, "prepared material assistantBranchId");
|
|
76
|
+
assertIdentifier(cloned.assistantParentTurnId, "prepared material assistantParentTurnId");
|
|
77
|
+
assertDigest(cloned.baselineSnapshotDigest, "prepared material baselineSnapshotDigest");
|
|
78
|
+
const baseline = cloned.baseline;
|
|
79
|
+
assertOwnershipBaselineShape({
|
|
80
|
+
schemaVersion: OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION,
|
|
81
|
+
operationId: cloned.parentOperationId,
|
|
82
|
+
requestDigest: cloned.parentRequestDigest,
|
|
83
|
+
targetBindingDigest: cloned.parentTargetBindingDigest,
|
|
84
|
+
actionId: cloned.controlActionId,
|
|
85
|
+
baseline,
|
|
86
|
+
observedAt: BASELINE_EPOCH
|
|
87
|
+
});
|
|
88
|
+
if (baseline.snapshotDigest !== cloned.baselineSnapshotDigest)
|
|
89
|
+
throw new Error("baseline digest mismatch");
|
|
90
|
+
if (baseline.completeness !== "complete"
|
|
91
|
+
|| baseline.target.canonicalThreadUrl.status !== "unavailable"
|
|
92
|
+
|| baseline.target.canonicalThreadUrl.reason !== "redacted")
|
|
93
|
+
throw new Error("baseline is not complete and redacted");
|
|
94
|
+
const assistants = baseline.assistantTurns.filter(turn => turn.stableId === cloned.expectedAssistantTurnId);
|
|
95
|
+
if (assistants.length !== 1
|
|
96
|
+
|| assistants[0].branchStableId !== cloned.assistantBranchId
|
|
97
|
+
|| assistants[0].parentStableId !== cloned.assistantParentTurnId)
|
|
98
|
+
throw new Error("assistant identity is not derived from baseline");
|
|
99
|
+
return deepFreeze({
|
|
100
|
+
schemaVersion: CONTROL_STEER_PREPARED_MATERIAL_SCHEMA_VERSION,
|
|
101
|
+
operationId: cloned.parentOperationId,
|
|
102
|
+
parentRequestDigest: cloned.parentRequestDigest,
|
|
103
|
+
targetBindingDigest: cloned.parentTargetBindingDigest,
|
|
104
|
+
controlActionId: cloned.controlActionId,
|
|
105
|
+
action: "work_steer",
|
|
106
|
+
expectedAssistantTurnId: cloned.expectedAssistantTurnId,
|
|
107
|
+
assistantBranchId: cloned.assistantBranchId,
|
|
108
|
+
assistantParentTurnId: cloned.assistantParentTurnId,
|
|
109
|
+
baselineSnapshotDigest: cloned.baselineSnapshotDigest,
|
|
110
|
+
baseline
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new ControlInputError("operation_state_corrupt", "Prepared steer digest material is invalid.");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Default bounded settle window for provider Stop controls. */
|
|
118
|
+
export const CONTROL_POSTCONDITION_RETRY_POLICY = Object.freeze({
|
|
119
|
+
maxAttempts: 32,
|
|
120
|
+
intervalMs: 250
|
|
121
|
+
});
|
|
122
|
+
export class ControlInputError extends Error {
|
|
123
|
+
code;
|
|
124
|
+
constructor(code, message) {
|
|
125
|
+
super(message);
|
|
126
|
+
this.code = code;
|
|
127
|
+
this.name = "ControlInputError";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Run one operation-bound control action. A durable action intent authorizes
|
|
132
|
+
* at most one executeOnce call. Any later call with the same action ID is
|
|
133
|
+
* observation-only, even when the previous call rejected after the provider
|
|
134
|
+
* may already have acted.
|
|
135
|
+
*/
|
|
136
|
+
export async function runOperationControl(request, requestDigest, ports, options = {}) {
|
|
137
|
+
let normalized;
|
|
138
|
+
try {
|
|
139
|
+
// Validate the object graph without reading accessor-backed properties.
|
|
140
|
+
// A hostile getter must not run merely because the operation is being
|
|
141
|
+
// converted into a redacted blocker result.
|
|
142
|
+
if (hasAccessorInPlainData(request) || hasAccessorInPlainData(options)) {
|
|
143
|
+
throw new ControlInputError("operation_state_corrupt", "Control input contains accessor-backed identity data.");
|
|
144
|
+
}
|
|
145
|
+
normalized = normalizeInput(request, requestDigest, ports, options);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const identity = safeBase(request, requestDigest);
|
|
149
|
+
const code = error instanceof ControlInputError ? error.code : "operation_state_corrupt";
|
|
150
|
+
return blockedResult(identity, code, false, "none");
|
|
151
|
+
}
|
|
152
|
+
if (isCancelled(normalized)) {
|
|
153
|
+
return blockedResult(normalized.base, cancellationCode(normalized), false, "none");
|
|
154
|
+
}
|
|
155
|
+
let parent;
|
|
156
|
+
try {
|
|
157
|
+
parent = await normalized.ports.readParent({
|
|
158
|
+
operationId: normalized.request.parent.operationId,
|
|
159
|
+
parentRequestDigest: normalized.request.parent.requestDigest,
|
|
160
|
+
parentTargetBindingDigest: normalized.request.parent.targetBindingDigest,
|
|
161
|
+
controlActionId: normalized.request.controlActionId,
|
|
162
|
+
action: normalized.request.action,
|
|
163
|
+
expectedAssistantTurnId: normalized.request.expectedAssistantTurnId,
|
|
164
|
+
requestDigest: normalized.requestDigest
|
|
165
|
+
});
|
|
166
|
+
validateParentSnapshot(normalized.request, normalized.requestDigest, parent);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const code = parentErrorCode(error);
|
|
170
|
+
return blockedResult(normalized.base, code, false, "none");
|
|
171
|
+
}
|
|
172
|
+
const existing = findExistingAction(parent.state, normalized.request.controlActionId);
|
|
173
|
+
if (existing === "corrupt") {
|
|
174
|
+
return blockedResult(normalized.base, "operation_state_corrupt", false, parent.state.mutationBoundary);
|
|
175
|
+
}
|
|
176
|
+
if (existing !== undefined && !actionMatches(existing, normalized.request, normalized.requestDigest)) {
|
|
177
|
+
return blockedResult(normalized.base, existing.requestDigest !== normalized.requestDigest ? "operation_request_mismatch" : "target_binding_mismatch", false, parent.state.mutationBoundary);
|
|
178
|
+
}
|
|
179
|
+
if (parent.existingReceipt !== undefined) {
|
|
180
|
+
return resultFromReceipt(normalized.base, parent.existingReceipt);
|
|
181
|
+
}
|
|
182
|
+
// Once an intent exists, even one recovered from a previous process, this
|
|
183
|
+
// path cannot call a browser execution port. Work steer requires the
|
|
184
|
+
// caller-supplied authenticated baseline and uses recovery only; it never
|
|
185
|
+
// falls back to the generic postcondition or mutation path.
|
|
186
|
+
if (existing !== undefined) {
|
|
187
|
+
if (normalized.request.action === "steer") {
|
|
188
|
+
return await reconcileExistingSteer(normalized, parent);
|
|
189
|
+
}
|
|
190
|
+
return await reconcileExisting(normalized, parent.state.mutationBoundary);
|
|
191
|
+
}
|
|
192
|
+
if (normalized.request.action === "steer") {
|
|
193
|
+
return await runSteerPhases(normalized, parent);
|
|
194
|
+
}
|
|
195
|
+
const precondition = await observeTurnSafely(normalized);
|
|
196
|
+
if (precondition.kind !== "exact") {
|
|
197
|
+
return blockedResult(normalized.base, precondition.code, precondition.observationRequired, parent.state.mutationBoundary, precondition.evidenceDigest);
|
|
198
|
+
}
|
|
199
|
+
// The intent is the fencing point. If this call rejects, no browser call
|
|
200
|
+
// is attempted: the caller must reload state before deciding whether the
|
|
201
|
+
// intent committed.
|
|
202
|
+
try {
|
|
203
|
+
await normalized.ports.persistActionIntent({
|
|
204
|
+
operationId: normalized.base.parentOperationId,
|
|
205
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
206
|
+
targetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
207
|
+
controlActionId: normalized.base.controlActionId,
|
|
208
|
+
action: normalized.base.action,
|
|
209
|
+
requestDigest: normalized.base.requestDigest
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return blockedResult(normalized.base, "backend_unavailable", false, parent.state.mutationBoundary);
|
|
214
|
+
}
|
|
215
|
+
// Cancellation/timeout after intent is never a licence to retry later by
|
|
216
|
+
// calling executeOnce again. We make one read-only reconciliation attempt
|
|
217
|
+
// when possible and otherwise return a durable uncertain receipt.
|
|
218
|
+
if (isCancelled(normalized)) {
|
|
219
|
+
return await settleAfterIntent(normalized, parent.state.mutationBoundary, cancellationCode(normalized));
|
|
220
|
+
}
|
|
221
|
+
let execution;
|
|
222
|
+
try {
|
|
223
|
+
execution = await normalized.ports.executeOnce({
|
|
224
|
+
operationId: normalized.base.parentOperationId,
|
|
225
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
226
|
+
targetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
227
|
+
controlActionId: normalized.base.controlActionId,
|
|
228
|
+
action: normalized.base.action,
|
|
229
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
230
|
+
requestDigest: normalized.base.requestDigest,
|
|
231
|
+
signal: normalized.signal,
|
|
232
|
+
deadlineAt: normalized.deadlineAt
|
|
233
|
+
});
|
|
234
|
+
validateExecutionResult(execution, normalized.base.expectedAssistantTurnId);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
execution = { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
238
|
+
}
|
|
239
|
+
if (execution.status === "satisfied") {
|
|
240
|
+
return await persistOutcome(normalized, parent.state.mutationBoundary, {
|
|
241
|
+
outcome: "satisfied",
|
|
242
|
+
evidenceDigest: execution.evidenceDigest
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (execution.status === "not_satisfied") {
|
|
246
|
+
return await persistOutcome(normalized, parent.state.mutationBoundary, {
|
|
247
|
+
outcome: "not_satisfied",
|
|
248
|
+
blockerCode: execution.blockerCode,
|
|
249
|
+
...(execution.evidenceDigest === undefined ? {} : { evidenceDigest: execution.evidenceDigest })
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// An uncertain or throwing execution is reconciled exactly once. This
|
|
253
|
+
// observation has no mutation capability and is safe to repeat on a later
|
|
254
|
+
// caller invocation, while executeOnce remains permanently forbidden.
|
|
255
|
+
return await reconcileAfterIntent(normalized, parent.state.mutationBoundary, execution);
|
|
256
|
+
}
|
|
257
|
+
/** Run the four-phase Work-steer coordinator after the parent is reloaded. */
|
|
258
|
+
async function runSteerPhases(normalized, parent) {
|
|
259
|
+
const preparePort = normalized.ports.prepareSteer;
|
|
260
|
+
const persistPort = normalized.ports.persistSteerIntentAndBaseline;
|
|
261
|
+
const executePort = normalized.ports.executeSteerPrepared;
|
|
262
|
+
const verifyPort = normalized.ports.verifySteer;
|
|
263
|
+
const recoverPort = normalized.ports.recoverSteer;
|
|
264
|
+
// normalizeInput enforces this branch. Keep the guard here as a second
|
|
265
|
+
// fail-closed seam in case a mutable port object changes after validation.
|
|
266
|
+
if (preparePort === undefined || persistPort === undefined || executePort === undefined || verifyPort === undefined || recoverPort === undefined) {
|
|
267
|
+
return blockedResult(normalized.base, "backend_unavailable", true, parent.state.mutationBoundary);
|
|
268
|
+
}
|
|
269
|
+
let preparedResult;
|
|
270
|
+
try {
|
|
271
|
+
preparedResult = validateSteerPhaseResult(await preparePort({
|
|
272
|
+
schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
|
|
273
|
+
parentOperationId: normalized.base.parentOperationId,
|
|
274
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
275
|
+
parentTargetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
276
|
+
controlActionId: normalized.base.controlActionId,
|
|
277
|
+
requestDigest: normalized.base.requestDigest,
|
|
278
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
279
|
+
signal: normalized.signal,
|
|
280
|
+
deadlineAt: normalized.deadlineAt
|
|
281
|
+
}), "prepare", normalized);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return blockedResult(normalized.base, cancellationCode(normalized) ?? (error instanceof ControlInputError ? error.code : "target_evidence_unavailable"), true, parent.state.mutationBoundary);
|
|
285
|
+
}
|
|
286
|
+
if (preparedResult.status !== "prepared") {
|
|
287
|
+
return steerPhaseResultToControl(normalized, preparedResult, parent.state.mutationBoundary);
|
|
288
|
+
}
|
|
289
|
+
const prepared = preparedResult.prepared;
|
|
290
|
+
try {
|
|
291
|
+
validateSteerPreparedTarget(prepared, parent.state);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
return blockedResult(normalized.base, error instanceof ControlInputError ? error.code : "target_binding_mismatch", true, parent.state.mutationBoundary);
|
|
295
|
+
}
|
|
296
|
+
if (isCancelled(normalized)) {
|
|
297
|
+
return blockedResult(normalized.base, cancellationCode(normalized) ?? "operation_timeout", false, parent.state.mutationBoundary);
|
|
298
|
+
}
|
|
299
|
+
const persistenceRequest = {
|
|
300
|
+
schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
|
|
301
|
+
parentOperationId: normalized.base.parentOperationId,
|
|
302
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
303
|
+
parentTargetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
304
|
+
controlActionId: normalized.base.controlActionId,
|
|
305
|
+
action: "steer",
|
|
306
|
+
requestDigest: normalized.base.requestDigest,
|
|
307
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
308
|
+
assistantBranchId: prepared.assistantBranchId,
|
|
309
|
+
assistantParentTurnId: prepared.assistantParentTurnId,
|
|
310
|
+
baselineSnapshotDigest: prepared.baselineSnapshotDigest,
|
|
311
|
+
preparedDigest: prepared.preparedDigest,
|
|
312
|
+
baseline: prepared.baseline
|
|
313
|
+
};
|
|
314
|
+
let persistenceResult;
|
|
315
|
+
try {
|
|
316
|
+
persistenceResult = validateSteerPersistenceResult(await persistPort(persistenceRequest));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
// A durable identity/baseline conflict is a state or request error, not
|
|
320
|
+
// an ambiguous append acknowledgement. Surface it directly and never
|
|
321
|
+
// turn corruption into a recovery attempt for a different action.
|
|
322
|
+
const durableErrorCode = steerPersistenceFailureCode(error);
|
|
323
|
+
if (durableErrorCode !== undefined) {
|
|
324
|
+
return blockedResult(normalized.base, durableErrorCode, true, parent.state.mutationBoundary);
|
|
325
|
+
}
|
|
326
|
+
// A journal append can commit and throw. Reload once and converge to the
|
|
327
|
+
// durable prefix; never execute merely because the write raised.
|
|
328
|
+
const converged = await reloadSteerAfterPersistenceFailure(normalized);
|
|
329
|
+
if (converged !== undefined)
|
|
330
|
+
return await reconcileExistingSteer(normalized, converged);
|
|
331
|
+
// The write may have committed even though the reload failed. Quarantine
|
|
332
|
+
// the action until a later caller can reload the authenticated baseline;
|
|
333
|
+
// never advertise this ambiguous prefix as a clean pre-intent block.
|
|
334
|
+
return steerFailureResult(normalized, "uncertain", "backend_unavailable", true);
|
|
335
|
+
}
|
|
336
|
+
// A same-action replay/commit-then-throw may have supplied an already-durable
|
|
337
|
+
// action and baseline. It is never allowed to execute its request-local
|
|
338
|
+
// prepared record: reload the authenticated durable prefix and recover
|
|
339
|
+
// observation only. A different unresolved action is an explicit typed
|
|
340
|
+
// block; there is no current action for this invocation to recover.
|
|
341
|
+
if (persistenceResult.disposition === "blocked") {
|
|
342
|
+
return blockedResult(normalized.base, persistenceResult.blockerCode, true, parent.state.mutationBoundary);
|
|
343
|
+
}
|
|
344
|
+
if (persistenceResult.disposition === "same_action_recovery") {
|
|
345
|
+
const converged = await reloadSteerAfterPersistenceFailure(normalized);
|
|
346
|
+
if (converged !== undefined)
|
|
347
|
+
return await reconcileExistingSteer(normalized, converged);
|
|
348
|
+
return steerFailureResult(normalized, "uncertain", "backend_unavailable", true);
|
|
349
|
+
}
|
|
350
|
+
if (isCancelled(normalized)) {
|
|
351
|
+
// The fence was acquired, but this invocation has not called the browser
|
|
352
|
+
// mutation port. Record a clean rejection so a retry returns the same
|
|
353
|
+
// durable outcome without attempting executePrepared.
|
|
354
|
+
return await persistOutcome(normalized, parent.state.mutationBoundary, {
|
|
355
|
+
outcome: "not_satisfied",
|
|
356
|
+
blockerCode: cancellationCode(normalized)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
let executionResult;
|
|
360
|
+
try {
|
|
361
|
+
executionResult = validateSteerPhaseResult(await executePort({ schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION, prepared, signal: normalized.signal, deadlineAt: normalized.deadlineAt }), "execute_prepared", normalized, prepared);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
executionResult = uncertainSteerPhase(normalized, "execute_prepared", prepared, cancellationCode(normalized) ?? "send_control_unavailable");
|
|
365
|
+
}
|
|
366
|
+
// A failed final recheck is a durable, non-mutating rejection. Persist its
|
|
367
|
+
// bounded blocker rather than invoking another browser phase. The complete
|
|
368
|
+
// baseline remains useful evidence that no mutation crossed this fence.
|
|
369
|
+
if (executionResult.status === "blocked" && executionResult.mutationBoundary === "none" && !executionResult.observationRequired) {
|
|
370
|
+
return await persistOutcome(normalized, parent.state.mutationBoundary, {
|
|
371
|
+
outcome: "not_satisfied",
|
|
372
|
+
blockerCode: executionResult.blockerCode,
|
|
373
|
+
...(executionResult.evidenceDigest === undefined ? {} : { evidenceDigest: executionResult.evidenceDigest })
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (executionResult.status === "blocked" && executionResult.mutationBoundary === "none" && executionResult.observationRequired) {
|
|
377
|
+
return await verifySteerAfterIntent(normalized, prepared, verifyPort, "target_evidence_unavailable");
|
|
378
|
+
}
|
|
379
|
+
// Executed, uncertain, and any provider-ambiguous result all converge via
|
|
380
|
+
// exactly one read-only verify. executePrepared is never retried here.
|
|
381
|
+
return await verifySteerAfterIntent(normalized, prepared, verifyPort, executionResult.status === "uncertain" ? executionResult.blockerCode : "send_control_unavailable");
|
|
382
|
+
}
|
|
383
|
+
function steerPersistenceFailureCode(error) {
|
|
384
|
+
if (error instanceof ControlInputError)
|
|
385
|
+
return error.code === "operation_state_corrupt" || error.code === "operation_request_mismatch" || error.code === "target_binding_mismatch"
|
|
386
|
+
? error.code
|
|
387
|
+
: undefined;
|
|
388
|
+
if (error instanceof Error) {
|
|
389
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
390
|
+
const code = descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
|
|
391
|
+
return code === "operation_state_corrupt" || code === "operation_request_mismatch" || code === "target_binding_mismatch"
|
|
392
|
+
? code
|
|
393
|
+
: undefined;
|
|
394
|
+
}
|
|
395
|
+
if (!isRecord(error))
|
|
396
|
+
return undefined;
|
|
397
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
398
|
+
const code = descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
|
|
399
|
+
return code === "operation_state_corrupt" || code === "operation_request_mismatch" || code === "target_binding_mismatch"
|
|
400
|
+
? code
|
|
401
|
+
: undefined;
|
|
402
|
+
}
|
|
403
|
+
async function reconcileExistingSteer(normalized, parent) {
|
|
404
|
+
if (parent.existingReceipt !== undefined)
|
|
405
|
+
return resultFromReceipt(normalized.base, parent.existingReceipt);
|
|
406
|
+
const recoverPort = normalized.ports.recoverSteer;
|
|
407
|
+
const durable = parent.existingSteerIntent;
|
|
408
|
+
if (recoverPort === undefined || durable === undefined) {
|
|
409
|
+
return blockedResult(normalized.base, "operation_state_corrupt", true, "control_may_have_occurred");
|
|
410
|
+
}
|
|
411
|
+
let prepared;
|
|
412
|
+
try {
|
|
413
|
+
prepared = validateSteerPrepared(durable, normalized, true);
|
|
414
|
+
validateSteerPreparedTarget(prepared, parent.state);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return blockedResult(normalized.base, error instanceof ControlInputError ? error.code : "operation_state_corrupt", true, "control_may_have_occurred");
|
|
418
|
+
}
|
|
419
|
+
let recovery;
|
|
420
|
+
try {
|
|
421
|
+
recovery = validateSteerPhaseResult(await recoverPort({
|
|
422
|
+
schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
|
|
423
|
+
prepared,
|
|
424
|
+
baseline: prepared.baseline,
|
|
425
|
+
signal: normalized.signal,
|
|
426
|
+
deadlineAt: normalized.deadlineAt
|
|
427
|
+
}), "recovery", normalized, prepared);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
recovery = uncertainSteerPhase(normalized, "recovery", prepared, cancellationCode(normalized) ?? "send_control_unavailable");
|
|
431
|
+
}
|
|
432
|
+
return await settleSteerObservation(normalized, recovery, "send_control_unavailable");
|
|
433
|
+
}
|
|
434
|
+
async function reloadSteerAfterPersistenceFailure(normalized) {
|
|
435
|
+
try {
|
|
436
|
+
const reloaded = await normalized.ports.readParent({
|
|
437
|
+
operationId: normalized.base.parentOperationId,
|
|
438
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
439
|
+
parentTargetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
440
|
+
controlActionId: normalized.base.controlActionId,
|
|
441
|
+
action: "steer",
|
|
442
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
443
|
+
requestDigest: normalized.base.requestDigest
|
|
444
|
+
});
|
|
445
|
+
validateParentSnapshot(normalized.request, normalized.requestDigest, reloaded);
|
|
446
|
+
if (reloaded.existingReceipt !== undefined || reloaded.existingSteerIntent !== undefined)
|
|
447
|
+
return reloaded;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// The original persistence result remains ambiguous; do not expose a
|
|
451
|
+
// provider/journal error or make a second mutation decision.
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
async function verifySteerAfterIntent(normalized, prepared, verifyPort, fallback) {
|
|
456
|
+
if (!canAttemptObservation(normalized)) {
|
|
457
|
+
return steerFailureResult(normalized, "uncertain", cancellationCode(normalized) ?? fallback, true);
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const verification = validateSteerPhaseResult(await verifyPort({ schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION, prepared, signal: normalized.signal, deadlineAt: normalized.deadlineAt }), "verify", normalized, prepared);
|
|
461
|
+
return await settleSteerObservation(normalized, verification, fallback);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return steerFailureResult(normalized, "uncertain", cancellationCode(normalized) ?? fallback, true);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function settleSteerObservation(normalized, observation, fallback) {
|
|
468
|
+
if (observation.status === "satisfied") {
|
|
469
|
+
return await persistOutcome(normalized, "control_may_have_occurred", {
|
|
470
|
+
outcome: "satisfied",
|
|
471
|
+
evidenceDigest: observation.receipt.evidenceDigest,
|
|
472
|
+
steerReceipt: observation.receipt
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (observation.status === "blocked") {
|
|
476
|
+
return steerFailureResult(normalized, "blocked", observation.blockerCode, observation.observationRequired, observation.evidenceDigest);
|
|
477
|
+
}
|
|
478
|
+
if (observation.status === "uncertain") {
|
|
479
|
+
return steerFailureResult(normalized, "uncertain", observation.blockerCode ?? cancellationCode(normalized) ?? fallback, true, observation.evidenceDigest);
|
|
480
|
+
}
|
|
481
|
+
return steerFailureResult(normalized, "uncertain", "operation_state_corrupt", true);
|
|
482
|
+
}
|
|
483
|
+
function steerFailureResult(normalized, kind, code, observationRequired, evidenceDigest) {
|
|
484
|
+
if (kind === "blocked")
|
|
485
|
+
return blockedResult(normalized.base, code, observationRequired, "control_may_have_occurred", evidenceDigest);
|
|
486
|
+
return {
|
|
487
|
+
...normalized.base,
|
|
488
|
+
kind: "uncertain",
|
|
489
|
+
blocker: blocker(code, true, "control_may_have_occurred", evidenceDigest)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function steerPhaseResultToControl(normalized, result, boundary) {
|
|
493
|
+
if (result.status === "blocked") {
|
|
494
|
+
return blockedResult(normalized.base, result.blockerCode, result.observationRequired, maxBoundary(boundary, result.mutationBoundary), result.evidenceDigest);
|
|
495
|
+
}
|
|
496
|
+
if (result.status === "uncertain") {
|
|
497
|
+
if (result.phase === "prepare") {
|
|
498
|
+
return blockedResult(normalized.base, result.blockerCode, result.observationRequired, boundary, result.evidenceDigest);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
...normalized.base,
|
|
502
|
+
kind: "uncertain",
|
|
503
|
+
blocker: blocker(result.blockerCode, true, "control_may_have_occurred", result.evidenceDigest)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
...normalized.base,
|
|
508
|
+
kind: "blocked",
|
|
509
|
+
blocker: blocker("operation_state_corrupt", true, boundary)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function maxBoundary(left, right) {
|
|
513
|
+
return BOUNDARY_RANK[left] >= BOUNDARY_RANK[right] ? left : right;
|
|
514
|
+
}
|
|
515
|
+
function uncertainSteerPhase(normalized, phase, prepared, blockerCode) {
|
|
516
|
+
return {
|
|
517
|
+
...steerPhaseBase(normalized, phase, prepared),
|
|
518
|
+
status: "uncertain",
|
|
519
|
+
blockerCode,
|
|
520
|
+
observationRequired: true,
|
|
521
|
+
mutationBoundary: "control_may_have_occurred",
|
|
522
|
+
quarantine: "caller"
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function steerPhaseBase(normalized, phase, prepared) {
|
|
526
|
+
return {
|
|
527
|
+
schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
|
|
528
|
+
phase,
|
|
529
|
+
parentOperationId: normalized.base.parentOperationId,
|
|
530
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
531
|
+
parentTargetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
532
|
+
controlActionId: normalized.base.controlActionId,
|
|
533
|
+
action: "steer",
|
|
534
|
+
requestDigest: normalized.base.requestDigest,
|
|
535
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
536
|
+
...(prepared === undefined ? {} : {
|
|
537
|
+
assistantBranchId: prepared.assistantBranchId,
|
|
538
|
+
assistantParentTurnId: prepared.assistantParentTurnId,
|
|
539
|
+
baselineSnapshotDigest: prepared.baselineSnapshotDigest,
|
|
540
|
+
preparedDigest: prepared.preparedDigest
|
|
541
|
+
})
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function validateSteerPersistenceResult(value) {
|
|
545
|
+
const cloned = clonePlainData(value);
|
|
546
|
+
if (!isRecord(cloned))
|
|
547
|
+
throw new ControlInputError("operation_state_corrupt", "Steer persistence result is invalid.");
|
|
548
|
+
if (cloned.schemaVersion !== CONTROL_COORDINATOR_SCHEMA_VERSION || typeof cloned.disposition !== "string") {
|
|
549
|
+
throw new ControlInputError("operation_state_corrupt", "Steer persistence result schema is invalid.");
|
|
550
|
+
}
|
|
551
|
+
if (cloned.disposition === "acquired" || cloned.disposition === "same_action_recovery") {
|
|
552
|
+
assertExactRecord(cloned, ["schemaVersion", "disposition"], ["schemaVersion", "disposition"]);
|
|
553
|
+
return deepFreeze(cloned);
|
|
554
|
+
}
|
|
555
|
+
if (cloned.disposition === "blocked") {
|
|
556
|
+
assertExactRecord(cloned, ["schemaVersion", "disposition", "blockerCode"], ["schemaVersion", "disposition", "blockerCode"]);
|
|
557
|
+
if (cloned.blockerCode !== "provider_concurrency_unsupported") {
|
|
558
|
+
throw new ControlInputError("operation_state_corrupt", "Steer persistence blocker is invalid.");
|
|
559
|
+
}
|
|
560
|
+
return deepFreeze(cloned);
|
|
561
|
+
}
|
|
562
|
+
throw new ControlInputError("operation_state_corrupt", "Steer persistence disposition is invalid.");
|
|
563
|
+
}
|
|
564
|
+
function validateSteerPhaseResult(value, phase, normalized, prepared) {
|
|
565
|
+
const cloned = clonePlainData(value);
|
|
566
|
+
if (!isRecord(cloned))
|
|
567
|
+
throw new ControlInputError("operation_state_corrupt", "Steer phase result is invalid.");
|
|
568
|
+
assertExactRecord(cloned, [
|
|
569
|
+
"schemaVersion", "phase", "parentOperationId", "parentRequestDigest", "parentTargetBindingDigest", "controlActionId",
|
|
570
|
+
"action", "requestDigest", "expectedAssistantTurnId", "assistantBranchId", "assistantParentTurnId",
|
|
571
|
+
"baselineSnapshotDigest", "preparedDigest", "status", "observationRequired", "mutationBoundary",
|
|
572
|
+
"prepared", "receipt", "blockerCode", "quarantine", "evidenceDigest"
|
|
573
|
+
], ["schemaVersion", "phase", "parentOperationId", "parentRequestDigest", "parentTargetBindingDigest", "controlActionId", "action", "requestDigest", "expectedAssistantTurnId", "status", "observationRequired", "mutationBoundary"]);
|
|
574
|
+
if (cloned.schemaVersion !== CONTROL_COORDINATOR_SCHEMA_VERSION
|
|
575
|
+
|| cloned.phase !== phase
|
|
576
|
+
|| cloned.parentOperationId !== normalized.base.parentOperationId
|
|
577
|
+
|| cloned.parentRequestDigest !== normalized.base.parentRequestDigest
|
|
578
|
+
|| cloned.parentTargetBindingDigest !== normalized.base.parentTargetBindingDigest
|
|
579
|
+
|| cloned.controlActionId !== normalized.base.controlActionId
|
|
580
|
+
|| cloned.action !== "steer"
|
|
581
|
+
|| cloned.requestDigest !== normalized.base.requestDigest
|
|
582
|
+
|| cloned.expectedAssistantTurnId !== normalized.base.expectedAssistantTurnId) {
|
|
583
|
+
throw new ControlInputError("operation_request_mismatch", "Steer phase identity does not match the control request.");
|
|
584
|
+
}
|
|
585
|
+
if (typeof cloned.observationRequired !== "boolean")
|
|
586
|
+
throw new ControlInputError("operation_state_corrupt", "Steer phase observation flag is invalid.");
|
|
587
|
+
if (cloned.mutationBoundary !== "none" && cloned.mutationBoundary !== "control_may_have_occurred")
|
|
588
|
+
throw new ControlInputError("operation_state_corrupt", "Steer phase mutation boundary is invalid.");
|
|
589
|
+
if (phase === "prepare" && cloned.mutationBoundary !== "none")
|
|
590
|
+
throw new ControlInputError("operation_state_corrupt", "Read-only steer preparation crossed a mutation boundary.");
|
|
591
|
+
if (cloned.assistantBranchId !== undefined)
|
|
592
|
+
assertIdentifier(cloned.assistantBranchId, "steer.assistantBranchId");
|
|
593
|
+
if (cloned.assistantParentTurnId !== undefined)
|
|
594
|
+
assertIdentifier(cloned.assistantParentTurnId, "steer.assistantParentTurnId");
|
|
595
|
+
if (cloned.baselineSnapshotDigest !== undefined)
|
|
596
|
+
assertDigest(cloned.baselineSnapshotDigest, "steer.baselineSnapshotDigest");
|
|
597
|
+
if (cloned.preparedDigest !== undefined)
|
|
598
|
+
assertDigest(cloned.preparedDigest, "steer.preparedDigest");
|
|
599
|
+
if (cloned.evidenceDigest !== undefined)
|
|
600
|
+
assertDigest(cloned.evidenceDigest, "steer.evidenceDigest");
|
|
601
|
+
if (cloned.status === "prepared") {
|
|
602
|
+
if (phase !== "prepare" || cloned.observationRequired !== false || cloned.mutationBoundary !== "none" || cloned.prepared === undefined)
|
|
603
|
+
throw new ControlInputError("operation_state_corrupt", "Steer preparation result branch is invalid.");
|
|
604
|
+
const checked = validateSteerPrepared(cloned.prepared, normalized, false);
|
|
605
|
+
if (checked.parentOperationId !== normalized.base.parentOperationId || checked.requestDigest !== normalized.base.requestDigest)
|
|
606
|
+
throw new ControlInputError("operation_request_mismatch", "Prepared steer identity does not match the request.");
|
|
607
|
+
const result = { ...cloned, prepared: checked };
|
|
608
|
+
return deepFreeze(result);
|
|
609
|
+
}
|
|
610
|
+
if (cloned.status === "executed") {
|
|
611
|
+
if (phase !== "execute_prepared" || cloned.observationRequired !== true || cloned.mutationBoundary !== "control_may_have_occurred" || cloned.prepared !== undefined || cloned.receipt !== undefined)
|
|
612
|
+
throw new ControlInputError("operation_state_corrupt", "Steer execution result branch is invalid.");
|
|
613
|
+
if (prepared === undefined)
|
|
614
|
+
throw new ControlInputError("operation_state_corrupt", "Steer execution result lacks its prepared identity.");
|
|
615
|
+
return deepFreeze(cloned);
|
|
616
|
+
}
|
|
617
|
+
if (cloned.status === "satisfied") {
|
|
618
|
+
if ((phase !== "verify" && phase !== "recovery") || cloned.observationRequired !== false || cloned.mutationBoundary !== "control_may_have_occurred" || cloned.receipt === undefined)
|
|
619
|
+
throw new ControlInputError("operation_state_corrupt", "Steer verification result branch is invalid.");
|
|
620
|
+
if (prepared === undefined)
|
|
621
|
+
throw new ControlInputError("operation_state_corrupt", "Steer verification result lacks its prepared identity.");
|
|
622
|
+
const receipt = validateSteerReceipt(cloned.receipt, prepared);
|
|
623
|
+
if (cloned.assistantBranchId !== prepared.assistantBranchId || cloned.assistantParentTurnId !== prepared.assistantParentTurnId || cloned.baselineSnapshotDigest !== prepared.baselineSnapshotDigest || cloned.preparedDigest !== prepared.preparedDigest)
|
|
624
|
+
throw new ControlInputError("operation_request_mismatch", "Steer verification identity does not match the prepared action.");
|
|
625
|
+
return deepFreeze({ ...cloned, receipt });
|
|
626
|
+
}
|
|
627
|
+
if (cloned.status === "blocked") {
|
|
628
|
+
if (cloned.blockerCode === undefined)
|
|
629
|
+
throw new ControlInputError("operation_state_corrupt", "Steer blocker is missing a code.");
|
|
630
|
+
assertBlockerCode(cloned.blockerCode);
|
|
631
|
+
if (cloned.quarantine !== undefined)
|
|
632
|
+
throw new ControlInputError("operation_state_corrupt", "Blocked steer result cannot carry a quarantine.");
|
|
633
|
+
return deepFreeze(cloned);
|
|
634
|
+
}
|
|
635
|
+
if (cloned.status === "uncertain") {
|
|
636
|
+
if (cloned.blockerCode === undefined || cloned.observationRequired !== true || cloned.mutationBoundary !== "control_may_have_occurred" || (cloned.quarantine !== "caller" && cloned.quarantine !== "provider"))
|
|
637
|
+
throw new ControlInputError("operation_state_corrupt", "Steer uncertainty branch is invalid.");
|
|
638
|
+
assertBlockerCode(cloned.blockerCode);
|
|
639
|
+
return deepFreeze(cloned);
|
|
640
|
+
}
|
|
641
|
+
throw new ControlInputError("operation_state_corrupt", "Steer phase result status is invalid.");
|
|
642
|
+
}
|
|
643
|
+
function validateSteerPrepared(value, normalized, durable) {
|
|
644
|
+
const cloned = clonePlainData(value);
|
|
645
|
+
if (!isRecord(cloned))
|
|
646
|
+
throw new ControlInputError("operation_state_corrupt", "Prepared steer value is invalid.");
|
|
647
|
+
assertExactRecord(cloned, ["schemaVersion", "parentOperationId", "parentRequestDigest", "parentTargetBindingDigest", "controlActionId", "action", "requestDigest", "expectedAssistantTurnId", "assistantBranchId", "assistantParentTurnId", "baselineSnapshotDigest", "preparedDigest", "baseline"], ["schemaVersion", "parentOperationId", "parentRequestDigest", "parentTargetBindingDigest", "controlActionId", "action", "requestDigest", "expectedAssistantTurnId", "assistantBranchId", "assistantParentTurnId", "baselineSnapshotDigest", "preparedDigest", "baseline"]);
|
|
648
|
+
if (cloned.schemaVersion !== CONTROL_COORDINATOR_SCHEMA_VERSION || cloned.action !== "steer")
|
|
649
|
+
throw new ControlInputError("operation_state_corrupt", "Prepared steer schema is invalid.");
|
|
650
|
+
if (cloned.parentOperationId !== normalized.base.parentOperationId || cloned.parentRequestDigest !== normalized.base.parentRequestDigest || cloned.parentTargetBindingDigest !== normalized.base.parentTargetBindingDigest || cloned.controlActionId !== normalized.base.controlActionId || cloned.requestDigest !== normalized.base.requestDigest || cloned.expectedAssistantTurnId !== normalized.base.expectedAssistantTurnId)
|
|
651
|
+
throw new ControlInputError("operation_request_mismatch", "Prepared steer identity does not match the request.");
|
|
652
|
+
assertUuid(cloned.parentOperationId, "prepared.parentOperationId");
|
|
653
|
+
assertDigest(cloned.parentRequestDigest, "prepared.parentRequestDigest");
|
|
654
|
+
assertDigest(cloned.parentTargetBindingDigest, "prepared.parentTargetBindingDigest");
|
|
655
|
+
assertUuid(cloned.controlActionId, "prepared.controlActionId");
|
|
656
|
+
assertDigest(cloned.requestDigest, "prepared.requestDigest");
|
|
657
|
+
if (cloned.requestDigest === cloned.parentRequestDigest)
|
|
658
|
+
throw new ControlInputError("operation_request_mismatch", "Steer child request digest must remain distinct from its parent digest.");
|
|
659
|
+
assertIdentifier(cloned.expectedAssistantTurnId, "prepared.expectedAssistantTurnId");
|
|
660
|
+
assertIdentifier(cloned.assistantBranchId, "prepared.assistantBranchId");
|
|
661
|
+
assertIdentifier(cloned.assistantParentTurnId, "prepared.assistantParentTurnId");
|
|
662
|
+
assertDigest(cloned.baselineSnapshotDigest, "prepared.baselineSnapshotDigest");
|
|
663
|
+
assertDigest(cloned.preparedDigest, "prepared.preparedDigest");
|
|
664
|
+
try {
|
|
665
|
+
assertOwnershipBaselineShape({
|
|
666
|
+
schemaVersion: OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION,
|
|
667
|
+
operationId: cloned.parentOperationId,
|
|
668
|
+
requestDigest: cloned.parentRequestDigest,
|
|
669
|
+
targetBindingDigest: cloned.parentTargetBindingDigest,
|
|
670
|
+
actionId: cloned.controlActionId,
|
|
671
|
+
baseline: cloned.baseline,
|
|
672
|
+
observedAt: BASELINE_EPOCH
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
throw new ControlInputError("operation_state_corrupt", "Prepared steer baseline is invalid.");
|
|
677
|
+
}
|
|
678
|
+
const baseline = cloned.baseline;
|
|
679
|
+
if (baseline.snapshotDigest !== cloned.baselineSnapshotDigest)
|
|
680
|
+
throw new ControlInputError("operation_state_corrupt", "Prepared steer baseline digest is inconsistent.");
|
|
681
|
+
if (baseline.target.canonicalThreadUrl.status !== "unavailable" || baseline.target.canonicalThreadUrl.reason !== "redacted")
|
|
682
|
+
throw new ControlInputError("operation_state_corrupt", "Prepared steer baseline is not redacted.");
|
|
683
|
+
const assistants = baseline.assistantTurns.filter(turn => turn.stableId === cloned.expectedAssistantTurnId);
|
|
684
|
+
if (assistants.length !== 1 || assistants[0].branchStableId !== cloned.assistantBranchId || assistants[0].parentStableId !== cloned.assistantParentTurnId)
|
|
685
|
+
throw new ControlInputError("target_binding_mismatch", "Prepared steer parent identity is not derived from the baseline.");
|
|
686
|
+
if (durable && baseline.completeness !== "complete")
|
|
687
|
+
throw new ControlInputError("operation_state_corrupt", "Durable steer baseline is incomplete.");
|
|
688
|
+
return deepFreeze({ ...cloned, baseline });
|
|
689
|
+
}
|
|
690
|
+
function validateSteerReceipt(value, prepared) {
|
|
691
|
+
const receipt = clonePlainData(value);
|
|
692
|
+
if (!isRecord(receipt))
|
|
693
|
+
throw new ControlInputError("operation_state_corrupt", "Steer verification receipt is invalid.");
|
|
694
|
+
assertExactRecord(receipt, ["schemaVersion", "baselineSnapshotDigest", "preparedDigest", "assistantTurnId", "assistantBranchId", "assistantParentTurnId", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest", "evidenceDigest"], ["schemaVersion", "baselineSnapshotDigest", "preparedDigest", "assistantTurnId", "assistantBranchId", "assistantParentTurnId", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest", "evidenceDigest"]);
|
|
695
|
+
if (receipt.schemaVersion !== CONTROL_COORDINATOR_SCHEMA_VERSION)
|
|
696
|
+
throw new ControlInputError("operation_state_corrupt", "Steer verification receipt schema is invalid.");
|
|
697
|
+
assertDigest(receipt.baselineSnapshotDigest, "steer.receipt.baselineSnapshotDigest");
|
|
698
|
+
assertDigest(receipt.preparedDigest, "steer.receipt.preparedDigest");
|
|
699
|
+
assertIdentifier(receipt.assistantTurnId, "steer.receipt.assistantTurnId");
|
|
700
|
+
assertIdentifier(receipt.assistantBranchId, "steer.receipt.assistantBranchId");
|
|
701
|
+
assertIdentifier(receipt.assistantParentTurnId, "steer.receipt.assistantParentTurnId");
|
|
702
|
+
assertIdentifier(receipt.userTurnId, "steer.receipt.userTurnId");
|
|
703
|
+
assertDigest(receipt.userTurnEvidenceDigest, "steer.receipt.userTurnEvidenceDigest");
|
|
704
|
+
assertDigest(receipt.postSendDeltaDigest, "steer.receipt.postSendDeltaDigest");
|
|
705
|
+
assertDigest(receipt.evidenceDigest, "steer.receipt.evidenceDigest");
|
|
706
|
+
if (receipt.baselineSnapshotDigest !== prepared.baselineSnapshotDigest || receipt.preparedDigest !== prepared.preparedDigest || receipt.assistantTurnId !== prepared.expectedAssistantTurnId || receipt.assistantBranchId !== prepared.assistantBranchId || receipt.assistantParentTurnId !== prepared.assistantParentTurnId)
|
|
707
|
+
throw new ControlInputError("target_binding_mismatch", "Steer receipt is not bound to the prepared parent.");
|
|
708
|
+
return deepFreeze(receipt);
|
|
709
|
+
}
|
|
710
|
+
function validateSteerPreparedTarget(prepared, state) {
|
|
711
|
+
const target = state.target;
|
|
712
|
+
if (target === undefined)
|
|
713
|
+
throw new ControlInputError("target_binding_mismatch", "Prepared steer has no durable target binding.");
|
|
714
|
+
const available = (value) => value.status === "available" ? value.value : undefined;
|
|
715
|
+
const baselineTarget = prepared.baseline.target;
|
|
716
|
+
if (available(baselineTarget.provider) !== target.providerId
|
|
717
|
+
|| available(baselineTarget.browser) !== target.browserId
|
|
718
|
+
|| available(baselineTarget.tab) !== target.tabId
|
|
719
|
+
|| baselineTarget.coordinationScope !== target.coordinationScope) {
|
|
720
|
+
throw new ControlInputError("target_binding_mismatch", "Prepared steer baseline target changed.");
|
|
721
|
+
}
|
|
722
|
+
if (target.conversationId !== undefined && available(baselineTarget.conversation) !== target.conversationId) {
|
|
723
|
+
throw new ControlInputError("target_binding_mismatch", "Prepared steer conversation target changed.");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function reconcileExisting(normalized, boundary) {
|
|
727
|
+
const observation = await observePostconditionUntilSettled(normalized);
|
|
728
|
+
return await settleObservation(normalized, boundary, observation, "send_control_unavailable");
|
|
729
|
+
}
|
|
730
|
+
async function settleAfterIntent(normalized, boundary, cancellation) {
|
|
731
|
+
if (canAttemptObservation(normalized)) {
|
|
732
|
+
const observation = await observePostconditionUntilSettled(normalized);
|
|
733
|
+
return await settleObservation(normalized, boundary, observation, cancellation);
|
|
734
|
+
}
|
|
735
|
+
return await persistOutcome(normalized, boundary, {
|
|
736
|
+
outcome: "uncertain",
|
|
737
|
+
blockerCode: cancellation
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
async function reconcileAfterIntent(normalized, boundary, execution) {
|
|
741
|
+
if (canAttemptObservation(normalized)) {
|
|
742
|
+
const observation = await observePostconditionUntilSettled(normalized);
|
|
743
|
+
return await settleObservation(normalized, boundary, observation, cancellationCode(normalized) ?? execution.blockerCode ?? "send_control_unavailable");
|
|
744
|
+
}
|
|
745
|
+
return await persistOutcome(normalized, boundary, {
|
|
746
|
+
outcome: "uncertain",
|
|
747
|
+
blockerCode: cancellationCode(normalized) ?? execution.blockerCode ?? "send_control_unavailable",
|
|
748
|
+
...(execution.evidenceDigest === undefined ? {} : { evidenceDigest: execution.evidenceDigest })
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
async function settleObservation(normalized, boundary, observation, fallback) {
|
|
752
|
+
try {
|
|
753
|
+
validatePostconditionObservation(observation, normalized.base.expectedAssistantTurnId);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
return await persistOutcome(normalized, boundary, {
|
|
757
|
+
outcome: "uncertain",
|
|
758
|
+
blockerCode: cancellationCode(normalized) ?? "operation_state_corrupt"
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (observation.status === "satisfied") {
|
|
762
|
+
return await persistOutcome(normalized, boundary, {
|
|
763
|
+
outcome: "satisfied",
|
|
764
|
+
evidenceDigest: observation.evidenceDigest
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (observation.status === "not_satisfied") {
|
|
768
|
+
return await persistOutcome(normalized, boundary, {
|
|
769
|
+
outcome: "not_satisfied",
|
|
770
|
+
blockerCode: observation.blockerCode,
|
|
771
|
+
...(observation.evidenceDigest === undefined ? {} : { evidenceDigest: observation.evidenceDigest })
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return await persistOutcome(normalized, boundary, {
|
|
775
|
+
outcome: "uncertain",
|
|
776
|
+
blockerCode: observation.blockerCode ?? cancellationCode(normalized) ?? fallback,
|
|
777
|
+
...(observation.evidenceDigest === undefined ? {} : { evidenceDigest: observation.evidenceDigest })
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async function persistOutcome(normalized, boundary, outcome) {
|
|
781
|
+
const receipt = makeReceipt(normalized, outcome);
|
|
782
|
+
try {
|
|
783
|
+
validateReceipt(receipt, normalized.base);
|
|
784
|
+
await normalized.ports.persistReceipt({
|
|
785
|
+
receipt,
|
|
786
|
+
...(outcome.steerReceipt === undefined ? {} : { steerReceipt: outcome.steerReceipt })
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
return {
|
|
791
|
+
...normalized.base,
|
|
792
|
+
kind: "uncertain",
|
|
793
|
+
blocker: blocker("backend_unavailable", true, "control_may_have_occurred", outcome.evidenceDigest),
|
|
794
|
+
...(outcome.steerReceipt === undefined ? {} : { steerReceipt: outcome.steerReceipt })
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
// The intent has crossed the control fencing point before this helper is
|
|
798
|
+
// reached. Report the stronger boundary even if the reloaded snapshot still
|
|
799
|
+
// reflected the pre-intent Send boundary.
|
|
800
|
+
return resultFromReceipt(normalized.base, receipt, "control_may_have_occurred", outcome.steerReceipt);
|
|
801
|
+
}
|
|
802
|
+
function makeReceipt(normalized, outcome) {
|
|
803
|
+
const receipt = {
|
|
804
|
+
schemaVersion: OPERATION_CONTROL_RECEIPT_SCHEMA_VERSION,
|
|
805
|
+
controlActionId: normalized.base.controlActionId,
|
|
806
|
+
parentOperationId: normalized.base.parentOperationId,
|
|
807
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
808
|
+
parentTargetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
809
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
810
|
+
requestDigest: normalized.base.requestDigest,
|
|
811
|
+
action: normalized.base.action,
|
|
812
|
+
outcome: outcome.outcome,
|
|
813
|
+
...(outcome.evidenceDigest === undefined ? {} : { evidenceDigest: outcome.evidenceDigest }),
|
|
814
|
+
...(outcome.blockerCode === undefined ? {} : { blockerCode: outcome.blockerCode }),
|
|
815
|
+
observedAt: new Date(normalized.now()).toISOString()
|
|
816
|
+
};
|
|
817
|
+
return receipt;
|
|
818
|
+
}
|
|
819
|
+
function resultFromReceipt(base, receipt, boundary = "control_may_have_occurred", steerReceipt) {
|
|
820
|
+
if (receipt.outcome === "satisfied") {
|
|
821
|
+
return { ...base, kind: "completed", receipt, ...(steerReceipt === undefined ? {} : { steerReceipt }) };
|
|
822
|
+
}
|
|
823
|
+
if (receipt.outcome === "not_satisfied") {
|
|
824
|
+
return {
|
|
825
|
+
...base,
|
|
826
|
+
kind: "blocked",
|
|
827
|
+
blocker: blocker(receipt.blockerCode ?? "send_control_unavailable", false, boundary, receipt.evidenceDigest),
|
|
828
|
+
receipt,
|
|
829
|
+
...(steerReceipt === undefined ? {} : { steerReceipt })
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
...base,
|
|
834
|
+
kind: "uncertain",
|
|
835
|
+
blocker: blocker(receipt.blockerCode ?? "send_control_unavailable", true, "control_may_have_occurred", receipt.evidenceDigest),
|
|
836
|
+
receipt,
|
|
837
|
+
...(steerReceipt === undefined ? {} : { steerReceipt })
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async function observeTurnSafely(normalized) {
|
|
841
|
+
if (isCancelled(normalized)) {
|
|
842
|
+
return { kind: "blocked", code: cancellationCode(normalized), observationRequired: false };
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const observation = await normalized.ports.observeTurn({
|
|
846
|
+
operationId: normalized.base.parentOperationId,
|
|
847
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
848
|
+
targetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
849
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
850
|
+
signal: normalized.signal,
|
|
851
|
+
deadlineAt: normalized.deadlineAt
|
|
852
|
+
});
|
|
853
|
+
validateTurnObservation(observation);
|
|
854
|
+
if (observation.status === "generating" && observation.assistantTurnId === normalized.base.expectedAssistantTurnId) {
|
|
855
|
+
return { kind: "exact", evidenceDigest: observation.evidenceDigest };
|
|
856
|
+
}
|
|
857
|
+
if (observation.status === "mismatch" || observation.assistantTurnId !== undefined && observation.assistantTurnId !== normalized.base.expectedAssistantTurnId) {
|
|
858
|
+
return { kind: "blocked", code: "target_binding_mismatch", observationRequired: false, ...(observation.evidenceDigest === undefined ? {} : { evidenceDigest: observation.evidenceDigest }) };
|
|
859
|
+
}
|
|
860
|
+
if (observation.status === "terminal" || observation.status === "not_found") {
|
|
861
|
+
return { kind: "blocked", code: "send_control_unavailable", observationRequired: false, ...(observation.evidenceDigest === undefined ? {} : { evidenceDigest: observation.evidenceDigest }) };
|
|
862
|
+
}
|
|
863
|
+
return { kind: "blocked", code: "turn_ownership_ambiguous", observationRequired: true, ...(observation.evidenceDigest === undefined ? {} : { evidenceDigest: observation.evidenceDigest }) };
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return {
|
|
867
|
+
kind: "blocked",
|
|
868
|
+
code: cancellationCode(normalized) ?? "operation_state_corrupt",
|
|
869
|
+
observationRequired: true
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function observePostconditionSafely(normalized) {
|
|
874
|
+
try {
|
|
875
|
+
const observation = await normalized.ports.observePostcondition({
|
|
876
|
+
operationId: normalized.base.parentOperationId,
|
|
877
|
+
parentRequestDigest: normalized.base.parentRequestDigest,
|
|
878
|
+
targetBindingDigest: normalized.base.parentTargetBindingDigest,
|
|
879
|
+
action: normalized.base.action,
|
|
880
|
+
controlActionId: normalized.base.controlActionId,
|
|
881
|
+
expectedAssistantTurnId: normalized.base.expectedAssistantTurnId,
|
|
882
|
+
requestDigest: normalized.base.requestDigest,
|
|
883
|
+
signal: normalized.signal,
|
|
884
|
+
deadlineAt: normalized.deadlineAt
|
|
885
|
+
});
|
|
886
|
+
validatePostconditionObservation(observation, normalized.base.expectedAssistantTurnId);
|
|
887
|
+
return observation;
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
return {
|
|
891
|
+
status: "uncertain",
|
|
892
|
+
blockerCode: cancellationCode(normalized)
|
|
893
|
+
?? (error instanceof ControlInputError ? error.code : "operation_state_corrupt")
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function observePostconditionUntilSettled(normalized) {
|
|
898
|
+
let observation = await observePostconditionSafely(normalized);
|
|
899
|
+
const policy = normalized.ports.postconditionRetry;
|
|
900
|
+
if (policy === undefined)
|
|
901
|
+
return observation;
|
|
902
|
+
for (let attempt = 1; attempt < policy.maxAttempts && retryablePostcondition(observation); attempt += 1) {
|
|
903
|
+
if (!await waitForPostconditionRetry(normalized, policy.intervalMs)) {
|
|
904
|
+
const code = cancellationCode(normalized);
|
|
905
|
+
return code === undefined
|
|
906
|
+
? observation
|
|
907
|
+
: {
|
|
908
|
+
status: "uncertain",
|
|
909
|
+
blockerCode: code,
|
|
910
|
+
...(observation.evidenceDigest === undefined ? {} : { evidenceDigest: observation.evidenceDigest })
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
observation = await observePostconditionSafely(normalized);
|
|
914
|
+
}
|
|
915
|
+
return observation;
|
|
916
|
+
}
|
|
917
|
+
function retryablePostcondition(observation) {
|
|
918
|
+
return observation.status !== "satisfied"
|
|
919
|
+
&& (observation.blockerCode === "send_control_unavailable"
|
|
920
|
+
|| observation.blockerCode === "target_evidence_unavailable");
|
|
921
|
+
}
|
|
922
|
+
async function waitForPostconditionRetry(normalized, milliseconds) {
|
|
923
|
+
if (normalized.signal.aborted)
|
|
924
|
+
return false;
|
|
925
|
+
let remaining;
|
|
926
|
+
try {
|
|
927
|
+
remaining = normalized.deadlineAt - normalized.now();
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
if (remaining <= 0)
|
|
933
|
+
return false;
|
|
934
|
+
const delay = Math.min(milliseconds, remaining);
|
|
935
|
+
if (delay === 0)
|
|
936
|
+
return true;
|
|
937
|
+
return await new Promise(resolve => {
|
|
938
|
+
const timer = setTimeout(() => {
|
|
939
|
+
normalized.signal.removeEventListener("abort", onAbort);
|
|
940
|
+
resolve(true);
|
|
941
|
+
}, delay);
|
|
942
|
+
const onAbort = () => {
|
|
943
|
+
clearTimeout(timer);
|
|
944
|
+
normalized.signal.removeEventListener("abort", onAbort);
|
|
945
|
+
resolve(false);
|
|
946
|
+
};
|
|
947
|
+
normalized.signal.addEventListener("abort", onAbort, { once: true });
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function normalizeInput(request, requestDigest, ports, options) {
|
|
951
|
+
validateRequest(request);
|
|
952
|
+
assertDigest(requestDigest, "requestDigest");
|
|
953
|
+
if (hasAccessorInPlainData(ports) || !isRecord(ports) || typeof ports.readParent !== "function" || typeof ports.persistReceipt !== "function") {
|
|
954
|
+
throw new ControlInputError("operation_state_corrupt", "Control ports are incomplete.");
|
|
955
|
+
}
|
|
956
|
+
if (request.action === "stop" && (typeof ports.observeTurn !== "function" || typeof ports.persistActionIntent !== "function" || typeof ports.executeOnce !== "function" || typeof ports.observePostcondition !== "function")) {
|
|
957
|
+
throw new ControlInputError("operation_state_corrupt", "Stop control ports are incomplete.");
|
|
958
|
+
}
|
|
959
|
+
if (ports.postconditionRetry !== undefined) {
|
|
960
|
+
const policy = ports.postconditionRetry;
|
|
961
|
+
if (!isRecord(policy)
|
|
962
|
+
|| !Number.isSafeInteger(policy.maxAttempts)
|
|
963
|
+
|| policy.maxAttempts < 1
|
|
964
|
+
|| policy.maxAttempts > MAX_POSTCONDITION_RETRY_ATTEMPTS
|
|
965
|
+
|| !Number.isSafeInteger(policy.intervalMs)
|
|
966
|
+
|| policy.intervalMs < 0
|
|
967
|
+
|| policy.intervalMs > MAX_POSTCONDITION_RETRY_INTERVAL_MS
|
|
968
|
+
|| (policy.maxAttempts - 1) * policy.intervalMs > MAX_POSTCONDITION_RETRY_WINDOW_MS) {
|
|
969
|
+
throw new ControlInputError("operation_state_corrupt", "Control postcondition retry policy is invalid.");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (request.action === "steer" && (typeof ports.prepareSteer !== "function" || typeof ports.persistSteerIntentAndBaseline !== "function" || typeof ports.executeSteerPrepared !== "function" || typeof ports.verifySteer !== "function" || typeof ports.recoverSteer !== "function")) {
|
|
973
|
+
throw new ControlInputError("operation_state_corrupt", "Work-steer phase ports are incomplete.");
|
|
974
|
+
}
|
|
975
|
+
if (!isRecord(options))
|
|
976
|
+
throw new ControlInputError("operation_state_corrupt", "Control options are invalid.");
|
|
977
|
+
assertExactRecord(options, ["signal", "deadlineAt", "now"], []);
|
|
978
|
+
if (options.signal !== undefined && !isAbortSignalLike(options.signal)) {
|
|
979
|
+
throw new ControlInputError("operation_state_corrupt", "Cancellation signal is invalid.");
|
|
980
|
+
}
|
|
981
|
+
if (options.deadlineAt !== undefined && (!Number.isSafeInteger(options.deadlineAt) || options.deadlineAt < 0 || options.deadlineAt > MAX_DEADLINE_AT)) {
|
|
982
|
+
throw new ControlInputError("operation_state_corrupt", "Absolute deadline is invalid.");
|
|
983
|
+
}
|
|
984
|
+
if (options.now !== undefined && typeof options.now !== "function") {
|
|
985
|
+
throw new ControlInputError("operation_state_corrupt", "Clock is invalid.");
|
|
986
|
+
}
|
|
987
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
988
|
+
const now = options.now ?? Date.now;
|
|
989
|
+
const initialNow = checkedNow(now);
|
|
990
|
+
const requestTimeout = request.timeoutMs ?? 30_000;
|
|
991
|
+
if (!Number.isSafeInteger(requestTimeout) || requestTimeout < 0 || requestTimeout > MAX_TIMEOUT_MS) {
|
|
992
|
+
throw new ControlInputError("operation_timeout", "Control timeout is invalid.");
|
|
993
|
+
}
|
|
994
|
+
const requestDeadline = initialNow + requestTimeout;
|
|
995
|
+
const deadlineAt = options.deadlineAt === undefined ? requestDeadline : Math.min(options.deadlineAt, requestDeadline);
|
|
996
|
+
if (!Number.isSafeInteger(deadlineAt) || deadlineAt < 0 || deadlineAt > MAX_DEADLINE_AT) {
|
|
997
|
+
throw new ControlInputError("operation_timeout", "Control deadline is invalid.");
|
|
998
|
+
}
|
|
999
|
+
const base = {
|
|
1000
|
+
controlActionId: request.controlActionId,
|
|
1001
|
+
parentOperationId: request.parent.operationId,
|
|
1002
|
+
parentRequestDigest: request.parent.requestDigest,
|
|
1003
|
+
parentTargetBindingDigest: request.parent.targetBindingDigest,
|
|
1004
|
+
requestDigest,
|
|
1005
|
+
action: request.action,
|
|
1006
|
+
expectedAssistantTurnId: request.expectedAssistantTurnId
|
|
1007
|
+
};
|
|
1008
|
+
let lastNow = initialNow;
|
|
1009
|
+
const monotonicNow = () => {
|
|
1010
|
+
const value = checkedNow(now);
|
|
1011
|
+
if (value < lastNow)
|
|
1012
|
+
throw new ControlInputError("operation_state_corrupt", "Clock moved backwards.");
|
|
1013
|
+
lastNow = value;
|
|
1014
|
+
return value;
|
|
1015
|
+
};
|
|
1016
|
+
return { request, requestDigest, signal, deadlineAt, now: monotonicNow, base, ports };
|
|
1017
|
+
}
|
|
1018
|
+
function validateRequest(request) {
|
|
1019
|
+
assertExactRecord(request, ["schemaVersion", "controlActionId", "parent", "action", "expectedAssistantTurnId", "steerPrompt", "timeoutMs"], ["schemaVersion", "controlActionId", "parent", "action", "expectedAssistantTurnId"]);
|
|
1020
|
+
if (request.schemaVersion !== OPERATION_CONTROL_REQUEST_SCHEMA_VERSION)
|
|
1021
|
+
throw new ControlInputError("operation_state_corrupt", "Unsupported control request schema.");
|
|
1022
|
+
assertUuid(request.controlActionId, "controlActionId");
|
|
1023
|
+
validateHandle(request.parent);
|
|
1024
|
+
if (request.parent.phase !== "generating" || request.parent.targetBindingDigest === undefined)
|
|
1025
|
+
throw new ControlInputError("target_binding_mismatch", "Control requires an exact generating parent target.");
|
|
1026
|
+
if (request.action !== "stop" && request.action !== "steer")
|
|
1027
|
+
throw new ControlInputError("operation_state_corrupt", "Control action is invalid.");
|
|
1028
|
+
if (request.action === "steer" && (typeof request.steerPrompt !== "string" || request.steerPrompt.length === 0))
|
|
1029
|
+
throw new ControlInputError("operation_state_corrupt", "Steer requires an in-memory prompt.");
|
|
1030
|
+
if (request.action === "stop" && request.steerPrompt !== undefined)
|
|
1031
|
+
throw new ControlInputError("operation_state_corrupt", "Stop must not carry steer text.");
|
|
1032
|
+
if (request.steerPrompt !== undefined && Buffer.byteLength(request.steerPrompt, "utf8") > MAX_STEER_PROMPT_BYTES)
|
|
1033
|
+
throw new ControlInputError("operation_state_corrupt", "Steer prompt exceeds the bounded limit.");
|
|
1034
|
+
if (request.expectedAssistantTurnId.length > 512 || !ID_PATTERN.test(request.expectedAssistantTurnId))
|
|
1035
|
+
throw new ControlInputError("target_binding_mismatch", "Assistant turn identity is invalid.");
|
|
1036
|
+
if (request.timeoutMs !== undefined && (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs < 0 || request.timeoutMs > MAX_TIMEOUT_MS))
|
|
1037
|
+
throw new ControlInputError("operation_timeout", "Control timeout is invalid.");
|
|
1038
|
+
}
|
|
1039
|
+
function validateParentSnapshot(request, requestDigest, snapshot) {
|
|
1040
|
+
if (!isRecord(snapshot))
|
|
1041
|
+
throw new ControlInputError("operation_state_corrupt", "Parent snapshot is not an object.");
|
|
1042
|
+
assertExactRecord(snapshot, ["state", "handle", "existingReceipt", "existingSteerIntent"], ["state", "handle"]);
|
|
1043
|
+
assertOperationStateShape(snapshot.state);
|
|
1044
|
+
validateHandle(snapshot.handle);
|
|
1045
|
+
const state = snapshot.state;
|
|
1046
|
+
const parent = request.parent;
|
|
1047
|
+
if (state.schemaVersion !== OPERATION_SCHEMA_VERSION || state.operationId !== parent.operationId || state.requestDigest !== parent.requestDigest || state.surface !== parent.surface)
|
|
1048
|
+
throw new ControlInputError("operation_request_mismatch", "Parent durable identity does not match the request.");
|
|
1049
|
+
if (snapshot.handle.operationId !== state.operationId || snapshot.handle.requestDigest !== state.requestDigest || snapshot.handle.surface !== state.surface || snapshot.handle.revision !== state.revision || snapshot.handle.phase !== state.phase || snapshot.handle.mutationBoundary !== state.mutationBoundary)
|
|
1050
|
+
throw new ControlInputError("operation_state_corrupt", "Parent handle is not derived from durable state.");
|
|
1051
|
+
if (state.revision < parent.revision)
|
|
1052
|
+
throw new ControlInputError("operation_state_corrupt", "Durable parent is older than the supplied handle.");
|
|
1053
|
+
if (snapshot.handle.targetBindingDigest !== parent.targetBindingDigest || snapshot.handle.targetBindingDigest === undefined)
|
|
1054
|
+
throw new ControlInputError("target_binding_mismatch", "Parent target binding changed.");
|
|
1055
|
+
const existingAction = state.actions[request.controlActionId];
|
|
1056
|
+
const isPersistedSteer = request.action === "steer" && existingAction?.kind === "work_steer";
|
|
1057
|
+
// A parent can legitimately become terminal after a steer. Only that exact
|
|
1058
|
+
// already-fenced steer action may use the terminal snapshot for recovery;
|
|
1059
|
+
// a fresh action still requires a generating parent.
|
|
1060
|
+
if (state.phase !== "generating" && !isPersistedSteer)
|
|
1061
|
+
throw new ControlInputError("send_control_unavailable", "Control requires a durably generating parent operation.");
|
|
1062
|
+
if (request.action === "steer" && state.surface !== "work")
|
|
1063
|
+
throw new ControlInputError("operation_request_mismatch", "Steer is available only on Work operations.");
|
|
1064
|
+
if (state.mutationBoundary !== "send_may_have_occurred" && state.mutationBoundary !== "control_may_have_occurred")
|
|
1065
|
+
throw new ControlInputError("operation_state_corrupt", "Control parent lacks a submitted mutation boundary.");
|
|
1066
|
+
const controls = Object.values(state.actions).filter(action => action.kind === "stop" || action.kind === "work_steer");
|
|
1067
|
+
const controlIds = new Set(controls.map(action => action.actionId));
|
|
1068
|
+
if (controlIds.size !== controls.length)
|
|
1069
|
+
throw new ControlInputError("operation_state_corrupt", "Parent contains duplicate control action identities.");
|
|
1070
|
+
if (state.mutationBoundary === "control_may_have_occurred" && controls.length === 0)
|
|
1071
|
+
throw new ControlInputError("operation_state_corrupt", "Control boundary has no causal action.");
|
|
1072
|
+
if (snapshot.existingReceipt !== undefined && hasAccessorInPlainData(snapshot.existingReceipt))
|
|
1073
|
+
throw new ControlInputError("operation_state_corrupt", "Control receipt contains unsafe data.");
|
|
1074
|
+
if (snapshot.existingReceipt !== undefined)
|
|
1075
|
+
validateReceipt(snapshot.existingReceipt, {
|
|
1076
|
+
controlActionId: request.controlActionId,
|
|
1077
|
+
parentOperationId: request.parent.operationId,
|
|
1078
|
+
parentRequestDigest: request.parent.requestDigest,
|
|
1079
|
+
parentTargetBindingDigest: request.parent.targetBindingDigest,
|
|
1080
|
+
requestDigest,
|
|
1081
|
+
action: request.action,
|
|
1082
|
+
expectedAssistantTurnId: request.expectedAssistantTurnId
|
|
1083
|
+
});
|
|
1084
|
+
if (snapshot.existingReceipt !== undefined && !controls.some(action => action.actionId === request.controlActionId))
|
|
1085
|
+
throw new ControlInputError("operation_state_corrupt", "Control receipt has no matching durable intent.");
|
|
1086
|
+
if (snapshot.existingSteerIntent !== undefined) {
|
|
1087
|
+
if (request.action !== "steer" || existingAction?.kind !== "work_steer")
|
|
1088
|
+
throw new ControlInputError("operation_state_corrupt", "Steer baseline is attached to a non-steer action.");
|
|
1089
|
+
// The full identity/baseline is checked again before recover. This early
|
|
1090
|
+
// graph check prevents accessors/proxies from reaching an adapter port.
|
|
1091
|
+
if (hasAccessorInPlainData(snapshot.existingSteerIntent))
|
|
1092
|
+
throw new ControlInputError("operation_state_corrupt", "Steer baseline contains unsafe data.");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function validateHandle(handle) {
|
|
1096
|
+
assertExactRecord(handle, ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary", "targetBindingDigest"], ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary"]);
|
|
1097
|
+
if (handle.schemaVersion !== OPERATION_HANDLE_SCHEMA_VERSION)
|
|
1098
|
+
throw new ControlInputError("operation_state_corrupt", "Unsupported operation handle schema.");
|
|
1099
|
+
assertUuid(handle.operationId, "handle.operationId");
|
|
1100
|
+
assertDigest(handle.requestDigest, "handle.requestDigest");
|
|
1101
|
+
if (handle.surface !== "chat" && handle.surface !== "work")
|
|
1102
|
+
throw new ControlInputError("operation_state_corrupt", "Handle surface is invalid.");
|
|
1103
|
+
if (!Number.isSafeInteger(handle.revision) || handle.revision < 1)
|
|
1104
|
+
throw new ControlInputError("operation_state_corrupt", "Handle revision is invalid.");
|
|
1105
|
+
if (!(handle.phase in { prepared: 1, handoff_pending: 1, ready: 1, send_pending: 1, submitted: 1, generating: 1, capturing: 1, completed: 1, uncertain: 1 }))
|
|
1106
|
+
throw new ControlInputError("operation_state_corrupt", "Handle phase is invalid.");
|
|
1107
|
+
if (!(handle.mutationBoundary in BOUNDARY_RANK))
|
|
1108
|
+
throw new ControlInputError("operation_state_corrupt", "Handle mutation boundary is invalid.");
|
|
1109
|
+
if (handle.targetBindingDigest !== undefined)
|
|
1110
|
+
assertDigest(handle.targetBindingDigest, "handle.targetBindingDigest");
|
|
1111
|
+
}
|
|
1112
|
+
function validateTurnObservation(value) {
|
|
1113
|
+
if (!isRecord(value) || typeof value.status !== "string")
|
|
1114
|
+
throw new ControlInputError("operation_state_corrupt", "Turn observation is invalid.");
|
|
1115
|
+
if (value.status === "generating") {
|
|
1116
|
+
assertExactRecord(value, ["status", "assistantTurnId", "evidenceDigest"], ["status", "assistantTurnId", "evidenceDigest"]);
|
|
1117
|
+
assertIdentifier(value.assistantTurnId, "assistantTurnId");
|
|
1118
|
+
assertDigest(value.evidenceDigest, "turn.evidenceDigest");
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (value.status !== "terminal" && value.status !== "not_found" && value.status !== "mismatch" && value.status !== "uncertain")
|
|
1122
|
+
throw new ControlInputError("operation_state_corrupt", "Turn observation status is invalid.");
|
|
1123
|
+
assertExactRecord(value, ["status", "assistantTurnId", "evidenceDigest", "reason"], ["status"]);
|
|
1124
|
+
if (value.assistantTurnId !== undefined)
|
|
1125
|
+
assertIdentifier(value.assistantTurnId, "assistantTurnId");
|
|
1126
|
+
if (value.evidenceDigest !== undefined)
|
|
1127
|
+
assertDigest(value.evidenceDigest, "turn.evidenceDigest");
|
|
1128
|
+
if (value.reason !== undefined && !["different_turn", "not_generating", "target_mismatch", "unavailable"].includes(value.reason))
|
|
1129
|
+
throw new ControlInputError("operation_state_corrupt", "Turn observation reason is invalid.");
|
|
1130
|
+
}
|
|
1131
|
+
function validateExecutionResult(value, expectedAssistantTurnId) {
|
|
1132
|
+
validatePostconditionObservation(value, expectedAssistantTurnId);
|
|
1133
|
+
}
|
|
1134
|
+
function validatePostconditionObservation(value, expectedAssistantTurnId) {
|
|
1135
|
+
if (!isRecord(value) || typeof value.status !== "string")
|
|
1136
|
+
throw new ControlInputError("operation_state_corrupt", "Control observation is invalid.");
|
|
1137
|
+
if (value.status === "satisfied") {
|
|
1138
|
+
assertExactRecord(value, ["status", "assistantTurnId", "evidenceDigest"], ["status", "assistantTurnId", "evidenceDigest"]);
|
|
1139
|
+
assertIdentifier(value.assistantTurnId, "assistantTurnId");
|
|
1140
|
+
if (value.assistantTurnId !== expectedAssistantTurnId)
|
|
1141
|
+
throw new ControlInputError("target_binding_mismatch", "Control evidence belongs to another assistant turn.");
|
|
1142
|
+
assertDigest(value.evidenceDigest, "control.evidenceDigest");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (value.status === "not_satisfied") {
|
|
1146
|
+
assertExactRecord(value, ["status", "assistantTurnId", "blockerCode", "evidenceDigest"], ["status", "blockerCode"]);
|
|
1147
|
+
assertBlockerCode(value.blockerCode);
|
|
1148
|
+
if (value.assistantTurnId !== undefined) {
|
|
1149
|
+
assertIdentifier(value.assistantTurnId, "assistantTurnId");
|
|
1150
|
+
if (value.assistantTurnId !== expectedAssistantTurnId)
|
|
1151
|
+
throw new ControlInputError("target_binding_mismatch", "Control blocker belongs to another assistant turn.");
|
|
1152
|
+
}
|
|
1153
|
+
if (value.evidenceDigest !== undefined)
|
|
1154
|
+
assertDigest(value.evidenceDigest, "control.evidenceDigest");
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (value.status !== "uncertain")
|
|
1158
|
+
throw new ControlInputError("operation_state_corrupt", "Control result status is invalid.");
|
|
1159
|
+
assertExactRecord(value, ["status", "assistantTurnId", "blockerCode", "evidenceDigest"], ["status"]);
|
|
1160
|
+
if (value.assistantTurnId !== undefined) {
|
|
1161
|
+
assertIdentifier(value.assistantTurnId, "assistantTurnId");
|
|
1162
|
+
if (value.assistantTurnId !== expectedAssistantTurnId)
|
|
1163
|
+
throw new ControlInputError("target_binding_mismatch", "Control uncertainty belongs to another assistant turn.");
|
|
1164
|
+
}
|
|
1165
|
+
if (value.blockerCode !== undefined)
|
|
1166
|
+
assertBlockerCode(value.blockerCode);
|
|
1167
|
+
if (value.evidenceDigest !== undefined)
|
|
1168
|
+
assertDigest(value.evidenceDigest, "control.evidenceDigest");
|
|
1169
|
+
}
|
|
1170
|
+
function validateReceipt(receipt, base) {
|
|
1171
|
+
assertExactRecord(receipt, ["schemaVersion", "controlActionId", "parentOperationId", "parentRequestDigest", "parentTargetBindingDigest", "expectedAssistantTurnId", "requestDigest", "action", "outcome", "evidenceDigest", "blockerCode", "observedAt"], ["schemaVersion", "controlActionId", "parentOperationId", "parentRequestDigest", "parentTargetBindingDigest", "expectedAssistantTurnId", "requestDigest", "action", "outcome", "observedAt"]);
|
|
1172
|
+
if (receipt.schemaVersion !== OPERATION_CONTROL_RECEIPT_SCHEMA_VERSION || receipt.controlActionId !== base.controlActionId || receipt.parentOperationId !== base.parentOperationId || receipt.parentRequestDigest !== base.parentRequestDigest || receipt.parentTargetBindingDigest !== base.parentTargetBindingDigest || receipt.expectedAssistantTurnId !== base.expectedAssistantTurnId || receipt.requestDigest !== base.requestDigest || receipt.action !== base.action)
|
|
1173
|
+
throw new ControlInputError("operation_request_mismatch", "Control receipt identity does not match the request.");
|
|
1174
|
+
assertUuid(receipt.controlActionId, "receipt.controlActionId");
|
|
1175
|
+
assertUuid(receipt.parentOperationId, "receipt.parentOperationId");
|
|
1176
|
+
assertDigest(receipt.parentRequestDigest, "receipt.parentRequestDigest");
|
|
1177
|
+
assertDigest(receipt.parentTargetBindingDigest, "receipt.parentTargetBindingDigest");
|
|
1178
|
+
assertDigest(receipt.requestDigest, "receipt.requestDigest");
|
|
1179
|
+
assertIdentifier(receipt.expectedAssistantTurnId, "receipt.expectedAssistantTurnId");
|
|
1180
|
+
if (receipt.action !== "stop" && receipt.action !== "steer")
|
|
1181
|
+
throw new ControlInputError("operation_state_corrupt", "Receipt action is invalid.");
|
|
1182
|
+
if (receipt.outcome !== "satisfied" && receipt.outcome !== "not_satisfied" && receipt.outcome !== "uncertain")
|
|
1183
|
+
throw new ControlInputError("operation_state_corrupt", "Receipt outcome is invalid.");
|
|
1184
|
+
if (!INSTANT_PATTERN.test(receipt.observedAt) || new Date(receipt.observedAt).toISOString() !== receipt.observedAt)
|
|
1185
|
+
throw new ControlInputError("operation_state_corrupt", "Receipt timestamp is invalid.");
|
|
1186
|
+
if (receipt.evidenceDigest !== undefined)
|
|
1187
|
+
assertDigest(receipt.evidenceDigest, "receipt.evidenceDigest");
|
|
1188
|
+
if (receipt.blockerCode !== undefined)
|
|
1189
|
+
assertBlockerCode(receipt.blockerCode);
|
|
1190
|
+
if (receipt.outcome === "satisfied" && (receipt.evidenceDigest === undefined || receipt.blockerCode !== undefined))
|
|
1191
|
+
throw new ControlInputError("operation_state_corrupt", "Satisfied receipt evidence branch is invalid.");
|
|
1192
|
+
if (receipt.outcome === "not_satisfied" && receipt.blockerCode === undefined)
|
|
1193
|
+
throw new ControlInputError("operation_state_corrupt", "Blocked receipt requires blockerCode.");
|
|
1194
|
+
if (receipt.outcome === "uncertain" && receipt.blockerCode === undefined)
|
|
1195
|
+
throw new ControlInputError("operation_state_corrupt", "Uncertain receipt requires blockerCode.");
|
|
1196
|
+
}
|
|
1197
|
+
function findExistingAction(state, actionId) {
|
|
1198
|
+
const actions = Object.values(state.actions).filter(action => action.kind === "stop" || action.kind === "work_steer");
|
|
1199
|
+
if (actions.some((left, index) => actions.some((right, rightIndex) => index !== rightIndex && left.actionId === right.actionId)))
|
|
1200
|
+
return "corrupt";
|
|
1201
|
+
const found = actions.find(action => action.actionId === actionId);
|
|
1202
|
+
return found;
|
|
1203
|
+
}
|
|
1204
|
+
function actionMatches(action, request, requestDigest) {
|
|
1205
|
+
return action.actionId === request.controlActionId
|
|
1206
|
+
&& action.kind === controlActionKind(request.action)
|
|
1207
|
+
&& action.repeatPolicy === "observe_only_after_intent"
|
|
1208
|
+
&& action.requestDigest === requestDigest
|
|
1209
|
+
&& action.targetDigest === request.parent.targetBindingDigest;
|
|
1210
|
+
}
|
|
1211
|
+
function controlActionKind(action) {
|
|
1212
|
+
return action === "steer" ? "work_steer" : "stop";
|
|
1213
|
+
}
|
|
1214
|
+
function parentErrorCode(error) {
|
|
1215
|
+
if (error instanceof ControlInputError)
|
|
1216
|
+
return error.code;
|
|
1217
|
+
if (isRecord(error) && typeof error.code === "string" && isBlockerCode(error.code))
|
|
1218
|
+
return error.code;
|
|
1219
|
+
return "operation_state_corrupt";
|
|
1220
|
+
}
|
|
1221
|
+
function cancellationCode(normalized) {
|
|
1222
|
+
if (normalized.signal.aborted)
|
|
1223
|
+
return "operation_cancelled";
|
|
1224
|
+
try {
|
|
1225
|
+
if (normalized.now() >= normalized.deadlineAt)
|
|
1226
|
+
return "operation_timeout";
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
return "operation_timeout";
|
|
1230
|
+
}
|
|
1231
|
+
return undefined;
|
|
1232
|
+
}
|
|
1233
|
+
function isCancelled(normalized) {
|
|
1234
|
+
return normalized.signal.aborted || cancellationCode(normalized) === "operation_timeout";
|
|
1235
|
+
}
|
|
1236
|
+
function canAttemptObservation(normalized) {
|
|
1237
|
+
// A read-only reconciliation may proceed after caller cancellation, but not
|
|
1238
|
+
// after a hard deadline: returning a durable uncertain receipt is bounded.
|
|
1239
|
+
try {
|
|
1240
|
+
return normalized.now() < normalized.deadlineAt;
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function safeBase(request, requestDigest) {
|
|
1247
|
+
const value = safeDataRecord(request);
|
|
1248
|
+
const parent = safeDataRecord(readOwnData(value, "parent"));
|
|
1249
|
+
const validDigest = isDigest(requestDigest) ? requestDigest : INVALID_DIGEST;
|
|
1250
|
+
const operationId = readOwnData(parent, "operationId");
|
|
1251
|
+
const parentRequestDigest = readOwnData(parent, "requestDigest");
|
|
1252
|
+
const parentTargetBindingDigest = readOwnData(parent, "targetBindingDigest");
|
|
1253
|
+
const controlActionId = readOwnData(value, "controlActionId");
|
|
1254
|
+
const action = readOwnData(value, "action");
|
|
1255
|
+
const expectedAssistantTurnId = readOwnData(value, "expectedAssistantTurnId");
|
|
1256
|
+
return {
|
|
1257
|
+
controlActionId: isUuid(controlActionId) ? controlActionId : INVALID_ACTION_ID,
|
|
1258
|
+
parentOperationId: isUuid(operationId) ? operationId : INVALID_OPERATION_ID,
|
|
1259
|
+
parentRequestDigest: isDigest(parentRequestDigest) ? parentRequestDigest : INVALID_DIGEST,
|
|
1260
|
+
parentTargetBindingDigest: isDigest(parentTargetBindingDigest) ? parentTargetBindingDigest : INVALID_DIGEST,
|
|
1261
|
+
requestDigest: validDigest,
|
|
1262
|
+
action: action === "steer" ? "steer" : "stop",
|
|
1263
|
+
expectedAssistantTurnId: isSafeIdentifier(expectedAssistantTurnId) ? expectedAssistantTurnId : INVALID_TURN_ID
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function blockedResult(base, code, observationRequired, boundary, evidenceDigest) {
|
|
1267
|
+
return {
|
|
1268
|
+
...base,
|
|
1269
|
+
kind: "blocked",
|
|
1270
|
+
blocker: blocker(code, observationRequired, boundary, evidenceDigest)
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function blocker(code, observationRequired, mutationBoundary, evidenceDigest) {
|
|
1274
|
+
return {
|
|
1275
|
+
code,
|
|
1276
|
+
observationRequired,
|
|
1277
|
+
mutationBoundary,
|
|
1278
|
+
...(evidenceDigest === undefined ? {} : { evidenceDigest })
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Copy a bounded redacted result graph without invoking accessors or keeping
|
|
1283
|
+
* aliases into a caller-owned object. Boundary values are intentionally
|
|
1284
|
+
* restricted to JSON-like plain data; cycles, proxies with unstable traps,
|
|
1285
|
+
* functions, symbols, and non-enumerable fields fail closed.
|
|
1286
|
+
*/
|
|
1287
|
+
function clonePlainData(value, active = new Set(), depth = 0, nodes = { count: 0 }) {
|
|
1288
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number")
|
|
1289
|
+
return value;
|
|
1290
|
+
if (value === undefined)
|
|
1291
|
+
return undefined;
|
|
1292
|
+
if (typeof value !== "object" || depth > 40 || nodes.count++ > 16_384)
|
|
1293
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary data graph is invalid.");
|
|
1294
|
+
if (active.has(value))
|
|
1295
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary data graph is cyclic.");
|
|
1296
|
+
let prototype;
|
|
1297
|
+
let keys;
|
|
1298
|
+
try {
|
|
1299
|
+
prototype = Object.getPrototypeOf(value);
|
|
1300
|
+
keys = Reflect.ownKeys(value);
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary data graph is inaccessible.");
|
|
1304
|
+
}
|
|
1305
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
|
|
1306
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary data graph is not plain.");
|
|
1307
|
+
active.add(value);
|
|
1308
|
+
try {
|
|
1309
|
+
if (Array.isArray(value)) {
|
|
1310
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
1311
|
+
if (lengthDescriptor === undefined || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > 16_384)
|
|
1312
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary array length is invalid.");
|
|
1313
|
+
const output = [];
|
|
1314
|
+
for (let index = 0; index < lengthDescriptor.value; index += 1) {
|
|
1315
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1316
|
+
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor))
|
|
1317
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary array contains an unsafe item.");
|
|
1318
|
+
output.push(clonePlainData(descriptor.value, active, depth + 1, nodes));
|
|
1319
|
+
}
|
|
1320
|
+
for (const key of keys) {
|
|
1321
|
+
if (key === "length")
|
|
1322
|
+
continue;
|
|
1323
|
+
if (typeof key !== "string" || !/^\d+$/u.test(key) || String(Number(key)) !== key || Number(key) >= lengthDescriptor.value) {
|
|
1324
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary array contains an unsupported field.");
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return output;
|
|
1328
|
+
}
|
|
1329
|
+
const output = {};
|
|
1330
|
+
for (const key of keys) {
|
|
1331
|
+
if (typeof key !== "string")
|
|
1332
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary data contains a symbol key.");
|
|
1333
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1334
|
+
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor))
|
|
1335
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary data contains an unsafe property.");
|
|
1336
|
+
Object.defineProperty(output, key, {
|
|
1337
|
+
configurable: true,
|
|
1338
|
+
enumerable: true,
|
|
1339
|
+
writable: true,
|
|
1340
|
+
value: clonePlainData(descriptor.value, active, depth + 1, nodes)
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
return output;
|
|
1344
|
+
}
|
|
1345
|
+
finally {
|
|
1346
|
+
active.delete(value);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function deepFreeze(value, seen = new Set()) {
|
|
1350
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
1351
|
+
return value;
|
|
1352
|
+
seen.add(value);
|
|
1353
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1354
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1355
|
+
if (descriptor !== undefined && "value" in descriptor)
|
|
1356
|
+
deepFreeze(descriptor.value, seen);
|
|
1357
|
+
}
|
|
1358
|
+
return Object.freeze(value);
|
|
1359
|
+
}
|
|
1360
|
+
function assertExactRecord(value, allowed, required) {
|
|
1361
|
+
if (!isRecord(value))
|
|
1362
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary value must be a plain object.");
|
|
1363
|
+
const allowedSet = new Set(allowed);
|
|
1364
|
+
for (const key of Object.keys(value))
|
|
1365
|
+
if (!allowedSet.has(key))
|
|
1366
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary value contains an unsupported field.");
|
|
1367
|
+
for (const key of required)
|
|
1368
|
+
if (!Object.prototype.hasOwnProperty.call(value, key))
|
|
1369
|
+
throw new ControlInputError("operation_state_corrupt", "Boundary value is missing a required field.");
|
|
1370
|
+
}
|
|
1371
|
+
function assertUuid(value, label) {
|
|
1372
|
+
if (typeof value !== "string" || !UUID_PATTERN.test(value))
|
|
1373
|
+
throw new ControlInputError("operation_state_corrupt", `${label} is not a canonical UUID.`);
|
|
1374
|
+
}
|
|
1375
|
+
function isUuid(value) {
|
|
1376
|
+
return typeof value === "string" && UUID_PATTERN.test(value);
|
|
1377
|
+
}
|
|
1378
|
+
function assertDigest(value, label) {
|
|
1379
|
+
if (typeof value !== "string" || !DIGEST_PATTERN.test(value))
|
|
1380
|
+
throw new ControlInputError("operation_state_corrupt", `${label} is not a canonical digest.`);
|
|
1381
|
+
}
|
|
1382
|
+
function isDigest(value) {
|
|
1383
|
+
return typeof value === "string" && DIGEST_PATTERN.test(value);
|
|
1384
|
+
}
|
|
1385
|
+
function isSafeIdentifier(value) {
|
|
1386
|
+
return typeof value === "string"
|
|
1387
|
+
&& ID_PATTERN.test(value)
|
|
1388
|
+
&& !/[\u0000-\u001f\u007f]/u.test(value)
|
|
1389
|
+
&& value.trim().length > 0;
|
|
1390
|
+
}
|
|
1391
|
+
function readOwnData(value, key) {
|
|
1392
|
+
if (!isRecord(value))
|
|
1393
|
+
return undefined;
|
|
1394
|
+
try {
|
|
1395
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1396
|
+
return descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
|
|
1397
|
+
}
|
|
1398
|
+
catch {
|
|
1399
|
+
return undefined;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function safeDataRecord(value) {
|
|
1403
|
+
return isRecord(value) ? value : undefined;
|
|
1404
|
+
}
|
|
1405
|
+
function hasAccessorInPlainData(value, seen = new Set(), depth = 0) {
|
|
1406
|
+
if (value === null || typeof value !== "object")
|
|
1407
|
+
return false;
|
|
1408
|
+
let prototype;
|
|
1409
|
+
try {
|
|
1410
|
+
prototype = Object.getPrototypeOf(value);
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
|
|
1416
|
+
return false;
|
|
1417
|
+
if (seen.has(value))
|
|
1418
|
+
return false;
|
|
1419
|
+
if (depth > 32)
|
|
1420
|
+
return true;
|
|
1421
|
+
seen.add(value);
|
|
1422
|
+
let descriptors;
|
|
1423
|
+
try {
|
|
1424
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1430
|
+
if (!("value" in descriptor))
|
|
1431
|
+
return true;
|
|
1432
|
+
if (hasAccessorInPlainData(descriptor.value, seen, depth + 1))
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
function assertIdentifier(value, label) {
|
|
1438
|
+
if (typeof value !== "string" || !ID_PATTERN.test(value) || /[\u0000-\u001f\u007f]/u.test(value) || value.trim().length === 0)
|
|
1439
|
+
throw new ControlInputError("operation_state_corrupt", `${label} is not a bounded opaque identity.`);
|
|
1440
|
+
}
|
|
1441
|
+
function assertBlockerCode(value) {
|
|
1442
|
+
if (typeof value !== "string" || !CODE_PATTERN.test(value) || !isBlockerCode(value))
|
|
1443
|
+
throw new ControlInputError("operation_state_corrupt", "Control blocker code is invalid.");
|
|
1444
|
+
}
|
|
1445
|
+
function isBlockerCode(value) {
|
|
1446
|
+
return [
|
|
1447
|
+
"operation_not_found", "operation_request_mismatch", "operation_state_corrupt", "operation_receipt_expired", "operation_quota_exceeded", "operation_cancelled", "operation_timeout", "ambiguous_file_handoff", "ambiguous_submit", "attachment_manifest_mismatch", "input_file_changed", "target_binding_mismatch", "target_evidence_unavailable", "turn_ownership_ambiguous", "concurrent_user_turn", "configuration_drift", "tab_ownership_conflict", "provider_concurrency_unsupported", "runtime_incompatible", "backend_unavailable", "browser_bridge_unavailable", "login_required", "captcha", "rate_limited", "permission_required", "needs_confirmation", "selector_drift", "send_control_unavailable", "capture_ownership_lost", "artifact_unavailable", "artifact_transfer_partial", "output_collision", "output_commit_indeterminate", "clipboard_restore_failed"
|
|
1448
|
+
].includes(value);
|
|
1449
|
+
}
|
|
1450
|
+
function checkedNow(now) {
|
|
1451
|
+
const value = now();
|
|
1452
|
+
if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value < 0 || value > MAX_DEADLINE_AT)
|
|
1453
|
+
throw new ControlInputError("operation_state_corrupt", "Clock returned an invalid time.");
|
|
1454
|
+
return value;
|
|
1455
|
+
}
|
|
1456
|
+
function isAbortSignalLike(value) {
|
|
1457
|
+
if (value === null || typeof value !== "object")
|
|
1458
|
+
return false;
|
|
1459
|
+
try {
|
|
1460
|
+
if (typeof AbortSignal !== "undefined" && value instanceof AbortSignal)
|
|
1461
|
+
return true;
|
|
1462
|
+
if (Object.prototype.toString.call(value) !== "[object AbortSignal]")
|
|
1463
|
+
return false;
|
|
1464
|
+
let prototype = Object.getPrototypeOf(value);
|
|
1465
|
+
let abortedAccessor = false;
|
|
1466
|
+
let addMethod = false;
|
|
1467
|
+
let removeMethod = false;
|
|
1468
|
+
const seen = new Set();
|
|
1469
|
+
while (prototype !== null && !seen.has(prototype)) {
|
|
1470
|
+
seen.add(prototype);
|
|
1471
|
+
const aborted = Object.getOwnPropertyDescriptor(prototype, "aborted");
|
|
1472
|
+
if (aborted !== undefined && "get" in aborted)
|
|
1473
|
+
abortedAccessor = true;
|
|
1474
|
+
const add = Object.getOwnPropertyDescriptor(prototype, "addEventListener");
|
|
1475
|
+
if (add !== undefined && "value" in add && typeof add.value === "function")
|
|
1476
|
+
addMethod = true;
|
|
1477
|
+
const remove = Object.getOwnPropertyDescriptor(prototype, "removeEventListener");
|
|
1478
|
+
if (remove !== undefined && "value" in remove && typeof remove.value === "function")
|
|
1479
|
+
removeMethod = true;
|
|
1480
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
1481
|
+
}
|
|
1482
|
+
return abortedAccessor && addMethod && removeMethod;
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function isRecord(value) {
|
|
1489
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1490
|
+
return false;
|
|
1491
|
+
try {
|
|
1492
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1493
|
+
return prototype === Object.prototype || prototype === null;
|
|
1494
|
+
}
|
|
1495
|
+
catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|