codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +43 -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/download-blocked-by-browser.json +22 -0
  7. package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
  8. package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
  9. package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
  10. package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
  11. package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
  12. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  13. package/contracts/v1/fixtures/operation-action.json +13 -0
  14. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  15. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  16. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  17. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  18. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  19. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  20. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  21. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  22. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  23. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  24. package/contracts/v1/fixtures/operation-event.json +18 -0
  25. package/contracts/v1/fixtures/operation-handle.json +10 -0
  26. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  27. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  28. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  29. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  30. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  31. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  32. package/contracts/v1/fixtures/operation-request.json +33 -0
  33. package/contracts/v1/fixtures/operation-state.json +144 -0
  34. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  35. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  36. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  37. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  38. package/contracts/v1/manifest.json +185 -1
  39. package/contracts/v1/parity-suite.json +106 -8
  40. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  41. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  42. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  43. package/contracts/v1/schemas/manifest.schema.json +57 -0
  44. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  45. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  46. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  47. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  48. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  49. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  50. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  51. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  52. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  53. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  54. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  55. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  56. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  57. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  58. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  59. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  60. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  61. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  62. package/contracts/v1/surface-drift-policy.json +50 -0
  63. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  64. package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
  65. package/dist/codex-chatgpt-control-journal.mjs +5547 -0
  66. package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
  67. package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
  68. package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
  69. package/dist/src/backend/client.d.ts +103 -2
  70. package/dist/src/backend/client.js +1436 -97
  71. package/dist/src/backend/compatibility.d.ts +12 -0
  72. package/dist/src/backend/compatibility.js +208 -0
  73. package/dist/src/backend/protocol.d.ts +84 -1
  74. package/dist/src/backend/protocol.js +31 -5
  75. package/dist/src/backend/runtime-identity.d.ts +10 -0
  76. package/dist/src/backend/runtime-identity.js +141 -0
  77. package/dist/src/backend/session.d.ts +10 -2
  78. package/dist/src/backend/session.js +478 -7
  79. package/dist/src/backend/stdio-server.d.ts +4 -1
  80. package/dist/src/backend/stdio-server.js +240 -28
  81. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  82. package/dist/src/browser/active-composer-file-input.js +33 -0
  83. package/dist/src/browser/attach.d.ts +4 -1
  84. package/dist/src/browser/attach.js +136 -28
  85. package/dist/src/browser/chatgpt-url.d.ts +2 -0
  86. package/dist/src/browser/chatgpt-url.js +9 -0
  87. package/dist/src/browser/downloads.d.ts +17 -1
  88. package/dist/src/browser/downloads.js +150 -28
  89. package/dist/src/browser/page-state.js +6 -0
  90. package/dist/src/client.d.ts +43 -0
  91. package/dist/src/client.js +1279 -59
  92. package/dist/src/commands/artifacts.js +6 -4
  93. package/dist/src/commands/chat-popover.d.ts +37 -0
  94. package/dist/src/commands/chat-popover.js +537 -0
  95. package/dist/src/commands/configuration.d.ts +5 -0
  96. package/dist/src/commands/configuration.js +134 -21
  97. package/dist/src/commands/context.js +10 -3
  98. package/dist/src/commands/doctor.d.ts +1 -1
  99. package/dist/src/commands/doctor.js +27 -4
  100. package/dist/src/commands/experience.d.ts +5 -0
  101. package/dist/src/commands/experience.js +104 -21
  102. package/dist/src/commands/files.js +14 -33
  103. package/dist/src/commands/modes.js +85 -21
  104. package/dist/src/commands/power-discovery.d.ts +130 -0
  105. package/dist/src/commands/power-discovery.js +669 -0
  106. package/dist/src/commands/project-sources.js +325 -31
  107. package/dist/src/commands/sequence.js +59 -32
  108. package/dist/src/commands/session.js +19 -6
  109. package/dist/src/commands/work.js +31 -0
  110. package/dist/src/dom/composer-text.d.ts +6 -0
  111. package/dist/src/dom/composer-text.js +85 -0
  112. package/dist/src/errors.d.ts +9 -0
  113. package/dist/src/errors.js +22 -0
  114. package/dist/src/index.d.ts +6 -0
  115. package/dist/src/index.js +9 -0
  116. package/dist/src/operations/artifact-output.d.ts +69 -0
  117. package/dist/src/operations/artifact-output.js +1748 -0
  118. package/dist/src/operations/artifact-stream.d.ts +19 -0
  119. package/dist/src/operations/artifact-stream.js +35 -0
  120. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  121. package/dist/src/operations/artifact-transfer.js +1312 -0
  122. package/dist/src/operations/browser-adapter.d.ts +167 -0
  123. package/dist/src/operations/browser-adapter.js +2103 -0
  124. package/dist/src/operations/browser-observation.d.ts +110 -0
  125. package/dist/src/operations/browser-observation.js +1256 -0
  126. package/dist/src/operations/browser-target.d.ts +105 -0
  127. package/dist/src/operations/browser-target.js +531 -0
  128. package/dist/src/operations/canonical.d.ts +4 -0
  129. package/dist/src/operations/canonical.js +412 -0
  130. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  131. package/dist/src/operations/chatgpt-runtime.js +1232 -0
  132. package/dist/src/operations/client.d.ts +138 -0
  133. package/dist/src/operations/client.js +1145 -0
  134. package/dist/src/operations/collector.d.ts +195 -0
  135. package/dist/src/operations/collector.js +1039 -0
  136. package/dist/src/operations/configuration-routing.d.ts +4 -0
  137. package/dist/src/operations/configuration-routing.js +14 -0
  138. package/dist/src/operations/control.d.ts +372 -0
  139. package/dist/src/operations/control.js +1498 -0
  140. package/dist/src/operations/file-identity.d.ts +33 -0
  141. package/dist/src/operations/file-identity.js +153 -0
  142. package/dist/src/operations/handle.d.ts +19 -0
  143. package/dist/src/operations/handle.js +727 -0
  144. package/dist/src/operations/index.d.ts +25 -0
  145. package/dist/src/operations/index.js +25 -0
  146. package/dist/src/operations/journal-authority.d.ts +12 -0
  147. package/dist/src/operations/journal-authority.js +1 -0
  148. package/dist/src/operations/journal-rpc-client.d.ts +12 -0
  149. package/dist/src/operations/journal-rpc-client.js +225 -0
  150. package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
  151. package/dist/src/operations/journal-rpc-protocol.js +214 -0
  152. package/dist/src/operations/journal-rpc-server.d.ts +15 -0
  153. package/dist/src/operations/journal-rpc-server.js +272 -0
  154. package/dist/src/operations/journal.d.ts +143 -0
  155. package/dist/src/operations/journal.js +1957 -0
  156. package/dist/src/operations/production-attachments.d.ts +118 -0
  157. package/dist/src/operations/production-attachments.js +1035 -0
  158. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  159. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  160. package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
  161. package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
  162. package/dist/src/operations/production-configuration.d.ts +42 -0
  163. package/dist/src/operations/production-configuration.js +1544 -0
  164. package/dist/src/operations/production-primitives.d.ts +53 -0
  165. package/dist/src/operations/production-primitives.js +856 -0
  166. package/dist/src/operations/production-work-steer.d.ts +203 -0
  167. package/dist/src/operations/production-work-steer.js +1218 -0
  168. package/dist/src/operations/recovery.d.ts +80 -0
  169. package/dist/src/operations/recovery.js +174 -0
  170. package/dist/src/operations/runtime-adapter.d.ts +126 -0
  171. package/dist/src/operations/runtime-adapter.js +802 -0
  172. package/dist/src/operations/send-once.d.ts +224 -0
  173. package/dist/src/operations/send-once.js +1081 -0
  174. package/dist/src/operations/service.d.ts +286 -0
  175. package/dist/src/operations/service.js +2831 -0
  176. package/dist/src/operations/staging.d.ts +136 -0
  177. package/dist/src/operations/staging.js +630 -0
  178. package/dist/src/operations/state-machine.d.ts +29 -0
  179. package/dist/src/operations/state-machine.js +2047 -0
  180. package/dist/src/operations/submission.d.ts +423 -0
  181. package/dist/src/operations/submission.js +1676 -0
  182. package/dist/src/operations/transactional-chat-power.d.ts +17 -0
  183. package/dist/src/operations/transactional-chat-power.js +212 -0
  184. package/dist/src/operations/turn-ownership.d.ts +202 -0
  185. package/dist/src/operations/turn-ownership.js +700 -0
  186. package/dist/src/operations/types.d.ts +454 -0
  187. package/dist/src/operations/types.js +20 -0
  188. package/dist/src/operations/wire-requests.d.ts +21 -0
  189. package/dist/src/operations/wire-requests.js +396 -0
  190. package/dist/src/operations/wire-results.d.ts +97 -0
  191. package/dist/src/operations/wire-results.js +818 -0
  192. package/dist/src/runner/responses.d.ts +3 -1
  193. package/dist/src/runner/responses.js +19 -1
  194. package/dist/src/runner/result.js +227 -55
  195. package/dist/src/runner/types.d.ts +12 -0
  196. package/dist/src/runtime/command-routing.d.ts +149 -0
  197. package/dist/src/runtime/command-routing.js +431 -0
  198. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  199. package/dist/src/runtime/coordinated-browser.js +315 -0
  200. package/dist/src/runtime/coordinated-page.d.ts +41 -0
  201. package/dist/src/runtime/coordinated-page.js +651 -0
  202. package/dist/src/runtime/operation-context.d.ts +142 -0
  203. package/dist/src/runtime/operation-context.js +410 -0
  204. package/dist/src/runtime/runtime-session.d.ts +95 -0
  205. package/dist/src/runtime/runtime-session.js +314 -0
  206. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  207. package/dist/src/runtime/tab-coordinator.js +1200 -0
  208. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  209. package/dist/src/runtime/value-boundaries.js +49 -0
  210. package/dist/src/safety/blockers.js +8 -1
  211. package/dist/src/safety/untrusted-output.js +2 -1
  212. package/dist/src/scripts/backend-server.js +4 -1
  213. package/dist/src/scripts/capture-surface-profile.js +7 -2
  214. package/dist/src/scripts/journal-server.d.ts +2 -0
  215. package/dist/src/scripts/journal-server.js +37 -0
  216. package/dist/src/scripts/live-smoke/harness.js +79 -7
  217. package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
  218. package/dist/src/scripts/live-smoke/scenarios.js +147 -30
  219. package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
  220. package/dist/src/scripts/live-smoke/transactional.js +254 -0
  221. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  222. package/dist/src/scripts/release-canary-module.js +17 -4
  223. package/dist/src/types.d.ts +38 -1
  224. package/package.json +8 -3
  225. package/references/2026-08-16-transactional-operations.md +459 -0
  226. package/references/2026-09-06-journal-service.md +140 -0
  227. package/references/agents-runner.md +34 -0
  228. package/references/backend-protocol.md +176 -0
  229. package/references/python-parity.md +137 -0
  230. package/references/responses-adapter.md +19 -0
  231. package/references/streaming.md +6 -0
