codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4
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 +42 -0
- package/README.md +43 -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/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- 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 +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- 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 +45882 -9361
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
- 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/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -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 +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -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 +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -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 +25 -0
- package/dist/src/operations/index.js +25 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -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 +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -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 +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -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 +286 -0
- package/dist/src/operations/service.js +2831 -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/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -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 +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -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/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- 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 +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { composerTextbox, stopGenerationButton } from "../dom/selectors.js";
|
|
2
|
+
import { inspectComposerText } from "../dom/composer-text.js";
|
|
3
|
+
import { readChatGPTEmptyAttachmentState } from "./production-chatgpt-attachments.js";
|
|
4
|
+
import { observeBrowserPage } from "./browser-observation.js";
|
|
5
|
+
import { COLLECTOR_SCHEMA_VERSION } from "./collector.js";
|
|
6
|
+
import { classifyTurnOwnership, TURN_OWNERSHIP_SCHEMA_VERSION, } from "./turn-ownership.js";
|
|
7
|
+
/**
|
|
8
|
+
* A small, provider-facing primitive layer for the transactional adapter.
|
|
9
|
+
*
|
|
10
|
+
* This module intentionally does not call a legacy command. Each callback
|
|
11
|
+
* performs a bounded locator/evaluation operation, or one browser activation,
|
|
12
|
+
* and returns only keyed evidence. Attachment handoff, configuration,
|
|
13
|
+
* The base factory deliberately leaves configuration, attachments, and Work
|
|
14
|
+
* steer to their dedicated provider modules. `chatgpt-runtime.ts` composes
|
|
15
|
+
* those modules into the default ChatGPT operation runtime; the inventory
|
|
16
|
+
* below describes this base factory, not the completed composite runtime.
|
|
17
|
+
*/
|
|
18
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
|
|
19
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
|
|
20
|
+
const OPAQUE_THREAD_URL_PATTERN = /^https:\/\/opaque\.invalid\/thread\/[0-9a-f]{64}$/u;
|
|
21
|
+
const MAX_LOCATOR_CANDIDATES = 128;
|
|
22
|
+
const MAX_COMPOSER_CHARS = 8 * 1024 * 1024;
|
|
23
|
+
export const PRODUCTION_PRIMITIVE_CAPABILITIES = Object.freeze([
|
|
24
|
+
"composer_set",
|
|
25
|
+
"empty_attachment_observation",
|
|
26
|
+
"send_activation",
|
|
27
|
+
"collector_snapshot",
|
|
28
|
+
"durable_baseline_projection",
|
|
29
|
+
"submission_witness_recovery",
|
|
30
|
+
"stop_control"
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* These are not soft feature flags. They are an inventory of deliberately
|
|
34
|
+
* missing proof, so a caller cannot mistake an unavailable primitive for a
|
|
35
|
+
* best-effort browser fallback.
|
|
36
|
+
*/
|
|
37
|
+
export const UNWIRED_PRODUCTION_PRIMITIVES = Object.freeze([
|
|
38
|
+
"configuration_set",
|
|
39
|
+
"tool_selection",
|
|
40
|
+
"power_select",
|
|
41
|
+
"file_chooser_handoff",
|
|
42
|
+
"attachment_identity_for_nonempty_manifest",
|
|
43
|
+
"work_steer_activation"
|
|
44
|
+
]);
|
|
45
|
+
export const PRODUCTION_OPERATION_PRIMITIVE_INVENTORY = Object.freeze({
|
|
46
|
+
scope: "base_primitive_factory",
|
|
47
|
+
wired: PRODUCTION_PRIMITIVE_CAPABILITIES,
|
|
48
|
+
unwired: UNWIRED_PRODUCTION_PRIMITIVES
|
|
49
|
+
});
|
|
50
|
+
export class ProductionPrimitiveError extends Error {
|
|
51
|
+
code;
|
|
52
|
+
constructor(code) {
|
|
53
|
+
super("The provider-specific operation primitive could not prove the requested action safely.");
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.name = "ProductionPrimitiveError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create one request-scoped set of production operation primitives.
|
|
60
|
+
*
|
|
61
|
+
* `operationId`, `requestDigest`, and the composer value should normally be
|
|
62
|
+
* supplied by the lazy runtime capture after the journal has created the
|
|
63
|
+
* operation. If either identity is absent, Send fails closed rather than
|
|
64
|
+
* fabricating an evidence domain.
|
|
65
|
+
*/
|
|
66
|
+
export function createProductionOperationPrimitives(options) {
|
|
67
|
+
validateOptions(options);
|
|
68
|
+
const state = {
|
|
69
|
+
...(options.operationId === undefined ? {} : { operationId: options.operationId }),
|
|
70
|
+
...(options.requestDigest === undefined ? {} : { requestDigest: options.requestDigest }),
|
|
71
|
+
...(options.desiredComposerText !== undefined
|
|
72
|
+
? { desiredComposerText: options.desiredComposerText }
|
|
73
|
+
: options.composerText === undefined ? {} : { desiredComposerText: options.composerText }),
|
|
74
|
+
...(options.authoritativeTabClaim === undefined ? {} : { authoritativeTabClaim: options.authoritativeTabClaim }),
|
|
75
|
+
...(options.target === undefined ? {} : { target: options.target })
|
|
76
|
+
};
|
|
77
|
+
const staging = Object.freeze({
|
|
78
|
+
readCurrent: request => readStaging(request, options.evidenceDigest, state),
|
|
79
|
+
mutateOnce: request => mutateStagingOnce(request, options.evidenceDigest, state),
|
|
80
|
+
observe: request => readStaging(request, options.evidenceDigest, state)
|
|
81
|
+
});
|
|
82
|
+
const sendObservers = Object.freeze({
|
|
83
|
+
observePrecondition: request => observeSendPrecondition(request, options.evidenceDigest, state, options.observeAttachments),
|
|
84
|
+
observePostcondition: async (request) => {
|
|
85
|
+
const result = await observeSendPostcondition(request, options.evidenceDigest, state);
|
|
86
|
+
return result.status === "blocked"
|
|
87
|
+
&& (result.blockerCode === "ambiguous_submit" || result.blockerCode === "target_evidence_unavailable")
|
|
88
|
+
? { result, retryable: true }
|
|
89
|
+
: result;
|
|
90
|
+
},
|
|
91
|
+
// Every retry is an observation-only transaction. The delay remains
|
|
92
|
+
// outside the tab actor and can never repeat the Send activation.
|
|
93
|
+
sleep: sleepOutsideBrowser,
|
|
94
|
+
// ChatGPT can establish the canonical conversation URL before it exposes
|
|
95
|
+
// stable user/assistant DOM identities. Keep that provider settling window
|
|
96
|
+
// bounded without collapsing a successful one-shot Send into uncertainty.
|
|
97
|
+
maxPostconditionAttempts: 32,
|
|
98
|
+
postconditionIntervalMs: 250,
|
|
99
|
+
postconditionTimeoutMs: 15_000
|
|
100
|
+
});
|
|
101
|
+
const submission = Object.freeze({
|
|
102
|
+
observeStaging: (request, page, target) => observeSubmissionStaging(request, page, target, options.evidenceDigest, state),
|
|
103
|
+
// The file identity layer revalidates paths outside the actor. There is
|
|
104
|
+
// still no identity-grade provider chooser primitive here, so never begin
|
|
105
|
+
// a chooser mutation under an invented selector or wait loop.
|
|
106
|
+
handoffFiles: async () => ({
|
|
107
|
+
status: "not_satisfied",
|
|
108
|
+
blockerCode: "attachment_manifest_mismatch"
|
|
109
|
+
}),
|
|
110
|
+
observeAttachments: (request, page, target) => observeSubmissionAttachments(request, page, target, options.evidenceDigest, state, options.observeAttachments),
|
|
111
|
+
sendObservers
|
|
112
|
+
});
|
|
113
|
+
const collector = Object.freeze({
|
|
114
|
+
readContext: (request, page, target) => readCollectorContext(request, page, target, options.evidenceDigest, state),
|
|
115
|
+
observe: (request, page, target, context) => observeCollector(request, page, target, context, options.evidenceDigest, state),
|
|
116
|
+
// This timer is intentionally outside any tab transaction. The browser
|
|
117
|
+
// adapter invokes it after its short observation transaction has settled.
|
|
118
|
+
sleep: sleepOutsideBrowser
|
|
119
|
+
});
|
|
120
|
+
const control = Object.freeze({
|
|
121
|
+
observeTurn: (request, page, target) => observeControlTurn(request, page, target, options.evidenceDigest, state),
|
|
122
|
+
executeOnce: (request, page, target) => executeControlOnce(request, page, target, options.evidenceDigest, state),
|
|
123
|
+
observePostcondition: (request, page, target) => observeControlPostcondition(request, page, target, options.evidenceDigest, state)
|
|
124
|
+
});
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
staging,
|
|
127
|
+
submission,
|
|
128
|
+
collector,
|
|
129
|
+
control
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Descriptive aliases for integrations that name the layer after the adapter. */
|
|
133
|
+
export const createOperationProductionPrimitives = createProductionOperationPrimitives;
|
|
134
|
+
export const createProductionPrimitives = createProductionOperationPrimitives;
|
|
135
|
+
async function readStaging(request, evidenceDigest, state) {
|
|
136
|
+
if (request.kind !== "composer_set") {
|
|
137
|
+
return unavailableStaging(request, stagingUnwiredCode(request.kind));
|
|
138
|
+
}
|
|
139
|
+
if (state.desiredComposerText === undefined) {
|
|
140
|
+
return unavailableStaging(request, "composer_primitive_unwired");
|
|
141
|
+
}
|
|
142
|
+
if (!await matchesExpectedStagingDigest(request, evidenceDigest)) {
|
|
143
|
+
return unavailableStaging(request, "composer_request_mismatch");
|
|
144
|
+
}
|
|
145
|
+
const current = await readComposerState(request.page, request.target, request.operationId, evidenceDigest);
|
|
146
|
+
if (current === undefined)
|
|
147
|
+
return unavailableStaging(request, "composer_control_unavailable");
|
|
148
|
+
const satisfied = current.text === state.desiredComposerText;
|
|
149
|
+
const evidence = await digest(evidenceDigest, "composer-observation", {
|
|
150
|
+
operationId: request.operationId,
|
|
151
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
152
|
+
currentStateDigest: current.currentStateDigest,
|
|
153
|
+
status: satisfied ? "satisfied" : "not_satisfied"
|
|
154
|
+
});
|
|
155
|
+
if (evidence === undefined)
|
|
156
|
+
return unavailableStaging(request, "evidence_digest_failed");
|
|
157
|
+
return {
|
|
158
|
+
status: satisfied ? "satisfied" : "not_satisfied",
|
|
159
|
+
desiredStateDigest: request.desiredStateDigest,
|
|
160
|
+
currentStateDigest: current.currentStateDigest,
|
|
161
|
+
evidenceDigest: evidence
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function mutateStagingOnce(request, evidenceDigest, state) {
|
|
165
|
+
if (request.kind !== "composer_set" || state.desiredComposerText === undefined) {
|
|
166
|
+
throw new ProductionPrimitiveError(request.kind === "composer_set" ? "composer_primitive_unwired" : stagingUnwiredCode(request.kind));
|
|
167
|
+
}
|
|
168
|
+
if (!isDigest(request.desiredStateDigest) || !isDigest(request.requestDigest)) {
|
|
169
|
+
throw new ProductionPrimitiveError("composer_request_mismatch");
|
|
170
|
+
}
|
|
171
|
+
const expected = await safeDigestWith(evidenceDigest, "staging-desired", { requestDigest: request.requestDigest, kind: request.kind });
|
|
172
|
+
if (expected === undefined || expected !== request.desiredStateDigest) {
|
|
173
|
+
throw new ProductionPrimitiveError("composer_request_mismatch");
|
|
174
|
+
}
|
|
175
|
+
assertStagingActive(request);
|
|
176
|
+
const locator = await uniqueVisibleLocator(request.page, composerTextbox);
|
|
177
|
+
if (locator === undefined || typeof locator.fill !== "function") {
|
|
178
|
+
throw new ProductionPrimitiveError("composer_control_unavailable");
|
|
179
|
+
}
|
|
180
|
+
// The sole reversible composer mutation. There is no readiness wait and no
|
|
181
|
+
// fallback press/click path if this call rejects.
|
|
182
|
+
assertStagingActive(request);
|
|
183
|
+
await locator.fill(state.desiredComposerText);
|
|
184
|
+
return { status: "started" };
|
|
185
|
+
}
|
|
186
|
+
function assertStagingActive(request) {
|
|
187
|
+
if (request.signal?.aborted)
|
|
188
|
+
throw new ProductionPrimitiveError("operation_cancelled");
|
|
189
|
+
if (request.deadlineAt !== undefined && Date.now() >= request.deadlineAt)
|
|
190
|
+
throw new ProductionPrimitiveError("operation_deadline_exceeded");
|
|
191
|
+
}
|
|
192
|
+
function stagingUnwiredCode(kind) {
|
|
193
|
+
switch (kind) {
|
|
194
|
+
case "configuration_set": return "configuration_primitive_unwired";
|
|
195
|
+
case "tool_set": return "tool_primitive_unwired";
|
|
196
|
+
case "power_select": return "power_primitive_unwired";
|
|
197
|
+
case "composer_set": return "composer_primitive_unwired";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function unavailableStaging(request, blockerCode) {
|
|
201
|
+
return {
|
|
202
|
+
status: "unavailable",
|
|
203
|
+
desiredStateDigest: request.desiredStateDigest,
|
|
204
|
+
blockerCode
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function matchesExpectedStagingDigest(request, evidenceDigest) {
|
|
208
|
+
return await safeDigestWith(evidenceDigest, "staging-desired", {
|
|
209
|
+
requestDigest: request.requestDigest,
|
|
210
|
+
kind: request.kind
|
|
211
|
+
}) === request.desiredStateDigest;
|
|
212
|
+
}
|
|
213
|
+
async function observeSubmissionStaging(request, page, target, evidenceDigest, state) {
|
|
214
|
+
state.target = target;
|
|
215
|
+
if (!isDigest(request.configurationReceiptDigest) || !isDigest(request.composerReceiptDigest)) {
|
|
216
|
+
return { status: "unavailable", reason: "unknown" };
|
|
217
|
+
}
|
|
218
|
+
const expectedConfiguration = state.requestDigest === undefined
|
|
219
|
+
? undefined
|
|
220
|
+
: await safeDigestWith(evidenceDigest, "configuration-request", state.requestDigest);
|
|
221
|
+
const expectedComposer = state.requestDigest === undefined
|
|
222
|
+
? undefined
|
|
223
|
+
: await safeDigestWith(evidenceDigest, "composer-request", state.requestDigest);
|
|
224
|
+
if (expectedConfiguration === undefined || expectedComposer === undefined) {
|
|
225
|
+
return { status: "unavailable", reason: "target" };
|
|
226
|
+
}
|
|
227
|
+
if (request.configurationReceiptDigest !== expectedConfiguration) {
|
|
228
|
+
const evidence = await safeDigestWith(evidenceDigest, "submission-stage", { operationId: request.operationId, reason: "configuration" });
|
|
229
|
+
return evidence === undefined
|
|
230
|
+
? { status: "mismatch", reason: "configuration" }
|
|
231
|
+
: { status: "mismatch", reason: "configuration", evidenceDigest: evidence };
|
|
232
|
+
}
|
|
233
|
+
if (request.composerReceiptDigest !== expectedComposer || state.desiredComposerText === undefined) {
|
|
234
|
+
return { status: "unavailable", reason: "composer" };
|
|
235
|
+
}
|
|
236
|
+
const current = await readComposerState(page, target, request.operationId, evidenceDigest);
|
|
237
|
+
if (current === undefined)
|
|
238
|
+
return { status: "unavailable", reason: "composer" };
|
|
239
|
+
if (current.text !== state.desiredComposerText) {
|
|
240
|
+
return { status: "mismatch", reason: "composer", evidenceDigest: current.evidenceDigest };
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
status: "exact",
|
|
244
|
+
evidenceDigest: current.evidenceDigest
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async function observeSendPrecondition(request, evidenceDigest, state, attachmentObserver) {
|
|
248
|
+
const identity = sendIdentity(state, request.expected);
|
|
249
|
+
if (identity === undefined)
|
|
250
|
+
return { status: "unavailable", code: "target_evidence_unavailable" };
|
|
251
|
+
const target = targetForObservation(request.expected, state);
|
|
252
|
+
if (target === undefined)
|
|
253
|
+
return { status: "unavailable", code: "target_evidence_unavailable" };
|
|
254
|
+
// Recovery after a durable Send intent is observation-only. A pending new
|
|
255
|
+
// target necessarily no longer renders the blank surface, so re-running the
|
|
256
|
+
// normal precondition would reject the very post-Send state we must inspect.
|
|
257
|
+
// The durable blank-task snapshot is the bounded baseline authority here;
|
|
258
|
+
// the subsequent postcondition still has to prove exactly one new user turn
|
|
259
|
+
// and complete provider conversation identity before establishment.
|
|
260
|
+
if (request.mode === "observe_only" && state.target?.targetLifecycle === "new_pending") {
|
|
261
|
+
const recoveryBaseline = recoverPendingBlankBaseline(state);
|
|
262
|
+
if (recoveryBaseline === undefined)
|
|
263
|
+
return { status: "unavailable", code: "target_evidence_unavailable" };
|
|
264
|
+
return {
|
|
265
|
+
status: "exact",
|
|
266
|
+
targetBindingDigest: request.expected.targetBindingDigest,
|
|
267
|
+
configurationReceiptDigest: request.expected.configurationReceiptDigest,
|
|
268
|
+
composerReceiptDigest: request.expected.composerReceiptDigest,
|
|
269
|
+
attachments: {
|
|
270
|
+
count: request.expected.attachmentManifest.count,
|
|
271
|
+
orderPolicy: "exact",
|
|
272
|
+
identityDigests: request.expected.attachmentManifest.identities.map(entry => entry.identityDigest)
|
|
273
|
+
},
|
|
274
|
+
baseline: {
|
|
275
|
+
ownershipBaseline: recoveryBaseline,
|
|
276
|
+
userTurnEvidenceDigest: await safeDigestWith(evidenceDigest, "send-baseline", {
|
|
277
|
+
snapshotDigest: recoveryBaseline.snapshotDigest,
|
|
278
|
+
userTurns: []
|
|
279
|
+
}) ?? recoveryBaseline.snapshotDigest
|
|
280
|
+
},
|
|
281
|
+
evidenceDigest: recoveryBaseline.snapshotDigest
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (state.desiredComposerText === undefined)
|
|
285
|
+
return { status: "unavailable", code: "composer_drift" };
|
|
286
|
+
const expectedComposer = await safeDigestWith(evidenceDigest, "composer-request", state.requestDigest);
|
|
287
|
+
const expectedConfiguration = await safeDigestWith(evidenceDigest, "configuration-request", state.requestDigest);
|
|
288
|
+
if (expectedComposer === undefined || expectedConfiguration === undefined) {
|
|
289
|
+
return { status: "unavailable", code: "target_evidence_unavailable" };
|
|
290
|
+
}
|
|
291
|
+
if (request.expected.composerReceiptDigest !== expectedComposer) {
|
|
292
|
+
return { status: "mismatch", code: "composer_drift" };
|
|
293
|
+
}
|
|
294
|
+
if (request.expected.configurationReceiptDigest !== expectedConfiguration) {
|
|
295
|
+
return { status: "mismatch", code: "configuration_drift" };
|
|
296
|
+
}
|
|
297
|
+
let observation;
|
|
298
|
+
try {
|
|
299
|
+
observation = await observeBrowserPage(request.page, {
|
|
300
|
+
operationId: identity,
|
|
301
|
+
target,
|
|
302
|
+
evidenceDigest,
|
|
303
|
+
responseContent: "metadata"
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return { status: "unavailable", code: "target_evidence_unavailable" };
|
|
308
|
+
}
|
|
309
|
+
const snapshot = observation.snapshot;
|
|
310
|
+
if (state.target?.targetLifecycle === "new_pending") {
|
|
311
|
+
const anchor = observation.newTargetAnchor;
|
|
312
|
+
if (anchor === undefined
|
|
313
|
+
|| anchor.anchorDigest !== state.target.newTargetAnchorDigest
|
|
314
|
+
|| anchor.blankTaskEvidenceDigest !== state.target.blankTaskEvidenceDigest
|
|
315
|
+
|| snapshot.target.thread.status !== "unavailable"
|
|
316
|
+
|| snapshot.target.conversation.status !== "unavailable"
|
|
317
|
+
|| snapshot.target.canonicalThreadUrl.status !== "unavailable"
|
|
318
|
+
|| snapshot.userTurns.length !== 0
|
|
319
|
+
|| snapshot.assistantTurns.length !== 0) {
|
|
320
|
+
return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!stableBaseline(snapshot)) {
|
|
324
|
+
return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
|
|
325
|
+
}
|
|
326
|
+
const composerTarget = state.target;
|
|
327
|
+
if (composerTarget === undefined)
|
|
328
|
+
return { status: "unavailable", code: "target_evidence_unavailable" };
|
|
329
|
+
const composer = await readComposerState(request.page, composerTarget, identity, evidenceDigest);
|
|
330
|
+
if (composer === undefined)
|
|
331
|
+
return { status: "unavailable", code: "composer_drift", evidenceDigest: snapshot.snapshotDigest };
|
|
332
|
+
if (composer.text !== state.desiredComposerText) {
|
|
333
|
+
return { status: "mismatch", code: "composer_drift", evidenceDigest: composer.evidenceDigest };
|
|
334
|
+
}
|
|
335
|
+
const attachments = await observeAttachmentEnvelope({
|
|
336
|
+
operationId: identity,
|
|
337
|
+
requestDigest: state.requestDigest,
|
|
338
|
+
surface: request.expected.surface,
|
|
339
|
+
targetBindingDigest: request.expected.targetBindingDigest,
|
|
340
|
+
manifest: request.expected.attachmentManifest
|
|
341
|
+
}, request.page, composerTarget, evidenceDigest, state, attachmentObserver);
|
|
342
|
+
if (attachments.status !== "absent" && attachments.status !== "exact") {
|
|
343
|
+
return { status: "unavailable", code: "attachment_manifest_mismatch", evidenceDigest: snapshot.snapshotDigest };
|
|
344
|
+
}
|
|
345
|
+
const baseline = await baselineForSnapshot(snapshot, evidenceDigest, identity, request.expected.targetBindingDigest);
|
|
346
|
+
if (baseline === undefined)
|
|
347
|
+
return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
|
|
348
|
+
const sendAttachments = {
|
|
349
|
+
count: attachments.count,
|
|
350
|
+
orderPolicy: "exact",
|
|
351
|
+
identityDigests: [...attachments.identityDigests]
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
status: "exact",
|
|
355
|
+
targetBindingDigest: request.expected.targetBindingDigest,
|
|
356
|
+
configurationReceiptDigest: request.expected.configurationReceiptDigest,
|
|
357
|
+
composerReceiptDigest: request.expected.composerReceiptDigest,
|
|
358
|
+
attachments: sendAttachments,
|
|
359
|
+
baseline: {
|
|
360
|
+
...(baseline.userTurns.at(-1)?.stableId === undefined ? {} : { userTurnId: baseline.userTurns.at(-1).stableId }),
|
|
361
|
+
ownershipBaseline: baseline,
|
|
362
|
+
userTurnEvidenceDigest: await safeDigestWith(evidenceDigest, "send-baseline", {
|
|
363
|
+
snapshotDigest: baseline.snapshotDigest,
|
|
364
|
+
userTurns: baseline.userTurns.map(turn => turn.evidenceDigest)
|
|
365
|
+
}) ?? baseline.snapshotDigest
|
|
366
|
+
},
|
|
367
|
+
evidenceDigest: snapshot.snapshotDigest
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function observeSendPostcondition(request, evidenceDigest, state) {
|
|
371
|
+
const identity = sendIdentity(state, request.expected);
|
|
372
|
+
if (identity === undefined)
|
|
373
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable" };
|
|
374
|
+
// The SendOnce coordinator carries the complete baseline that was either
|
|
375
|
+
// durably appended before activation or projected by the service after a
|
|
376
|
+
// restart. Never fall back to the request-scoped map/current page here.
|
|
377
|
+
const baseline = request.baseline.ownershipBaseline;
|
|
378
|
+
if (baseline === undefined)
|
|
379
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable" };
|
|
380
|
+
// A process restart loses the in-memory baseline map. The durable pending
|
|
381
|
+
// target's blank-task evidence is the only safe recovery prefix: it proves
|
|
382
|
+
// that the operation owned zero turns before its one durable Send intent.
|
|
383
|
+
const target = targetForObservation(request.expected, state);
|
|
384
|
+
if (target === undefined)
|
|
385
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable" };
|
|
386
|
+
let observation;
|
|
387
|
+
try {
|
|
388
|
+
observation = await observeBrowserPage(request.page, {
|
|
389
|
+
operationId: identity,
|
|
390
|
+
target,
|
|
391
|
+
evidenceDigest,
|
|
392
|
+
responseContent: "metadata",
|
|
393
|
+
baseline
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable" };
|
|
398
|
+
}
|
|
399
|
+
const delta = observation.snapshot.postSendDelta;
|
|
400
|
+
if (delta === undefined || delta.baselineSnapshotDigest !== baseline.snapshotDigest) {
|
|
401
|
+
return { status: "blocked", blockerCode: "ambiguous_submit", evidenceDigest: observation.snapshot.snapshotDigest };
|
|
402
|
+
}
|
|
403
|
+
const added = observation.snapshot.userTurns.filter(turn => delta.addedUserEvidenceDigests.includes(turn.evidenceDigest));
|
|
404
|
+
if (added.length !== 1) {
|
|
405
|
+
return {
|
|
406
|
+
status: "blocked",
|
|
407
|
+
blockerCode: added.length > 1 ? "concurrent_user_turn" : "ambiguous_submit",
|
|
408
|
+
evidenceDigest: observation.snapshot.snapshotDigest
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const user = added[0];
|
|
412
|
+
if (user?.stableId === undefined) {
|
|
413
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
|
|
414
|
+
}
|
|
415
|
+
const assistant = observation.snapshot.assistantTurns.find(turn => turn.parentStableId === user.stableId);
|
|
416
|
+
const status = request.mode === "observe_only" ? "already_submitted" : "submitted";
|
|
417
|
+
const established = state.target?.targetLifecycle === "new_pending"
|
|
418
|
+
? (() => {
|
|
419
|
+
const conversation = observation.snapshot.target.conversation;
|
|
420
|
+
const canonicalThreadUrl = observation.snapshot.target.canonicalThreadUrl;
|
|
421
|
+
if (conversation.status !== "available"
|
|
422
|
+
|| canonicalThreadUrl.status !== "available"
|
|
423
|
+
|| state.target.newTargetAnchorDigest === undefined)
|
|
424
|
+
return undefined;
|
|
425
|
+
return {
|
|
426
|
+
targetBindingDigest: request.expected.targetBindingDigest,
|
|
427
|
+
anchorDigest: state.target.newTargetAnchorDigest,
|
|
428
|
+
causalSendActionId: request.actionId,
|
|
429
|
+
conversationId: conversation.value,
|
|
430
|
+
canonicalThreadUrl: canonicalThreadUrl.value,
|
|
431
|
+
userTurnId: user.stableId,
|
|
432
|
+
userTurnEvidenceDigest: user.evidenceDigest,
|
|
433
|
+
postSendDeltaDigest: delta.deltaDigest,
|
|
434
|
+
evidenceDigest: observation.snapshot.snapshotDigest
|
|
435
|
+
};
|
|
436
|
+
})()
|
|
437
|
+
: undefined;
|
|
438
|
+
if (state.target?.targetLifecycle === "new_pending" && established === undefined) {
|
|
439
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
status,
|
|
443
|
+
targetBindingDigest: request.expected.targetBindingDigest,
|
|
444
|
+
evidenceDigest: observation.snapshot.snapshotDigest,
|
|
445
|
+
userTurnId: user.stableId,
|
|
446
|
+
userTurnEvidenceDigest: user.evidenceDigest,
|
|
447
|
+
postSendDeltaDigest: delta.deltaDigest,
|
|
448
|
+
...(assistant?.stableId === undefined ? {} : { assistantTurnId: assistant.stableId }),
|
|
449
|
+
...(established === undefined ? {} : { targetEstablishment: established })
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function observeSubmissionAttachments(request, page, target, evidenceDigest, state, attachmentObserver) {
|
|
453
|
+
state.target = target;
|
|
454
|
+
return await observeAttachmentEnvelope(request, page, target, evidenceDigest, state, attachmentObserver);
|
|
455
|
+
}
|
|
456
|
+
async function observeAttachmentEnvelope(request, page, target, evidenceDigest, state, attachmentObserver) {
|
|
457
|
+
if (attachmentObserver !== undefined && request.manifest.count > 0) {
|
|
458
|
+
try {
|
|
459
|
+
return await attachmentObserver(request, page, target);
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return { status: "unavailable" };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (request.manifest.count > 0)
|
|
466
|
+
return { status: "unavailable" };
|
|
467
|
+
const result = await readChatGPTEmptyAttachmentState(page);
|
|
468
|
+
if (result === undefined || !result.supported)
|
|
469
|
+
return { status: "unavailable" };
|
|
470
|
+
if (result.count !== 0 || result.visibleAttachmentCount !== 0)
|
|
471
|
+
return { status: "mismatch" };
|
|
472
|
+
const evidence = await safeDigestWith(evidenceDigest, "composer-attachments", {
|
|
473
|
+
operationId: request.operationId,
|
|
474
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
475
|
+
count: 0
|
|
476
|
+
});
|
|
477
|
+
if (evidence === undefined)
|
|
478
|
+
return { status: "unavailable" };
|
|
479
|
+
return {
|
|
480
|
+
status: "absent",
|
|
481
|
+
evidenceDigest: evidence,
|
|
482
|
+
count: 0,
|
|
483
|
+
orderPolicy: "exact",
|
|
484
|
+
identityDigests: []
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function readComposerState(page, target, operationId, evidenceDigest) {
|
|
488
|
+
void target;
|
|
489
|
+
const locator = await uniqueVisibleLocator(page, composerTextbox);
|
|
490
|
+
if (locator === undefined)
|
|
491
|
+
return undefined;
|
|
492
|
+
const text = await readLocatorText(locator);
|
|
493
|
+
if (text === undefined || text.length > MAX_COMPOSER_CHARS || text.includes("\u0000"))
|
|
494
|
+
return undefined;
|
|
495
|
+
const currentStateDigest = await safeDigestWith(evidenceDigest, "composer-state", {
|
|
496
|
+
operationId,
|
|
497
|
+
text
|
|
498
|
+
});
|
|
499
|
+
if (currentStateDigest === undefined)
|
|
500
|
+
return undefined;
|
|
501
|
+
const observationDigest = await safeDigestWith(evidenceDigest, "composer-observation", {
|
|
502
|
+
operationId,
|
|
503
|
+
currentStateDigest
|
|
504
|
+
});
|
|
505
|
+
if (observationDigest === undefined)
|
|
506
|
+
return undefined;
|
|
507
|
+
return { text, currentStateDigest, evidenceDigest: observationDigest };
|
|
508
|
+
}
|
|
509
|
+
async function readLocatorText(locator) {
|
|
510
|
+
try {
|
|
511
|
+
// The locator textContent/innerText fallbacks expose no provider-side
|
|
512
|
+
// maximum and would materialize an unbounded string before this adapter
|
|
513
|
+
// could inspect it. The transactional path therefore requires evaluate
|
|
514
|
+
// so the cap is enforced inside the browser realm.
|
|
515
|
+
if (typeof locator.evaluate !== "function")
|
|
516
|
+
return undefined;
|
|
517
|
+
const value = await locator.evaluate(inspectComposerText);
|
|
518
|
+
return typeof value === "string" && value.length <= MAX_COMPOSER_CHARS ? value : undefined;
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
async function uniqueVisibleLocator(page, factory) {
|
|
526
|
+
if (typeof page.getByRole !== "function")
|
|
527
|
+
return undefined;
|
|
528
|
+
let locator;
|
|
529
|
+
try {
|
|
530
|
+
locator = factory(page);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
if (typeof locator.count !== "function")
|
|
536
|
+
return undefined;
|
|
537
|
+
let count;
|
|
538
|
+
try {
|
|
539
|
+
count = await locator.count();
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > MAX_LOCATOR_CANDIDATES)
|
|
545
|
+
return undefined;
|
|
546
|
+
const visible = [];
|
|
547
|
+
for (let index = 0; index < count; index += 1) {
|
|
548
|
+
const candidate = count === 1 ? locator : locator.nth?.(index);
|
|
549
|
+
if (candidate === undefined || typeof candidate.isVisible !== "function")
|
|
550
|
+
return undefined;
|
|
551
|
+
try {
|
|
552
|
+
if (await candidate.isVisible())
|
|
553
|
+
visible.push(candidate);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return visible.length === 1 ? visible[0] : undefined;
|
|
560
|
+
}
|
|
561
|
+
function targetForObservation(expected, state) {
|
|
562
|
+
void expected;
|
|
563
|
+
return state.target === undefined ? undefined : buildObservationTarget(state.target, state.authoritativeTabClaim);
|
|
564
|
+
}
|
|
565
|
+
function sendIdentity(state, expected) {
|
|
566
|
+
if (state.operationId === undefined || !isId(state.operationId) || state.requestDigest === undefined || !isDigest(state.requestDigest))
|
|
567
|
+
return undefined;
|
|
568
|
+
if (!isDigest(expected.targetBindingDigest))
|
|
569
|
+
return undefined;
|
|
570
|
+
return state.operationId;
|
|
571
|
+
}
|
|
572
|
+
function stableBaseline(snapshot) {
|
|
573
|
+
return snapshot.completeness === "complete"
|
|
574
|
+
&& snapshot.userTurns.every(turn => turn.stableId !== undefined);
|
|
575
|
+
}
|
|
576
|
+
async function baselineForSnapshot(snapshot, evidenceDigest, operationId, targetBindingDigest) {
|
|
577
|
+
if (snapshot.completeness !== "complete" || !stableBaseline(snapshot))
|
|
578
|
+
return undefined;
|
|
579
|
+
const snapshotDigest = await safeDigestWith(evidenceDigest, "send-baseline-snapshot", {
|
|
580
|
+
operationId,
|
|
581
|
+
targetBindingDigest,
|
|
582
|
+
snapshotDigest: snapshot.snapshotDigest
|
|
583
|
+
}) ?? snapshot.snapshotDigest;
|
|
584
|
+
return {
|
|
585
|
+
schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
|
|
586
|
+
snapshotDigest,
|
|
587
|
+
target: snapshot.target,
|
|
588
|
+
userTurns: snapshot.userTurns,
|
|
589
|
+
assistantTurns: snapshot.assistantTurns,
|
|
590
|
+
completeness: "complete"
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function recoverPendingBlankBaseline(state) {
|
|
594
|
+
const target = state.target;
|
|
595
|
+
if (target === undefined
|
|
596
|
+
|| target.targetLifecycle !== "new_pending"
|
|
597
|
+
|| target.blankTaskEvidenceDigest === undefined
|
|
598
|
+
|| !isDigest(target.blankTaskEvidenceDigest))
|
|
599
|
+
return undefined;
|
|
600
|
+
const observationTarget = buildObservationTarget(target, state.authoritativeTabClaim);
|
|
601
|
+
if (observationTarget === undefined)
|
|
602
|
+
return undefined;
|
|
603
|
+
const available = (value) => ({ status: "available", value });
|
|
604
|
+
const unavailable = () => ({ status: "unavailable", reason: "not_observed" });
|
|
605
|
+
const targetEvidence = {
|
|
606
|
+
provider: available(observationTarget.providerId),
|
|
607
|
+
browser: available(observationTarget.browserId),
|
|
608
|
+
tab: available(observationTarget.tabId),
|
|
609
|
+
thread: unavailable(),
|
|
610
|
+
conversation: unavailable(),
|
|
611
|
+
canonicalThreadUrl: unavailable(),
|
|
612
|
+
authoritativeTabClaim: observationTarget.authoritativeTabClaim === undefined
|
|
613
|
+
? { status: "unavailable", reason: "not_exposed" }
|
|
614
|
+
: available(observationTarget.authoritativeTabClaim),
|
|
615
|
+
coordinationScope: observationTarget.coordinationScope
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
|
|
619
|
+
snapshotDigest: target.blankTaskEvidenceDigest,
|
|
620
|
+
target: targetEvidence,
|
|
621
|
+
userTurns: [],
|
|
622
|
+
assistantTurns: [],
|
|
623
|
+
completeness: "complete"
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function buildObservationTarget(target, authoritativeTabClaim) {
|
|
627
|
+
if (!isId(target.providerId)
|
|
628
|
+
|| !isId(target.browserId)
|
|
629
|
+
|| !isId(target.tabId)) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
const lifecycle = target.targetLifecycle ?? "fixed";
|
|
633
|
+
if (lifecycle !== "new_pending" && (target.conversationId === undefined
|
|
634
|
+
|| !isId(target.conversationId)
|
|
635
|
+
|| target.canonicalThreadUrl === undefined
|
|
636
|
+
|| !OPAQUE_THREAD_URL_PATTERN.test(target.canonicalThreadUrl)))
|
|
637
|
+
return undefined;
|
|
638
|
+
if (lifecycle === "new_pending" && (target.newTargetAnchorDigest === undefined
|
|
639
|
+
|| target.blankTaskEvidenceDigest === undefined
|
|
640
|
+
|| !isDigest(target.newTargetAnchorDigest)
|
|
641
|
+
|| !isDigest(target.blankTaskEvidenceDigest)))
|
|
642
|
+
return undefined;
|
|
643
|
+
if (target.coordinationScope === "provider" && (authoritativeTabClaim === undefined || !isId(authoritativeTabClaim)))
|
|
644
|
+
return undefined;
|
|
645
|
+
return {
|
|
646
|
+
providerId: target.providerId,
|
|
647
|
+
browserId: target.browserId,
|
|
648
|
+
tabId: target.tabId,
|
|
649
|
+
coordinationScope: target.coordinationScope,
|
|
650
|
+
targetLifecycle: lifecycle,
|
|
651
|
+
...(authoritativeTabClaim === undefined ? {} : { authoritativeTabClaim }),
|
|
652
|
+
...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId })
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function makeObservationTarget(target, state) {
|
|
656
|
+
return buildObservationTarget(target, state.authoritativeTabClaim);
|
|
657
|
+
}
|
|
658
|
+
async function readCollectorContext(request, page, target, evidenceDigest, state) {
|
|
659
|
+
if (request.submissionActionId === undefined || !isId(request.submissionActionId))
|
|
660
|
+
throw new ProductionPrimitiveError("submission_witness_unwired");
|
|
661
|
+
// The service projects the authenticated causal baseline into every
|
|
662
|
+
// collect attempt. Use it for this context read as well, so a later
|
|
663
|
+
// observation can request terminal metadata for the exact assistant turn
|
|
664
|
+
// even when a previous wait:false call returned to the caller.
|
|
665
|
+
const baseline = request.baseline;
|
|
666
|
+
if (baseline === undefined)
|
|
667
|
+
throw new ProductionPrimitiveError("target_evidence_unavailable");
|
|
668
|
+
const observationTarget = makeObservationTarget(target, state);
|
|
669
|
+
if (observationTarget === undefined)
|
|
670
|
+
throw new ProductionPrimitiveError("target_evidence_unavailable");
|
|
671
|
+
const observation = await observeBrowserPage(page, {
|
|
672
|
+
operationId: request.operationId,
|
|
673
|
+
target: observationTarget,
|
|
674
|
+
evidenceDigest,
|
|
675
|
+
responseContent: "metadata",
|
|
676
|
+
baseline
|
|
677
|
+
});
|
|
678
|
+
const binding = {
|
|
679
|
+
schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
|
|
680
|
+
operationId: request.operationId,
|
|
681
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
682
|
+
target: observation.snapshot.target,
|
|
683
|
+
evidenceProfile: {
|
|
684
|
+
stableConversationId: "required",
|
|
685
|
+
stableUserTurnId: "required",
|
|
686
|
+
stableAssistantTurnId: "required",
|
|
687
|
+
stableBranchId: "required",
|
|
688
|
+
authoritativeTabClaim: target.coordinationScope === "provider" ? "required" : "unavailable"
|
|
689
|
+
},
|
|
690
|
+
replacementTabRecovery: false,
|
|
691
|
+
actionId: request.submissionActionId,
|
|
692
|
+
actionKind: request.submissionActionKind ?? "send"
|
|
693
|
+
};
|
|
694
|
+
// This cursor is only a candidate from the immediately preceding read. The
|
|
695
|
+
// service keeps the journal baseline/witness authoritative, and the
|
|
696
|
+
// collector reclassifies the next snapshot against both before using it.
|
|
697
|
+
// Never fabricate a cursor when the authenticated witness is absent or the
|
|
698
|
+
// exact delta cannot be classified.
|
|
699
|
+
let prior;
|
|
700
|
+
if (request.submissionWitness !== undefined) {
|
|
701
|
+
try {
|
|
702
|
+
prior = classifyTurnOwnership({
|
|
703
|
+
binding,
|
|
704
|
+
baseline,
|
|
705
|
+
snapshot: observation.snapshot,
|
|
706
|
+
submissionWitness: request.submissionWitness
|
|
707
|
+
}).cursor;
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
prior = undefined;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
binding,
|
|
715
|
+
baseline,
|
|
716
|
+
...(prior === undefined ? {} : { prior })
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
async function observeCollector(request, page, target, context, evidenceDigest, state) {
|
|
720
|
+
const observationTarget = makeObservationTarget(target, state);
|
|
721
|
+
if (observationTarget === undefined)
|
|
722
|
+
throw new ProductionPrimitiveError("target_evidence_unavailable");
|
|
723
|
+
const result = await observeBrowserPage(page, {
|
|
724
|
+
operationId: request.operationId,
|
|
725
|
+
target: observationTarget,
|
|
726
|
+
evidenceDigest,
|
|
727
|
+
responseContent: request.responseContent,
|
|
728
|
+
...(request.responseFormat === undefined ? {} : { responseFormat: request.responseFormat }),
|
|
729
|
+
...(context.baseline === undefined ? {} : { baseline: context.baseline }),
|
|
730
|
+
...(context.prior?.assistantTurnId === undefined ? {} : {
|
|
731
|
+
terminalAssistantTurnId: context.prior.assistantTurnId,
|
|
732
|
+
...(request.responseContent === "include" ? { rawAssistantTurnId: context.prior.assistantTurnId } : {})
|
|
733
|
+
})
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
schemaVersion: COLLECTOR_SCHEMA_VERSION,
|
|
737
|
+
snapshot: result.snapshot,
|
|
738
|
+
...(result.terminal === undefined ? {} : { terminal: result.terminal })
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
async function sleepOutsideBrowser(milliseconds, signal) {
|
|
742
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 60_000)
|
|
743
|
+
throw new ProductionPrimitiveError("invalid_sleep");
|
|
744
|
+
if (signal.aborted)
|
|
745
|
+
throw new ProductionPrimitiveError("operation_cancelled");
|
|
746
|
+
await new Promise((resolve, reject) => {
|
|
747
|
+
const timer = setTimeout(() => {
|
|
748
|
+
signal.removeEventListener("abort", onAbort);
|
|
749
|
+
resolve();
|
|
750
|
+
}, milliseconds);
|
|
751
|
+
const onAbort = () => {
|
|
752
|
+
clearTimeout(timer);
|
|
753
|
+
signal.removeEventListener("abort", onAbort);
|
|
754
|
+
reject(new ProductionPrimitiveError("operation_cancelled"));
|
|
755
|
+
};
|
|
756
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async function observeControlTurn(request, page, target, evidenceDigest, state) {
|
|
760
|
+
const observation = await observeControlSnapshot(request.operationId, page, target, evidenceDigest, state);
|
|
761
|
+
if (observation === undefined)
|
|
762
|
+
return { status: "uncertain", reason: "unavailable" };
|
|
763
|
+
const assistant = observation.snapshot.assistantTurns.find(turn => turn.stableId === request.expectedAssistantTurnId);
|
|
764
|
+
if (assistant === undefined)
|
|
765
|
+
return { status: "mismatch", reason: "different_turn", evidenceDigest: observation.snapshot.snapshotDigest };
|
|
766
|
+
if (assistant.state === "generating") {
|
|
767
|
+
return { status: "generating", assistantTurnId: request.expectedAssistantTurnId, evidenceDigest: assistant.evidenceDigest };
|
|
768
|
+
}
|
|
769
|
+
return { status: "terminal", assistantTurnId: request.expectedAssistantTurnId, reason: "not_generating", evidenceDigest: assistant.evidenceDigest };
|
|
770
|
+
}
|
|
771
|
+
async function executeControlOnce(request, page, target, evidenceDigest, state) {
|
|
772
|
+
if (request.action !== "stop")
|
|
773
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
774
|
+
const locator = await uniqueVisibleLocator(page, stopGenerationButton);
|
|
775
|
+
if (locator === undefined || typeof locator.click !== "function")
|
|
776
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
777
|
+
try {
|
|
778
|
+
// Sole Stop activation. A rejection is not retried and is reconciled by
|
|
779
|
+
// the control coordinator's observation-only path.
|
|
780
|
+
await locator.click();
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
784
|
+
}
|
|
785
|
+
// Release the mutation actor before reading the postcondition. Returning an
|
|
786
|
+
// uncertain result instructs the control coordinator to reacquire the tab
|
|
787
|
+
// through its observation-only port; it must never make this primitive hold
|
|
788
|
+
// the actor across both activation and reconciliation.
|
|
789
|
+
return { status: "uncertain" };
|
|
790
|
+
}
|
|
791
|
+
async function observeControlPostcondition(request, page, target, evidenceDigest, state) {
|
|
792
|
+
if (request.action !== "stop")
|
|
793
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
794
|
+
const observation = await observeControlSnapshot(request.operationId, page, target, evidenceDigest, state);
|
|
795
|
+
if (observation === undefined)
|
|
796
|
+
return { status: "uncertain", blockerCode: "target_evidence_unavailable" };
|
|
797
|
+
const assistant = observation.snapshot.assistantTurns.find(turn => turn.stableId === request.expectedAssistantTurnId);
|
|
798
|
+
if (assistant === undefined)
|
|
799
|
+
return { status: "not_satisfied", blockerCode: "target_binding_mismatch", evidenceDigest: observation.snapshot.snapshotDigest };
|
|
800
|
+
if (assistant.state === "generating")
|
|
801
|
+
return { status: "not_satisfied", blockerCode: "send_control_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
|
|
802
|
+
return { status: "satisfied", assistantTurnId: request.expectedAssistantTurnId, evidenceDigest: assistant.evidenceDigest };
|
|
803
|
+
}
|
|
804
|
+
async function observeControlSnapshot(operationId, page, target, evidenceDigest, state) {
|
|
805
|
+
const observationTarget = makeObservationTarget(target, state);
|
|
806
|
+
if (observationTarget === undefined)
|
|
807
|
+
return undefined;
|
|
808
|
+
try {
|
|
809
|
+
return await observeBrowserPage(page, {
|
|
810
|
+
operationId,
|
|
811
|
+
target: observationTarget,
|
|
812
|
+
evidenceDigest,
|
|
813
|
+
responseContent: "metadata"
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function safeDigestWith(evidenceDigest, domain, material) {
|
|
821
|
+
try {
|
|
822
|
+
const value = await evidenceDigest(domain, material);
|
|
823
|
+
return isDigest(value) ? value : undefined;
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async function digest(evidenceDigest, domain, material) {
|
|
830
|
+
return await safeDigestWith(evidenceDigest, domain, material);
|
|
831
|
+
}
|
|
832
|
+
function isDigest(value) {
|
|
833
|
+
return typeof value === "string" && DIGEST_PATTERN.test(value);
|
|
834
|
+
}
|
|
835
|
+
function isId(value) {
|
|
836
|
+
return typeof value === "string" && ID_PATTERN.test(value);
|
|
837
|
+
}
|
|
838
|
+
function validateOptions(options) {
|
|
839
|
+
if (options === null || typeof options !== "object" || Array.isArray(options) || typeof options.evidenceDigest !== "function") {
|
|
840
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
841
|
+
}
|
|
842
|
+
if (options.operationId !== undefined && !isId(options.operationId))
|
|
843
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
844
|
+
if (options.requestDigest !== undefined && !isDigest(options.requestDigest))
|
|
845
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
846
|
+
if (options.authoritativeTabClaim !== undefined && !isId(options.authoritativeTabClaim))
|
|
847
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
848
|
+
if (options.desiredComposerText !== undefined && typeof options.desiredComposerText !== "string")
|
|
849
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
850
|
+
if (options.composerText !== undefined && typeof options.composerText !== "string")
|
|
851
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
852
|
+
if (options.observeAttachments !== undefined && typeof options.observeAttachments !== "function")
|
|
853
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
854
|
+
if (options.target !== undefined && (options.target === null || typeof options.target !== "object" || Array.isArray(options.target)))
|
|
855
|
+
throw new ProductionPrimitiveError("invalid_options");
|
|
856
|
+
}
|