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.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. 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
+ }