@@ -0,0 +1,1256 @@
1
+ import { formatMessageHtml, normalizeResponseFormat } from "../dom/message-format.js";
2
+ import { TURN_OWNERSHIP_SCHEMA_VERSION } from "./turn-ownership.js";
3
+ import { COLLECTOR_TERMINAL_SCHEMA_VERSION } from "./collector.js";
4
+ import { isPlainDataRecord } from "../runtime/value-boundaries.js";
5
+ const ERROR_MESSAGES = {
6
+ page_evaluation_unavailable: "Browser observation requires a read-only page evaluation boundary.",
7
+ page_evaluation_failed: "Browser observation could not complete its bounded DOM transaction.",
8
+ provider_shape_drift: "Browser observation found an unsupported provider DOM shape.",
9
+ missing_identity: "Browser observation is missing a required stable identity.",
10
+ duplicate_identity: "Browser observation found duplicate stable identities.",
11
+ unstable_identity: "Browser observation found an identity that changed within one DOM snapshot.",
12
+ incomplete_dom: "Browser observation found an incomplete or out-of-order conversation DOM.",
13
+ navigation_ambiguous: "Browser observation could not prove one canonical conversation navigation target.",
14
+ branch_ambiguous: "Browser observation could not prove one stable assistant branch.",
15
+ bounded_limit_exceeded: "Browser observation exceeded a bounded DOM or content limit.",
16
+ evidence_digest_failed: "Browser observation could not create privacy-preserving evidence.",
17
+ raw_content_unavailable: "The requested exact terminal assistant content is unavailable."
18
+ };
19
+ export class BrowserObservationError extends Error {
20
+ code;
21
+ constructor(code) {
22
+ super(ERROR_MESSAGES[code]);
23
+ this.name = "BrowserObservationError";
24
+ this.code = code;
25
+ }
26
+ }
27
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
28
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
29
+ const MAX_TURNS = 256;
30
+ const MAX_ARTIFACTS_PER_TURN = 32;
31
+ const MAX_TEXT_CHARS = 1_000_000;
32
+ const MAX_TOTAL_TEXT_CHARS = 8_000_000;
33
+ const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
34
+ const MAX_NODES = 32_768;
35
+ const MAX_ATTRIBUTE_LENGTH = 4096;
36
+ const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;
37
+ const OPAQUE_URL_PREFIX = "https://opaque.invalid/thread/";
38
+ /**
39
+ * Perform one bounded read-only page transaction and normalize its result.
40
+ * `page.evaluate` is the only browser call made by this adapter.
41
+ */
42
+ export async function observeBrowserPage(page, options) {
43
+ validateOptions(options);
44
+ if (typeof page.evaluate !== "function") {
45
+ throw new BrowserObservationError("page_evaluation_unavailable");
46
+ }
47
+ const responseContent = options.responseContent ?? "metadata";
48
+ if (responseContent === "include" && options.rawAssistantTurnId === undefined) {
49
+ throw new BrowserObservationError("raw_content_unavailable");
50
+ }
51
+ if (options.rawAssistantTurnId !== undefined
52
+ && options.terminalAssistantTurnId !== undefined
53
+ && options.rawAssistantTurnId !== options.terminalAssistantTurnId) {
54
+ throw new BrowserObservationError("raw_content_unavailable");
55
+ }
56
+ const evaluateArgs = {
57
+ maxTurns: boundedPositive(options.maxTurns, MAX_TURNS),
58
+ maxTextChars: boundedPositive(options.maxTextChars, MAX_TEXT_CHARS),
59
+ maxArtifactsPerTurn: boundedPositive(options.maxArtifactsPerTurn, MAX_ARTIFACTS_PER_TURN),
60
+ ...((options.terminalAssistantTurnId ?? options.rawAssistantTurnId) === undefined
61
+ ? {}
62
+ : { captureAssistantTurnId: options.terminalAssistantTurnId ?? options.rawAssistantTurnId }),
63
+ ...(options.target.targetLifecycle === "new_pending" ? { allowBlankTask: true } : {})
64
+ };
65
+ let raw;
66
+ try {
67
+ raw = await page.evaluate(readPageObservation, evaluateArgs);
68
+ }
69
+ catch {
70
+ throw new BrowserObservationError("page_evaluation_failed");
71
+ }
72
+ const parsed = parseRawObservation(raw, evaluateArgs);
73
+ const target = await buildTarget(parsed, options);
74
+ const normalized = await normalizeTurns(parsed.turns, options);
75
+ const postSendDelta = options.baseline === undefined
76
+ ? undefined
77
+ : await makePostSendDelta(options.baseline, normalized.userTurns, options.evidenceDigest);
78
+ const snapshotDigest = await digest(options.evidenceDigest, "browser-observation-snapshot", {
79
+ operationId: options.operationId,
80
+ target: targetMaterial(target),
81
+ userTurns: normalized.userTurns.map(turnMaterial),
82
+ assistantTurns: normalized.assistantTurns.map(turnMaterial),
83
+ completeness: parsed.completeness,
84
+ terminalState: parsed.terminalState,
85
+ // A pending target cannot persist a conversation URL yet, but the keyed
86
+ // navigation evidence still makes the blank-task anchor sensitive to a
87
+ // pre-Send navigation change without exposing the raw URL.
88
+ ...(options.target.targetLifecycle === "new_pending"
89
+ ? { blankTaskNavigationDigest: await digest(options.evidenceDigest, "browser-observation-blank-task-navigation", parsed.canonicalUrl) }
90
+ : {}),
91
+ ...(postSendDelta === undefined ? {} : { postSendDelta })
92
+ });
93
+ const snapshot = Object.freeze({
94
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
95
+ snapshotDigest,
96
+ target,
97
+ userTurns: Object.freeze(normalized.userTurns),
98
+ assistantTurns: Object.freeze(normalized.assistantTurns),
99
+ completeness: parsed.completeness,
100
+ terminalState: parsed.terminalState,
101
+ ...(postSendDelta === undefined ? {} : { postSendDelta })
102
+ });
103
+ const terminalTurn = resolveTerminalTurn(parsed.turns, normalized.assistantTurns, options);
104
+ const terminal = terminalTurn === undefined
105
+ ? undefined
106
+ : await terminalObservation(terminalTurn.raw, terminalTurn.normalized, normalized.userTurns, options, target);
107
+ const newTargetAnchor = options.target.targetLifecycle === "new_pending"
108
+ && target.conversation.status === "unavailable"
109
+ && target.canonicalThreadUrl.status === "unavailable"
110
+ && normalized.userTurns.length === 0
111
+ && normalized.assistantTurns.length === 0
112
+ ? await (async () => {
113
+ const blankTaskEvidenceDigest = snapshot.snapshotDigest;
114
+ const anchorDigest = await digest(options.evidenceDigest, "browser-observation-new-target-anchor", {
115
+ operationId: options.operationId,
116
+ target: targetMaterial(target),
117
+ blankTaskEvidenceDigest
118
+ });
119
+ return Object.freeze({ anchorDigest, blankTaskEvidenceDigest });
120
+ })()
121
+ : undefined;
122
+ return Object.freeze({
123
+ snapshot,
124
+ ...(terminal === undefined ? {} : { terminal }),
125
+ ...(newTargetAnchor === undefined ? {} : { newTargetAnchor })
126
+ });
127
+ }
128
+ /**
129
+ * This function is intentionally self-contained because it is serialized into
130
+ * the browser evaluation boundary. It returns bounded, transient raw material;
131
+ * the outer adapter converts ownership material to HMAC evidence and never
132
+ * stores prompt/response text in the normalized snapshot. Exact response text
133
+ * is exposed only in the request-scoped terminal result when explicitly asked.
134
+ */
135
+ export function readPageObservation(args) {
136
+ // IMPORTANT: page.evaluate serializes this function. Every runtime value it
137
+ // uses must therefore be declared inside this body; imported/module helpers
138
+ // are not part of the browser realm. Keep the outer twins below for SDK-side
139
+ // validation only.
140
+ const MAX_TURNS = 256;
141
+ const MAX_ARTIFACTS_PER_TURN = 32;
142
+ const MAX_TEXT_CHARS = 1_000_000;
143
+ const MAX_TOTAL_TEXT_CHARS = 8_000_000;
144
+ const MAX_NODES = 32_768;
145
+ const MAX_ATTRIBUTE_LENGTH = 4096;
146
+ const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;
147
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
148
+ const boundedPositive = (value, fallback) => Number.isSafeInteger(value) && value > 0
149
+ ? Math.min(value, fallback)
150
+ : fallback;
151
+ const isBoundedId = (value) => typeof value === "string" && ID_PATTERN.test(value);
152
+ const isBoundedText = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max && !value.includes("\u0000");
153
+ const isContentDigest = (value) => typeof value === "string" && /^(?:hmac-sha256:|sha256:)[0-9a-f]{64}$/.test(value);
154
+ const canonicalizeUrl = (value) => {
155
+ const url = new URL(value);
156
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") {
157
+ throw new Error("non-canonical navigation");
158
+ }
159
+ url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/$/u, "") || "/";
160
+ return url.toString();
161
+ };
162
+ const conversationIdFromUrl = (urlValue) => {
163
+ const url = new URL(urlValue);
164
+ const parts = url.pathname.split("/").filter(Boolean);
165
+ const index = parts.findIndex(part => part === "c" || part === "conversation");
166
+ const candidate = index >= 0 ? parts[index + 1] : undefined;
167
+ return candidate !== undefined && ID_PATTERN.test(candidate) ? candidate : undefined;
168
+ };
169
+ const uniqueNodeAttribute = (root, roleNodes, names) => {
170
+ const values = new Set();
171
+ for (const node of [root, ...roleNodes]) {
172
+ for (const name of names) {
173
+ const value = node.getAttribute(name);
174
+ if (value !== null && value.length > 0) {
175
+ if (value.length > MAX_ATTRIBUTE_LENGTH || !ID_PATTERN.test(value))
176
+ throw new Error("attribute drift");
177
+ values.add(value);
178
+ }
179
+ }
180
+ }
181
+ if (values.size > 1)
182
+ throw new Error("unstable identity");
183
+ return [...values][0];
184
+ };
185
+ const preferredNodeAttribute = (root, roleNodes, names) => {
186
+ for (const name of names) {
187
+ const values = new Set();
188
+ for (const node of [root, ...roleNodes]) {
189
+ const value = node.getAttribute(name);
190
+ if (value !== null && value.length > 0) {
191
+ if (value.length > MAX_ATTRIBUTE_LENGTH || !ID_PATTERN.test(value))
192
+ throw new Error("attribute drift");
193
+ values.add(value);
194
+ }
195
+ }
196
+ if (values.size > 1)
197
+ throw new Error("unstable identity");
198
+ const selected = [...values][0];
199
+ if (selected !== undefined)
200
+ return selected;
201
+ }
202
+ return undefined;
203
+ };
204
+ const boundedText = (value, max) => {
205
+ if (value.length > max || value.includes("\u0000"))
206
+ throw new Error("text limit exceeded");
207
+ return value;
208
+ };
209
+ const boundedMarkup = (value, max) => {
210
+ if (typeof value !== "string" || value.length > max || value.includes("\u0000"))
211
+ throw new Error("markup limit exceeded");
212
+ return value;
213
+ };
214
+ const readTurnState = (root, roleNode) => {
215
+ const values = [root, roleNode].flatMap(node => [
216
+ node.getAttribute("data-generation-state"),
217
+ node.getAttribute("data-state"),
218
+ node.getAttribute("data-status"),
219
+ node.getAttribute("data-turn-state"),
220
+ node.getAttribute("data-generating") === "true" ? "generating" : node.getAttribute("data-generating") === "false" ? "terminal" : null,
221
+ node.getAttribute("aria-busy") === "true" ? "generating" : node.getAttribute("aria-busy") === "false" ? "terminal" : null
222
+ ]).filter((value) => value !== null && value !== undefined).map(value => value.trim().toLowerCase());
223
+ const generating = values.some(value => ["generating", "streaming", "in_progress", "in-progress", "true"].includes(value));
224
+ const terminal = values.some(value => ["terminal", "complete", "completed", "done", "false"].includes(value));
225
+ if (generating && terminal)
226
+ throw new Error("unstable generation state");
227
+ return generating ? "generating" : terminal ? "terminal" : undefined;
228
+ };
229
+ const readFinishReason = (root, roleNode) => {
230
+ for (const node of [root, roleNode]) {
231
+ for (const name of ["data-finish-reason", "data-stop-reason", "data-completion-reason"]) {
232
+ const value = node.getAttribute(name);
233
+ if (value !== null && value.length > 0)
234
+ return value.trim().slice(0, 128);
235
+ }
236
+ }
237
+ return undefined;
238
+ };
239
+ const validateRawTurnOrdering = (turns) => {
240
+ const counters = { user: 0, assistant: 0 };
241
+ for (const turn of turns) {
242
+ if (turn.ordinal !== counters[turn.role])
243
+ throw new Error("out-of-order turn");
244
+ counters[turn.role] += 1;
245
+ }
246
+ const users = new Set(turns.filter(turn => turn.role === "user").map(turn => turn.stableId));
247
+ for (const turn of turns) {
248
+ if (turn.role === "assistant" && (turn.parentStableId === undefined || !users.has(turn.parentStableId))) {
249
+ throw new Error("branch ambiguity");
250
+ }
251
+ }
252
+ };
253
+ const maxTurns = boundedPositive(args?.maxTurns, MAX_TURNS);
254
+ const maxTextChars = boundedPositive(args?.maxTextChars, MAX_TEXT_CHARS);
255
+ const maxArtifactsPerTurn = boundedPositive(args?.maxArtifactsPerTurn, MAX_ARTIFACTS_PER_TURN);
256
+ const captureAssistantTurnId = args?.captureAssistantTurnId;
257
+ if (captureAssistantTurnId !== undefined && !isBoundedId(captureAssistantTurnId))
258
+ throw new Error("capture identity unavailable");
259
+ const allowBlankTask = args?.allowBlankTask === true;
260
+ // Chrome's serialized evaluation realm exposes the ambient DOM globals but
261
+ // can intentionally shadow `globalThis`. Use guarded ambient bindings so
262
+ // this self-contained callback works in that realm and still fails closed
263
+ // when reconstructed outside a browser.
264
+ const documentRoot = typeof document === "undefined" ? undefined : document;
265
+ if (documentRoot === undefined)
266
+ throw new Error("document unavailable");
267
+ const locationRoot = typeof location === "undefined" ? undefined : location;
268
+ const currentUrl = locationRoot?.href;
269
+ if (typeof currentUrl !== "string" || currentUrl.length === 0 || currentUrl.length > MAX_ATTRIBUTE_LENGTH) {
270
+ throw new Error("navigation unavailable");
271
+ }
272
+ const canonicalUrl = canonicalizeUrl(currentUrl);
273
+ const fromUrl = conversationIdFromUrl(canonicalUrl);
274
+ const conversationValues = new Set();
275
+ const threadValues = new Set();
276
+ const roots = [];
277
+ const activeArtifacts = [];
278
+ const activeElements = [];
279
+ let roleNodeCount = 0;
280
+ let blankTaskMarker = false;
281
+ let visibleComposerCount = 0;
282
+ let visibleStructuralStopControlCount = 0;
283
+ let completenessMarker;
284
+ let visitedNodes = 0;
285
+ const consumeNode = () => {
286
+ visitedNodes += 1;
287
+ if (visitedNodes > MAX_NODES)
288
+ throw new Error("node limit exceeded");
289
+ };
290
+ const addIdentityValues = (element, names, values) => {
291
+ for (const name of names) {
292
+ const value = element.getAttribute(name);
293
+ if (value !== null && value.length > 0) {
294
+ if (value.length > maxTextChars || !ID_PATTERN.test(value))
295
+ throw new Error("attribute drift");
296
+ values.add(value);
297
+ if (values.size > 1)
298
+ throw new Error("ambiguous identity");
299
+ }
300
+ }
301
+ };
302
+ const isStructuralTurnRoot = (element) => {
303
+ const testId = element.getAttribute("data-testid");
304
+ return (testId !== null && testId.startsWith("conversation-turn"))
305
+ || element.hasAttribute("data-conversation-turn-id")
306
+ || element.hasAttribute("data-turn-id");
307
+ };
308
+ const isFallbackTurnRoot = (element) => element.hasAttribute("data-message-id")
309
+ && element.hasAttribute("data-message-author-role");
310
+ const isRendered = (element) => {
311
+ const style = typeof getComputedStyle === "function" ? getComputedStyle(element) : undefined;
312
+ if (style !== undefined && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0"))
313
+ return false;
314
+ const rect = typeof element.getBoundingClientRect === "function" ? element.getBoundingClientRect() : undefined;
315
+ return rect === undefined || rect.width > 0 || rect.height > 0;
316
+ };
317
+ const isStructuralStopControl = (element) => element.tagName.toLowerCase() === "button"
318
+ && (element.getAttribute("data-testid") === "stop-button" || element.getAttribute("id") === "composer-stop-button");
319
+ const isIgnoredMessageElement = (element) => {
320
+ const tag = element.tagName.toLowerCase();
321
+ return tag === "button"
322
+ || tag === "script"
323
+ || tag === "style"
324
+ || tag === "svg"
325
+ || element.getAttribute("role") === "button"
326
+ || element.getAttribute("aria-hidden") === "true";
327
+ };
328
+ const isArtifact = (element) => {
329
+ const testId = (element.getAttribute("data-testid") ?? "").toLowerCase();
330
+ const tag = element.tagName.toLowerCase();
331
+ return element.hasAttribute("data-artifact-id")
332
+ || element.hasAttribute("data-file-id")
333
+ || element.hasAttribute("data-attachment-id")
334
+ || element.hasAttribute("data-image-id")
335
+ || testId.includes("artifact")
336
+ || testId.includes("file")
337
+ || testId.includes("image")
338
+ || (tag === "a" && element.hasAttribute("download"));
339
+ };
340
+ const isBlankTaskMarker = (element) => {
341
+ const hasMarker = element.hasAttribute("data-new-task")
342
+ || element.hasAttribute("data-new-conversation")
343
+ || ((element.getAttribute("data-testid") ?? "").toLowerCase().includes("new-chat"))
344
+ || ((element.getAttribute("data-testid") ?? "").toLowerCase().includes("new-conversation"));
345
+ if (!hasMarker)
346
+ return false;
347
+ const explicit = element.getAttribute("data-new-task") ?? element.getAttribute("data-new-conversation");
348
+ if (explicit === null || explicit === "" || explicit.toLowerCase() === "true")
349
+ return true;
350
+ const testId = (element.getAttribute("data-testid") ?? "").toLowerCase();
351
+ return testId.includes("new-chat") || testId.includes("new-conversation");
352
+ };
353
+ const isComposer = (element) => {
354
+ const tag = element.tagName.toLowerCase();
355
+ return tag === "textarea"
356
+ || element.getAttribute("contenteditable") === "true"
357
+ || element.getAttribute("role") === "textbox";
358
+ };
359
+ const readArtifact = (element, turn) => {
360
+ const identityValues = new Set();
361
+ for (const name of ["data-artifact-id", "data-file-id", "data-attachment-id", "data-image-id"]) {
362
+ const value = element.getAttribute(name);
363
+ if (value !== null && value.length > 0) {
364
+ if (!isBoundedId(value))
365
+ throw new Error("artifact identity unavailable");
366
+ identityValues.add(value);
367
+ }
368
+ }
369
+ if (identityValues.size > 1)
370
+ throw new Error("artifact identity unavailable");
371
+ const identity = [...identityValues][0] ?? (element.getAttribute("data-testid") ?? "");
372
+ if (!isBoundedId(identity) || turn.artifactIds.has(identity))
373
+ throw new Error("artifact identity unavailable");
374
+ turn.artifactIds.add(identity);
375
+ if (turn.artifacts.length >= maxArtifactsPerTurn)
376
+ throw new Error("artifact limit exceeded");
377
+ const testId = (element.getAttribute("data-testid") ?? "").toLowerCase();
378
+ const tag = element.tagName.toLowerCase();
379
+ const kind = tag === "img" || testId.includes("image") || element.hasAttribute("data-image-id")
380
+ ? "image"
381
+ : testId.includes("file") || element.hasAttribute("data-file-id") || element.hasAttribute("data-attachment-id") || tag === "a"
382
+ ? "file"
383
+ : "other";
384
+ const contentDigest = element.getAttribute("data-content-sha256") ?? element.getAttribute("data-sha256") ?? undefined;
385
+ if (contentDigest !== undefined && !isContentDigest(contentDigest))
386
+ throw new Error("artifact digest drift");
387
+ const bytesRaw = element.getAttribute("data-bytes") ?? element.getAttribute("data-size") ?? undefined;
388
+ const bytes = bytesRaw === undefined ? undefined : Number(bytesRaw);
389
+ if (bytes !== undefined && (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > MAX_ARTIFACT_BYTES))
390
+ throw new Error("artifact bytes limit");
391
+ const mimeType = element.getAttribute("data-mime-type") ?? element.getAttribute("type") ?? undefined;
392
+ if (mimeType !== undefined && !isBoundedText(mimeType, 128))
393
+ throw new Error("artifact MIME drift");
394
+ const raw = {
395
+ kind,
396
+ identity,
397
+ ...(contentDigest === undefined ? {} : { contentDigest }),
398
+ ...(bytes === undefined ? {} : { bytes }),
399
+ ...(mimeType === undefined ? {} : { mimeType })
400
+ };
401
+ turn.artifacts.push(raw);
402
+ const state = { raw, textChars: 0 };
403
+ activeArtifacts.push(state);
404
+ return state;
405
+ };
406
+ // The Chrome bridge exposes firstChild/nextSibling but intentionally omits
407
+ // TreeWalker and NodeFilter. Walk depth-first with bounded DOM pointers so
408
+ // every element, text node, and comment still consumes exactly one slot.
409
+ const nextDepthFirstNode = (node, root) => {
410
+ if (node.firstChild !== null)
411
+ return node.firstChild;
412
+ let cursor = node;
413
+ while (cursor !== null && cursor !== root) {
414
+ if (cursor.nextSibling !== null)
415
+ return cursor.nextSibling;
416
+ cursor = cursor.parentNode;
417
+ }
418
+ return null;
419
+ };
420
+ const mainRoot = typeof documentRoot.getElementById === "function" ? documentRoot.getElementById("main") : null;
421
+ const traversalRoot = mainRoot !== null && mainRoot.tagName.toLowerCase() === "main" ? mainRoot : documentRoot;
422
+ let current = traversalRoot.firstChild;
423
+ while (current !== null) {
424
+ consumeNode();
425
+ while (activeElements.length > 0
426
+ && activeElements[activeElements.length - 1]?.element !== current.parentNode) {
427
+ const closed = activeElements.pop();
428
+ if (closed?.artifact !== undefined) {
429
+ const popped = activeArtifacts.pop();
430
+ if (popped !== closed.artifact)
431
+ throw new Error("artifact traversal drift");
432
+ }
433
+ }
434
+ if (current.nodeType === 1) {
435
+ const element = current;
436
+ const parent = activeElements[activeElements.length - 1];
437
+ const inheritedTurn = parent?.turn;
438
+ const inheritedHidden = parent?.hiddenAncestor ?? false;
439
+ const ownHidden = element.hidden === true
440
+ || element.hasAttribute("hidden")
441
+ || element.hasAttribute("inert")
442
+ || element.getAttribute("aria-hidden") === "true";
443
+ const hiddenAncestor = inheritedHidden || ownHidden;
444
+ const role = element.getAttribute("data-message-author-role");
445
+ const structuralTurnCandidate = isStructuralTurnRoot(element);
446
+ const fallbackTurnCandidate = inheritedTurn === undefined && isFallbackTurnRoot(element);
447
+ const identityCandidate = element.hasAttribute("data-conversation-id")
448
+ || element.hasAttribute("data-chat-id")
449
+ || element.hasAttribute("data-thread-id")
450
+ || element.hasAttribute("data-conversation-thread-id");
451
+ const completenessCandidate = element.hasAttribute("data-observation-completeness")
452
+ || element.hasAttribute("data-conversation-completeness")
453
+ || element.hasAttribute("data-turns-truncated");
454
+ const blankTaskCandidate = isBlankTaskMarker(element);
455
+ const composerCandidate = isComposer(element);
456
+ const stopControlCandidate = isStructuralStopControl(element);
457
+ const artifactCandidate = inheritedTurn !== undefined && isArtifact(element);
458
+ // Layout/style reads are the expensive part of the aggregate DOM probe.
459
+ // Only nodes that can affect a returned semantic fact need one; ordinary
460
+ // message wrappers and text descendants remain covered by the same node
461
+ // and hidden-ancestor budgets without forcing layout per element.
462
+ const needsRenderedCheck = structuralTurnCandidate
463
+ || fallbackTurnCandidate
464
+ || role !== null
465
+ || identityCandidate
466
+ || completenessCandidate
467
+ || blankTaskCandidate
468
+ || composerCandidate
469
+ || stopControlCandidate
470
+ || artifactCandidate;
471
+ const semanticVisible = !hiddenAncestor && (!needsRenderedCheck || isRendered(element));
472
+ const structuralRoot = semanticVisible && structuralTurnCandidate;
473
+ const rootCandidate = structuralRoot || (semanticVisible && fallbackTurnCandidate);
474
+ // A nested provider root would otherwise make one role/artifact subtree
475
+ // belong to two owners. Reject it before descending so the exact visit
476
+ // budget cannot be multiplied by overlapping ownership interpretations.
477
+ if (structuralRoot && inheritedTurn !== undefined)
478
+ throw new Error("nested turn root");
479
+ let turn = rootCandidate
480
+ ? {
481
+ root: element,
482
+ roleMarkers: [],
483
+ directChildCount: 0,
484
+ artifacts: [],
485
+ artifactIds: new Set(),
486
+ messageChunks: [],
487
+ messageChars: 0
488
+ }
489
+ : inheritedTurn;
490
+ if (rootCandidate) {
491
+ if (turn === undefined)
492
+ throw new Error("turn traversal drift");
493
+ roots.push(turn);
494
+ }
495
+ if (parent !== undefined && parent.turn?.root === parent.element)
496
+ parent.turn.directChildCount += 1;
497
+ if (role !== null && semanticVisible) {
498
+ roleNodeCount += 1;
499
+ if (role !== "user" && role !== "assistant")
500
+ throw new Error("role drift");
501
+ if (turn === undefined) {
502
+ turn = {
503
+ root: element,
504
+ roleMarkers: [],
505
+ directChildCount: 0,
506
+ artifacts: [],
507
+ artifactIds: new Set(),
508
+ messageChunks: [],
509
+ messageChars: 0
510
+ };
511
+ roots.push(turn);
512
+ }
513
+ turn.roleMarkers.push(element);
514
+ if (turn.firstRoleNode === undefined)
515
+ turn.firstRoleNode = element;
516
+ }
517
+ if (semanticVisible)
518
+ addIdentityValues(element, ["data-conversation-id", "data-chat-id"], conversationValues);
519
+ if (semanticVisible)
520
+ addIdentityValues(element, ["data-thread-id", "data-conversation-thread-id"], threadValues);
521
+ if (semanticVisible && completenessMarker === undefined && completenessCandidate) {
522
+ completenessMarker = (element.getAttribute("data-observation-completeness")
523
+ ?? element.getAttribute("data-conversation-completeness")
524
+ ?? element.getAttribute("data-turns-truncated")
525
+ ?? "").toLowerCase();
526
+ }
527
+ if (semanticVisible && blankTaskCandidate)
528
+ blankTaskMarker = true;
529
+ if (semanticVisible && composerCandidate)
530
+ visibleComposerCount += 1;
531
+ if (semanticVisible && stopControlCandidate)
532
+ visibleStructuralStopControlCount += 1;
533
+ const selectedRoleNode = turn?.firstRoleNode === element;
534
+ const messageOwner = selectedRoleNode ? turn : parent?.messageOwner;
535
+ const messageIgnored = selectedRoleNode
536
+ ? false
537
+ : (parent?.messageIgnored ?? false) || hiddenAncestor || isIgnoredMessageElement(element);
538
+ let artifact;
539
+ if (semanticVisible && turn !== undefined && turn.root !== element && artifactCandidate)
540
+ artifact = readArtifact(element, turn);
541
+ activeElements.push({
542
+ element,
543
+ ...(turn === undefined ? {} : { turn }),
544
+ ...(messageOwner === undefined ? {} : { messageOwner }),
545
+ messageIgnored,
546
+ ...(artifact === undefined ? {} : { artifact }),
547
+ hiddenAncestor
548
+ });
549
+ }
550
+ else if (current.nodeType === 3) {
551
+ const value = current.nodeValue ?? "";
552
+ for (const artifact of activeArtifacts) {
553
+ artifact.textChars += value.length;
554
+ if (artifact.textChars > maxTextChars)
555
+ throw new Error("artifact text limit");
556
+ }
557
+ const owner = activeElements[activeElements.length - 1];
558
+ if (owner?.messageOwner !== undefined && !owner.messageIgnored) {
559
+ owner.messageOwner.messageChars += value.length;
560
+ if (owner.messageOwner.messageChars > maxTextChars)
561
+ throw new Error("text limit exceeded");
562
+ if (value.length > 0)
563
+ owner.messageOwner.messageChunks.push(value);
564
+ }
565
+ }
566
+ current = nextDepthFirstNode(current, traversalRoot);
567
+ }
568
+ while (activeElements.length > 0) {
569
+ const closed = activeElements.pop();
570
+ if (closed?.artifact !== undefined)
571
+ activeArtifacts.pop();
572
+ }
573
+ const conversationId = [...conversationValues][0];
574
+ const threadId = [...threadValues][0];
575
+ // A stale rendered conversation must never override a different current
576
+ // navigation target. Both sources are independently useful, but when both
577
+ // are exposed they must identify the same provider conversation exactly.
578
+ if (conversationId !== undefined && fromUrl !== undefined && conversationId !== fromUrl) {
579
+ throw new Error("conversation navigation mismatch");
580
+ }
581
+ const resolvedConversationId = conversationId ?? fromUrl;
582
+ const resolvedThreadId = threadId ?? resolvedConversationId;
583
+ if ((resolvedConversationId === undefined || resolvedThreadId === undefined)
584
+ && (!allowBlankTask || roleNodeCount !== 0 || (!blankTaskMarker && visibleComposerCount !== 1))) {
585
+ throw new Error("conversation identity unavailable");
586
+ }
587
+ const outputRoots = roots.filter(root => root.roleMarkers.length > 0);
588
+ if (outputRoots.length > maxTurns)
589
+ throw new Error("turn limit exceeded");
590
+ let totalTextChars = 0;
591
+ let turns = [];
592
+ for (const root of outputRoots) {
593
+ const roleMarkers = root.roleMarkers;
594
+ const distinctRoles = new Set(roleMarkers.map(node => node.getAttribute("data-message-author-role")));
595
+ if (distinctRoles.size !== 1)
596
+ throw new Error("role ambiguity");
597
+ const role = [...distinctRoles][0];
598
+ if (role !== "user" && role !== "assistant")
599
+ throw new Error("role drift");
600
+ const roleNode = roleMarkers[0];
601
+ if (roleNode === undefined)
602
+ throw new Error("role missing");
603
+ const stableId = preferredNodeAttribute(root.root, roleMarkers, ["data-message-id", "data-conversation-turn-id", "data-turn-id"]);
604
+ if (stableId === undefined)
605
+ throw new Error("turn identity unavailable");
606
+ const explicitParentStableId = uniqueNodeAttribute(root.root, roleMarkers, ["data-parent-message-id", "data-parent-turn-id", "data-parent-id"]);
607
+ const explicitBranchStableId = uniqueNodeAttribute(root.root, roleMarkers, ["data-branch-id", "data-conversation-branch-id", "data-message-branch-id"]);
608
+ const previousTurn = turns.at(-1);
609
+ const parentStableId = role === "assistant"
610
+ ? explicitParentStableId ?? (previousTurn?.role === "user" ? previousTurn.stableId : undefined)
611
+ : explicitParentStableId;
612
+ if (role === "assistant" && parentStableId === undefined)
613
+ throw new Error("branch ambiguity");
614
+ const branchStableId = role === "assistant" ? explicitBranchStableId ?? stableId : explicitBranchStableId;
615
+ const text = boundedText(root.messageChunks.join("").replace(/\s+/g, " ").trim().normalize("NFC"), maxTextChars);
616
+ totalTextChars += text.length;
617
+ if (totalTextChars > MAX_TOTAL_TEXT_CHARS)
618
+ throw new Error("text limit exceeded");
619
+ const artifacts = root.artifacts;
620
+ const state = readTurnState(root.root, roleNode);
621
+ const finishReason = readFinishReason(root.root, roleNode);
622
+ // Only read innerHTML for the exact assistant ID requested by the outer
623
+ // transaction. This keeps transient markup bounded and prevents a
624
+ // page-wide/latest-message fallback from entering the response path.
625
+ const contentHtml = role === "assistant" && captureAssistantTurnId === stableId
626
+ ? boundedMarkup(roleNode.innerHTML, maxTextChars)
627
+ : undefined;
628
+ turns.push({
629
+ role,
630
+ stableId,
631
+ ...(parentStableId === undefined ? {} : { parentStableId }),
632
+ ...(branchStableId === undefined ? {} : { branchStableId }),
633
+ ordinal: role === "user" ? turns.filter(turn => turn.role === "user").length : turns.filter(turn => turn.role === "assistant").length,
634
+ text,
635
+ ...(contentHtml === undefined ? {} : { contentHtml }),
636
+ structure: {
637
+ tag: root.root.tagName.toLowerCase().slice(0, 64),
638
+ childCount: Math.min(root.directChildCount, MAX_NODES),
639
+ artifactCount: artifacts.length
640
+ },
641
+ ...(state === undefined ? {} : { state }),
642
+ ...(finishReason === undefined ? {} : { finishReason }),
643
+ artifacts
644
+ });
645
+ }
646
+ if (visibleStructuralStopControlCount > 1)
647
+ throw new Error("generation state ambiguity");
648
+ const assistantIndexes = turns.flatMap((turn, index) => turn.role === "assistant" ? [index] : []);
649
+ if (visibleStructuralStopControlCount === 1 && assistantIndexes.length === 0)
650
+ throw new Error("assistant identity unavailable");
651
+ const latestAssistantIndex = assistantIndexes.at(-1);
652
+ turns = turns.map((turn, index) => {
653
+ if (turn.role !== "assistant")
654
+ return turn;
655
+ const inferredState = visibleStructuralStopControlCount === 1 && index === latestAssistantIndex
656
+ ? "generating"
657
+ : "terminal";
658
+ if (turn.state !== undefined && turn.state !== inferredState)
659
+ throw new Error("unstable generation state");
660
+ return {
661
+ ...turn,
662
+ state: turn.state ?? inferredState,
663
+ ...((turn.state ?? inferredState) === "terminal" && turn.finishReason === undefined
664
+ ? { finishReason: "provider_terminal" }
665
+ : {})
666
+ };
667
+ });
668
+ validateRawTurnOrdering(turns);
669
+ const assistantTurns = turns.filter(turn => turn.role === "assistant");
670
+ if (assistantTurns.some(turn => turn.state === undefined))
671
+ throw new Error("assistant state unavailable");
672
+ const generating = assistantTurns.some(turn => turn.state === "generating");
673
+ const terminal = assistantTurns.length > 0 && assistantTurns.every(turn => turn.state === "terminal");
674
+ const terminalState = generating
675
+ ? "generating"
676
+ : terminal
677
+ ? "terminal"
678
+ : assistantTurns.length === 0
679
+ ? "idle"
680
+ : "unknown";
681
+ const completeness = completenessMarker === "truncated" || completenessMarker === "true"
682
+ ? "truncated"
683
+ : completenessMarker === "incomplete"
684
+ ? "incomplete"
685
+ : completenessMarker === "out_of_order"
686
+ ? "out_of_order"
687
+ : "complete";
688
+ if (completeness !== "complete")
689
+ throw new Error("incomplete conversation DOM");
690
+ return {
691
+ canonicalUrl,
692
+ ...(resolvedConversationId === undefined ? {} : { conversationId: resolvedConversationId }),
693
+ ...(resolvedThreadId === undefined ? {} : { threadId: resolvedThreadId }),
694
+ turns,
695
+ completeness,
696
+ terminalState
697
+ };
698
+ }
699
+ function validateOptions(options) {
700
+ if (!isRecord(options) || !isBoundedId(options.operationId))
701
+ throw new BrowserObservationError("provider_shape_drift");
702
+ if (typeof options.evidenceDigest !== "function")
703
+ throw new BrowserObservationError("evidence_digest_failed");
704
+ if (options.responseContent !== undefined && options.responseContent !== "include" && options.responseContent !== "metadata") {
705
+ throw new BrowserObservationError("provider_shape_drift");
706
+ }
707
+ if (options.responseFormat !== undefined && options.responseFormat !== "markdown" && options.responseFormat !== "text") {
708
+ throw new BrowserObservationError("provider_shape_drift");
709
+ }
710
+ const target = options.target;
711
+ if (!isRecord(target)
712
+ || !isBoundedId(target.providerId)
713
+ || !isBoundedId(target.browserId)
714
+ || !isBoundedId(target.tabId)
715
+ || (target.coordinationScope !== "process" && target.coordinationScope !== "provider")
716
+ || (target.targetLifecycle !== undefined
717
+ && target.targetLifecycle !== "fixed"
718
+ && target.targetLifecycle !== "new_pending"
719
+ && target.targetLifecycle !== "new_established")
720
+ || (target.coordinationScope === "provider" && target.authoritativeTabClaim === undefined)
721
+ || (target.authoritativeTabClaim !== undefined && !isBoundedId(target.authoritativeTabClaim))) {
722
+ throw new BrowserObservationError("missing_identity");
723
+ }
724
+ for (const value of [target.expectedConversationId, target.expectedThreadId, options.terminalAssistantTurnId, options.rawAssistantTurnId]) {
725
+ if (value !== undefined && !isBoundedId(value))
726
+ throw new BrowserObservationError("provider_shape_drift");
727
+ }
728
+ for (const value of [options.maxTurns, options.maxTextChars, options.maxArtifactsPerTurn]) {
729
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
730
+ throw new BrowserObservationError("bounded_limit_exceeded");
731
+ }
732
+ if (options.baseline !== undefined && options.baseline.completeness !== "complete") {
733
+ throw new BrowserObservationError("incomplete_dom");
734
+ }
735
+ }
736
+ function parseRawObservation(value, args) {
737
+ if (!isRecord(value))
738
+ throw new BrowserObservationError("provider_shape_drift");
739
+ assertExactKeys(value, ["canonicalUrl", "conversationId", "threadId", "turns", "completeness", "terminalState"]);
740
+ if (typeof value.canonicalUrl !== "string") {
741
+ throw new BrowserObservationError("navigation_ambiguous");
742
+ }
743
+ if (!isBoundedUrl(value.canonicalUrl)
744
+ || (value.conversationId !== undefined && !isBoundedId(value.conversationId))
745
+ || (value.threadId !== undefined && !isBoundedId(value.threadId))) {
746
+ throw new BrowserObservationError("navigation_ambiguous");
747
+ }
748
+ if ((value.conversationId === undefined || value.threadId === undefined)
749
+ && (!args.allowBlankTask || value.conversationId !== undefined || value.threadId !== undefined)) {
750
+ throw new BrowserObservationError("navigation_ambiguous");
751
+ }
752
+ if (!Array.isArray(value.turns) || value.turns.length > args.maxTurns)
753
+ throw new BrowserObservationError("bounded_limit_exceeded");
754
+ if (value.completeness !== "complete" && value.completeness !== "truncated" && value.completeness !== "incomplete" && value.completeness !== "out_of_order") {
755
+ throw new BrowserObservationError("provider_shape_drift");
756
+ }
757
+ if (value.terminalState !== "idle" && value.terminalState !== "generating" && value.terminalState !== "terminal" && value.terminalState !== "unknown") {
758
+ throw new BrowserObservationError("provider_shape_drift");
759
+ }
760
+ const turns = [];
761
+ const seenIds = new Set();
762
+ let totalTextChars = 0;
763
+ for (const candidate of value.turns) {
764
+ if (!isRecord(candidate))
765
+ throw new BrowserObservationError("provider_shape_drift");
766
+ assertExactKeys(candidate, ["role", "stableId", "parentStableId", "branchStableId", "ordinal", "text", "contentHtml", "structure", "state", "finishReason", "artifacts"]);
767
+ if (candidate.role !== "user" && candidate.role !== "assistant")
768
+ throw new BrowserObservationError("provider_shape_drift");
769
+ if (!isBoundedId(candidate.stableId))
770
+ throw new BrowserObservationError("missing_identity");
771
+ if (seenIds.has(candidate.stableId))
772
+ throw new BrowserObservationError("duplicate_identity");
773
+ seenIds.add(candidate.stableId);
774
+ if (!Number.isSafeInteger(candidate.ordinal) || candidate.ordinal < 0 || candidate.ordinal >= args.maxTurns)
775
+ throw new BrowserObservationError("provider_shape_drift");
776
+ if (typeof candidate.text !== "string" || candidate.text.length > args.maxTextChars)
777
+ throw new BrowserObservationError("bounded_limit_exceeded");
778
+ totalTextChars += candidate.text.length;
779
+ if (totalTextChars > MAX_TOTAL_TEXT_CHARS)
780
+ throw new BrowserObservationError("bounded_limit_exceeded");
781
+ if (candidate.contentHtml !== undefined) {
782
+ // A serialized page probe is only allowed to return markup for the
783
+ // exact requested assistant. Reject unsolicited/page-wide markup
784
+ // rather than silently treating it as a valid capture.
785
+ if (candidate.role !== "assistant" || args.captureAssistantTurnId === undefined || candidate.stableId !== args.captureAssistantTurnId) {
786
+ throw new BrowserObservationError("provider_shape_drift");
787
+ }
788
+ if (typeof candidate.contentHtml !== "string" || candidate.contentHtml.length > args.maxTextChars || candidate.contentHtml.includes("\u0000")) {
789
+ throw new BrowserObservationError("bounded_limit_exceeded");
790
+ }
791
+ }
792
+ if (candidate.parentStableId !== undefined && !isBoundedId(candidate.parentStableId))
793
+ throw new BrowserObservationError("unstable_identity");
794
+ if (candidate.branchStableId !== undefined && !isBoundedId(candidate.branchStableId))
795
+ throw new BrowserObservationError("unstable_identity");
796
+ if (candidate.state !== undefined && candidate.state !== "generating" && candidate.state !== "terminal")
797
+ throw new BrowserObservationError("provider_shape_drift");
798
+ if (candidate.role === "assistant" && (candidate.parentStableId === undefined || candidate.branchStableId === undefined))
799
+ throw new BrowserObservationError("branch_ambiguous");
800
+ if (candidate.role === "assistant" && candidate.state === undefined)
801
+ throw new BrowserObservationError("provider_shape_drift");
802
+ if (candidate.finishReason !== undefined && (!isBoundedText(candidate.finishReason, 128) || /[\u0000-\u001f\u007f]/u.test(candidate.finishReason)))
803
+ throw new BrowserObservationError("provider_shape_drift");
804
+ if (!isRecord(candidate.structure)
805
+ || typeof candidate.structure.tag !== "string"
806
+ || !Number.isSafeInteger(candidate.structure.childCount)
807
+ || !Number.isSafeInteger(candidate.structure.artifactCount)
808
+ || candidate.structure.childCount < 0
809
+ || candidate.structure.childCount > MAX_NODES
810
+ || candidate.structure.artifactCount < 0) {
811
+ throw new BrowserObservationError("provider_shape_drift");
812
+ }
813
+ assertExactKeys(candidate.structure, ["tag", "childCount", "artifactCount"]);
814
+ if (!Array.isArray(candidate.artifacts) || candidate.artifacts.length > args.maxArtifactsPerTurn)
815
+ throw new BrowserObservationError("bounded_limit_exceeded");
816
+ const artifactIds = new Set();
817
+ const artifacts = [];
818
+ for (const rawArtifact of candidate.artifacts) {
819
+ if (!isRecord(rawArtifact)
820
+ || (rawArtifact.kind !== "file" && rawArtifact.kind !== "image" && rawArtifact.kind !== "other")
821
+ || !isBoundedId(rawArtifact.identity))
822
+ throw new BrowserObservationError("provider_shape_drift");
823
+ assertExactKeys(rawArtifact, ["kind", "identity", "contentDigest", "bytes", "mimeType"]);
824
+ if (artifactIds.has(rawArtifact.identity))
825
+ throw new BrowserObservationError("duplicate_identity");
826
+ artifactIds.add(rawArtifact.identity);
827
+ if (rawArtifact.contentDigest !== undefined && !isContentDigest(rawArtifact.contentDigest))
828
+ throw new BrowserObservationError("provider_shape_drift");
829
+ if (rawArtifact.bytes !== undefined && (!Number.isSafeInteger(rawArtifact.bytes) || rawArtifact.bytes < 0 || rawArtifact.bytes > MAX_ARTIFACT_BYTES))
830
+ throw new BrowserObservationError("bounded_limit_exceeded");
831
+ if (rawArtifact.mimeType !== undefined && !isBoundedText(rawArtifact.mimeType, 128))
832
+ throw new BrowserObservationError("provider_shape_drift");
833
+ artifacts.push(Object.freeze({
834
+ kind: rawArtifact.kind,
835
+ identity: rawArtifact.identity,
836
+ ...(rawArtifact.contentDigest === undefined ? {} : { contentDigest: rawArtifact.contentDigest }),
837
+ ...(rawArtifact.bytes === undefined ? {} : { bytes: rawArtifact.bytes }),
838
+ ...(rawArtifact.mimeType === undefined ? {} : { mimeType: rawArtifact.mimeType })
839
+ }));
840
+ }
841
+ turns.push(Object.freeze({
842
+ role: candidate.role,
843
+ stableId: candidate.stableId,
844
+ ...(candidate.parentStableId === undefined ? {} : { parentStableId: candidate.parentStableId }),
845
+ ...(candidate.branchStableId === undefined ? {} : { branchStableId: candidate.branchStableId }),
846
+ ordinal: candidate.ordinal,
847
+ text: candidate.text,
848
+ ...(candidate.contentHtml === undefined ? {} : { contentHtml: candidate.contentHtml }),
849
+ structure: Object.freeze({
850
+ tag: candidate.structure.tag,
851
+ childCount: candidate.structure.childCount,
852
+ artifactCount: candidate.structure.artifactCount
853
+ }),
854
+ ...(candidate.state === undefined ? {} : { state: candidate.state }),
855
+ ...(candidate.finishReason === undefined ? {} : { finishReason: candidate.finishReason }),
856
+ artifacts: Object.freeze(artifacts)
857
+ }));
858
+ }
859
+ validateRawTurnOrdering(turns);
860
+ const assistant = turns.filter(turn => turn.role === "assistant");
861
+ if (assistant.some(turn => turn.state === undefined))
862
+ throw new BrowserObservationError("provider_shape_drift");
863
+ const hasGeneratingAssistant = assistant.some(turn => turn.state === "generating");
864
+ const hasOnlyTerminalAssistants = assistant.length > 0 && assistant.every(turn => turn.state === "terminal");
865
+ const expectedTerminalState = hasGeneratingAssistant
866
+ ? "generating"
867
+ : hasOnlyTerminalAssistants
868
+ ? "terminal"
869
+ : assistant.length === 0
870
+ ? "idle"
871
+ : "unknown";
872
+ if (value.terminalState !== expectedTerminalState)
873
+ throw new BrowserObservationError("unstable_identity");
874
+ if (value.completeness !== "complete")
875
+ throw new BrowserObservationError("incomplete_dom");
876
+ return Object.freeze({
877
+ canonicalUrl: value.canonicalUrl,
878
+ conversationId: value.conversationId,
879
+ threadId: value.threadId,
880
+ turns: Object.freeze(turns),
881
+ completeness: value.completeness,
882
+ terminalState: value.terminalState
883
+ });
884
+ }
885
+ async function normalizeTurns(turns, options) {
886
+ const artifacts = turns.map(raw => new Array(raw.artifacts.length));
887
+ const evidence = new Array(turns.length);
888
+ const structure = new Array(turns.length);
889
+ // Artifact digests feed both later identities. Run each dependency stage
890
+ // through four workers, with lazy jobs and deterministic result positions.
891
+ await runTurnDigestJobs((function* () {
892
+ for (const [index, raw] of turns.entries()) {
893
+ for (const [ordinal, artifact] of raw.artifacts.entries()) {
894
+ yield async () => {
895
+ artifacts[index][ordinal] = await digest(options.evidenceDigest, "browser-observation-artifact", {
896
+ operationId: options.operationId,
897
+ turnId: raw.stableId,
898
+ ordinal,
899
+ kind: artifact.kind,
900
+ identity: artifact.identity,
901
+ ...(artifact.contentDigest === undefined ? {} : { contentDigest: artifact.contentDigest }),
902
+ ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
903
+ ...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType })
904
+ });
905
+ };
906
+ }
907
+ }
908
+ })());
909
+ await runTurnDigestJobs((function* () {
910
+ for (const [index, raw] of turns.entries()) {
911
+ const artifactEvidenceDigests = artifacts[index];
912
+ yield async () => {
913
+ evidence[index] = await digest(options.evidenceDigest, "browser-observation-turn", {
914
+ operationId: options.operationId,
915
+ role: raw.role,
916
+ stableId: raw.stableId,
917
+ ...(raw.parentStableId === undefined ? {} : { parentStableId: raw.parentStableId }),
918
+ ...(raw.branchStableId === undefined ? {} : { branchStableId: raw.branchStableId }),
919
+ ordinal: raw.ordinal,
920
+ text: raw.text,
921
+ artifacts: artifactEvidenceDigests
922
+ });
923
+ };
924
+ yield async () => {
925
+ structure[index] = await digest(options.evidenceDigest, "browser-observation-structure", {
926
+ operationId: options.operationId,
927
+ role: raw.role,
928
+ stableId: raw.stableId,
929
+ ordinal: raw.ordinal,
930
+ structure: raw.structure,
931
+ artifacts: artifactEvidenceDigests
932
+ });
933
+ };
934
+ }
935
+ })());
936
+ const users = [];
937
+ const assistants = [];
938
+ const assistantParents = new Map();
939
+ for (const [index, raw] of turns.entries()) {
940
+ const turn = Object.freeze({
941
+ stableId: raw.stableId,
942
+ evidenceDigest: evidence[index],
943
+ structureDigest: structure[index],
944
+ ordinal: raw.ordinal,
945
+ ...(raw.parentStableId === undefined ? {} : { parentStableId: raw.parentStableId }),
946
+ ...(raw.branchStableId === undefined ? {} : { branchStableId: raw.branchStableId }),
947
+ ...(raw.state === undefined ? {} : { state: raw.state }),
948
+ artifactEvidenceDigests: Object.freeze(artifacts[index])
949
+ });
950
+ if (raw.role === "user")
951
+ users.push(turn);
952
+ else {
953
+ assistants.push(turn);
954
+ const parent = raw.parentStableId;
955
+ const branches = assistantParents.get(parent) ?? new Set();
956
+ if (branches.has(raw.branchStableId))
957
+ throw new BrowserObservationError("branch_ambiguous");
958
+ branches.add(raw.branchStableId);
959
+ assistantParents.set(parent, branches);
960
+ }
961
+ }
962
+ return { userTurns: users, assistantTurns: assistants };
963
+ }
964
+ async function runTurnDigestJobs(jobs) {
965
+ const iterator = jobs[Symbol.iterator]();
966
+ let failed = false;
967
+ let failure;
968
+ const worker = async () => {
969
+ while (!failed) {
970
+ try {
971
+ const next = iterator.next();
972
+ if (next.done)
973
+ return;
974
+ await next.value();
975
+ }
976
+ catch (error) {
977
+ if (!failed) {
978
+ failed = true;
979
+ failure = error;
980
+ }
981
+ }
982
+ }
983
+ };
984
+ // Drain already-started digest calls before returning a failure. No queued
985
+ // jobs are admitted after the first failed digest and none outlive this pass.
986
+ await Promise.all(Array.from({ length: 4 }, worker));
987
+ if (failed)
988
+ throw failure;
989
+ }
990
+ function resolveTerminalTurn(rawTurns, assistantTurns, options) {
991
+ const requestedId = options.terminalAssistantTurnId ?? options.rawAssistantTurnId;
992
+ if (requestedId === undefined)
993
+ return undefined;
994
+ const raw = rawTurns.find(turn => turn.role === "assistant" && turn.stableId === requestedId);
995
+ const normalized = assistantTurns.find(turn => turn.stableId === requestedId);
996
+ if (raw === undefined || normalized === undefined || raw.state !== "terminal")
997
+ throw new BrowserObservationError("raw_content_unavailable");
998
+ if (raw.finishReason === undefined)
999
+ throw new BrowserObservationError("provider_shape_drift");
1000
+ return { raw, normalized };
1001
+ }
1002
+ async function terminalObservation(raw, normalized, userTurns, options, _target) {
1003
+ if (raw.parentStableId === undefined || raw.branchStableId === undefined || raw.state !== "terminal" || raw.finishReason === undefined) {
1004
+ throw new BrowserObservationError("provider_shape_drift");
1005
+ }
1006
+ const user = userTurns.find(turn => turn.stableId === raw.parentStableId);
1007
+ if (user === undefined)
1008
+ throw new BrowserObservationError("branch_ambiguous");
1009
+ if (raw.contentHtml === undefined)
1010
+ throw new BrowserObservationError("raw_content_unavailable");
1011
+ const requestedFormat = options.responseFormat ?? "markdown";
1012
+ const normalizedFormat = normalizeResponseFormat(requestedFormat);
1013
+ if (normalizedFormat !== "markdown" && normalizedFormat !== "normalized_text") {
1014
+ throw new BrowserObservationError("provider_shape_drift");
1015
+ }
1016
+ const maxResponseChars = boundedPositive(options.maxTextChars, MAX_TEXT_CHARS);
1017
+ let formatted;
1018
+ try {
1019
+ formatted = formatMessageHtml(raw.contentHtml, requestedFormat, maxResponseChars);
1020
+ }
1021
+ catch {
1022
+ throw new BrowserObservationError("provider_shape_drift");
1023
+ }
1024
+ if (formatted.captureLimit?.clipped === true)
1025
+ throw new BrowserObservationError("bounded_limit_exceeded");
1026
+ if (formatted.text.length === 0 && raw.text.length > 0)
1027
+ throw new BrowserObservationError("provider_shape_drift");
1028
+ if (formatted.text.length > maxResponseChars || utf8Bytes(formatted.text) > MAX_RESPONSE_BYTES) {
1029
+ throw new BrowserObservationError("bounded_limit_exceeded");
1030
+ }
1031
+ const textDigest = await digest(options.evidenceDigest, "browser-observation-response", {
1032
+ operationId: options.operationId,
1033
+ assistantTurnId: raw.stableId,
1034
+ responseFormat: requestedFormat,
1035
+ text: formatted.text
1036
+ });
1037
+ const text = Object.freeze({
1038
+ digest: textDigest,
1039
+ bytes: utf8Bytes(formatted.text),
1040
+ chars: formatted.text.length
1041
+ });
1042
+ const artifacts = raw.artifacts.map((artifact, ordinal) => {
1043
+ const sourceIdentityDigest = normalized.artifactEvidenceDigests?.[ordinal];
1044
+ if (sourceIdentityDigest === undefined)
1045
+ throw new BrowserObservationError("provider_shape_drift");
1046
+ return Object.freeze({
1047
+ kind: artifact.kind,
1048
+ ordinal,
1049
+ sourceIdentityDigest,
1050
+ ...(artifact.contentDigest === undefined ? {} : { contentDigest: artifact.contentDigest }),
1051
+ ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
1052
+ ...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType })
1053
+ });
1054
+ });
1055
+ return Object.freeze({
1056
+ schemaVersion: COLLECTOR_TERMINAL_SCHEMA_VERSION,
1057
+ userTurnId: raw.parentStableId,
1058
+ assistantTurnId: raw.stableId,
1059
+ userTurnEvidenceDigest: user.evidenceDigest,
1060
+ assistantTurnEvidenceDigest: normalized.evidenceDigest,
1061
+ userOrdinal: user.ordinal,
1062
+ assistantOrdinal: raw.ordinal,
1063
+ branchStableId: raw.branchStableId,
1064
+ text,
1065
+ responseFormat: requestedFormat,
1066
+ ...(options.responseContent === "include" && options.rawAssistantTurnId === raw.stableId ? { rawText: formatted.text } : {}),
1067
+ artifacts: Object.freeze(artifacts),
1068
+ finishReason: raw.finishReason
1069
+ });
1070
+ }
1071
+ async function makePostSendDelta(baseline, users, evidenceDigest) {
1072
+ const baselineIds = new Set();
1073
+ let baselineCursor = 0;
1074
+ for (const turn of baseline.userTurns) {
1075
+ if (turn.stableId === undefined || baselineIds.has(turn.stableId))
1076
+ throw new BrowserObservationError("unstable_identity");
1077
+ baselineIds.add(turn.stableId);
1078
+ const fresh = users[baselineCursor];
1079
+ if (fresh?.stableId !== turn.stableId || fresh.evidenceDigest !== turn.evidenceDigest)
1080
+ throw new BrowserObservationError("incomplete_dom");
1081
+ baselineCursor += 1;
1082
+ }
1083
+ const added = users.slice(baselineCursor);
1084
+ const addedUserEvidenceDigests = added.map(turn => turn.evidenceDigest);
1085
+ const deltaDigest = await digest(evidenceDigest, "browser-observation-post-send-delta", {
1086
+ baselineSnapshotDigest: baseline.snapshotDigest,
1087
+ addedUserEvidenceDigests
1088
+ });
1089
+ return Object.freeze({
1090
+ baselineSnapshotDigest: baseline.snapshotDigest,
1091
+ addedUserEvidenceDigests: Object.freeze(addedUserEvidenceDigests),
1092
+ deltaDigest
1093
+ });
1094
+ }
1095
+ async function buildTarget(raw, options) {
1096
+ if (options.target.expectedConversationId !== undefined && options.target.expectedConversationId !== raw.conversationId)
1097
+ throw new BrowserObservationError("navigation_ambiguous");
1098
+ if (options.target.expectedThreadId !== undefined && options.target.expectedThreadId !== raw.threadId)
1099
+ throw new BrowserObservationError("navigation_ambiguous");
1100
+ const hasConversationIdentity = raw.conversationId !== undefined && raw.threadId !== undefined;
1101
+ const canonicalDigest = hasConversationIdentity
1102
+ ? await digest(options.evidenceDigest, "browser-observation-url", raw.canonicalUrl)
1103
+ : undefined;
1104
+ const target = {
1105
+ provider: availableIdentity(options.target.providerId),
1106
+ browser: availableIdentity(options.target.browserId),
1107
+ tab: availableIdentity(options.target.tabId),
1108
+ thread: raw.threadId === undefined ? unavailableIdentity("not_observed") : availableIdentity(raw.threadId),
1109
+ conversation: raw.conversationId === undefined ? unavailableIdentity("not_observed") : availableIdentity(raw.conversationId),
1110
+ canonicalThreadUrl: canonicalDigest === undefined
1111
+ ? unavailableIdentity("not_observed")
1112
+ : availableUrlIdentity(`${OPAQUE_URL_PREFIX}${canonicalDigest.slice("hmac-sha256:".length)}`),
1113
+ authoritativeTabClaim: options.target.authoritativeTabClaim === undefined
1114
+ ? unavailableIdentity("not_exposed")
1115
+ : availableIdentity(options.target.authoritativeTabClaim),
1116
+ coordinationScope: options.target.coordinationScope
1117
+ };
1118
+ return Object.freeze(target);
1119
+ }
1120
+ function targetMaterial(target) {
1121
+ return {
1122
+ provider: target.provider,
1123
+ browser: target.browser,
1124
+ tab: target.tab,
1125
+ thread: target.thread,
1126
+ conversation: target.conversation,
1127
+ canonicalThreadUrl: target.canonicalThreadUrl,
1128
+ authoritativeTabClaim: target.authoritativeTabClaim,
1129
+ coordinationScope: target.coordinationScope
1130
+ };
1131
+ }
1132
+ function turnMaterial(turn) {
1133
+ return {
1134
+ stableId: turn.stableId,
1135
+ evidenceDigest: turn.evidenceDigest,
1136
+ structureDigest: turn.structureDigest,
1137
+ ordinal: turn.ordinal,
1138
+ ...(turn.parentStableId === undefined ? {} : { parentStableId: turn.parentStableId }),
1139
+ ...(turn.branchStableId === undefined ? {} : { branchStableId: turn.branchStableId }),
1140
+ ...(turn.state === undefined ? {} : { state: turn.state }),
1141
+ artifactEvidenceDigests: turn.artifactEvidenceDigests ?? []
1142
+ };
1143
+ }
1144
+ async function digest(fn, domain, material) {
1145
+ let result;
1146
+ try {
1147
+ result = await fn(domain, material);
1148
+ }
1149
+ catch {
1150
+ throw new BrowserObservationError("evidence_digest_failed");
1151
+ }
1152
+ if (typeof result !== "string" || !DIGEST_PATTERN.test(result))
1153
+ throw new BrowserObservationError("evidence_digest_failed");
1154
+ return result;
1155
+ }
1156
+ function availableIdentity(value) {
1157
+ return Object.freeze({ status: "available", value });
1158
+ }
1159
+ function availableUrlIdentity(value) {
1160
+ return Object.freeze({ status: "available", value });
1161
+ }
1162
+ function unavailableIdentity(reason) {
1163
+ return Object.freeze({ status: "unavailable", reason });
1164
+ }
1165
+ function validateRawTurnOrdering(turns) {
1166
+ const counters = { user: 0, assistant: 0 };
1167
+ for (const turn of turns) {
1168
+ if (turn.ordinal !== counters[turn.role])
1169
+ throw new BrowserObservationError("incomplete_dom");
1170
+ counters[turn.role] += 1;
1171
+ }
1172
+ const users = new Set(turns.filter(turn => turn.role === "user").map(turn => turn.stableId));
1173
+ for (const turn of turns) {
1174
+ if (turn.role === "assistant" && (turn.parentStableId === undefined || !users.has(turn.parentStableId)))
1175
+ throw new BrowserObservationError("branch_ambiguous");
1176
+ }
1177
+ }
1178
+ function isRecord(value) {
1179
+ return isPlainDataRecord(value);
1180
+ }
1181
+ function assertExactKeys(value, allowed) {
1182
+ const allowedSet = new Set(allowed);
1183
+ for (const key of Object.keys(value)) {
1184
+ if (!allowedSet.has(key))
1185
+ throw new BrowserObservationError("provider_shape_drift");
1186
+ }
1187
+ }
1188
+ function isBoundedId(value) {
1189
+ return typeof value === "string" && ID_PATTERN.test(value);
1190
+ }
1191
+ function isBoundedText(value, max) {
1192
+ return typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000]/u.test(value);
1193
+ }
1194
+ function isBoundedUrl(value) {
1195
+ if (typeof value !== "string" || value.length < 1 || value.length > MAX_ATTRIBUTE_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
1196
+ return false;
1197
+ try {
1198
+ const url = new URL(value);
1199
+ return url.protocol === "https:" && url.username === "" && url.password === "" && url.search === "" && url.hash === "";
1200
+ }
1201
+ catch {
1202
+ return false;
1203
+ }
1204
+ }
1205
+ function isContentDigest(value) {
1206
+ return typeof value === "string" && /^(?:hmac-sha256:|sha256:)[0-9a-f]{64}$/.test(value);
1207
+ }
1208
+ function boundedPositive(value, fallback) {
1209
+ return Number.isSafeInteger(value) && value > 0 ? Math.min(value, fallback) : fallback;
1210
+ }
1211
+ function canonicalizeUrl(value) {
1212
+ const url = new URL(value);
1213
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "")
1214
+ throw new Error("non-canonical navigation");
1215
+ url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/$/u, "") || "/";
1216
+ return url.toString();
1217
+ }
1218
+ function conversationIdFromUrl(urlValue) {
1219
+ const url = new URL(urlValue);
1220
+ const parts = url.pathname.split("/").filter(Boolean);
1221
+ const index = parts.findIndex(part => part === "c" || part === "conversation");
1222
+ const candidate = index >= 0 ? parts[index + 1] : undefined;
1223
+ return candidate !== undefined && ID_PATTERN.test(candidate) ? candidate : undefined;
1224
+ }
1225
+ function readTurnState(root, roleNode) {
1226
+ const values = [root, roleNode].flatMap(node => [
1227
+ node.getAttribute("data-generation-state"),
1228
+ node.getAttribute("data-state"),
1229
+ node.getAttribute("data-status"),
1230
+ node.getAttribute("data-turn-state"),
1231
+ node.getAttribute("data-generating") === "true" ? "generating" : node.getAttribute("data-generating") === "false" ? "terminal" : null,
1232
+ node.getAttribute("aria-busy") === "true" ? "generating" : node.getAttribute("aria-busy") === "false" ? "terminal" : null
1233
+ ]).filter((value) => value !== null && value !== undefined).map(value => value.trim().toLowerCase());
1234
+ const generating = values.filter(value => ["generating", "streaming", "in_progress", "in-progress", "true"].includes(value));
1235
+ const terminal = values.filter(value => ["terminal", "complete", "completed", "done", "false"].includes(value));
1236
+ if (generating.length > 0 && terminal.length > 0)
1237
+ throw new Error("unstable generation state");
1238
+ if (generating.length > 0)
1239
+ return "generating";
1240
+ if (terminal.length > 0)
1241
+ return "terminal";
1242
+ return undefined;
1243
+ }
1244
+ function readFinishReason(root, roleNode) {
1245
+ for (const node of [root, roleNode]) {
1246
+ for (const name of ["data-finish-reason", "data-stop-reason", "data-completion-reason"]) {
1247
+ const value = node.getAttribute(name);
1248
+ if (value !== null && value.length > 0)
1249
+ return value.trim().slice(0, 128);
1250
+ }
1251
+ }
1252
+ return undefined;
1253
+ }
1254
+ function utf8Bytes(value) {
1255
+ return new TextEncoder().encode(value).byteLength;
1256
+ }