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,700 @@
|
|
|
1
|
+
import { canonicalJson } from "./canonical.js";
|
|
2
|
+
/**
|
|
3
|
+
* The turn ownership classifier deliberately accepts normalized, already
|
|
4
|
+
* redacted observations. It must never be handed DOM nodes or prompt text.
|
|
5
|
+
* Adapters are responsible for producing the bounded digests in these types.
|
|
6
|
+
*/
|
|
7
|
+
export const TURN_OWNERSHIP_SCHEMA_VERSION = "chatgpt.browser_control.turn_ownership.v1";
|
|
8
|
+
const MAX_IDENTIFIER_LENGTH = 512;
|
|
9
|
+
const MAX_DIGEST_LENGTH = 512;
|
|
10
|
+
const MAX_TURNS = 256;
|
|
11
|
+
const MAX_ARTIFACTS_PER_TURN = 32;
|
|
12
|
+
const DIGEST_PATTERN = /^[a-z][a-z0-9-]{1,31}:[A-Za-z0-9_-]{16,512}$/;
|
|
13
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
|
|
14
|
+
/**
|
|
15
|
+
* Classify one normalized snapshot. This function has no browser, DOM, or
|
|
16
|
+
* I/O dependency. It only permits an owned user turn when the adapter has
|
|
17
|
+
* supplied the exact post-Send delta digest and operation-specific evidence.
|
|
18
|
+
*/
|
|
19
|
+
export function classifyTurnOwnership(input) {
|
|
20
|
+
validateInput(input);
|
|
21
|
+
const { binding, baseline, snapshot, submissionWitness, prior } = input;
|
|
22
|
+
const target = compareBindingToBaselineTarget(binding, baseline, submissionWitness);
|
|
23
|
+
if (target.status === "mismatch" || target.status === "replacement") {
|
|
24
|
+
return result(input, "target_mismatch", "baseline_target_mismatch", target, undefined, undefined, undefined, 0, 0);
|
|
25
|
+
}
|
|
26
|
+
if (target.status === "unavailable") {
|
|
27
|
+
return result(input, "target_evidence_unavailable", "target_evidence_unavailable", target, undefined, undefined, undefined, 0, 0);
|
|
28
|
+
}
|
|
29
|
+
const freshTarget = compareBindingToTarget(binding, snapshot.target);
|
|
30
|
+
if (freshTarget.status === "mismatch") {
|
|
31
|
+
return result(input, "target_mismatch", "target_binding_mismatch", freshTarget, undefined, undefined, undefined, 0, 0);
|
|
32
|
+
}
|
|
33
|
+
if (freshTarget.status === "unavailable") {
|
|
34
|
+
return result(input, "target_evidence_unavailable", "target_evidence_unavailable", freshTarget, undefined, undefined, undefined, 0, 0);
|
|
35
|
+
}
|
|
36
|
+
if (snapshot.completeness !== "complete") {
|
|
37
|
+
const reason = snapshot.completeness === "out_of_order"
|
|
38
|
+
? "out_of_order_snapshot"
|
|
39
|
+
: "incomplete_snapshot";
|
|
40
|
+
return result(input, "ownership_ambiguous", reason, freshTarget, undefined, undefined, undefined, 0, 0);
|
|
41
|
+
}
|
|
42
|
+
const users = analyzeDelta(baseline.userTurns, snapshot.userTurns);
|
|
43
|
+
const assistants = analyzeDelta(baseline.assistantTurns, snapshot.assistantTurns);
|
|
44
|
+
if (!users.valid) {
|
|
45
|
+
return result(input, "ownership_ambiguous", users.reason ?? "baseline_turn_missing", freshTarget, undefined, undefined, undefined, users.added.length, assistants.added.length);
|
|
46
|
+
}
|
|
47
|
+
if (!assistants.valid) {
|
|
48
|
+
return result(input, "ownership_ambiguous", assistants.reason ?? "baseline_turn_missing", freshTarget, undefined, undefined, undefined, users.added.length, assistants.added.length);
|
|
49
|
+
}
|
|
50
|
+
if (prior !== undefined) {
|
|
51
|
+
return classifyWithPrior(input, freshTarget, users, assistants, prior);
|
|
52
|
+
}
|
|
53
|
+
if (freshTarget.status === "replacement") {
|
|
54
|
+
// A replacement tab is never trusted merely because it has the same URL.
|
|
55
|
+
// The exact operation-owned user evidence below is the recovery anchor.
|
|
56
|
+
if (users.added.length === 0) {
|
|
57
|
+
return result(input, "target_mismatch", "replacement_tab_requires_owned_user", freshTarget, undefined, undefined, undefined, users.added.length, assistants.added.length);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (users.intervening) {
|
|
61
|
+
return result(input, "concurrent_user_turn", "intervening_user_turn", freshTarget, users.added[0], undefined, undefined, users.added.length, assistants.added.length);
|
|
62
|
+
}
|
|
63
|
+
if (users.added.length === 0) {
|
|
64
|
+
if (snapshot.postSendDelta !== undefined
|
|
65
|
+
&& (snapshot.postSendDelta.baselineSnapshotDigest !== baseline.snapshotDigest
|
|
66
|
+
|| snapshot.postSendDelta.addedUserEvidenceDigests.length !== 0)) {
|
|
67
|
+
return result(input, "ownership_ambiguous", "post_send_delta_mismatch", freshTarget, undefined, assistants.added[0], undefined, 0, assistants.added.length);
|
|
68
|
+
}
|
|
69
|
+
if (assistants.added.length > 0) {
|
|
70
|
+
return result(input, "ownership_ambiguous", "unexpected_new_assistant_turn", freshTarget, undefined, assistants.added[0], undefined, 0, assistants.added.length);
|
|
71
|
+
}
|
|
72
|
+
return result(input, "no_operation_turn", "none", freshTarget, undefined, undefined, undefined, 0, 0);
|
|
73
|
+
}
|
|
74
|
+
if (users.added.length > 1) {
|
|
75
|
+
return result(input, "concurrent_user_turn", "multiple_new_user_turns", freshTarget, users.added[0], undefined, undefined, users.added.length, assistants.added.length);
|
|
76
|
+
}
|
|
77
|
+
const user = users.added[0];
|
|
78
|
+
if (user === undefined) {
|
|
79
|
+
return result(input, "ownership_ambiguous", "stable_user_turn_id_unavailable", freshTarget, undefined, undefined, undefined, 0, assistants.added.length);
|
|
80
|
+
}
|
|
81
|
+
const proof = proveSubmittedUser(input, user);
|
|
82
|
+
if (!proof.ok) {
|
|
83
|
+
return result(input, proof.status, proof.reason, freshTarget, user, assistants.added[0], undefined, users.added.length, assistants.added.length);
|
|
84
|
+
}
|
|
85
|
+
return classifyFreshOwnedUser(input, freshTarget, user, assistants);
|
|
86
|
+
}
|
|
87
|
+
/** Return the deterministic, redacted JSON input a caller may HMAC. */
|
|
88
|
+
export function ownershipEvidenceJson(material) {
|
|
89
|
+
return canonicalJson(material);
|
|
90
|
+
}
|
|
91
|
+
/** Validate a cursor before persisting or supplying it to a later snapshot. */
|
|
92
|
+
export function validateOwnershipCursor(cursor) {
|
|
93
|
+
if (!isRecord(cursor))
|
|
94
|
+
throw new TypeError("Ownership cursor must be an object.");
|
|
95
|
+
assertExactKeys(cursor, "cursor", ["schemaVersion", "operationId", "targetBindingDigest", "phase", "userTurnId", "userTurnEvidenceDigest", "assistantTurnId", "assistantBranchId", "snapshotDigest", "assistantEvidenceDigest", "assistantStructureDigest"], ["schemaVersion", "operationId", "targetBindingDigest", "phase", "userTurnId", "userTurnEvidenceDigest", "snapshotDigest"]);
|
|
96
|
+
if (cursor.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION)
|
|
97
|
+
throw new TypeError("Unsupported ownership cursor schema version.");
|
|
98
|
+
if (cursor.phase !== "owned_user_turn" && cursor.phase !== "owned_assistant_generating" && cursor.phase !== "owned_assistant_terminal") {
|
|
99
|
+
throw new TypeError("Ownership cursor phase is invalid.");
|
|
100
|
+
}
|
|
101
|
+
assertIdentifier(cursor.operationId, "cursor.operationId");
|
|
102
|
+
assertDigest(cursor.targetBindingDigest, "cursor.targetBindingDigest");
|
|
103
|
+
assertIdentifier(cursor.userTurnId, "cursor.userTurnId");
|
|
104
|
+
assertDigest(cursor.userTurnEvidenceDigest, "cursor.userTurnEvidenceDigest");
|
|
105
|
+
assertDigest(cursor.snapshotDigest, "cursor.snapshotDigest");
|
|
106
|
+
if (cursor.phase === "owned_user_turn") {
|
|
107
|
+
if (cursor.assistantTurnId !== undefined || cursor.assistantBranchId !== undefined
|
|
108
|
+
|| cursor.assistantEvidenceDigest !== undefined || cursor.assistantStructureDigest !== undefined) {
|
|
109
|
+
throw new TypeError("User-turn cursor cannot contain assistant identity.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (cursor.assistantTurnId === undefined || cursor.assistantBranchId === undefined
|
|
114
|
+
|| cursor.assistantEvidenceDigest === undefined || cursor.assistantStructureDigest === undefined) {
|
|
115
|
+
throw new TypeError("Assistant cursor requires stable assistant, branch, and evidence identities.");
|
|
116
|
+
}
|
|
117
|
+
assertIdentifier(cursor.assistantTurnId, "cursor.assistantTurnId");
|
|
118
|
+
assertIdentifier(cursor.assistantBranchId, "cursor.assistantBranchId");
|
|
119
|
+
assertDigest(cursor.assistantEvidenceDigest, "cursor.assistantEvidenceDigest");
|
|
120
|
+
assertDigest(cursor.assistantStructureDigest, "cursor.assistantStructureDigest");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function classifyWithPrior(input, target, users, assistants, prior) {
|
|
124
|
+
const { binding, snapshot } = input;
|
|
125
|
+
if (prior.operationId !== binding.operationId || prior.targetBindingDigest !== binding.targetBindingDigest) {
|
|
126
|
+
return result(input, "ownership_ambiguous", "prior_cursor_mismatch", target, undefined, undefined, undefined, users.added.length, assistants.added.length);
|
|
127
|
+
}
|
|
128
|
+
const user = snapshot.userTurns.find(turn => turn.stableId === prior.userTurnId);
|
|
129
|
+
if (user === undefined) {
|
|
130
|
+
return result(input, "ownership_ambiguous", "prior_owned_turn_missing", target, undefined, undefined, undefined, users.added.length, assistants.added.length);
|
|
131
|
+
}
|
|
132
|
+
if (user.evidenceDigest !== prior.userTurnEvidenceDigest) {
|
|
133
|
+
return result(input, "ownership_ambiguous", "prior_owned_turn_changed", target, user, undefined, undefined, users.added.length, assistants.added.length);
|
|
134
|
+
}
|
|
135
|
+
if (users.added.length !== 1 || users.added[0]?.stableId !== prior.userTurnId) {
|
|
136
|
+
return result(input, "concurrent_user_turn", "multiple_new_user_turns", target, user, undefined, undefined, users.added.length, assistants.added.length);
|
|
137
|
+
}
|
|
138
|
+
if (target.status === "replacement" && !hasStableConversationAndClaim(binding, snapshot)) {
|
|
139
|
+
return result(input, "target_evidence_unavailable", "target_evidence_unavailable", target, user, undefined, undefined, users.added.length, assistants.added.length);
|
|
140
|
+
}
|
|
141
|
+
const ownedAssistant = snapshot.assistantTurns.find(turn => turn.stableId === prior.assistantTurnId);
|
|
142
|
+
if (prior.phase !== "owned_user_turn") {
|
|
143
|
+
if (ownedAssistant === undefined) {
|
|
144
|
+
return result(input, "ownership_ambiguous", "prior_owned_turn_missing", target, user, undefined, undefined, users.added.length, assistants.added.length);
|
|
145
|
+
}
|
|
146
|
+
const unexpected = assistants.added.filter(turn => turn.stableId !== prior.assistantTurnId);
|
|
147
|
+
if (unexpected.length > 0) {
|
|
148
|
+
return result(input, "regeneration_ambiguous", "regeneration_siblings", target, user, unexpected[0], undefined, users.added.length, assistants.added.length);
|
|
149
|
+
}
|
|
150
|
+
if (ownedAssistant.parentStableId !== user.stableId || ownedAssistant.branchStableId !== prior.assistantBranchId) {
|
|
151
|
+
return result(input, "ownership_ambiguous", "stable_parent_identity_unavailable", target, user, ownedAssistant, undefined, users.added.length, assistants.added.length);
|
|
152
|
+
}
|
|
153
|
+
if (prior.phase === "owned_assistant_terminal"
|
|
154
|
+
&& (ownedAssistant.state !== "terminal"
|
|
155
|
+
|| ownedAssistant.evidenceDigest !== prior.assistantEvidenceDigest
|
|
156
|
+
|| ownedAssistant.structureDigest !== prior.assistantStructureDigest)) {
|
|
157
|
+
return result(input, "ownership_ambiguous", "prior_owned_turn_changed", target, user, ownedAssistant, undefined, users.added.length, assistants.added.length);
|
|
158
|
+
}
|
|
159
|
+
const phase = assistantPhase(input.binding, input.snapshot, ownedAssistant);
|
|
160
|
+
if (phase === undefined) {
|
|
161
|
+
return result(input, "ownership_ambiguous", "terminal_state_mismatch", target, user, ownedAssistant, undefined, users.added.length, assistants.added.length);
|
|
162
|
+
}
|
|
163
|
+
const cursor = makeCursor(binding, user, ownedAssistant, phase, snapshot.snapshotDigest);
|
|
164
|
+
return result(input, phase, "none", target, user, ownedAssistant, cursor, users.added.length, assistants.added.length);
|
|
165
|
+
}
|
|
166
|
+
const children = assistants.added.filter(turn => turn.parentStableId === user.stableId);
|
|
167
|
+
const unrelated = assistants.added.filter(turn => turn.parentStableId !== user.stableId);
|
|
168
|
+
if (unrelated.length > 0) {
|
|
169
|
+
return result(input, "ownership_ambiguous", "assistant_parent_mismatch", target, user, unrelated[0], undefined, users.added.length, assistants.added.length);
|
|
170
|
+
}
|
|
171
|
+
if (children.length === 0) {
|
|
172
|
+
if (snapshot.terminalState === "terminal") {
|
|
173
|
+
return result(input, "ownership_ambiguous", "terminal_state_mismatch", target, user, undefined, undefined, users.added.length, 0);
|
|
174
|
+
}
|
|
175
|
+
const cursor = makeCursor(binding, user, undefined, "owned_user_turn", snapshot.snapshotDigest);
|
|
176
|
+
return result(input, "owned_user_turn", "none", target, user, undefined, cursor, users.added.length, 0);
|
|
177
|
+
}
|
|
178
|
+
if (children.length > 1) {
|
|
179
|
+
return result(input, "regeneration_ambiguous", "regeneration_siblings", target, user, children[0], undefined, users.added.length, assistants.added.length);
|
|
180
|
+
}
|
|
181
|
+
const assistant = children[0];
|
|
182
|
+
if (assistant === undefined) {
|
|
183
|
+
return result(input, "ownership_ambiguous", "stable_assistant_turn_id_unavailable", target, user, undefined, undefined, users.added.length, assistants.added.length);
|
|
184
|
+
}
|
|
185
|
+
const phase = assistantPhase(binding, snapshot, assistant);
|
|
186
|
+
if (phase === undefined) {
|
|
187
|
+
return result(input, "ownership_ambiguous", assistant.state === undefined ? "assistant_state_unknown" : "terminal_state_mismatch", target, user, assistant, undefined, users.added.length, assistants.added.length);
|
|
188
|
+
}
|
|
189
|
+
const cursor = makeCursor(binding, user, assistant, phase, snapshot.snapshotDigest);
|
|
190
|
+
return result(input, phase, "none", target, user, assistant, cursor, users.added.length, assistants.added.length);
|
|
191
|
+
}
|
|
192
|
+
function proveSubmittedUser(input, user) {
|
|
193
|
+
const { binding, baseline, snapshot, submissionWitness } = input;
|
|
194
|
+
if (user.stableId === undefined || binding.evidenceProfile.stableUserTurnId === "unavailable") {
|
|
195
|
+
return { ok: false, status: "ownership_ambiguous", reason: "stable_user_turn_id_unavailable" };
|
|
196
|
+
}
|
|
197
|
+
if (submissionWitness === undefined)
|
|
198
|
+
return { ok: false, status: "ownership_ambiguous", reason: "post_send_delta_missing" };
|
|
199
|
+
if (submissionWitness.actionId !== binding.actionId || submissionWitness.actionKind !== binding.actionKind)
|
|
200
|
+
return { ok: false, status: "ownership_ambiguous", reason: "operation_user_evidence_mismatch" };
|
|
201
|
+
if (submissionWitness.baselineSnapshotDigest !== baseline.snapshotDigest)
|
|
202
|
+
return { ok: false, status: "ownership_ambiguous", reason: "post_send_delta_mismatch" };
|
|
203
|
+
const delta = snapshot.postSendDelta;
|
|
204
|
+
if (delta === undefined)
|
|
205
|
+
return { ok: false, status: "ownership_ambiguous", reason: "post_send_delta_missing" };
|
|
206
|
+
if (delta.baselineSnapshotDigest !== baseline.snapshotDigest)
|
|
207
|
+
return { ok: false, status: "ownership_ambiguous", reason: "post_send_delta_mismatch" };
|
|
208
|
+
if (delta.deltaDigest !== submissionWitness.postSendDeltaDigest)
|
|
209
|
+
return { ok: false, status: "ownership_ambiguous", reason: "post_send_delta_mismatch" };
|
|
210
|
+
if (delta.addedUserEvidenceDigests.length !== 1 || delta.addedUserEvidenceDigests[0] !== user.evidenceDigest) {
|
|
211
|
+
return { ok: false, status: "concurrent_user_turn", reason: "multiple_new_user_turns" };
|
|
212
|
+
}
|
|
213
|
+
if (user.evidenceDigest !== submissionWitness.operationUserEvidenceDigest) {
|
|
214
|
+
return { ok: false, status: "concurrent_user_turn", reason: "operation_user_evidence_mismatch" };
|
|
215
|
+
}
|
|
216
|
+
if (submissionWitness.userTurnStableId !== undefined && submissionWitness.userTurnStableId !== user.stableId) {
|
|
217
|
+
return { ok: false, status: "concurrent_user_turn", reason: "operation_user_evidence_mismatch" };
|
|
218
|
+
}
|
|
219
|
+
return { ok: true };
|
|
220
|
+
}
|
|
221
|
+
function classifyFreshOwnedUser(input, target, user, assistants) {
|
|
222
|
+
const { binding, snapshot } = input;
|
|
223
|
+
const children = assistants.added.filter(turn => turn.parentStableId === user.stableId);
|
|
224
|
+
const unrelated = assistants.added.filter(turn => turn.parentStableId !== user.stableId);
|
|
225
|
+
if (unrelated.length > 0) {
|
|
226
|
+
return result(input, "ownership_ambiguous", "assistant_parent_mismatch", target, user, unrelated[0], undefined, 1, assistants.added.length);
|
|
227
|
+
}
|
|
228
|
+
if (children.length === 0) {
|
|
229
|
+
if (snapshot.terminalState === "terminal") {
|
|
230
|
+
return result(input, "ownership_ambiguous", "terminal_state_mismatch", target, user, undefined, undefined, 1, 0);
|
|
231
|
+
}
|
|
232
|
+
const cursor = makeCursor(binding, user, undefined, "owned_user_turn", snapshot.snapshotDigest);
|
|
233
|
+
return result(input, "owned_user_turn", "none", target, user, undefined, cursor, 1, 0);
|
|
234
|
+
}
|
|
235
|
+
if (children.length > 1) {
|
|
236
|
+
return result(input, "regeneration_ambiguous", "regeneration_siblings", target, user, children[0], undefined, 1, assistants.added.length);
|
|
237
|
+
}
|
|
238
|
+
const assistant = children[0];
|
|
239
|
+
if (assistant === undefined) {
|
|
240
|
+
return result(input, "ownership_ambiguous", "stable_assistant_turn_id_unavailable", target, user, undefined, undefined, 1, assistants.added.length);
|
|
241
|
+
}
|
|
242
|
+
const phase = assistantPhase(binding, snapshot, assistant);
|
|
243
|
+
if (phase === undefined) {
|
|
244
|
+
const reason = assistant.state === undefined
|
|
245
|
+
? "assistant_state_unknown"
|
|
246
|
+
: assistant.parentStableId === undefined
|
|
247
|
+
? "stable_parent_identity_unavailable"
|
|
248
|
+
: assistant.branchStableId === undefined
|
|
249
|
+
? "stable_branch_identity_unavailable"
|
|
250
|
+
: "terminal_state_mismatch";
|
|
251
|
+
return result(input, "ownership_ambiguous", reason, target, user, assistant, undefined, 1, assistants.added.length);
|
|
252
|
+
}
|
|
253
|
+
const cursor = makeCursor(binding, user, assistant, phase, snapshot.snapshotDigest);
|
|
254
|
+
return result(input, phase, "none", target, user, assistant, cursor, 1, assistants.added.length);
|
|
255
|
+
}
|
|
256
|
+
function assistantPhase(binding, snapshot, assistant) {
|
|
257
|
+
if (binding.evidenceProfile.stableAssistantTurnId === "unavailable" || assistant.stableId === undefined)
|
|
258
|
+
return undefined;
|
|
259
|
+
if (binding.evidenceProfile.stableBranchId === "unavailable" || assistant.branchStableId === undefined || assistant.parentStableId === undefined)
|
|
260
|
+
return undefined;
|
|
261
|
+
if (assistant.state === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
if (assistant.state === "generating") {
|
|
264
|
+
return snapshot.terminalState === "generating" ? "owned_assistant_generating" : undefined;
|
|
265
|
+
}
|
|
266
|
+
return snapshot.terminalState === "terminal" ? "owned_assistant_terminal" : undefined;
|
|
267
|
+
}
|
|
268
|
+
function makeCursor(binding, user, assistant, phase, snapshotDigest) {
|
|
269
|
+
if (user.stableId === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
if (assistant !== undefined && (assistant.stableId === undefined || assistant.branchStableId === undefined))
|
|
272
|
+
return undefined;
|
|
273
|
+
const cursor = {
|
|
274
|
+
schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
|
|
275
|
+
operationId: binding.operationId,
|
|
276
|
+
targetBindingDigest: binding.targetBindingDigest,
|
|
277
|
+
phase,
|
|
278
|
+
userTurnId: user.stableId,
|
|
279
|
+
userTurnEvidenceDigest: user.evidenceDigest,
|
|
280
|
+
...(assistant === undefined ? {} : {
|
|
281
|
+
assistantTurnId: assistant.stableId,
|
|
282
|
+
assistantBranchId: assistant.branchStableId,
|
|
283
|
+
assistantEvidenceDigest: assistant.evidenceDigest,
|
|
284
|
+
assistantStructureDigest: assistant.structureDigest
|
|
285
|
+
}),
|
|
286
|
+
snapshotDigest
|
|
287
|
+
};
|
|
288
|
+
validateOwnershipCursor(cursor);
|
|
289
|
+
return Object.freeze(cursor);
|
|
290
|
+
}
|
|
291
|
+
function result(input, status, reason, target, user, assistant, cursor, addedUserCount, addedAssistantCount) {
|
|
292
|
+
const evidence = freezeEvidence({
|
|
293
|
+
schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
|
|
294
|
+
operationId: input.binding.operationId,
|
|
295
|
+
status,
|
|
296
|
+
reason,
|
|
297
|
+
target: targetMaterial(input.binding, input.snapshot.target, target),
|
|
298
|
+
baselineSnapshotDigest: input.baseline.snapshotDigest,
|
|
299
|
+
snapshotDigest: input.snapshot.snapshotDigest,
|
|
300
|
+
...(input.snapshot.postSendDelta === undefined ? {} : { postSendDeltaDigest: input.snapshot.postSendDelta.deltaDigest }),
|
|
301
|
+
addedUserCount,
|
|
302
|
+
addedAssistantCount,
|
|
303
|
+
terminalState: input.snapshot.terminalState,
|
|
304
|
+
...(user === undefined ? {} : { userTurn: turnMaterial(user) }),
|
|
305
|
+
...(assistant === undefined ? {} : { assistantTurn: turnMaterial(assistant) })
|
|
306
|
+
});
|
|
307
|
+
const recoveryObservation = status === "no_operation_turn"
|
|
308
|
+
? { status: "not_observed" }
|
|
309
|
+
: status === "owned_user_turn" && user?.stableId !== undefined
|
|
310
|
+
? { status: "owned_user_turn", userTurnId: user.stableId, evidenceDigest: input.snapshot.snapshotDigest }
|
|
311
|
+
: status === "owned_assistant_generating" && user?.stableId !== undefined && assistant?.stableId !== undefined
|
|
312
|
+
? { status: "owned_assistant_generating", userTurnId: user.stableId, assistantTurnId: assistant.stableId, evidenceDigest: input.snapshot.snapshotDigest }
|
|
313
|
+
: status === "owned_assistant_terminal" && user?.stableId !== undefined && assistant?.stableId !== undefined
|
|
314
|
+
? { status: "owned_assistant_terminal", userTurnId: user.stableId, assistantTurnId: assistant.stableId, evidenceDigest: input.snapshot.snapshotDigest }
|
|
315
|
+
: { status: "ambiguous", evidenceDigest: input.snapshot.snapshotDigest };
|
|
316
|
+
return Object.freeze({
|
|
317
|
+
status,
|
|
318
|
+
reason,
|
|
319
|
+
evidence,
|
|
320
|
+
recoveryObservation,
|
|
321
|
+
...(cursor === undefined ? {} : { cursor })
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function targetMaterial(binding, snapshotTarget, check) {
|
|
325
|
+
return {
|
|
326
|
+
provider: snapshotTarget.provider.status,
|
|
327
|
+
browser: snapshotTarget.browser.status,
|
|
328
|
+
tab: snapshotTarget.tab.status,
|
|
329
|
+
thread: snapshotTarget.thread.status,
|
|
330
|
+
conversation: snapshotTarget.conversation.status,
|
|
331
|
+
canonicalThreadUrl: snapshotTarget.canonicalThreadUrl.status,
|
|
332
|
+
authoritativeTabClaim: snapshotTarget.authoritativeTabClaim.status,
|
|
333
|
+
replacedTab: check.replacedTab && binding.replacementTabRecovery
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function turnMaterial(turn) {
|
|
337
|
+
return {
|
|
338
|
+
evidenceDigest: turn.evidenceDigest,
|
|
339
|
+
structureDigest: turn.structureDigest,
|
|
340
|
+
ordinal: turn.ordinal,
|
|
341
|
+
stableIdAvailable: turn.stableId !== undefined,
|
|
342
|
+
parentStableIdAvailable: turn.parentStableId !== undefined,
|
|
343
|
+
branchStableIdAvailable: turn.branchStableId !== undefined,
|
|
344
|
+
artifactEvidenceDigests: Object.freeze([...(turn.artifactEvidenceDigests ?? [])]),
|
|
345
|
+
...(turn.state === undefined ? {} : { state: turn.state })
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function freezeEvidence(material) {
|
|
349
|
+
const target = Object.freeze({ ...material.target });
|
|
350
|
+
const userTurn = material.userTurn === undefined ? undefined : Object.freeze({ ...material.userTurn, artifactEvidenceDigests: Object.freeze([...material.userTurn.artifactEvidenceDigests]) });
|
|
351
|
+
const assistantTurn = material.assistantTurn === undefined ? undefined : Object.freeze({ ...material.assistantTurn, artifactEvidenceDigests: Object.freeze([...material.assistantTurn.artifactEvidenceDigests]) });
|
|
352
|
+
return Object.freeze({ ...material, target, ...(userTurn === undefined ? {} : { userTurn }), ...(assistantTurn === undefined ? {} : { assistantTurn }) });
|
|
353
|
+
}
|
|
354
|
+
function compareBindingToTarget(binding, target) {
|
|
355
|
+
const bound = binding.target;
|
|
356
|
+
if (bound.coordinationScope !== target.coordinationScope)
|
|
357
|
+
return { status: "mismatch", replacedTab: false };
|
|
358
|
+
const exactKeys = [
|
|
359
|
+
"provider",
|
|
360
|
+
"browser",
|
|
361
|
+
"thread",
|
|
362
|
+
"conversation",
|
|
363
|
+
"canonicalThreadUrl"
|
|
364
|
+
];
|
|
365
|
+
for (const key of exactKeys) {
|
|
366
|
+
const left = bound[key];
|
|
367
|
+
const right = target[key];
|
|
368
|
+
if (left.status === "unavailable" || right.status === "unavailable") {
|
|
369
|
+
// An explicitly unavailable optional identity is only safe when both
|
|
370
|
+
// sides agree that it is unavailable. A newly observed value cannot
|
|
371
|
+
// silently bypass the operation's exact binding.
|
|
372
|
+
if (left.status === "unavailable" && right.status === "unavailable"
|
|
373
|
+
&& (key === "thread" || key === "conversation" || key === "canonicalThreadUrl"))
|
|
374
|
+
continue;
|
|
375
|
+
return { status: "unavailable", replacedTab: false };
|
|
376
|
+
}
|
|
377
|
+
if (left.value !== right.value)
|
|
378
|
+
return { status: "mismatch", replacedTab: false };
|
|
379
|
+
}
|
|
380
|
+
if (bound.tab.status === "unavailable" || target.tab.status === "unavailable")
|
|
381
|
+
return { status: "unavailable", replacedTab: false };
|
|
382
|
+
if (binding.evidenceProfile.authoritativeTabClaim === "required"
|
|
383
|
+
&& (bound.authoritativeTabClaim.status === "unavailable" || target.authoritativeTabClaim.status === "unavailable")) {
|
|
384
|
+
return { status: "unavailable", replacedTab: false };
|
|
385
|
+
}
|
|
386
|
+
if (bound.tab.value !== target.tab.value) {
|
|
387
|
+
if (!binding.replacementTabRecovery)
|
|
388
|
+
return { status: "mismatch", replacedTab: false };
|
|
389
|
+
if (binding.evidenceProfile.stableConversationId !== "required")
|
|
390
|
+
return { status: "unavailable", replacedTab: false };
|
|
391
|
+
if (target.authoritativeTabClaim.status === "unavailable")
|
|
392
|
+
return { status: "unavailable", replacedTab: false };
|
|
393
|
+
return { status: "replacement", replacedTab: true };
|
|
394
|
+
}
|
|
395
|
+
if (binding.evidenceProfile.authoritativeTabClaim === "required"
|
|
396
|
+
&& bound.authoritativeTabClaim.status === "available"
|
|
397
|
+
&& target.authoritativeTabClaim.status === "available"
|
|
398
|
+
&& bound.authoritativeTabClaim.value !== target.authoritativeTabClaim.value) {
|
|
399
|
+
return { status: "mismatch", replacedTab: false };
|
|
400
|
+
}
|
|
401
|
+
return { status: "match", replacedTab: false };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* A genuine new target has no provider conversation identity in its durable
|
|
405
|
+
* pre-Send baseline. After the journal accepts the one-way establishment and
|
|
406
|
+
* exact post-Send witness, collection may compare the established binding to
|
|
407
|
+
* the live snapshot without pretending that identity existed before Send.
|
|
408
|
+
*/
|
|
409
|
+
function compareBindingToBaselineTarget(binding, baseline, submissionWitness) {
|
|
410
|
+
const direct = compareBindingToTarget(binding, baseline.target);
|
|
411
|
+
if (direct.status !== "unavailable")
|
|
412
|
+
return direct;
|
|
413
|
+
const pending = baseline.target;
|
|
414
|
+
const established = binding.target;
|
|
415
|
+
const pendingConversationIdentity = pending.thread.status === "unavailable"
|
|
416
|
+
&& pending.conversation.status === "unavailable"
|
|
417
|
+
&& pending.canonicalThreadUrl.status === "unavailable";
|
|
418
|
+
const establishedConversationIdentity = established.thread.status === "available"
|
|
419
|
+
&& established.conversation.status === "available"
|
|
420
|
+
&& established.canonicalThreadUrl.status === "available";
|
|
421
|
+
const exactWitness = submissionWitness !== undefined
|
|
422
|
+
&& submissionWitness.actionId === binding.actionId
|
|
423
|
+
&& submissionWitness.actionKind === "send"
|
|
424
|
+
&& binding.actionKind === "send"
|
|
425
|
+
&& submissionWitness.baselineSnapshotDigest === baseline.snapshotDigest;
|
|
426
|
+
if (!pendingConversationIdentity
|
|
427
|
+
|| !establishedConversationIdentity
|
|
428
|
+
|| baseline.userTurns.length !== 0
|
|
429
|
+
|| baseline.assistantTurns.length !== 0
|
|
430
|
+
|| binding.evidenceProfile.stableConversationId !== "required"
|
|
431
|
+
|| binding.evidenceProfile.stableUserTurnId !== "required"
|
|
432
|
+
|| !exactWitness)
|
|
433
|
+
return direct;
|
|
434
|
+
return compareBindingToTarget(binding, Object.freeze({
|
|
435
|
+
...pending,
|
|
436
|
+
thread: established.thread,
|
|
437
|
+
conversation: established.conversation,
|
|
438
|
+
canonicalThreadUrl: established.canonicalThreadUrl
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
function hasStableConversationAndClaim(binding, snapshot) {
|
|
442
|
+
return binding.evidenceProfile.stableConversationId === "required"
|
|
443
|
+
&& snapshot.target.conversation.status === "available"
|
|
444
|
+
&& snapshot.target.thread.status === "available"
|
|
445
|
+
&& snapshot.target.authoritativeTabClaim.status === "available";
|
|
446
|
+
}
|
|
447
|
+
function analyzeDelta(baseline, fresh) {
|
|
448
|
+
const baselineIndices = [];
|
|
449
|
+
const used = new Set();
|
|
450
|
+
const freshByIdentity = new Map();
|
|
451
|
+
for (let index = 0; index < fresh.length; index += 1) {
|
|
452
|
+
const turn = fresh[index];
|
|
453
|
+
if (turn === undefined)
|
|
454
|
+
continue;
|
|
455
|
+
// Validation has already rejected duplicates in one snapshot. Keeping a
|
|
456
|
+
// map here makes the delta proof O(n), rather than scanning the page for
|
|
457
|
+
// every baseline turn.
|
|
458
|
+
freshByIdentity.set(turnIdentityKey(turn), { index, turn });
|
|
459
|
+
}
|
|
460
|
+
for (const prior of baseline) {
|
|
461
|
+
const found = freshByIdentity.get(turnIdentityKey(prior));
|
|
462
|
+
if (found === undefined || used.has(found.index))
|
|
463
|
+
return { valid: false, reason: "baseline_turn_missing", added: [], intervening: false };
|
|
464
|
+
const match = found.index;
|
|
465
|
+
const matched = found.turn;
|
|
466
|
+
if (prior.evidenceDigest !== matched.evidenceDigest || prior.structureDigest !== matched.structureDigest) {
|
|
467
|
+
return { valid: false, reason: "baseline_turn_changed", added: [], intervening: false };
|
|
468
|
+
}
|
|
469
|
+
used.add(match);
|
|
470
|
+
baselineIndices.push(match);
|
|
471
|
+
}
|
|
472
|
+
for (let index = 1; index < baselineIndices.length; index += 1) {
|
|
473
|
+
if ((baselineIndices[index - 1] ?? -1) >= (baselineIndices[index] ?? -1)) {
|
|
474
|
+
return { valid: false, reason: "out_of_order_snapshot", added: [], intervening: false };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const added = [];
|
|
478
|
+
let baselineCursor = 0;
|
|
479
|
+
let intervening = false;
|
|
480
|
+
for (let index = 0; index < fresh.length; index += 1) {
|
|
481
|
+
if (baselineCursor < baselineIndices.length && baselineIndices[baselineCursor] === index) {
|
|
482
|
+
baselineCursor += 1;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const turn = fresh[index];
|
|
486
|
+
if (turn !== undefined)
|
|
487
|
+
added.push(turn);
|
|
488
|
+
if (baselineIndices.length > 0) {
|
|
489
|
+
const firstBaseline = baselineIndices[0] ?? 0;
|
|
490
|
+
const nextBaseline = baselineIndices[baselineCursor];
|
|
491
|
+
if (index < firstBaseline || (nextBaseline !== undefined && index < nextBaseline))
|
|
492
|
+
intervening = true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return { valid: true, added, intervening };
|
|
496
|
+
}
|
|
497
|
+
function turnIdentityKey(turn) {
|
|
498
|
+
return turn.stableId === undefined ? `e:${turn.evidenceDigest}` : `i:${turn.stableId}`;
|
|
499
|
+
}
|
|
500
|
+
function validateInput(input) {
|
|
501
|
+
if (!isRecord(input))
|
|
502
|
+
throw new TypeError("Turn ownership input must be an object.");
|
|
503
|
+
assertExactKeys(input, "input", ["binding", "baseline", "snapshot", "submissionWitness", "prior"], ["binding", "baseline", "snapshot"]);
|
|
504
|
+
validateBinding(input.binding);
|
|
505
|
+
validateBaseline(input.baseline);
|
|
506
|
+
validateSnapshot(input.snapshot);
|
|
507
|
+
if (input.submissionWitness !== undefined)
|
|
508
|
+
validateSubmissionWitness(input.submissionWitness);
|
|
509
|
+
if (input.prior !== undefined)
|
|
510
|
+
validateOwnershipCursor(input.prior);
|
|
511
|
+
}
|
|
512
|
+
function validateBinding(binding) {
|
|
513
|
+
if (!isRecord(binding) || binding.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION)
|
|
514
|
+
throw new TypeError("Unsupported ownership binding schema version.");
|
|
515
|
+
assertExactKeys(binding, "binding", ["schemaVersion", "operationId", "targetBindingDigest", "target", "evidenceProfile", "replacementTabRecovery", "actionId", "actionKind"], ["schemaVersion", "operationId", "targetBindingDigest", "target", "evidenceProfile", "replacementTabRecovery", "actionId", "actionKind"]);
|
|
516
|
+
assertIdentifier(binding.operationId, "binding.operationId");
|
|
517
|
+
assertDigest(binding.targetBindingDigest, "binding.targetBindingDigest");
|
|
518
|
+
assertIdentifier(binding.actionId, "binding.actionId");
|
|
519
|
+
if (binding.actionKind !== "send" && binding.actionKind !== "work_steer")
|
|
520
|
+
throw new TypeError("Invalid ownership action kind.");
|
|
521
|
+
if (typeof binding.replacementTabRecovery !== "boolean")
|
|
522
|
+
throw new TypeError("replacementTabRecovery must be boolean.");
|
|
523
|
+
validateTarget(binding.target, "binding.target");
|
|
524
|
+
if (!isRecord(binding.evidenceProfile))
|
|
525
|
+
throw new TypeError("binding.evidenceProfile must be an object.");
|
|
526
|
+
assertExactKeys(binding.evidenceProfile, "binding.evidenceProfile", ["stableConversationId", "stableUserTurnId", "stableAssistantTurnId", "stableBranchId", "authoritativeTabClaim"], ["stableConversationId", "stableUserTurnId", "stableAssistantTurnId", "stableBranchId", "authoritativeTabClaim"]);
|
|
527
|
+
for (const key of ["stableConversationId", "stableUserTurnId", "stableAssistantTurnId", "stableBranchId", "authoritativeTabClaim"]) {
|
|
528
|
+
if (binding.evidenceProfile[key] !== "required" && binding.evidenceProfile[key] !== "unavailable")
|
|
529
|
+
throw new TypeError(`Invalid evidence profile ${key}.`);
|
|
530
|
+
}
|
|
531
|
+
if (binding.evidenceProfile.stableConversationId === "required" && binding.target.conversation.status !== "available")
|
|
532
|
+
throw new TypeError("Stable conversation evidence is required but unavailable in the binding.");
|
|
533
|
+
if (binding.evidenceProfile.authoritativeTabClaim === "required" && binding.target.authoritativeTabClaim.status !== "available")
|
|
534
|
+
throw new TypeError("Authoritative tab claim is required but unavailable in the binding.");
|
|
535
|
+
if (binding.replacementTabRecovery
|
|
536
|
+
&& (binding.evidenceProfile.stableConversationId !== "required" || binding.evidenceProfile.authoritativeTabClaim !== "required")) {
|
|
537
|
+
throw new TypeError("Replacement-tab recovery requires stable conversation identity and authoritative tab claims.");
|
|
538
|
+
}
|
|
539
|
+
if (binding.target.coordinationScope === "provider" && binding.target.authoritativeTabClaim.status !== "available")
|
|
540
|
+
throw new TypeError("Provider-scoped ownership requires an authoritative tab claim.");
|
|
541
|
+
}
|
|
542
|
+
function validateBaseline(baseline) {
|
|
543
|
+
if (!isRecord(baseline) || baseline.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION || baseline.completeness !== "complete")
|
|
544
|
+
throw new TypeError("Baseline must be a complete ownership snapshot.");
|
|
545
|
+
assertExactKeys(baseline, "baseline", ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness"], ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness"]);
|
|
546
|
+
assertDigest(baseline.snapshotDigest, "baseline.snapshotDigest");
|
|
547
|
+
validateTarget(baseline.target, "baseline.target");
|
|
548
|
+
validateTurns(baseline.userTurns, "user", "baseline.userTurns");
|
|
549
|
+
validateTurns(baseline.assistantTurns, "assistant", "baseline.assistantTurns");
|
|
550
|
+
}
|
|
551
|
+
function validateSnapshot(snapshot) {
|
|
552
|
+
if (!isRecord(snapshot) || snapshot.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION)
|
|
553
|
+
throw new TypeError("Unsupported ownership snapshot schema version.");
|
|
554
|
+
assertExactKeys(snapshot, "snapshot", ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness", "terminalState", "postSendDelta"], ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness", "terminalState"]);
|
|
555
|
+
assertDigest(snapshot.snapshotDigest, "snapshot.snapshotDigest");
|
|
556
|
+
validateTarget(snapshot.target, "snapshot.target");
|
|
557
|
+
validateTurns(snapshot.userTurns, "user", "snapshot.userTurns");
|
|
558
|
+
validateTurns(snapshot.assistantTurns, "assistant", "snapshot.assistantTurns");
|
|
559
|
+
if (snapshot.completeness !== "complete" && snapshot.completeness !== "truncated" && snapshot.completeness !== "incomplete" && snapshot.completeness !== "out_of_order")
|
|
560
|
+
throw new TypeError("Invalid snapshot completeness.");
|
|
561
|
+
if (snapshot.terminalState !== "idle" && snapshot.terminalState !== "generating" && snapshot.terminalState !== "terminal" && snapshot.terminalState !== "unknown")
|
|
562
|
+
throw new TypeError("Invalid snapshot terminal state.");
|
|
563
|
+
if (snapshot.postSendDelta !== undefined) {
|
|
564
|
+
if (!isRecord(snapshot.postSendDelta))
|
|
565
|
+
throw new TypeError("snapshot.postSendDelta must be an object.");
|
|
566
|
+
assertExactKeys(snapshot.postSendDelta, "snapshot.postSendDelta", ["baselineSnapshotDigest", "addedUserEvidenceDigests", "deltaDigest"], ["baselineSnapshotDigest", "addedUserEvidenceDigests", "deltaDigest"]);
|
|
567
|
+
assertDigest(snapshot.postSendDelta.baselineSnapshotDigest, "snapshot.postSendDelta.baselineSnapshotDigest");
|
|
568
|
+
assertDigest(snapshot.postSendDelta.deltaDigest, "snapshot.postSendDelta.deltaDigest");
|
|
569
|
+
assertBoundedArray(snapshot.postSendDelta.addedUserEvidenceDigests, MAX_TURNS, "snapshot.postSendDelta.addedUserEvidenceDigests");
|
|
570
|
+
const seen = new Set();
|
|
571
|
+
for (const digest of snapshot.postSendDelta.addedUserEvidenceDigests) {
|
|
572
|
+
assertDigest(digest, "snapshot.postSendDelta.addedUserEvidenceDigests[]");
|
|
573
|
+
if (seen.has(digest))
|
|
574
|
+
throw new TypeError("Duplicate post-Send user evidence digest.");
|
|
575
|
+
seen.add(digest);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function validateSubmissionWitness(witness) {
|
|
580
|
+
if (!isRecord(witness))
|
|
581
|
+
throw new TypeError("submissionWitness must be an object.");
|
|
582
|
+
assertExactKeys(witness, "submissionWitness", ["actionId", "actionKind", "baselineSnapshotDigest", "postSendDeltaDigest", "operationUserEvidenceDigest", "userTurnStableId"], ["actionId", "actionKind", "baselineSnapshotDigest", "postSendDeltaDigest", "operationUserEvidenceDigest"]);
|
|
583
|
+
assertIdentifier(witness.actionId, "submissionWitness.actionId");
|
|
584
|
+
if (witness.actionKind !== "send" && witness.actionKind !== "work_steer")
|
|
585
|
+
throw new TypeError("Invalid submission witness action kind.");
|
|
586
|
+
assertDigest(witness.baselineSnapshotDigest, "submissionWitness.baselineSnapshotDigest");
|
|
587
|
+
assertDigest(witness.postSendDeltaDigest, "submissionWitness.postSendDeltaDigest");
|
|
588
|
+
assertDigest(witness.operationUserEvidenceDigest, "submissionWitness.operationUserEvidenceDigest");
|
|
589
|
+
if (witness.userTurnStableId !== undefined)
|
|
590
|
+
assertIdentifier(witness.userTurnStableId, "submissionWitness.userTurnStableId");
|
|
591
|
+
}
|
|
592
|
+
function validateTarget(target, label) {
|
|
593
|
+
if (!isRecord(target))
|
|
594
|
+
throw new TypeError(`${label} must be an object.`);
|
|
595
|
+
assertExactKeys(target, label, ["provider", "browser", "tab", "thread", "conversation", "canonicalThreadUrl", "authoritativeTabClaim", "coordinationScope"], ["provider", "browser", "tab", "thread", "conversation", "canonicalThreadUrl", "authoritativeTabClaim", "coordinationScope"]);
|
|
596
|
+
for (const key of ["provider", "browser", "tab", "thread", "conversation", "authoritativeTabClaim"])
|
|
597
|
+
validateIdentity(target[key], `${label}.${key}`);
|
|
598
|
+
validateIdentity(target.canonicalThreadUrl, `${label}.canonicalThreadUrl`, true);
|
|
599
|
+
if (target.coordinationScope !== "process" && target.coordinationScope !== "provider")
|
|
600
|
+
throw new TypeError(`${label}.coordinationScope is invalid.`);
|
|
601
|
+
if (target.coordinationScope === "provider" && target.authoritativeTabClaim.status !== "available")
|
|
602
|
+
throw new TypeError(`${label} requires an authoritative claim for provider coordination.`);
|
|
603
|
+
if (target.canonicalThreadUrl.status === "available") {
|
|
604
|
+
let url;
|
|
605
|
+
try {
|
|
606
|
+
url = new URL(target.canonicalThreadUrl.value);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
throw new TypeError(`${label}.canonicalThreadUrl is not a URL.`);
|
|
610
|
+
}
|
|
611
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "")
|
|
612
|
+
throw new TypeError(`${label}.canonicalThreadUrl must be canonical HTTPS without credentials, query, or fragment.`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function validateIdentity(identity, label, allowUrl = false) {
|
|
616
|
+
if (!isRecord(identity) || (identity.status !== "available" && identity.status !== "unavailable"))
|
|
617
|
+
throw new TypeError(`${label} has invalid availability.`);
|
|
618
|
+
if (identity.status === "available") {
|
|
619
|
+
assertExactKeys(identity, label, ["status", "value"], ["status", "value"]);
|
|
620
|
+
if (!allowUrl)
|
|
621
|
+
assertIdentifier(identity.value, `${label}.value`);
|
|
622
|
+
else if (typeof identity.value !== "string" || identity.value.length < 1 || identity.value.length > 4096 || /[\u0000-\u001f\u007f]/u.test(identity.value))
|
|
623
|
+
throw new TypeError(`${label}.value must be a bounded URL.`);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
assertExactKeys(identity, label, ["status", "reason"], ["status", "reason"]);
|
|
627
|
+
if (identity.reason !== "not_exposed" && identity.reason !== "not_observed" && identity.reason !== "redacted")
|
|
628
|
+
throw new TypeError(`${label}.reason is invalid.`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function validateTurns(turns, kind, label) {
|
|
632
|
+
assertBoundedArray(turns, MAX_TURNS, label);
|
|
633
|
+
const stableIds = new Set();
|
|
634
|
+
const evidence = new Set();
|
|
635
|
+
for (let index = 0; index < turns.length; index += 1) {
|
|
636
|
+
const turn = turns[index];
|
|
637
|
+
if (!isRecord(turn))
|
|
638
|
+
throw new TypeError(`${label}[${index}] must be an object.`);
|
|
639
|
+
assertExactKeys(turn, `${label}[${index}]`, ["stableId", "evidenceDigest", "structureDigest", "ordinal", "parentStableId", "branchStableId", "state", "artifactEvidenceDigests"], ["evidenceDigest", "structureDigest", "ordinal"]);
|
|
640
|
+
if (turn.ordinal !== index)
|
|
641
|
+
throw new TypeError(`${label} ordinals must be contiguous and ordered.`);
|
|
642
|
+
assertDigest(turn.evidenceDigest, `${label}[${index}].evidenceDigest`);
|
|
643
|
+
assertDigest(turn.structureDigest, `${label}[${index}].structureDigest`);
|
|
644
|
+
if (turn.stableId !== undefined) {
|
|
645
|
+
assertIdentifier(turn.stableId, `${label}[${index}].stableId`);
|
|
646
|
+
if (stableIds.has(turn.stableId))
|
|
647
|
+
throw new TypeError(`Duplicate stable turn ID in ${label}.`);
|
|
648
|
+
stableIds.add(turn.stableId);
|
|
649
|
+
}
|
|
650
|
+
if (kind === "user" && (turn.state !== undefined || turn.parentStableId !== undefined || turn.branchStableId !== undefined))
|
|
651
|
+
throw new TypeError("User turns cannot carry assistant state or lineage.");
|
|
652
|
+
if (kind === "assistant" && turn.state !== "generating" && turn.state !== "terminal")
|
|
653
|
+
throw new TypeError("Assistant turns require generating or terminal state.");
|
|
654
|
+
if (turn.parentStableId !== undefined)
|
|
655
|
+
assertIdentifier(turn.parentStableId, `${label}[${index}].parentStableId`);
|
|
656
|
+
if (turn.branchStableId !== undefined)
|
|
657
|
+
assertIdentifier(turn.branchStableId, `${label}[${index}].branchStableId`);
|
|
658
|
+
assertBoundedArray(turn.artifactEvidenceDigests ?? [], MAX_ARTIFACTS_PER_TURN, `${label}[${index}].artifactEvidenceDigests`);
|
|
659
|
+
const artifactDigests = new Set();
|
|
660
|
+
for (const digest of turn.artifactEvidenceDigests ?? []) {
|
|
661
|
+
assertDigest(digest, `${label}[${index}].artifactEvidenceDigests[]`);
|
|
662
|
+
if (artifactDigests.has(digest))
|
|
663
|
+
throw new TypeError(`Duplicate artifact evidence digest in ${label}.`);
|
|
664
|
+
artifactDigests.add(digest);
|
|
665
|
+
}
|
|
666
|
+
// Identical content is allowed when stable IDs differ, but two id-less
|
|
667
|
+
// turns with one evidence digest cannot be distinguished safely.
|
|
668
|
+
if (turn.stableId === undefined) {
|
|
669
|
+
if (evidence.has(turn.evidenceDigest))
|
|
670
|
+
throw new TypeError(`Duplicate id-less turn evidence in ${label}.`);
|
|
671
|
+
evidence.add(turn.evidenceDigest);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function assertIdentifier(value, label) {
|
|
676
|
+
if (typeof value !== "string" || value.length < 1 || value.length > MAX_IDENTIFIER_LENGTH || !IDENTIFIER_PATTERN.test(value))
|
|
677
|
+
throw new TypeError(`${label} must be a bounded opaque identifier.`);
|
|
678
|
+
}
|
|
679
|
+
function assertDigest(value, label) {
|
|
680
|
+
if (typeof value !== "string" || value.length > MAX_DIGEST_LENGTH || !DIGEST_PATTERN.test(value))
|
|
681
|
+
throw new TypeError(`${label} must be a bounded digest.`);
|
|
682
|
+
}
|
|
683
|
+
function assertBoundedArray(value, max, label) {
|
|
684
|
+
if (!Array.isArray(value) || value.length > max)
|
|
685
|
+
throw new TypeError(`${label} exceeds its bounded array cap.`);
|
|
686
|
+
}
|
|
687
|
+
function assertExactKeys(value, label, allowed, required) {
|
|
688
|
+
const allowedSet = new Set(allowed);
|
|
689
|
+
for (const key of Object.keys(value)) {
|
|
690
|
+
if (!allowedSet.has(key))
|
|
691
|
+
throw new TypeError(`${label} contains unsupported field ${key}.`);
|
|
692
|
+
}
|
|
693
|
+
for (const key of required) {
|
|
694
|
+
if (!Object.prototype.hasOwnProperty.call(value, key))
|
|
695
|
+
throw new TypeError(`${label} is missing required field ${key}.`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function isRecord(value) {
|
|
699
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
700
|
+
}
|