@bitkyc08/opencodex 2.57.0 → 2.59.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 (241) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +2 -2
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  10. package/src/adapters/command-code.ts +13 -4
  11. package/src/adapters/cursor/catalog.ts +11 -0
  12. package/src/adapters/cursor/cursor-errors.ts +15 -0
  13. package/src/adapters/cursor/discovery.ts +65 -1
  14. package/src/adapters/cursor/effort-map.ts +16 -2
  15. package/src/adapters/cursor/envelope-echo.ts +55 -2
  16. package/src/adapters/cursor/live-transport.ts +5 -1
  17. package/src/adapters/cursor/message-mapper.ts +3 -2
  18. package/src/adapters/cursor/protobuf-events.ts +110 -11
  19. package/src/adapters/cursor/protobuf-request.ts +27 -6
  20. package/src/adapters/cursor/request-builder.ts +14 -3
  21. package/src/adapters/cursor/text-toolcall.ts +230 -0
  22. package/src/adapters/cursor/thread-continuity.ts +141 -0
  23. package/src/adapters/cursor/tool-guidance.ts +5 -4
  24. package/src/adapters/cursor/types.ts +5 -0
  25. package/src/adapters/cursor.ts +97 -6
  26. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  27. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  28. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  29. package/src/adapters/devin.ts +75 -13
  30. package/src/adapters/google-antigravity-wire.ts +29 -2
  31. package/src/adapters/google-http.ts +45 -13
  32. package/src/adapters/google.ts +23 -4
  33. package/src/adapters/mimo-free.ts +32 -17
  34. package/src/adapters/ollama-native.ts +42 -8
  35. package/src/adapters/openai-chat/response-events.ts +61 -0
  36. package/src/adapters/openai-chat.ts +5 -10
  37. package/src/adapters/openai-responses/passthrough.ts +40 -5
  38. package/src/adapters/openai-responses/request-strips.ts +43 -0
  39. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  40. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  41. package/src/adapters/physical-send.ts +50 -0
  42. package/src/adapters/responses-tool-schema.ts +76 -46
  43. package/src/adapters/run-turn-queue.ts +17 -4
  44. package/src/bridge/response-json.ts +2 -2
  45. package/src/bridge/sse.ts +166 -25
  46. package/src/claude/context-windows.ts +22 -0
  47. package/src/claude/outbound.ts +46 -5
  48. package/src/cli/account-api.ts +4 -3
  49. package/src/cli/account-extended.ts +22 -2
  50. package/src/cli/account-orca-import.ts +63 -0
  51. package/src/cli/account.ts +32 -4
  52. package/src/cli/capabilities.ts +40 -0
  53. package/src/cli/claude.ts +29 -1
  54. package/src/cli/codex-cli-update.ts +97 -2
  55. package/src/cli/config-command.ts +35 -18
  56. package/src/cli/dispatch.ts +71 -4
  57. package/src/cli/doctor.ts +197 -2
  58. package/src/cli/help.ts +4 -1
  59. package/src/cli/index.ts +132 -22
  60. package/src/cli/models-runtime.ts +33 -4
  61. package/src/cli/registry.ts +11 -1
  62. package/src/cli/runtime-api.ts +44 -0
  63. package/src/cli/start-args.ts +94 -0
  64. package/src/cli/system-command.ts +72 -1
  65. package/src/cli/uninstall-client-state.ts +12 -0
  66. package/src/client/machine-api.ts +4 -3
  67. package/src/client/machine-listener.ts +14 -1
  68. package/src/clients/config-export/constants.ts +2 -3
  69. package/src/clients/config-export.ts +5 -5
  70. package/src/codex/account-store.ts +81 -5
  71. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  72. package/src/codex/auth-api/routes.ts +17 -2
  73. package/src/codex/auth-context.ts +58 -20
  74. package/src/codex/catalog/build-entries.ts +25 -4
  75. package/src/codex/catalog/derive-entry.ts +8 -1
  76. package/src/codex/catalog/effort.ts +10 -6
  77. package/src/codex/catalog/gather-capture.ts +1 -0
  78. package/src/codex/catalog/model-hints.ts +37 -5
  79. package/src/codex/catalog/parsing.ts +83 -5
  80. package/src/codex/catalog/reserve-warn.ts +96 -0
  81. package/src/codex/catalog/retained-sync.ts +19 -0
  82. package/src/codex/catalog/routed-gather.ts +42 -3
  83. package/src/codex/cli-installation-identity.ts +210 -0
  84. package/src/codex/cli-installation-targets.ts +158 -0
  85. package/src/codex/convergence.ts +5 -0
  86. package/src/codex/desktop-switches.ts +145 -0
  87. package/src/codex/history-job.ts +5 -1
  88. package/src/codex/history-provider.ts +37 -5
  89. package/src/codex/history-state-open.ts +105 -0
  90. package/src/codex/history-worker.ts +14 -1
  91. package/src/codex/inject/config-toml.ts +44 -2
  92. package/src/codex/inject/remove.ts +145 -7
  93. package/src/codex/inject/restore.ts +204 -32
  94. package/src/codex/inject.ts +6 -9
  95. package/src/codex/lineage.ts +83 -32
  96. package/src/codex/loopback-target.ts +40 -0
  97. package/src/codex/main-account-hard-lock.ts +2 -1
  98. package/src/codex/main-account.ts +10 -3
  99. package/src/codex/main-device-reauth.ts +17 -9
  100. package/src/codex/model-entitlements.ts +60 -1
  101. package/src/codex/native-profile-startup.ts +64 -20
  102. package/src/codex/observed-model-denials.ts +137 -0
  103. package/src/codex/orca-auth-source.ts +94 -0
  104. package/src/codex/orca-import.ts +219 -0
  105. package/src/codex/prompt-text-probe.ts +282 -12
  106. package/src/codex/quota-401-recovery.ts +12 -0
  107. package/src/codex/quota-types.ts +65 -0
  108. package/src/codex/quota.ts +24 -19
  109. package/src/codex/routing/cooldown-math.ts +8 -47
  110. package/src/codex/routing/pin-drain.ts +57 -0
  111. package/src/codex/routing.ts +13 -15
  112. package/src/codex/subagent-model-fallback.ts +94 -0
  113. package/src/codex/windows-installation-files.ts +224 -0
  114. package/src/combos/failover.ts +122 -5
  115. package/src/config/atomic-write.ts +83 -8
  116. package/src/config/diagnostics.ts +21 -0
  117. package/src/config/load-degrade.ts +15 -0
  118. package/src/config/pending-teardown.ts +8 -0
  119. package/src/config/process-state.ts +36 -3
  120. package/src/config/provider-relative-send-path.ts +16 -0
  121. package/src/config/proxy-env.ts +23 -5
  122. package/src/config/schema/config-schema.ts +23 -0
  123. package/src/config/schema/leaf-validators.ts +65 -17
  124. package/src/generated/compatibility-version.json +337 -201
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/lib/bounded-body.ts +4 -2
  127. package/src/lib/bounded-subprocess.ts +62 -10
  128. package/src/lib/destination-policy.ts +48 -6
  129. package/src/lib/errors.ts +3 -15
  130. package/src/lib/local-destinations.ts +32 -5
  131. package/src/lib/provider-outbound.ts +3 -3
  132. package/src/lib/proxy-env.ts +70 -3
  133. package/src/lib/request-execution-budget.ts +11 -3
  134. package/src/lib/response-body-inactivity.ts +193 -0
  135. package/src/lib/retry-delay.ts +69 -0
  136. package/src/lib/socks5-fetch.ts +631 -0
  137. package/src/lib/spend-reservation-ledger.ts +115 -9
  138. package/src/lib/windows-secret-acl.ts +151 -15
  139. package/src/lib/windows-user-principal.ts +5 -1
  140. package/src/lib/workflow-budget.ts +145 -8
  141. package/src/oauth/account-quota-rank.ts +72 -15
  142. package/src/oauth/generic-account-failover.ts +40 -27
  143. package/src/oauth/orcarouter.ts +15 -2
  144. package/src/oauth/store.ts +8 -0
  145. package/src/providers/codex-capacity.ts +9 -0
  146. package/src/providers/derive.ts +6 -0
  147. package/src/providers/devin-provider-merge-migration.ts +33 -12
  148. package/src/providers/free-directory.ts +20 -2
  149. package/src/providers/key-failover.ts +261 -7
  150. package/src/providers/model-discovery.ts +19 -7
  151. package/src/providers/model-rename-migration.ts +1 -0
  152. package/src/providers/openai-sidecar.ts +4 -0
  153. package/src/providers/opencode-go-transport.ts +14 -5
  154. package/src/providers/quota/report-cache.ts +3 -0
  155. package/src/providers/registry/entries-core.ts +11 -0
  156. package/src/providers/registry/entries-extended.ts +146 -28
  157. package/src/providers/registry/model-seeds.ts +136 -29
  158. package/src/providers/registry/types.ts +9 -0
  159. package/src/responses/apply-patch-envelope.ts +44 -11
  160. package/src/responses/bridge-search-replay-cache.ts +152 -0
  161. package/src/responses/code-mode-helper-compat.ts +26 -16
  162. package/src/responses/custom-tool-compat.ts +1 -1
  163. package/src/responses/hosted-tool-policy.ts +85 -2
  164. package/src/responses/schema.ts +9 -2
  165. package/src/responses/spill-store.ts +17 -0
  166. package/src/responses/state/body-policy.ts +25 -0
  167. package/src/responses/state/spill-queue.ts +8 -6
  168. package/src/responses/state.ts +3 -22
  169. package/src/router.ts +4 -0
  170. package/src/server/auth-cors.ts +27 -0
  171. package/src/server/chat-completions.ts +9 -4
  172. package/src/server/chat-native-sse.ts +26 -9
  173. package/src/server/chat-native.ts +10 -4
  174. package/src/server/claude-messages.ts +24 -2
  175. package/src/server/gui-static.ts +36 -2
  176. package/src/server/inbound-body-admission.ts +187 -0
  177. package/src/server/index/websocket-handler.ts +48 -1
  178. package/src/server/index.ts +15 -19
  179. package/src/server/management/api-access.ts +3 -4
  180. package/src/server/management/config-routes.ts +57 -10
  181. package/src/server/management/provider-capability-config.ts +35 -7
  182. package/src/server/management/provider-routes.ts +70 -18
  183. package/src/server/models-capabilities.ts +24 -3
  184. package/src/server/proxy-liveness.ts +97 -2
  185. package/src/server/relay.ts +17 -24
  186. package/src/server/request-log.ts +25 -1
  187. package/src/server/responses/adapter-continuation.ts +71 -27
  188. package/src/server/responses/adapter-delivery.ts +39 -8
  189. package/src/server/responses/adapter-dispatch.ts +52 -24
  190. package/src/server/responses/codex-ws-exchange.ts +65 -4
  191. package/src/server/responses/combo-stream-preflight.ts +68 -5
  192. package/src/server/responses/compact.ts +60 -11
  193. package/src/server/responses/core-codex-account.ts +83 -22
  194. package/src/server/responses/core-combo.ts +26 -0
  195. package/src/server/responses/core-normalize.ts +12 -5
  196. package/src/server/responses/core-options.ts +3 -0
  197. package/src/server/responses/fetch-helpers.ts +72 -3
  198. package/src/server/responses/native-injection-protocol.ts +42 -0
  199. package/src/server/responses/native-injection-replay.ts +105 -0
  200. package/src/server/responses/native-injection.ts +242 -0
  201. package/src/server/responses/native-response-control.ts +56 -0
  202. package/src/server/responses/native-response-json.ts +14 -0
  203. package/src/server/responses/native-response-output.ts +37 -0
  204. package/src/server/responses/native-steering-log.ts +44 -0
  205. package/src/server/responses/native-steering-policy.ts +49 -0
  206. package/src/server/responses/native-steering-replay.ts +126 -0
  207. package/src/server/responses/native-steering-settings.ts +76 -0
  208. package/src/server/responses/native-steering.ts +400 -0
  209. package/src/server/responses/native-tool-results.ts +130 -0
  210. package/src/server/responses/passthrough-delivery.ts +21 -1
  211. package/src/server/responses/passthrough-dispatch.ts +146 -49
  212. package/src/server/responses/passthrough-execution.ts +11 -1
  213. package/src/server/responses/request-prepare.ts +70 -0
  214. package/src/server/responses/request-send-budget.ts +84 -7
  215. package/src/server/responses/request-sidecar-auth.ts +16 -8
  216. package/src/server/responses/request-spend.ts +38 -9
  217. package/src/server/responses/request-transport.ts +13 -10
  218. package/src/server/responses/run-turn-execution.ts +20 -5
  219. package/src/server/responses/sidecar-execution.ts +2 -0
  220. package/src/server/responses/ws-upstream.ts +23 -2
  221. package/src/server/responses-custom-tool-repair.ts +2 -2
  222. package/src/server/sse-frame-buffer.ts +12 -10
  223. package/src/server/sse-payload-rewrite.ts +36 -9
  224. package/src/server/stop-teardown.ts +8 -1
  225. package/src/server/system-env-shell.ts +5 -1
  226. package/src/server/system-env.ts +7 -1
  227. package/src/server/workflow-refusal.ts +56 -2
  228. package/src/server/ws-bridge.ts +16 -1
  229. package/src/service/cli.ts +29 -7
  230. package/src/service/guards.ts +10 -0
  231. package/src/service/health.ts +43 -0
  232. package/src/service/state.ts +7 -2
  233. package/src/types/accounts.ts +4 -0
  234. package/src/types/config.ts +104 -3
  235. package/src/types/provider.ts +32 -0
  236. package/src/types/request.ts +7 -1
  237. package/src/types/wire.ts +9 -1
  238. package/src/usage/expected-prices.ts +28 -0
  239. package/src/usage/log.ts +87 -4
  240. package/src/web-search/passthrough-bridge.ts +39 -5
  241. package/gui/dist/assets/index-Cz7CLdif.js +0 -128
