@tormentalabs/claude-code-wire-compat 0.5.0 → 0.7.0-rc.1

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 (91) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +19 -7
  3. package/dist/betas.d.ts +65 -7
  4. package/dist/betas.d.ts.map +1 -1
  5. package/dist/betas.js +143 -2
  6. package/dist/betas.js.map +1 -1
  7. package/dist/build-request.d.ts +12 -6
  8. package/dist/build-request.d.ts.map +1 -1
  9. package/dist/build-request.js +93 -50
  10. package/dist/build-request.js.map +1 -1
  11. package/dist/contracts.d.ts +7 -2
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/fingerprint.d.ts.map +1 -1
  15. package/dist/fingerprint.js +5 -0
  16. package/dist/fingerprint.js.map +1 -1
  17. package/dist/headers.d.ts.map +1 -1
  18. package/dist/headers.js +5 -2
  19. package/dist/headers.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/limits.d.ts +3 -0
  25. package/dist/limits.d.ts.map +1 -0
  26. package/dist/limits.js +4 -0
  27. package/dist/limits.js.map +1 -0
  28. package/dist/model-capabilities.d.ts +3 -3
  29. package/dist/model-capabilities.d.ts.map +1 -1
  30. package/dist/model-capabilities.js +55 -19
  31. package/dist/model-capabilities.js.map +1 -1
  32. package/dist/model-identity.d.ts +9 -1
  33. package/dist/model-identity.d.ts.map +1 -1
  34. package/dist/model-identity.js +19 -1
  35. package/dist/model-identity.js.map +1 -1
  36. package/dist/model-queries.d.ts +17 -5
  37. package/dist/model-queries.d.ts.map +1 -1
  38. package/dist/model-queries.js +33 -6
  39. package/dist/model-queries.js.map +1 -1
  40. package/dist/models.js +1 -1
  41. package/dist/models.js.map +1 -1
  42. package/dist/profiles/beta-registry-2.1.280.d.ts +211 -0
  43. package/dist/profiles/beta-registry-2.1.280.d.ts.map +1 -0
  44. package/dist/profiles/beta-registry-2.1.280.js +292 -0
  45. package/dist/profiles/beta-registry-2.1.280.js.map +1 -0
  46. package/dist/profiles/claude-code-2.1.280.d.ts +3 -0
  47. package/dist/profiles/claude-code-2.1.280.d.ts.map +1 -0
  48. package/dist/profiles/claude-code-2.1.280.js +359 -0
  49. package/dist/profiles/claude-code-2.1.280.js.map +1 -0
  50. package/dist/redaction.d.ts.map +1 -1
  51. package/dist/redaction.js +48 -2
  52. package/dist/redaction.js.map +1 -1
  53. package/dist/request-body.d.ts +4 -2
  54. package/dist/request-body.d.ts.map +1 -1
  55. package/dist/request-body.js +77 -48
  56. package/dist/request-body.js.map +1 -1
  57. package/dist/system-prompt.d.ts.map +1 -1
  58. package/dist/system-prompt.js +23 -22
  59. package/dist/system-prompt.js.map +1 -1
  60. package/dist/thinking.d.ts +53 -2
  61. package/dist/thinking.d.ts.map +1 -1
  62. package/dist/thinking.js +124 -26
  63. package/dist/thinking.js.map +1 -1
  64. package/dist/unicode.d.ts +41 -0
  65. package/dist/unicode.d.ts.map +1 -1
  66. package/dist/unicode.js +37 -0
  67. package/dist/unicode.js.map +1 -1
  68. package/dist/violation.d.ts +23 -0
  69. package/dist/violation.d.ts.map +1 -0
  70. package/dist/violation.js +210 -0
  71. package/dist/violation.js.map +1 -0
  72. package/package.json +6 -2
  73. package/src/betas.ts +208 -9
  74. package/src/build-request.ts +120 -56
  75. package/src/contracts.ts +7 -2
  76. package/src/fingerprint.ts +5 -0
  77. package/src/headers.ts +4 -2
  78. package/src/index.ts +2 -0
  79. package/src/limits.ts +4 -0
  80. package/src/model-capabilities.ts +55 -19
  81. package/src/model-identity.ts +14 -1
  82. package/src/model-queries.ts +38 -6
  83. package/src/models.ts +1 -1
  84. package/src/profiles/beta-registry-2.1.280.ts +309 -0
  85. package/src/profiles/claude-code-2.1.280.ts +364 -0
  86. package/src/redaction.ts +65 -2
  87. package/src/request-body.ts +107 -49
  88. package/src/system-prompt.ts +32 -24
  89. package/src/thinking.ts +148 -26
  90. package/src/unicode.ts +71 -0
  91. package/src/violation.ts +226 -0
