@bitkyc08/opencodex 2.42.0 → 2.43.0

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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -0,0 +1,195 @@
1
+ import type { OcxTool } from "../../types";
2
+ import { CODEX_SHELL_COMMAND_TOOL, isBareCodexExecCommandTool, isBareCodexShellBridgeTool, isCodexShellBridgeToolName } from "./tool-naming";
3
+
4
+ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
5
+ type: "object",
6
+ properties: {
7
+ cmd: { type: "string", description: "Shell command to execute." },
8
+ workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." },
9
+ shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." },
10
+ tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
11
+ yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
12
+ max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
13
+ },
14
+ required: ["cmd"],
15
+ additionalProperties: false,
16
+ } as const;
17
+
18
+ /**
19
+ * Structured single-replacement schema advertised to Cursor models in addition to the freeform
20
+ * `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native
21
+ * Edit shape) but cannot produce Codex's freeform patch grammar, so every file edit attempt on the
22
+ * Cursor route produced malformed `apply_patch` payloads that the Codex client rejected locally
23
+ * (#1017). Calls to this tool are converted server-side into a valid apply_patch payload.
24
+ */
25
+ export const CURSOR_EDIT_FILE_INPUT_SCHEMA = {
26
+ type: "object",
27
+ properties: {
28
+ file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
29
+ old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
30
+ new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
31
+ },
32
+ required: ["file_path", "old_string", "new_string"],
33
+ additionalProperties: false,
34
+ } as const;
35
+
36
+ /** Structured multi-replacement schema; mirrors Cursor's native MultiEdit shape. */
37
+ export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = {
38
+ type: "object",
39
+ properties: {
40
+ file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
41
+ edits: {
42
+ type: "array",
43
+ items: {
44
+ type: "object",
45
+ properties: {
46
+ old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
47
+ new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
48
+ },
49
+ required: ["old_string", "new_string"],
50
+ additionalProperties: false,
51
+ },
52
+ description: "Ordered replacement edits for this file. Each old_string must match the current file content.",
53
+ },
54
+ },
55
+ required: ["file_path", "edits"],
56
+ additionalProperties: false,
57
+ } as const;
58
+
59
+ /**
60
+ * Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
61
+ * Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
62
+ * `command`. Keeping `cmd` out of this schema lets `normalizeArgKeys` rewrite `cmd` → `command`.
63
+ */
64
+ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
65
+ type: "object",
66
+ properties: {
67
+ command: { type: "string", description: "Shell command to execute." },
68
+ workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." },
69
+ shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." },
70
+ tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
71
+ yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
72
+ max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
73
+ max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
74
+ },
75
+ required: ["command"],
76
+ } as const;
77
+
78
+
79
+ /** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
80
+ export function cursorToolInputSchema(tool: OcxTool): unknown {
81
+ return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
82
+ }
83
+
84
+ /**
85
+ * Schema used to normalize completed Cursor tool args back to Responses/Codex field names.
86
+ * Must NOT reuse `cursorToolInputSchema` for the shell bridge: advertising `cmd` while also
87
+ * treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
88
+ */
89
+ export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
90
+ if (isBareCodexShellBridgeTool(tool)) {
91
+ return shellBridgeArgNormalizeSchema(tool);
92
+ }
93
+ return tool.parameters ?? {};
94
+ }
95
+
96
+ function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown {
97
+ const parameters = tool.parameters;
98
+ if (!parameters || typeof parameters !== "object") return CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA;
99
+ const base = parameters as Record<string, unknown>;
100
+ const rawProps = base.properties && typeof base.properties === "object"
101
+ ? { ...(base.properties as Record<string, unknown>) }
102
+ : {};
103
+ const required = Array.isArray(base.required) ? [...base.required as unknown[]] : [];
104
+ const requiresCommand = required.includes("command") || "command" in rawProps;
105
+ const requiresCmd = required.includes("cmd") || "cmd" in rawProps;
106
+ const shouldRewriteCmdToCommand = tool.name === CODEX_SHELL_COMMAND_TOOL || requiresCommand;
107
+
108
+ if (!shouldRewriteCmdToCommand && requiresCmd) {
109
+ return parameters;
110
+ }
111
+
112
+ // Drop Cursor-preferred aliases so normalizeArgKeys can rewrite them to Responses keys.
113
+ delete rawProps.cmd;
114
+ const properties = {
115
+ ...CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties,
116
+ ...rawProps,
117
+ command: rawProps.command ?? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties.command,
118
+ };
119
+ return {
120
+ ...base,
121
+ type: "object",
122
+ properties,
123
+ required: requiresCommand ? required : ["command"],
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Required command payload keys for a shell bridge tool, derived from the advertised schema when present.
129
+ */
130
+ export function shellBridgeRequiredCommandKeys(
131
+ toolName: string,
132
+ schema?: unknown,
133
+ ): readonly ("cmd" | "command")[] {
134
+ if (schema && typeof schema === "object") {
135
+ const required = (schema as Record<string, unknown>).required;
136
+ if (Array.isArray(required)) {
137
+ const keys = required.filter((key): key is "cmd" | "command" => key === "cmd" || key === "command");
138
+ if (keys.length > 0) return keys;
139
+ }
140
+ }
141
+ return toolName === CODEX_SHELL_COMMAND_TOOL ? ["command"] : ["cmd"];
142
+ }
143
+
144
+ /** Normalize-schema defaults used when validating stateless synthetic shell-bridge calls. */
145
+ export function defaultShellBridgeArgNormalizeSchema(toolName: string): unknown {
146
+ return toolName === CODEX_SHELL_COMMAND_TOOL
147
+ ? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA
148
+ : {
149
+ type: "object",
150
+ properties: CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties,
151
+ required: ["cmd"],
152
+ };
153
+ }
154
+
155
+ export function cursorShellBridgeDropError(toolName: string): string {
156
+ return `Cursor emitted ${toolName} without a non-empty command; the tool call was dropped.`;
157
+ }
158
+
159
+ /**
160
+ * Extract a non-empty shell command from completed Cursor bridge args using the schema's required
161
+ * command key (`cmd` for bare exec_command, `command` for shell_command).
162
+ */
163
+ export function nonEmptyShellBridgeCommandFromArgs(
164
+ finalArgs: string,
165
+ toolName: string,
166
+ schema?: unknown,
167
+ ): string | undefined {
168
+ let parsed: unknown;
169
+ try {
170
+ parsed = finalArgs.length > 0 ? JSON.parse(finalArgs) : {};
171
+ } catch {
172
+ return undefined;
173
+ }
174
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
175
+ const record = parsed as Record<string, unknown>;
176
+ const requiredKeys = shellBridgeRequiredCommandKeys(toolName, schema);
177
+ const candidateKeys = new Set<"cmd" | "command">([
178
+ ...requiredKeys,
179
+ requiredKeys.includes("cmd") ? "command" : "cmd",
180
+ ]);
181
+ for (const key of candidateKeys) {
182
+ const value = record[key];
183
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ export function cursorShellBridgeArgsValid(
189
+ finalArgs: string,
190
+ toolName: string,
191
+ schema?: unknown,
192
+ ): boolean {
193
+ return !isCodexShellBridgeToolName(toolName)
194
+ || nonEmptyShellBridgeCommandFromArgs(finalArgs, toolName, schema) !== undefined;
195
+ }
@@ -31,7 +31,7 @@ export interface CursorRunRequest {
31
31
  /**
32
32
  * Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
33
33
  * When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
34
- * the userMessageAction text instead of the standard continuation text; rawMessages stay
34
+ * the userMessageAction prefix instead of the standard continuation text; rawMessages stay
35
35
  * untouched so history replay is unchanged.
36
36
  */
37
37
  echoRetryContinuationText?: string;
@@ -40,9 +40,12 @@ export interface CursorRunRequest {
40
40
  messages: CursorRequestMessage[];
41
41
  rawMessages?: readonly OcxMessage[];
42
42
  /**
43
- * Images for the active user/developer turn. Encoded as SelectedImage blobIdWithData refs under
43
+ * Images for the active user/developer turn or an external model's trailing tool-result run.
44
+ * Encoded as SelectedImage blobIdWithData refs under
44
45
  * UserMessage.selected_context (bytes live in the request-scoped KV store for getBlobArgs
45
- * hydration). History stays text-only. data: URLs only in this slice.
46
+ * hydration). Bounded sourceLabel metadata from tool images is appended to the active action,
47
+ * outside prunable history; it is not a new image wire field. Native Composer selection stays
48
+ * unchanged. History stays text-only. data: URLs only in this slice.
46
49
  */
47
50
  selectedImages?: readonly ResolvedCursorImage[];
48
51
  tools?: OcxTool[];
@@ -113,7 +113,7 @@ export const EMPTY_EXEC_OUTPUT_MESSAGE =
113
113
  * that drifts apart is how a model gets told two different things about the same isolate.
114
114
  */
115
115
  export const CODE_MODE_RESULT_ECHO_SENTENCE =
116
- "Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: \"ls\"})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context.";
116
+ "Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: 'ls'})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context.";
117
117
 
118
118
  /**
119
119
  * Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
@@ -1,4 +1,5 @@
1
1
  import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
2
+ import { isLocationUnsupportedMessage } from "../lib/errors";
2
3
 
3
4
  /** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
4
5
  function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
@@ -65,9 +66,16 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
65
66
  if (status === 401 || enumStatus === "UNAUTHENTICATED" || lower.includes("unauthenticated") || lower.includes("invalid authentication") || lower.includes("expired")) {
66
67
  return `${label} authentication failed`;
67
68
  }
68
- if (status === 403 || enumStatus === "PERMISSION_DENIED" || lower.includes("permission denied") || lower.includes("access denied")) {
69
+ if (status === 403 || enumStatus === "PERMISSION_DENIED" || lower.includes("permission_denied") || lower.includes("permission denied") || lower.includes("access denied")) {
69
70
  return `${label} access denied`;
70
71
  }
72
+ // Google rejects unsupported geographic / datacenter locations with HTTP 400
73
+ // FAILED_PRECONDITION. The payload is not malformed, so it must not fall through to
74
+ // "invalid request" (#3467). Only the observed 400/precondition envelope permits
75
+ // this inference; other explicit enums and server statuses remain authoritative.
76
+ if (status === 400 && (!enumStatus || enumStatus === "FAILED_PRECONDITION") && isLocationUnsupportedMessage(lower)) {
77
+ return `${label} location not supported`;
78
+ }
71
79
  if (status === 503 || enumStatus === "UNAVAILABLE" || lower.includes("overloaded") || lower.includes("unavailable")) {
72
80
  return `${label} server overloaded`;
73
81
  }
@@ -52,6 +52,7 @@ const GOOGLE_BREVITY_INSTRUCTION = [
52
52
  "- Do detailed reasoning internally, not as visible intermediate output.",
53
53
  "- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
54
54
  "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
55
+ "- Formatting: The client environment renders standard Markdown and does not support LaTeX math delimiters ($...$, $$...$$, \\(...\\), \\[...\\]). Do not use LaTeX math delimiters or LaTeX markup (such as \\text{}, \\times, \\le, \\ge, etc.) for variables, formulas, dimensions, or units. Use clean plain text, Markdown, and Unicode symbols (e.g. 180°, 2560 × 1920 px, ≤, ≥, Δ, ±) instead.",
55
56
  ].join("\n");
56
57
 
57
58
  const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Per-conversation calibration of the Kiro input estimate.
3
+ *
4
+ * The estimator in `lib/token-estimate` is a fixed heuristic: constants derived from recorded
5
+ * traffic, applied to every account and every conversation identically. It is close on average
6
+ * and necessarily wrong in the particular, because how densely a prompt tokenizes depends on
7
+ * what is in it — a Korean design discussion and a repository of minified JSON do not share a
8
+ * ratio, and no constant can serve both.
9
+ *
10
+ * Kiro, however, tells us the answer. Mid-stream it reports `contextUsagePercentage`, which
11
+ * against a known window is an authoritative token count for the exact payload just sent. Today
12
+ * that number is used once, as a floor under the current turn's total, and then discarded. The
13
+ * conversation's own measured tokens-per-character is therefore recomputed from scratch, and
14
+ * mispredicted the same way, on every single turn.
15
+ *
16
+ * This module keeps it. After a TERMINAL attempt that produced both an estimate and a reported
17
+ * percentage, the realised ratio `charged / estimated` is folded into a per-conversation
18
+ * correction and applied to the next turn of that conversation, so a long conversation converges
19
+ * on the rate it actually exhibits rather than the rate the average conversation exhibits.
20
+ *
21
+ * "Terminal" matters: the adapter's bounded completion retry streams a second time for the same
22
+ * user turn against a rebuilt payload, so the caller commits an observation only once that turn
23
+ * is genuinely over.
24
+ *
25
+ * Four properties keep this from being able to make the gauge worse:
26
+ *
27
+ * - **Bounded.** The factor is clamped, so a single anomalous reading cannot distort the
28
+ * estimate by an unbounded amount, and a malformed or hostile percentage cannot either.
29
+ * - **Smoothed.** Each observation moves the factor part of the way rather than replacing it, so
30
+ * one cache-affected or truncated turn cannot swing the gauge.
31
+ * - **Bounded in memory.** Conversation-scoped with an eviction cap and no persistence. This is
32
+ * a hint that improves with use, not state anything depends on.
33
+ * - **Subordinate to the truth.** The existing upstream floor is untouched. Calibration only
34
+ * sharpens the estimate BEFORE upstream reports; it can never lower a value upstream has
35
+ * already justified.
36
+ *
37
+ * Deliberately not in `lib/token-estimate`: that module is pure, shared by every provider, and
38
+ * must stay so. This is Kiro-specific state and lives with the Kiro adapter.
39
+ */
40
+
41
+ /**
42
+ * Clamp for the correction factor.
43
+ *
44
+ * Wide enough to absorb the genuine spread between conversation kinds, narrow enough that a
45
+ * wrong reading cannot produce a nonsensical gauge. A factor below 1 shrinks the estimate, so
46
+ * the lower bound is the more dangerous side and is kept nearer 1.
47
+ */
48
+ const MIN_FACTOR = 0.7;
49
+ const MAX_FACTOR = 3;
50
+
51
+ /**
52
+ * Weight given to a new observation. 0.35 reaches most of the way to a persistent new rate
53
+ * within a few turns while still requiring more than one reading to move far.
54
+ */
55
+ const SMOOTHING = 0.35;
56
+
57
+ /**
58
+ * Maximum conversations tracked. Entries are small, but the map must not grow with uptime.
59
+ *
60
+ * Eviction is least-recently-USED, not oldest-inserted: `touch` re-inserts on every estimate and
61
+ * every observation, so an active long conversation survives an arbitrary number of short ones
62
+ * started after it. Insertion order would have evicted exactly the conversation most worth
63
+ * keeping.
64
+ */
65
+ const MAX_TRACKED_CONVERSATIONS = 256;
66
+
67
+ /**
68
+ * A ratio this far from 1 is not a mis-calibrated estimate, it is a different measurement:
69
+ * a compaction, a cache boundary, or a percentage reported against a window we did not expect.
70
+ * Learning from it would teach the wrong lesson, so it is ignored entirely.
71
+ */
72
+ const MAX_PLAUSIBLE_OBSERVATION = 6;
73
+
74
+ /**
75
+ * One conversation's calibration state.
76
+ *
77
+ * `factor` is the learned correction. `rawEstimate` is the uncorrected estimate most recently
78
+ * applied: the stream records what upstream charged, but by then the value it holds has already
79
+ * been corrected, and learning from that would make the factor measure its own residual error
80
+ * instead of the heuristic's — each round would close only the part it had not yet fixed and the
81
+ * factor would converge short of the true ratio.
82
+ *
83
+ * Both live in ONE entry so they cannot desynchronise. Held as two maps, a conversation could be
84
+ * refreshed in one and evicted from the other, and the pair could retain twice the documented
85
+ * bound in distinct ids.
86
+ */
87
+ interface Calibration {
88
+ factor?: number;
89
+ rawEstimate?: number;
90
+ }
91
+
92
+ /** Insertion-ordered, so the first key is the least recently touched entry. */
93
+ const calibrations = new Map<string, Calibration>();
94
+
95
+ /** Re-insert to move an entry to the most-recently-used end, then evict from the old end. */
96
+ function touch(conversationId: string, update: (current: Calibration) => Calibration): void {
97
+ const current = calibrations.get(conversationId) ?? {};
98
+ calibrations.delete(conversationId);
99
+ calibrations.set(conversationId, update(current));
100
+ while (calibrations.size > MAX_TRACKED_CONVERSATIONS) {
101
+ const oldest = calibrations.keys().next();
102
+ if (oldest.done) break;
103
+ calibrations.delete(oldest.value);
104
+ }
105
+ }
106
+
107
+ function clamp(value: number): number {
108
+ return Math.min(MAX_FACTOR, Math.max(MIN_FACTOR, value));
109
+ }
110
+
111
+ /**
112
+ * Record what a conversation was actually charged against what we estimated.
113
+ *
114
+ * `estimated` must be the RAW estimate — what the fixed heuristic produced before any correction
115
+ * was applied — and `charged` the token count implied by the upstream percentage. Passing the
116
+ * already-corrected value instead would make the factor measure its own residual error rather
117
+ * than the heuristic's, so each round would learn only the part it had not yet fixed and the
118
+ * factor would settle short of the true ratio, permanently under-correcting.
119
+ *
120
+ * Non-finite, non-positive, or implausible inputs are ignored rather than clamped, because a bad
121
+ * reading carries no information worth smoothing in.
122
+ */
123
+ export function recordKiroCalibration(
124
+ conversationId: string | undefined,
125
+ estimated: number,
126
+ charged: number,
127
+ ): void {
128
+ if (!conversationId) return;
129
+ if (!Number.isFinite(estimated) || !Number.isFinite(charged)) return;
130
+ if (estimated <= 0 || charged <= 0) return;
131
+ const entry = calibrations.get(conversationId);
132
+ // Prefer the raw estimate recorded at build time; fall back to the caller's value when this
133
+ // conversation was never seen by `calibrateKiroEstimate` (a first turn, or a rebuilt retry).
134
+ const baseline = entry?.rawEstimate ?? estimated;
135
+ if (!Number.isFinite(baseline) || baseline <= 0) return;
136
+ const observed = charged / baseline;
137
+ if (!Number.isFinite(observed) || observed <= 0 || observed > MAX_PLAUSIBLE_OBSERVATION) return;
138
+
139
+ // Smooth from 1 (the "no correction" prior) on the FIRST observation too. Jumping straight to
140
+ // `clamp(observed)` would let a single cache-affected or truncated turn set the factor outright,
141
+ // which is exactly the swing the smoothing exists to prevent.
142
+ const previous = entry?.factor ?? 1;
143
+ const next = clamp(previous + (observed - previous) * SMOOTHING);
144
+
145
+ // The observation for this turn is consumed: dropping the raw estimate keeps a later record for
146
+ // the same conversation from being scored a second time against a stale baseline, which would
147
+ // re-learn the same correction from a payload that has since grown.
148
+ touch(conversationId, () => ({ factor: next }));
149
+ }
150
+
151
+ /**
152
+ * Apply a conversation's learned correction to an estimate. Unknown conversations are returned
153
+ * unchanged, so the first turn behaves exactly as it did before calibration existed.
154
+ */
155
+ export function calibrateKiroEstimate(conversationId: string | undefined, estimate: number): number {
156
+ if (!conversationId || !Number.isFinite(estimate) || estimate <= 0) return estimate;
157
+ touch(conversationId, current => ({ ...current, rawEstimate: estimate }));
158
+ const factor = calibrations.get(conversationId)?.factor;
159
+ return factor === undefined ? estimate : Math.ceil(estimate * factor);
160
+ }
161
+
162
+ /**
163
+ * Carry a conversation's state to the id upstream actually returned.
164
+ *
165
+ * The estimate is stored under the id the request was BUILT with, but Kiro can answer with a
166
+ * different conversation id, and the stream then records under that one. Without this the record
167
+ * would miss its own raw estimate and silently fall back to the corrected value — the exact
168
+ * feedback bug the raw estimate exists to avoid.
169
+ */
170
+ export function rekeyKiroCalibration(fromConversationId: string | undefined, toConversationId: string | undefined): void {
171
+ if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return;
172
+ const entry = calibrations.get(fromConversationId);
173
+ if (!entry) return;
174
+ calibrations.delete(fromConversationId);
175
+ touch(toConversationId, current => ({ ...current, ...entry }));
176
+ }
177
+
178
+ /** Test seam: drop all learned state. */
179
+ export function resetKiroCalibration(): void {
180
+ calibrations.clear();
181
+ }
@@ -5,6 +5,7 @@ import { resolveKiroApiRegion, resolveKiroRequestProfile } from "../oauth/kiro";
5
5
  import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
6
6
  import { modelRecordValue } from "../reasoning-effort";
7
7
  import { parseKiroEvent } from "./kiro-events";
8
+ import { calibrateKiroEstimate, recordKiroCalibration, rekeyKiroCalibration } from "./kiro-calibration";
8
9
  import {
9
10
  classifyKiroEventError,
10
11
  classifyKiroHttpError,
@@ -186,6 +187,85 @@ function estimateKiroTokens(text: string, modelId?: string): number {
186
187
  return estimateTokens(text, modelId ? `kiro/${modelId}` : "kiro");
187
188
  }
188
189
 
190
+ /** Hangul/Han/kana ranges, matching the shared estimator's own CJK classification. */
191
+ function kiroCjkCount(text: string): number {
192
+ let cjk = 0;
193
+ for (let i = 0; i < text.length; i++) {
194
+ const c = text.charCodeAt(i);
195
+ if (
196
+ (c >= 0xac00 && c <= 0xd7a3) || (c >= 0x1100 && c <= 0x11ff) || (c >= 0x3130 && c <= 0x318f)
197
+ || (c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x3040 && c <= 0x30ff)
198
+ ) cjk++;
199
+ }
200
+ return cjk;
201
+ }
202
+
203
+ /**
204
+ * Token estimate for walked payload text, with the wire expansion applied to the Latin portion
205
+ * only. Splitting here rather than inside the shared estimator keeps that module pure and
206
+ * provider-neutral: the expansion is a fact about Kiro's wire, not about tokenization.
207
+ */
208
+ function estimateKiroWireTokens(text: string, modelId: string): number {
209
+ if (!text) return 0;
210
+ const cjk = kiroCjkCount(text);
211
+ if (cjk === 0) return Math.ceil(estimateKiroTokens(text, modelId) * KIRO_LATIN_WIRE_EXPANSION);
212
+ const latinTokens = estimateKiroTokens("x".repeat(text.length - cjk), modelId);
213
+ const cjkTokens = estimateKiroTokens("\uac00".repeat(cjk), modelId);
214
+ return Math.ceil(latinTokens * KIRO_LATIN_WIRE_EXPANSION + cjkTokens);
215
+ }
216
+
217
+ /**
218
+ * Structural cost of one conversation entry, in tokens.
219
+ *
220
+ * The walker below concatenates message TEXT, but the wire carries JSON: per-entry keys
221
+ * (`userInputMessage`, `content`, `modelId`, `origin`) and role framing. That is charged
222
+ * upstream and is invisible to a text-only count, so without it a long conversation drifts
223
+ * further below the real charge with every turn added — an error proportional to entry COUNT,
224
+ * which no per-character ratio can recover.
225
+ *
226
+ * Regressing serialized bodies against what the walker counts, over eleven payload sizes from
227
+ * 3 to 701 entries:
228
+ *
229
+ * bodyBytes = 1.0422 * walkedChars + 66.7 * entries + 68
230
+ *
231
+ * 66.7 bytes at the measured 2.433 bytes per charged token is 27.4 tokens per entry. The
232
+ * earlier value of 12 was a conservative hand-fit taken before that regression existed, and
233
+ * being less than half the real cost is precisely why the estimate decayed with conversation
234
+ * length: an under-charge of ~15 tokens per entry is invisible across four messages and
235
+ * dominant across seven hundred.
236
+ *
237
+ * Cross-checked against 4,090 recorded requests, where real traffic averages 1,310 bytes per
238
+ * message: 66.7 bytes is 5% of that, so this term charges framing and is not quietly absorbing
239
+ * message content.
240
+ */
241
+ const KIRO_ENTRY_FRAMING_TOKENS = 27;
242
+
243
+ /**
244
+ * Multiplier reconciling the LATIN text estimate with what the wire charges for that same text.
245
+ *
246
+ * The shared estimator counts Latin text at 2.8 chars/token, while the wire charges 2.433 bytes
247
+ * per token at 1.0422 bytes per walked character — an effective 2.334 chars/token, and
248
+ * 2.8 / 2.334 = 1.199.
249
+ *
250
+ * The evidence that the split between this term and `KIRO_ENTRY_FRAMING_TOKENS` is right is its
251
+ * stability: holding framing at 27, the multiplier the charge implies stays within 1.189-1.209
252
+ * across a 230x range of conversation sizes. A mis-specified split drifts with size, and the
253
+ * earlier 1.12/12 pair did — its accuracy fell from 0.92 at four messages to 0.87 at seven
254
+ * hundred.
255
+ *
256
+ * LATIN ONLY, deliberately. 2.433 bytes/token is a property of this traffic mix, which is Latin
257
+ * and code. A Hangul character is three UTF-8 bytes but roughly one token, so its bytes-per-token
258
+ * is entirely different and a Latin-derived byte rate says nothing about it. Scaling CJK by this
259
+ * factor bills Hangul at 1.25 chars/token, against recorded ground truth that already places the
260
+ * shared 1.5 ratio at 0.90 of the authoritative count — an over-charge that would compact Korean
261
+ * threads early.
262
+ *
263
+ * This is NOT JSON escaping, despite what an earlier version of this comment claimed. Measured
264
+ * directly, `JSON.stringify` expands prose by 1.012 (Latin) to 1.019 (Korean), nowhere near 1.2.
265
+ * Escaping is real but small, and is already inside the byte measurement this factor comes from.
266
+ */
267
+ const KIRO_LATIN_WIRE_EXPANSION = 1.2;
268
+
189
269
  function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelId: string): number {
190
270
  const conversationState = (payload as {
191
271
  conversationState?: {
@@ -216,7 +296,9 @@ function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelI
216
296
  if (assistant.toolUses?.length) parts.push(serializeForUsage(assistant.toolUses));
217
297
  }
218
298
  }
219
- return estimateKiroTokens(parts.join("\n"), modelId) + imageTokens;
299
+ return estimateKiroWireTokens(parts.join("\n"), modelId)
300
+ + imageTokens
301
+ + entries.length * KIRO_ENTRY_FRAMING_TOKENS;
220
302
  }
221
303
 
222
304
  function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
@@ -990,6 +1072,11 @@ async function* parseKiroAttempt(
990
1072
  // the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
991
1073
  const deferred: AdapterEvent[] = [];
992
1074
  const retention = createKiroAttemptRetention(budget);
1075
+ // Shared box: the inner parser stages its calibration observation here on the completion path,
1076
+ // and this wrapper decides whether the attempt was terminal enough to commit it. A box rather
1077
+ // than a return field because the completion path has a dozen terminal returns and threading a
1078
+ // field through every one of them is exactly the kind of edit that misses one.
1079
+ const attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } } = {};
993
1080
  const attempt = parseKiroAttemptEvents(
994
1081
  response,
995
1082
  budget,
@@ -1001,12 +1088,22 @@ async function* parseKiroAttempt(
1001
1088
  conversationId,
1002
1089
  deferred,
1003
1090
  retention,
1091
+ attemptCalibration,
1004
1092
  contextInputEstimate,
1005
1093
  priorEmittedOutput,
1006
1094
  );
1007
1095
  let handedOff = false;
1008
1096
  try {
1009
1097
  const result = yield* attempt;
1098
+ // A staged observation only counts when this attempt is the LAST one for the user turn. An
1099
+ // attempt that asks for the bounded fallback streams again against a rebuilt payload, so
1100
+ // committing here would move the factor twice for one turn and score the second observation
1101
+ // against a payload the first had already inflated.
1102
+ const staged = attemptCalibration.value;
1103
+ attemptCalibration.value = undefined;
1104
+ if (staged && !result.needsFallback) {
1105
+ recordKiroCalibration(staged.conversationId, staged.estimated, staged.charged);
1106
+ }
1010
1107
  for (const event of deferred.splice(0)) {
1011
1108
  try { yield event; } finally { retention.releaseEvent(event); }
1012
1109
  }
@@ -1028,10 +1125,13 @@ async function* parseKiroAttemptEvents(
1028
1125
  conversationId: string | undefined,
1029
1126
  deferred: AdapterEvent[],
1030
1127
  retention: KiroAttemptRetention,
1128
+ attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } },
1031
1129
  contextInputEstimate?: number,
1032
1130
  priorEmittedOutput = false,
1033
1131
  ): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
1034
1132
  const emptyResult = (): KiroAttemptParseResult => ({ assistantText: "", sawReasoning: false });
1133
+ // Every early return below is a failure path that stages nothing; only the completion path
1134
+ // writes `attemptCalibration`, and the wrapper decides whether to commit it.
1035
1135
  if (!response.body) {
1036
1136
  return {
1037
1137
  ...emptyResult(),
@@ -1346,7 +1446,13 @@ async function* parseKiroAttemptEvents(
1346
1446
  if (ev.stopReason !== undefined) stopReason = ev.stopReason;
1347
1447
  break;
1348
1448
  case "message_metadata":
1349
- if (isValidKiroConversationId(ev.conversationId)) returnedConversationId = ev.conversationId;
1449
+ if (isValidKiroConversationId(ev.conversationId)) {
1450
+ // Kiro can answer under a different conversation id than the request was built with.
1451
+ // Carry the calibration entry across so the record below finds its own raw estimate
1452
+ // instead of silently falling back to the already-corrected value.
1453
+ rekeyKiroCalibration(returnedConversationId, ev.conversationId);
1454
+ returnedConversationId = ev.conversationId;
1455
+ }
1350
1456
  break;
1351
1457
  case "content":
1352
1458
  if (ev.modelId) {
@@ -1475,6 +1581,29 @@ async function* parseKiroAttemptEvents(
1475
1581
  ...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
1476
1582
  });
1477
1583
  }
1584
+ // Upstream just told us what this payload cost. The ratio between that and our pre-request
1585
+ // estimate is this conversation's own measured error, and it is the only feedback the
1586
+ // estimator ever receives.
1587
+ //
1588
+ // Staged, not recorded. An attempt that sets `needsFallback` is not over: the adapter rebuilds
1589
+ // the payload and streams a second time for the SAME user turn. Learning here would apply the
1590
+ // fresh factor to that rebuild and then learn again from it, so one turn would move the factor
1591
+ // twice and the second observation would score a payload the first had already inflated. Only
1592
+ // the outer parser knows whether an attempt is terminal, so it commits.
1593
+ //
1594
+ // Subtract the output first. `contextUsageTotalFloor` is the absolute context size AFTER the
1595
+ // response (`OcxUsage.contextTotalTokens`, types/request.ts), while `contextInputEstimate`
1596
+ // covers the request payload alone. Dividing one by the other would charge generated tokens to
1597
+ // prompt-tokenization error, so a short prompt answered at length would learn a large factor
1598
+ // and inflate every later request in that conversation — the premature compaction this work
1599
+ // exists to prevent.
1600
+ const chargedTotal = contextUsageTotalFloor();
1601
+ if (chargedTotal !== undefined && contextInputEstimate !== undefined) {
1602
+ const chargedInput = chargedTotal - finalUsage.outputTokens;
1603
+ if (chargedInput > 0 && returnedConversationId) {
1604
+ attemptCalibration.value = { conversationId: returnedConversationId, estimated: contextInputEstimate, charged: chargedInput };
1605
+ }
1606
+ }
1478
1607
  // Native stop metadata proves that this inference ended, but it does not prove that ordinary
1479
1608
  // text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
1480
1609
  // still require the private completion call to distinguish commentary from completion (#531).
@@ -1934,7 +2063,10 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1934
2063
  if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
1935
2064
  const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
1936
2065
  await normalizeKiroImages(built.payload);
1937
- const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
2066
+ // Apply what earlier turns of THIS conversation measured. An unseen conversation is
2067
+ // unchanged, so a first turn behaves exactly as it would without calibration.
2068
+ const rawContextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
2069
+ const contextInputEstimate = calibrateKiroEstimate(built.conversationId, rawContextInputEstimate);
1938
2070
  const body = JSON.stringify(built.payload);
1939
2071
  debugProviderDiagnostic("kiro", "request", {
1940
2072
  region,