package/src/types/wire.ts CHANGED
@@ -46,7 +46,15 @@ export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet<string> = new Set([
46
46
  * Anthropic for these models.
47
47
  */
48
48
  const ANTHROPIC_WIRE_MODELS: Record<string, ReadonlySet<string>> = {
49
- "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3"]),
49
+ "opencode-go": new Set([
50
+ "minimax-m2.5",
51
+ "minimax-m2.7",
52
+ "minimax-m3",
53
+ // OpenCode's catalog identifies Union Alpha as @ai-sdk/anthropic while the
54
+ // provider defaults to OpenAI-compatible; direct Chat returns 500 and direct
55
+ // Messages reaches the session check (#4847).
56
+ "union-alpha",
57
+ ]),
50
58
  };
51
59
 
52
60
  function anthropicWireModelsForProvider(providerName: string): ReadonlySet<string> | undefined {
@@ -124,6 +124,16 @@ const META_MUSE_SPARK_13_CONTRIBUTOR: Cost4 = { input: 0.1, output: 0.2, cacheRe
124
124
  const META_SPARK_SOURCE = `Meta Model API published price ${META_MODEL_PRICING}`;
125
125
  const META_SPARK_CONTRIBUTOR_SOURCE = `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`;
126
126
  const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
127
+ /*
128
+ * DeepSeek V4.1-Flash list prices (USD / 1M tokens), verified 2026-09-17 against
129
+ * https://api-docs.deepseek.com/quick_start/pricing. The page prices a peak window
130
+ * (09:30-24:00 Beijing) and an off-peak window; the tuple below is the peak-window
131
+ * list rate and the off-peak discount (0.15 / 0.60, cache-hit 0.003) is deliberately
132
+ * not baked in — the same rule as the Devin time-boxed promos. cacheWrite=0 follows
133
+ * the existing deepseek-chat / deepseek-reasoner rows: DeepSeek publishes no
134
+ * cache-write charge.
135
+ */
136
+ const DEEPSEEK_V41_FLASH: Cost4 = { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 };
127
137
  // Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the
128
138
  // cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified.
129
139
  const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cacheWrite derived = input, Kimi auto-cache has no write billing)";
@@ -142,6 +152,14 @@ const BIGMODEL_NOTE = "z.ai international list price shown as estimate; domestic
142
152
  // anywhere. Cache stays 0 rather than inheriting the reseller's 0.15 — a reseller number
143
153
  // under a vendor-price label would be a wrong value wearing a verified badge.
144
154
  const QWEN38_MAX_PRICING = "https://qwen.ai/blog?id=qwen3.8 (Qwen release announcement; no Model Studio billing row yet; cache rates unpublished -> 0)";
155
+ // Qwen-published qwen3.8-flash rate ($0.16 in / $0.47 out per 1M tokens) via the
156
+ // Qwen3.8 release announcement, corroborated by API-vendor price tables. No cache
157
+ // rate is published anywhere, so cache stays 0 rather than borrowing a reseller's
158
+ // number — the same hold QWEN38_MAX takes. The announcement marks API availability
159
+ // as coming soon, but the id is already served (and logged) on OpenCode Go, so the
160
+ // estimate applies to real usage rows now.
161
+ const QWEN38_FLASH: Cost4 = { input: 0.16, output: 0.47, cacheRead: 0, cacheWrite: 0 };
162
+ const QWEN38_FLASH_PRICING = "https://qwen.ai/blog?id=qwen3.8-2026 (Qwen release announcement; API marked coming soon at announcement; cache rates unpublished -> 0; input/output corroborated by https://docs.b.ai/guides/models/qwen/qwen3.8-flash)";
145
163
 
146
164
  /*
147
165
  * Cognition/Devin list prices (USD / 1M tokens), verified 2026-09-13 against the
@@ -289,6 +307,16 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
289
307
  // for what that source does and does not cover.
290
308
  { provider: "alibaba-token-plan", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: QWEN38_MAX_PRICING, verifiedAt: "2026-08-04", status: "verified" },
291
309
  { provider: "alibaba-token-plan-intl", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: QWEN38_MAX_PRICING, verifiedAt: "2026-08-04", status: "verified" },
310
+ // OpenCode Go — five served ids with no jawcode bundle row and no vendor-level
311
+ // fallback (the fallback only searches jawcode metadata, never overlays), so the
312
+ // Usage estimated-cost column and the per-model breakdown rendered an em dash for
313
+ // every request through them. Each row reuses the vendor's own published list
314
+ // price as an estimate: Go itself is subscription-billed, hence verified-derived.
315
+ { provider: "opencode-go", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: `vendor list price applied to the OpenCode Go surface; ${QWEN38_MAX_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
316
+ { provider: "opencode-go", modelId: "qwen3.8-flash", cost4: QWEN38_FLASH, source: `vendor list price applied to the OpenCode Go surface; ${QWEN38_FLASH_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
317
+ { provider: "opencode-go", modelId: "deepseek-v4.1-flash", cost4: DEEPSEEK_V41_FLASH, source: `peak-window list rate applied to the OpenCode Go surface (off-peak 0.15/0.60 + cache-hit 0.003 not baked in); ${DEEPSEEK_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
318
+ { provider: "opencode-go", modelId: "glm-5.3-flash", cost4: GLM_53_FLASH, source: `z.ai list price applied to the OpenCode Go surface as an estimate; ${ZAI_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
319
+ { provider: "opencode-go", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: `Meta Model API Contributor-tier price applied to the OpenCode Go surface as an estimate; ${META_SPARK_CONTRIBUTOR_SOURCE}`, verifiedAt: "2026-09-17", status: "verified-derived" },
292
320
  // Cursor Auto router — Cursor's published fixed token price (verified).
293
321
  { provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" },
294
322
  // Z.AI GLM family — the zai bundle's rows are all-zero upstream, and the four
package/src/usage/log.ts CHANGED
@@ -77,6 +77,26 @@ export type AttemptRecoveryKind =
77
77
  | "empty-completion"
78
78
  | "reasoning-effort-downgrade";
79
79
 
80
+ /**
81
+ * Why a recovery this request was otherwise willing to make did not happen.
82
+ *
83
+ * Recorded separately from `recoveryKinds` and from `sendCount`, because the question it
84
+ * answers is different from either. A log showing one physical send and no recovery kind used
85
+ * to be ambiguous: it could mean nothing was eligible, or that something was eligible and the
86
+ * send budget withheld it. Those need opposite follow-ups, and the second one was invisible
87
+ * (#5044).
88
+ *
89
+ * `sendCount` deliberately does not move for these. A refused attempt is not a physical send,
90
+ * and inflating the count to signal the refusal would corrupt the one number that means
91
+ * "requests this proxy actually made".
92
+ *
93
+ * Bounded vocabulary on purpose: it is a wire value a maintainer reads, never a credential, an
94
+ * account id, an upstream body, prompt content, or exception text.
95
+ */
96
+ export type AttemptRecoveryWithheld =
97
+ | "retry-send-budget"
98
+ | "rotation-send-budget";
99
+
80
100
  /** Request-time upstream credential class, never a credential or account identifier. */
81
101
  export type UsageCredentialSource = "grok-oauth" | "xai-api-key";
82
102
 
@@ -139,6 +159,11 @@ export interface PersistedUsageAttempt {
139
159
  firstOutputMs?: number;
140
160
  sendCount: number;
141
161
  recoveryKinds: AttemptRecoveryKind[];
162
+ /**
163
+ * Recoveries this attempt was eligible for and did not make. Absent on ordinary attempts so
164
+ * old rows keep their exact shape.
165
+ */
166
+ recoveryWithheld?: AttemptRecoveryWithheld[];
142
167
  usageStatus: UsageStatus;
143
168
  /**
144
169
  * True when the proxy answered this turn locally and issued no upstream request. It travels on
@@ -487,6 +512,10 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
487
512
  "empty-completion",
488
513
  "reasoning-effort-downgrade",
489
514
  ]);
515
+ const ATTEMPT_RECOVERY_WITHHELD = new Set<AttemptRecoveryWithheld>([
516
+ "retry-send-budget",
517
+ "rotation-send-budget",
518
+ ]);
490
519
  const USAGE_STATUSES = new Set<UsageStatus>([
491
520
  "reported",
492
521
  "unreported",
@@ -620,6 +649,14 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
620
649
  && ATTEMPT_RECOVERY_KINDS.has(value as AttemptRecoveryKind),
621
650
  ))]
622
651
  : [];
652
+ // Same shape as `recoveryKinds`: unknown values are dropped rather than failing the row, so a
653
+ // log written by a newer build stays readable by an older one.
654
+ const recoveryWithheld = Array.isArray(attempt.recoveryWithheld)
655
+ ? [...new Set(attempt.recoveryWithheld.filter(
656
+ (value): value is AttemptRecoveryWithheld => typeof value === "string"
657
+ && ATTEMPT_RECOVERY_WITHHELD.has(value as AttemptRecoveryWithheld),
658
+ ))]
659
+ : [];
623
660
  return {
624
661
  ordinal: attempt.ordinal as number,
625
662
  provider: attempt.provider,
@@ -639,6 +676,7 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
639
676
  : {}),
640
677
  sendCount: attempt.sendCount as number,
641
678
  recoveryKinds,
679
+ ...(recoveryWithheld.length ? { recoveryWithheld } : {}),
642
680
  usageStatus: attempt.usageStatus as UsageStatus,
643
681
  ...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
644
682
  ? { accountLogLabel: attempt.accountLogLabel }
@@ -852,18 +890,61 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
852
890
  };
853
891
  }
854
892
 
855
- function ensureUsageLogDir(): void {
893
+ // Bound hot-path filesystem hardening to once per second while ensuring an external mode
894
+ // widening cannot suppress write-triggered repair for the lifetime of the process.
895
+ const USAGE_LOG_PERMISSION_RECHECK_MS = 1_000;
896
+
897
+ type UsageLogPermissionCheck = {
898
+ path: string;
899
+ checkedAt: number;
900
+ };
901
+
902
+ let ensuredUsageLogDir: UsageLogPermissionCheck | null = null;
903
+ let ensuredUsageLogFile: UsageLogPermissionCheck | null = null;
904
+
905
+ function usageLogPermissionCheckIsCurrent(
906
+ check: UsageLogPermissionCheck | null,
907
+ path: string,
908
+ now: number,
909
+ ): boolean {
910
+ return check?.path === path
911
+ && now >= check.checkedAt
912
+ && now - check.checkedAt < USAGE_LOG_PERMISSION_RECHECK_MS;
913
+ }
914
+
915
+ function ensureUsageLogDir(now: number): void {
856
916
  const dir = getConfigDir();
917
+ if (usageLogPermissionCheckIsCurrent(ensuredUsageLogDir, dir, now)) return;
857
918
  recordOwnedConfigPath(dir, usageLogPath());
858
919
  mkdirSync(dir, { recursive: true, mode: 0o700 });
859
920
  try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
921
+ ensuredUsageLogDir = { path: dir, checkedAt: now };
860
922
  }
861
923
 
862
924
  export function appendUsageEntry(entry: PersistedUsageEntry): void {
863
- ensureUsageLogDir();
925
+ const line = `${JSON.stringify(normalizeUsageEntry(entry))}\n`;
864
926
  const path = usageLogPath();
865
- appendFileSync(path, `${JSON.stringify(normalizeUsageEntry(entry))}\n`, { encoding: "utf-8", mode: 0o600 });
866
- try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
927
+ const now = Date.now();
928
+ const doAppend = (): void => {
929
+ ensureUsageLogDir(now);
930
+ const filePermissionsCurrent = usageLogPermissionCheckIsCurrent(ensuredUsageLogFile, path, now);
931
+ appendFileSync(path, line, { encoding: "utf-8", mode: 0o600 });
932
+ if (!filePermissionsCurrent) {
933
+ try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
934
+ ensuredUsageLogFile = { path, checkedAt: now };
935
+ }
936
+ };
937
+ try {
938
+ doAppend();
939
+ } catch (error: any) {
940
+ if (error?.code === "ENOENT") {
941
+ ensuredUsageLogDir = null;
942
+ ensuredUsageLogFile = null;
943
+ doAppend();
944
+ return;
945
+ }
946
+ throw error;
947
+ }
867
948
  }
868
949
 
869
950
  export type UsageLogRevision = {
@@ -1057,6 +1138,8 @@ export function resetUsageReadCacheForTests(): void {
1057
1138
  managementUsageReadInflight?.abort.abort();
1058
1139
  managementUsageReadInflight = null;
1059
1140
  retainedUsageSnapshot = null;
1141
+ ensuredUsageLogDir = null;
1142
+ ensuredUsageLogFile = null;
1060
1143
  }
1061
1144
 
1062
1145
  function readExactly(fd: number, length: number, position: number): Buffer | null {
@@ -75,6 +75,7 @@ import {
75
75
  } from "./sidecar-providers";
76
76
  import { providerDestinationConfigError } from "../lib/destination-policy";
77
77
  import { redactSecretString } from "../lib/redact";
78
+ import { rememberBridgeSearchReplay } from "../responses/bridge-search-replay-cache";
78
79
 
79
80
  /** Canonical Ollama Cloud origin. The only origin the "ollama" backend derives on its own. */
80
81
  export const OLLAMA_CLOUD_ORIGIN = "https://ollama.com";
@@ -377,11 +378,18 @@ export interface PassthroughWebSearchBridgeStreamOptions {
377
378
  /** Sends one continuation leg and resolves with its response. */
378
379
  send: (body: string) => Promise<Response>;
379
380
  execute: PassthroughWebSearchBridgeExecutor;
381
+ /**
382
+ * Destination identity for the executed-search memo (#4587). When absent nothing is recorded,
383
+ * and the next turn replays the hosted cell exactly as it does today.
384
+ */
385
+ destinationScope?: string;
380
386
  /**
381
387
  * Re-applies the caller's outbound body ceiling to a continuation body. Returns a refusal
382
388
  * message when the extended body may not be sent, or undefined when it is admitted.
383
389
  */
384
390
  checkOutboundBody?: (body: string) => string | undefined;
391
+ /** Releases request-scoped resources when the stream completes, fails, or is cancelled. */
392
+ onFinalize?: () => void;
385
393
  signal?: AbortSignal;
386
394
  }
387
395
 
@@ -1094,11 +1102,22 @@ async function* bridgeStreamBlocks(
1094
1102
  outcome = await options.execute(queries, options.signal);
1095
1103
  }
1096
1104
  yield* emit(state.searchEndFrames(call, queries, outcome));
1097
- turns.push({
1098
- call,
1099
- // The model needs a readable result either way; an executor error is reported as the
1100
- // tool result rather than as a turn failure, so it can still answer without the search.
1101
- output: outcome.error ? "Web search failed: " + outcome.error : outcome.text,
1105
+ // The model needs a readable result either way; an executor error is reported as the
1106
+ // tool result rather than as a turn failure, so it can still answer without the search.
1107
+ const output = outcome.error ? "Web search failed: " + outcome.error : outcome.text;
1108
+ turns.push({ call, output });
1109
+ // Record what a continuation leg WOULD put on the wire, whether or not this leg sends one
1110
+ // (#4587). The caller keeps the hosted cell and replays it next turn; the pre-dispatch
1111
+ // rewrite in the Responses adapter uses this to hand the destination back its own call and
1112
+ // result instead of an item type it never produced. Recording the same text that
1113
+ // appendBridgeSearchTurn would append is what keeps a replayed turn and a continued turn
1114
+ // showing the destination one consistent conversation.
1115
+ rememberBridgeSearchReplay(options.destinationScope, call.cellItemId, {
1116
+ callId: call.callId,
1117
+ sourceItemId: call.sourceItemId,
1118
+ name: WEB_SEARCH_TOOL_NAME,
1119
+ argumentsText: call.argumentsText,
1120
+ output,
1102
1121
  });
1103
1122
  }
1104
1123
 
@@ -1170,21 +1189,36 @@ export function createPassthroughWebSearchBridgeStream(
1170
1189
  const aborted = (): boolean => cancelled || options.signal?.aborted === true;
1171
1190
  const iterator = bridgeStreamBlocks(options, aborted)[Symbol.asyncIterator]();
1172
1191
  const encoder = new TextEncoder();
1192
+ let finalized = false;
1193
+ const finalize = (): void => {
1194
+ if (finalized) return;
1195
+ finalized = true;
1196
+ options.onFinalize?.();
1197
+ };
1173
1198
  return new ReadableStream<Uint8Array>({
1174
1199
  async pull(controller) {
1175
1200
  try {
1176
1201
  const next = await iterator.next();
1177
1202
  if (next.done) {
1203
+ finalize();
1178
1204
  controller.close();
1179
1205
  return;
1180
1206
  }
1181
1207
  controller.enqueue(encoder.encode(next.value));
1182
1208
  } catch (error) {
1209
+ finalize();
1183
1210
  controller.error(error);
1184
1211
  }
1185
1212
  },
1186
1213
  cancel(reason) {
1187
1214
  cancelled = true;
1215
+ // Release request-scoped authority immediately. An async generator cannot
1216
+ // process a queued return() while its active next() is blocked on an
1217
+ // upstream read, so deferring finalize until that settles would hold the
1218
+ // sidecar probe lease for as long as the abandoned upstream leg does.
1219
+ // Optional chaining would also skip finalize entirely for an iterator
1220
+ // with no return method.
1221
+ finalize();
1188
1222
  void iterator.return?.(reason);
1189
1223
  },
1190
1224
  });