@@ -0,0 +1,210 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ /*
3
+ * Safe validation-failure diagnostics (decision P1.T1, D4).
4
+ *
5
+ * Every value produced here is safe by construction: reasons come from a
6
+ * closed set, path segments from a closed vocabulary of schema-defined keys
7
+ * plus numeric indices and the `*` placeholder for user-controlled keys, and
8
+ * reported code units only ever come from control or surrogate ranges. No
9
+ * caller text, key name or excerpt can leak through these fields.
10
+ */
11
+ export const VIOLATION_REASONS = new Set([
12
+ "lone-surrogate",
13
+ "control-char",
14
+ ]);
15
+ /**
16
+ * Schema-defined keys allowed verbatim in a violation path. Any other object
17
+ * key (tool input names, `input_schema` property names, arbitrary metadata
18
+ * members) is user-controlled text and is reported as `*`.
19
+ */
20
+ const PATH_KEY_VOCABULARY = new Set([
21
+ "$schema",
22
+ "accept",
23
+ "additionalBetas",
24
+ "additionalProperties",
25
+ "anthropicAdditionalProtection",
26
+ "app",
27
+ "arch",
28
+ "accountUuid",
29
+ "body",
30
+ "cache_control",
31
+ "citations",
32
+ "clientApp",
33
+ "clientRequestId",
34
+ "claudeRemoteContainerId",
35
+ "claudeRemoteSessionId",
36
+ "content",
37
+ "context",
38
+ "crypto",
39
+ "data",
40
+ "default",
41
+ "defer_loading",
42
+ "description",
43
+ "deviceId",
44
+ "document",
45
+ "effort",
46
+ "enum",
47
+ "evidence",
48
+ "examples",
49
+ "extraHeaderPolicy",
50
+ "extraHeaders",
51
+ "headers",
52
+ "id",
53
+ "image",
54
+ "input",
55
+ "input_schema",
56
+ "input_examples",
57
+ "items",
58
+ "max_tokens",
59
+ "maxTokens",
60
+ "media_type",
61
+ "messages",
62
+ "metadata",
63
+ "metadataOverrides",
64
+ "method",
65
+ "model",
66
+ "name",
67
+ "os",
68
+ "output_config",
69
+ "profileOverride",
70
+ "properties",
71
+ "redacted_thinking",
72
+ "required",
73
+ "role",
74
+ "runtime",
75
+ "runtimeVersion",
76
+ "search_results",
77
+ "sessionId",
78
+ "signature",
79
+ "source",
80
+ "stainlessHelper",
81
+ "stainlessRetryCount",
82
+ "stop_sequences",
83
+ "stream",
84
+ "strict",
85
+ "system",
86
+ "text",
87
+ "thinking",
88
+ "timing",
89
+ "title",
90
+ "tool_name",
91
+ "tool_reference",
92
+ "tool_result",
93
+ "tool_use",
94
+ "tool_use_id",
95
+ "tools",
96
+ "type",
97
+ "url",
98
+ "user_id",
99
+ "userId",
100
+ "userIdFields",
101
+ ]);
102
+ const MAX_PATH_SEGMENTS = 16;
103
+ const MAX_PATH_LENGTH = 256;
104
+ const TRUNCATION_MARKER = "...";
105
+ const OPAQUE_SUBTREES = new Set([
106
+ "headers",
107
+ "input",
108
+ "input_schema",
109
+ "input_examples",
110
+ "metadata",
111
+ "metadataOverrides",
112
+ "userIdFields",
113
+ "extraHeaders",
114
+ "data",
115
+ "default",
116
+ "examples",
117
+ ]);
118
+ function mapSegment(segment, opaque) {
119
+ if (typeof segment === "number") {
120
+ return Number.isSafeInteger(segment) &&
121
+ !Object.is(segment, -0) &&
122
+ segment >= 0 &&
123
+ segment <= 999_999
124
+ ? String(segment)
125
+ : "*";
126
+ }
127
+ return !opaque && PATH_KEY_VOCABULARY.has(segment) ? segment : "*";
128
+ }
129
+ /**
130
+ * Renders a pointer-like path (`/messages/0/content`) from walk segments.
131
+ * At most 16 segments, then a trailing `/...`; at most 256 characters.
132
+ */
133
+ export function formatViolationPath(segments) {
134
+ let path = "";
135
+ let opaque = false;
136
+ for (let index = 0; index < segments.length; index += 1) {
137
+ const segment = mapSegment(segments[index] ?? "*", opaque);
138
+ const tailLength = index < segments.length - 1 ? 4 : 0;
139
+ if (index >= MAX_PATH_SEGMENTS ||
140
+ path.length + 1 + segment.length + tailLength > MAX_PATH_LENGTH) {
141
+ return `${path}/${TRUNCATION_MARKER}`;
142
+ }
143
+ path += `/${segment}`;
144
+ opaque ||= segment === "*" || OPAQUE_SUBTREES.has(segment);
145
+ }
146
+ return path || "/";
147
+ }
148
+ export function isValidViolationReason(value) {
149
+ return VIOLATION_REASONS.has(value);
150
+ }
151
+ const PATH_INDEX_SEGMENT = /^(?:0|[1-9]\d{0,5})$/u;
152
+ /**
153
+ * Revalidates a formatted violation path for the redaction allowlist. Every
154
+ * segment must be a vocabulary key, a decimal index of at most 6 digits, `*`,
155
+ * or the single trailing truncation marker; the whole string is capped.
156
+ */
157
+ export function isValidViolationPath(value) {
158
+ if (value.length === 0 || value.length > MAX_PATH_LENGTH)
159
+ return false;
160
+ if (!value.startsWith("/"))
161
+ return false;
162
+ if (value === "/")
163
+ return true;
164
+ const segments = value.slice(1).split("/");
165
+ if (segments.length > MAX_PATH_SEGMENTS + 1)
166
+ return false;
167
+ let opaque = false;
168
+ for (let index = 0; index < segments.length; index += 1) {
169
+ const segment = segments[index];
170
+ if (segment === undefined)
171
+ return false;
172
+ if (segment === TRUNCATION_MARKER)
173
+ return index === segments.length - 1;
174
+ if (index >= MAX_PATH_SEGMENTS)
175
+ return false;
176
+ if (segment === "*") {
177
+ opaque = true;
178
+ continue;
179
+ }
180
+ if (PATH_INDEX_SEGMENT.test(segment))
181
+ continue;
182
+ if (opaque || !PATH_KEY_VOCABULARY.has(segment))
183
+ return false;
184
+ opaque ||= OPAQUE_SUBTREES.has(segment);
185
+ }
186
+ return true;
187
+ }
188
+ /** Numeric range guard for `violationCodeUnit`: controls or surrogates only. */
189
+ export function isValidViolationCodeUnit(value) {
190
+ if (!Number.isInteger(value) || Object.is(value, -0) || value < 0)
191
+ return false;
192
+ return (value <= 0x1f ||
193
+ (value >= 0x7f && value <= 0x9f) ||
194
+ (value >= 0xd800 && value <= 0xdfff));
195
+ }
196
+ /**
197
+ * Builds the safeDetails record for one text violation found at `path`.
198
+ * `inKey` distinguishes an offending object key from an offending value.
199
+ */
200
+ export function violationDetails(violation, path, textLength, inKey) {
201
+ return {
202
+ violationReason: violation.reason,
203
+ violationPath: formatViolationPath(path),
204
+ violationOffset: violation.offset,
205
+ violationCodeUnit: violation.codeUnit,
206
+ violationTextLength: textLength,
207
+ violationInKey: inKey,
208
+ };
209
+ }
210
+ //# sourceMappingURL=violation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"violation.js","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tormentalabs/claude-code-wire-compat",
3
- "version": "0.5.0",
3
+ "version": "0.7.0-rc.1",
4
4
  "description": "Runtime-neutral Claude Code Messages wire compatibility primitives",
5
5
  "type": "module",
6
6
  "private": false,
@@ -37,6 +37,10 @@
37
37
  "./profiles/claude-code-2.1.233": {
38
38
  "types": "./dist/profiles/claude-code-2.1.233.d.ts",
39
39
  "import": "./dist/profiles/claude-code-2.1.233.js"
40
+ },
41
+ "./profiles/claude-code-2.1.280": {
42
+ "types": "./dist/profiles/claude-code-2.1.280.d.ts",
43
+ "import": "./dist/profiles/claude-code-2.1.280.js"
40
44
  }
41
45
  },
42
46
  "types": "./dist/index.d.ts",
@@ -51,7 +55,7 @@
51
55
  "format:check": "prettier --check .",
52
56
  "test": "vitest run",
53
57
  "test:coverage": "vitest run --coverage",
54
- "test:pack": "node scripts/verify-packed-consumers.mjs",
58
+ "test:pack": "npm run build && node scripts/verify-packed-consumers.mjs",
55
59
  "pack:check": "npm pack --dry-run && publint && attw --pack . --profile esm-only",
56
60
  "fixtures:check": "node scripts/seal-golden-fixtures.mjs --check",
57
61
  "fixtures:seal": "node scripts/seal-golden-fixtures.mjs --write",
package/src/betas.ts CHANGED
@@ -11,8 +11,10 @@ import {
11
11
  supportsStructuredOutputs,
12
12
  } from "./model-capabilities.js";
13
13
  import { BETA_REGISTRY_2_1_233 } from "./profiles/beta-registry-2.1.233.js";
14
+ import { BETA_REGISTRY_2_1_280 } from "./profiles/beta-registry-2.1.280.js";
14
15
  import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
15
16
  import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
17
+ import { CLAUDE_CODE_2_1_280_PROFILE } from "./profiles/claude-code-2.1.280.js";
16
18
 
17
19
  /*
18
20
  * Provenance. The emitted order is a port of the genuine client's base beta set
@@ -41,13 +43,22 @@ interface BetaRegistryEntry {
41
43
  * The entries the push sites below require, as a structural contract rather
42
44
  * than a reference to one concrete registry.
43
45
  *
44
- * Registry versions have different key sets. Every key here except
45
- * `NARRATION_SUMMARIES` is present in all of them, so those push sites index
46
- * directly. `NARRATION_SUMMARIES` is optional because upstream removed it after
47
- * 2.1.195 (see `src/profiles/beta-registry-2.1.233.ts`): its push site survives
48
- * and becomes inert when the resolved registry has no entry to push. Making the
49
- * optionality part of the TYPE is what forces every future registry to be
50
- * checked against the push sites at compile time instead of at runtime.
46
+ * Registry versions have different key sets. Every REQUIRED key here is present
47
+ * in all of them, so those push sites index directly. The optional ones are
48
+ * optional because they exist in some registry versions and not others, and a
49
+ * push site whose key is absent becomes inert: it evaluates its gates, finds no
50
+ * entry, pushes nothing, and the surrounding order closes up with no gap.
51
+ * Making the optionality part of the TYPE is what forces every future registry
52
+ * to be checked against the push sites at compile time instead of at runtime.
53
+ *
54
+ * `NARRATION_SUMMARIES` is optional in the older direction -- upstream removed
55
+ * it after 2.1.195 (see `src/profiles/beta-registry-2.1.233.ts`). The keys
56
+ * added for 2.1.280 -- the remaining optional members declared below -- are
57
+ * optional in the newer direction: they are absent from the 2.1.195 registry,
58
+ * and all but `PER_MESSAGE_EFFORT` are absent from the 2.1.233 one. `PER_MESSAGE_EFFORT` IS declared by the 2.1.233 registry, so
59
+ * registry absence does not keep its site inert there; what does is the
60
+ * catalogue, because no 2.1.233 model declares the backing capability. The two
61
+ * mechanisms are not interchangeable and both are load-bearing.
51
62
  */
52
63
  export interface ComposableBetaRegistry {
53
64
  readonly CLAUDE_CODE: BetaRegistryEntry;
@@ -60,7 +71,12 @@ export interface ComposableBetaRegistry {
60
71
  readonly STRUCTURED_OUTPUTS: BetaRegistryEntry;
61
72
  readonly PROMPT_CACHING_SCOPE: BetaRegistryEntry;
62
73
  readonly MID_CONVERSATION_SYSTEM: BetaRegistryEntry;
74
+ readonly PER_MESSAGE_EFFORT?: BetaRegistryEntry;
75
+ readonly MID_CONV_TOOL_CHANGE?: BetaRegistryEntry;
76
+ readonly MID_CONVERSATION_SYSTEM_CLEAR_AT?: BetaRegistryEntry;
63
77
  readonly EFFORT: BetaRegistryEntry;
78
+ readonly THINKING_BINDING_CONTROLS?: BetaRegistryEntry;
79
+ readonly THINKING_DISPLAY_UPDATES?: BetaRegistryEntry;
64
80
  readonly SPEED: BetaRegistryEntry;
65
81
  readonly AFK_MODE: BetaRegistryEntry;
66
82
  readonly EXTENDED_CACHE_TTL: BetaRegistryEntry;
@@ -73,6 +89,7 @@ const PROFILE_BETA_REGISTRIES: ReadonlyMap<string, ComposableBetaRegistry> =
73
89
  new Map<string, ComposableBetaRegistry>([
74
90
  [CLAUDE_CODE_2_1_195_PROFILE.id, BETA_REGISTRY],
75
91
  [CLAUDE_CODE_2_1_233_PROFILE.id, BETA_REGISTRY_2_1_233],
92
+ [CLAUDE_CODE_2_1_280_PROFILE.id, BETA_REGISTRY_2_1_280],
76
93
  ]);
77
94
 
78
95
  /**
@@ -84,8 +101,17 @@ const PROFILE_BETA_REGISTRIES: ReadonlyMap<string, ComposableBetaRegistry> =
84
101
  * give `composeBetas` a second, differently-worded opinion about profile
85
102
  * validity. Standalone callers keep the 2.1.195 behaviour they had before
86
103
  * profiles were a parameter.
104
+ *
105
+ * Exported for tests only, and deliberately NOT re-exported from
106
+ * `src/index.ts`: the public runtime surface stays closed. That fallback is
107
+ * precisely why a test needs to reach this function. A profile bound to the
108
+ * wrong registry, or to none, silently composes against 2.1.195 instead of
109
+ * failing, and every required key of `ComposableBetaRegistry` currently
110
+ * carries an identical header in all three registries -- so a mis-binding
111
+ * changes no emitted byte and no behavioural suite can see it. Asking this
112
+ * function directly is the only way to observe the binding at all.
87
113
  */
88
- function resolveBetaRegistry(
114
+ export function resolveBetaRegistry(
89
115
  profile: ClaudeCodeProtocolProfile,
90
116
  ): ComposableBetaRegistry {
91
117
  return PROFILE_BETA_REGISTRIES.get(profile.id) ?? BETA_REGISTRY;
@@ -96,6 +122,17 @@ export interface ComposeBetasInput {
96
122
  readonly normalizedId: string;
97
123
  readonly capabilities: ClaudeCodeCapabilities;
98
124
  readonly thinkingDisplayActive: boolean;
125
+ /**
126
+ * Whether a `thinking` object of type `adaptive` or `enabled` will reach the
127
+ * wire for this request AND the model is interleaved-thinking capable, i.e.
128
+ * upstream `ac` minus its `Fg()` term. Compute it with `isThinkingActive`
129
+ * from `./thinking.js`; never re-derive it at a call site.
130
+ *
131
+ * Required rather than optional: it is a total function of inputs every call
132
+ * site already holds, and requiring it is what forces a new call site to
133
+ * decide rather than silently inherit a default.
134
+ */
135
+ readonly thinkingActive: boolean;
99
136
  readonly cacheTtl?: "5m" | "1h" | null;
100
137
  readonly speed?: "standard" | "fast" | null;
101
138
  /**
@@ -129,6 +166,19 @@ export interface ComposeBetasInput {
129
166
  export interface ComposedBetas {
130
167
  readonly betas: readonly string[];
131
168
  readonly suppressedBetaNames: readonly string[];
169
+ /**
170
+ * Present only when push site 12b fired. This is the body-side half of a
171
+ * beta/body pair: the site pushes the display-updates beta AND decides that
172
+ * the body's `thinking` object carries `display: "updates"`. The consumer is
173
+ * the canonical body builder (`buildCanonicalBody`), which hands it to
174
+ * `resolveThinking`.
175
+ *
176
+ * `suppressBetas` cannot unset it. Suppressing the beta identifier removes
177
+ * the HEADER only; the body keeps `display: "updates"`. That matches the
178
+ * existing coupled pair where suppressing `effort-2025-11-24` leaves
179
+ * `output_config.effort` in the body.
180
+ */
181
+ readonly thinkingDisplayOverride?: "updates";
132
182
  }
133
183
 
134
184
  /**
@@ -240,10 +290,151 @@ export function composeBetasWithAudit(
240
290
 
241
291
  // No web-search beta: upstream pushes it only for vertex and foundry.
242
292
  if (experimental) out.push(registry.PROMPT_CACHING_SCOPE.header);
243
- if (supportsMidConversationSystem(input.normalizedId, profile))
293
+
294
+ /*
295
+ * Upstream `jR`. The local boolean below records that this site PUSHED, which
296
+ * is what sites 11b and 11c gate on -- upstream reads `Ee.includes(jR)`, the
297
+ * composed array, not the registry. Asking the registry instead would fire
298
+ * those two sites for a model this one skipped.
299
+ */
300
+ const midConversationSystemFired = supportsMidConversationSystem(
301
+ input.normalizedId,
302
+ profile,
303
+ );
304
+ if (midConversationSystemFired) {
244
305
  out.push(registry.MID_CONVERSATION_SYSTEM.header);
306
+ }
307
+
308
+ /*
309
+ * Site 11a. Upstream `wRt`, which reduces to
310
+ * `Fg() && mD(provider) && catalogue-declares-per_turn_effort`; the provider
311
+ * term is first-party here by construction.
312
+ */
313
+ const perMessageEffort = registry.PER_MESSAGE_EFFORT;
314
+ if (
315
+ experimental &&
316
+ input.capabilities.perTurnEffort &&
317
+ perMessageEffort !== undefined
318
+ ) {
319
+ out.push(perMessageEffort.header);
320
+ }
321
+
322
+ /*
323
+ * Site 11b. Upstream `oQt() && Tue(model)`. `Tue` opens with
324
+ * `if (!Fg() || !kue(e)) return false`, so the experimental gate is upstream's
325
+ * and not an addition. Step 3 of `Tue` -- which sends the beta for any model
326
+ * with NO catalogue entry -- is deliberately not ported; see the analysis
327
+ * document's divergence record.
328
+ */
329
+ const midConvToolChange = registry.MID_CONV_TOOL_CHANGE;
330
+ if (
331
+ experimental &&
332
+ midConversationSystemFired &&
333
+ input.capabilities.midConvToolChange &&
334
+ midConvToolChange !== undefined
335
+ ) {
336
+ out.push(midConvToolChange.header);
337
+ }
338
+
339
+ /*
340
+ * Site 11c. Upstream `Mo`, which is the one site whose experimental gate is
341
+ * invisible at the statement level -- `Mo` names no `Fg()` at all. The gate is
342
+ * reached three calls deep instead: `Mee` is `oRt(model) !== "off"`, `oRt`
343
+ * wraps `fur`, and `fur` opens by returning `"off"` when `Fg()` is false. So
344
+ * `experimental` is upstream's own conjunct here exactly as it is at 11a and
345
+ * 11b, and a reader who checks only the `Mo` expression will wrongly conclude
346
+ * it was invented. It was not.
347
+ *
348
+ * `Mo`'s remaining conjuncts are constants for this package. `!l7(querySource)`
349
+ * holds because `l7` is true only for the two auto-mode query sources. `!Vr`
350
+ * holds because `Vr` is a per-session latch, and a stateless package composes
351
+ * every request as a first request. `Mee`'s other terms are fixed on the
352
+ * first-party path. What is left is `Ee.includes(jR)`, which is precisely the
353
+ * `midConversationSystemFired` outcome recorded at site 11.
354
+ */
355
+ const midConversationSystemClearAt =
356
+ registry.MID_CONVERSATION_SYSTEM_CLEAR_AT;
357
+ if (
358
+ experimental &&
359
+ midConversationSystemFired &&
360
+ midConversationSystemClearAt !== undefined
361
+ ) {
362
+ out.push(midConversationSystemClearAt.header);
363
+ }
364
+
365
+ // Site 12.
245
366
  if (input.capabilities.effort) out.push(registry.EFFORT.header);
246
367
 
368
+ /*
369
+ * Site 12a. Upstream `er && Fg()` with the latch write, which on the pinned
370
+ * first-party path collapses to thinking-active AND experimental.
371
+ * The coupled body field `thinking.block_binding` is NOT emitted: it needs a
372
+ * host override this package cannot observe.
373
+ */
374
+ const thinkingBindingControls = registry.THINKING_BINDING_CONTROLS;
375
+ if (
376
+ experimental &&
377
+ input.thinkingActive &&
378
+ thinkingBindingControls !== undefined
379
+ ) {
380
+ out.push(thinkingBindingControls.header);
381
+ }
382
+
383
+ /*
384
+ * Site 12b. Upstream guards the push with
385
+ * `(yc?.type === "adaptive" || yc?.type === "enabled") && ac && firstParty
386
+ * && !callerSuppliedDisplay && ...` plus the simulate-proxy environment
387
+ * variable and a per-session latch. The reachable remainder of that guard is
388
+ * modelled below: `experimental` is upstream's `Fg()` term inside `ac`,
389
+ * `input.thinkingActive` is the thinking-type test together with the rest of
390
+ * `ac`, and `!input.thinkingDisplayActive` is the caller-supplied-display
391
+ * test. First-party is not modelled because this package only builds the
392
+ * first-party path, the simulate-proxy env var is not modelled because the
393
+ * package reads no environment, and the per-session latch is always empty in
394
+ * a stateless package that composes every request as a first request.
395
+ *
396
+ * The branch is additionally reached only when the display-mode resolver
397
+ * `Gxt` returns its connector-text result; its other results break out
398
+ * before the push. `!policy.thinkingSummariesShown` is that resolver's
399
+ * `sQt()` term, mapped onto the profile flag exactly as at the
400
+ * redact-thinking site above. See the 2.1.280 analysis document, §6.4, for
401
+ * the resolver itself.
402
+ *
403
+ * The resolver's other two diverting branches are already covered by
404
+ * `!thinkingDisplayActive`. A caller display of `"summarized"` takes the
405
+ * first branch, and `"omitted"` takes the second whenever the
406
+ * explicit-display flag is falsy, which is the only state this package can
407
+ * express -- it models no equivalent of that flag. Under a truthy
408
+ * explicit-display flag upstream would fall through and overwrite an
409
+ * explicit `"omitted"` with `"updates"`; that input is unreachable here, and
410
+ * is recorded rather than modelled.
411
+ *
412
+ * The site is coupled: it pushes the beta, records `display: "updates"` for
413
+ * the body builder, and removes the previously composed redact-thinking beta
414
+ * (upstream `qu()`).
415
+ *
416
+ * The splice sits INSIDE this block and is deliberately the LAST thing it
417
+ * does. At this point `out` holds only canonical pushes, because the caller
418
+ * `additionalBetas` merge runs later. So a caller who explicitly supplies
419
+ * `redact-thinking-2026-02-12` in `additionalBetas` still gets it, precisely
420
+ * because the merge's `if (!out.includes(beta))` test now succeeds. Moving
421
+ * the splice after the merge would silently eat that caller's entry.
422
+ */
423
+ const thinkingDisplayUpdates = registry.THINKING_DISPLAY_UPDATES;
424
+ let thinkingDisplayOverride: "updates" | undefined;
425
+ if (
426
+ experimental &&
427
+ input.thinkingActive &&
428
+ !policy.thinkingSummariesShown &&
429
+ !input.thinkingDisplayActive &&
430
+ thinkingDisplayUpdates !== undefined
431
+ ) {
432
+ out.push(thinkingDisplayUpdates.header);
433
+ thinkingDisplayOverride = "updates";
434
+ const redactIndex = out.indexOf(registry.REDACT_THINKING.header);
435
+ if (redactIndex !== -1) out.splice(redactIndex, 1);
436
+ }
437
+
247
438
  if (input.speed === "fast" && !out.includes(registry.SPEED.header)) {
248
439
  out.push(registry.SPEED.header);
249
440
  }
@@ -286,6 +477,9 @@ export function composeBetasWithAudit(
286
477
  return Object.freeze({
287
478
  betas: Object.freeze(out),
288
479
  suppressedBetaNames: NO_SUPPRESSED_BETAS,
480
+ ...(thinkingDisplayOverride === undefined
481
+ ? {}
482
+ : { thinkingDisplayOverride }),
289
483
  });
290
484
  }
291
485
  const suppressed = new Set(validateAdditionalBetas(input.suppressBetas));
@@ -298,5 +492,10 @@ export function composeBetasWithAudit(
298
492
  return Object.freeze({
299
493
  betas: Object.freeze(kept),
300
494
  suppressedBetaNames: Object.freeze(removed),
495
+ // The suppression filter above touches only the header list; the body
496
+ // half of the site-12b pair survives it by design.
497
+ ...(thinkingDisplayOverride === undefined
498
+ ? {}
499
+ : { thinkingDisplayOverride }),
301
500
  });
302
501
  }