@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 (173) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +47 -22
  3. package/gui/dist/assets/index-BDjpkcRN.js +67 -0
  4. package/gui/dist/assets/index-BHsKRFh9.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/discovery.ts +4 -1
  10. package/src/adapters/cursor/effort-map.ts +3 -0
  11. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  12. package/src/adapters/cursor/protobuf-events.ts +24 -2
  13. package/src/adapters/cursor/protobuf-request.ts +1 -2
  14. package/src/adapters/cursor/tool-definitions.ts +68 -29
  15. package/src/adapters/google-wire-compiler.ts +4 -0
  16. package/src/adapters/google.ts +128 -2
  17. package/src/adapters/identity.ts +12 -2
  18. package/src/adapters/kiro.ts +64 -7
  19. package/src/adapters/mimo-free.ts +2 -0
  20. package/src/adapters/openai-responses.ts +246 -59
  21. package/src/claude/agents-inject.ts +5 -0
  22. package/src/claude/alias.ts +94 -14
  23. package/src/claude/inbound.ts +26 -9
  24. package/src/claude/outbound.ts +6 -3
  25. package/src/cli/account-auth.ts +1 -1
  26. package/src/cli/agent-driven.ts +37 -0
  27. package/src/cli/catalog-prewarm.ts +24 -0
  28. package/src/cli/claude.ts +35 -10
  29. package/src/cli/doctor.ts +71 -19
  30. package/src/cli/help.ts +42 -6
  31. package/src/cli/index.ts +93 -19
  32. package/src/cli/interactive-confirm.ts +133 -0
  33. package/src/cli/opencode.ts +701 -0
  34. package/src/cli/provider-runtime.ts +3 -0
  35. package/src/cli/provider.ts +31 -10
  36. package/src/cli/star-prompt.ts +79 -18
  37. package/src/cli/status.ts +47 -13
  38. package/src/cli/v2.ts +10 -1
  39. package/src/codex/account-id.ts +34 -0
  40. package/src/codex/account-lifecycle.ts +4 -1
  41. package/src/codex/account-namespace-match.ts +63 -0
  42. package/src/codex/account-namespaces.ts +149 -0
  43. package/src/codex/account-pause.ts +20 -0
  44. package/src/codex/account-store.ts +2 -0
  45. package/src/codex/account-usability.ts +6 -1
  46. package/src/codex/app-server-processes.ts +511 -0
  47. package/src/codex/auth-api.ts +293 -34
  48. package/src/codex/auth-collision.ts +2 -1
  49. package/src/codex/auth-context.ts +60 -17
  50. package/src/codex/catalog/bundled.ts +9 -2
  51. package/src/codex/catalog/parsing.ts +42 -2
  52. package/src/codex/catalog/provider-fetch.ts +264 -70
  53. package/src/codex/catalog/sync.ts +45 -8
  54. package/src/codex/catalog.ts +2 -2
  55. package/src/codex/features.ts +524 -5
  56. package/src/codex/history-provider.ts +145 -1
  57. package/src/codex/inject.ts +114 -14
  58. package/src/codex/main-account.ts +2 -8
  59. package/src/codex/pool-rotation.ts +186 -0
  60. package/src/codex/quota.ts +92 -2
  61. package/src/codex/routing.ts +695 -106
  62. package/src/codex/runtime.ts +10 -1
  63. package/src/codex/shim.ts +4 -1
  64. package/src/codex/subagent-defaults.ts +550 -0
  65. package/src/codex/subagent-model-fallback.ts +2 -0
  66. package/src/codex/sync.ts +3 -0
  67. package/src/config.ts +574 -25
  68. package/src/generated/jawcode-model-metadata.ts +12 -12
  69. package/src/github/star-state.ts +191 -0
  70. package/src/images/artifacts.ts +516 -0
  71. package/src/images/fulfill-video.ts +163 -0
  72. package/src/images/fulfill.ts +111 -0
  73. package/src/images/index.ts +4 -0
  74. package/src/images/loop.ts +789 -0
  75. package/src/images/plan.ts +133 -0
  76. package/src/images/synthetic-tool.ts +133 -0
  77. package/src/images/types.ts +41 -0
  78. package/src/images/xai-client.ts +141 -0
  79. package/src/images/xai-video-client.ts +163 -0
  80. package/src/lib/admin-secrets.ts +25 -0
  81. package/src/lib/bun-binary-validator.d.mts +3 -0
  82. package/src/lib/bun-binary-validator.mjs +18 -0
  83. package/src/lib/bun-runtime.ts +6 -20
  84. package/src/lib/config-ownership.ts +327 -0
  85. package/src/lib/crash-guard.ts +2 -0
  86. package/src/lib/destination-policy.ts +132 -7
  87. package/src/lib/pinned-http.ts +151 -0
  88. package/src/lib/process-control.ts +2 -2
  89. package/src/lib/provider-outbound.ts +167 -0
  90. package/src/lib/provider-url.ts +14 -0
  91. package/src/lib/proxy-env.ts +18 -0
  92. package/src/lib/shadow-call.ts +30 -0
  93. package/src/lib/test-home-guard.ts +90 -0
  94. package/src/lib/win-exec.ts +12 -2
  95. package/src/lib/windows-elevation.ts +81 -3
  96. package/src/lib/windows-secret-acl.ts +189 -12
  97. package/src/lib/winsw.ts +2 -0
  98. package/src/oauth/anthropic-routing.ts +570 -0
  99. package/src/oauth/health.ts +6 -0
  100. package/src/oauth/index.ts +310 -75
  101. package/src/oauth/key-providers.ts +38 -8
  102. package/src/oauth/kimi.ts +2 -0
  103. package/src/oauth/kiro-credentials.ts +373 -12
  104. package/src/oauth/kiro.ts +424 -43
  105. package/src/oauth/login-cli.ts +33 -6
  106. package/src/oauth/store.ts +56 -4
  107. package/src/oauth/types.ts +11 -0
  108. package/src/providers/alibaba-region-migration.ts +16 -3
  109. package/src/providers/antigravity-models.ts +3 -0
  110. package/src/providers/api-keys.ts +13 -6
  111. package/src/providers/derive.ts +8 -2
  112. package/src/providers/key-failover.ts +24 -4
  113. package/src/providers/model-discovery.ts +356 -0
  114. package/src/providers/quota.ts +233 -29
  115. package/src/providers/registry.ts +125 -3
  116. package/src/responses/parser.ts +11 -0
  117. package/src/responses/state.ts +22 -8
  118. package/src/responses/tool-groups.ts +19 -0
  119. package/src/router.ts +19 -7
  120. package/src/server/auth-cors.ts +114 -24
  121. package/src/server/claude-messages.ts +8 -1
  122. package/src/server/gui-static.ts +30 -6
  123. package/src/server/images.ts +303 -9
  124. package/src/server/index.ts +77 -9
  125. package/src/server/lifecycle.ts +25 -1
  126. package/src/server/live.ts +75 -25
  127. package/src/server/management/agent-settings-routes.ts +106 -8
  128. package/src/server/management/combo-routes.ts +7 -0
  129. package/src/server/management/config-routes.ts +22 -7
  130. package/src/server/management/context.ts +11 -1
  131. package/src/server/management/logs-usage-routes.ts +167 -3
  132. package/src/server/management/model-routes.ts +46 -13
  133. package/src/server/management/oauth-account-routes.ts +163 -17
  134. package/src/server/management/provider-routes.ts +73 -10
  135. package/src/server/management/shared.ts +2 -2
  136. package/src/server/management/sidebar-routes.ts +39 -0
  137. package/src/server/management/system-restart.ts +172 -0
  138. package/src/server/management/system-routes.ts +33 -10
  139. package/src/server/management-api.ts +5 -3
  140. package/src/server/management-auth.ts +216 -0
  141. package/src/server/proxy-liveness.ts +14 -3
  142. package/src/server/responses/compact.ts +21 -13
  143. package/src/server/responses/core.ts +614 -172
  144. package/src/server/responses/upstream-error.ts +48 -0
  145. package/src/server/responses-image-gen-repair.ts +118 -0
  146. package/src/server/responses-item-id-repair.ts +10 -85
  147. package/src/server/sse-payload-rewrite.ts +116 -0
  148. package/src/server/startup-action-control.ts +30 -14
  149. package/src/server/system-env.ts +28 -10
  150. package/src/service.ts +284 -19
  151. package/src/storage/cleanup-job.ts +57 -0
  152. package/src/storage/cleanup.ts +1504 -28
  153. package/src/storage/policy-job.ts +387 -0
  154. package/src/storage/policy-scheduler.ts +40 -0
  155. package/src/storage/policy-worker.ts +53 -0
  156. package/src/storage/policy.ts +522 -0
  157. package/src/storage/restore-job.ts +253 -0
  158. package/src/storage/restore-worker.ts +52 -0
  159. package/src/storage/storage-mutation-coordinator.ts +109 -0
  160. package/src/storage/worker-lifecycle.ts +81 -0
  161. package/src/tray/windows.ts +34 -4
  162. package/src/types.ts +107 -1
  163. package/src/update/badge.ts +72 -0
  164. package/src/update/index.ts +36 -18
  165. package/src/update/job.ts +111 -16
  166. package/src/update/npm-invocation.d.mts +23 -0
  167. package/src/update/npm-invocation.mjs +94 -0
  168. package/src/usage/debug.ts +2 -0
  169. package/src/usage/expected-prices.ts +6 -5
  170. package/src/usage/log.ts +12 -0
  171. package/src/web-search/loop.ts +57 -16
  172. package/gui/dist/assets/index-CjKFJHSC.js +0 -65
  173. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -0,0 +1,94 @@
1
+ import { existsSync } from "node:fs";
2
+ import { win32 } from "node:path";
3
+
4
+ const CMD_META = /([()%!^"`<>&|;, *?])/g;
5
+
6
+ function escapeCmdArg(arg) {
7
+ let out = String(arg).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
8
+ return `"${out}"`.replace(CMD_META, "^$1");
9
+ }
10
+
11
+ function escapeCmdCommand(command) {
12
+ return command.replace(CMD_META, "^$1");
13
+ }
14
+
15
+ /**
16
+ * Whether a PATH entry *is* the current directory. The hijack this guards against is
17
+ * cmd.exe resolving a bare `npm` out of the directory opencodex was launched from, so
18
+ * only that exact directory has to be skipped — every candidate we hand to spawn is an
19
+ * absolute path, which is what actually defeats the implicit cwd-first search.
20
+ *
21
+ * Deliberately not a subtree test: npm's default Windows global prefix is
22
+ * `%AppData%\npm` (`C:\Users\x\AppData\Roaming\npm`), so excluding everything under the
23
+ * cwd would fail closed for anyone whose shell sits in their home directory — a normal
24
+ * setup, not the untrusted-project case this hardening is for.
25
+ */
26
+ function isCurrentDirectory(cwd, entry) {
27
+ const left = win32.resolve(entry);
28
+ const right = win32.resolve(cwd);
29
+ return left.toLowerCase() === right.toLowerCase();
30
+ }
31
+
32
+ function cleanPathEntry(entry) {
33
+ const trimmed = entry.trim();
34
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1);
35
+ return trimmed;
36
+ }
37
+
38
+ export function resolveNpmCommand(
39
+ platform = process.platform,
40
+ env = process.env,
41
+ deps = {},
42
+ ) {
43
+ if (platform !== "win32") return "npm";
44
+ const exists = deps.exists ?? existsSync;
45
+ const cwd = deps.cwd ?? process.cwd();
46
+ const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
47
+ .split(";")
48
+ .filter(Boolean);
49
+ const pathEntries = (env.PATH ?? env.Path ?? "")
50
+ .split(win32.delimiter)
51
+ .map(cleanPathEntry)
52
+ .filter(Boolean);
53
+
54
+ for (const entry of pathEntries) {
55
+ if (!win32.isAbsolute(entry)) continue;
56
+ if (isCurrentDirectory(cwd, entry)) continue;
57
+ for (const extension of extensions) {
58
+ const candidate = win32.join(entry, `npm${extension.toLowerCase()}`);
59
+ if (exists(candidate)) return win32.resolve(candidate);
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function systemCommandProcessor(env) {
66
+ const systemRoot = env.SystemRoot ?? env.windir;
67
+ if (systemRoot && win32.isAbsolute(systemRoot)) {
68
+ return win32.join(systemRoot, "System32", "cmd.exe");
69
+ }
70
+ const comSpec = env.ComSpec;
71
+ return comSpec && win32.isAbsolute(comSpec) ? win32.resolve(comSpec) : null;
72
+ }
73
+
74
+ export function npmInvocation(
75
+ args,
76
+ platform = process.platform,
77
+ env = process.env,
78
+ deps = {},
79
+ ) {
80
+ const npm = resolveNpmCommand(platform, env, deps);
81
+ if (!npm) return null;
82
+ if (platform !== "win32" || !/\.(cmd|bat)$/i.test(npm)) {
83
+ return { file: npm, args: [...args], options: {} };
84
+ }
85
+
86
+ const commandProcessor = systemCommandProcessor(env);
87
+ if (!commandProcessor) return null;
88
+ const line = [escapeCmdCommand(npm), ...args.map(escapeCmdArg)].join(" ");
89
+ return {
90
+ file: commandProcessor,
91
+ args: ["/d", "/s", "/c", `"${line}"`],
92
+ options: { windowsVerbatimArguments: true },
93
+ };
94
+ }
@@ -4,6 +4,7 @@ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFi
4
4
  import { join } from "node:path";
5
5
  import { getConfigDir } from "../config";
6
6
  import { DEBUG_ENV } from "../lib/debug-settings";
7
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
7
8
  import type { DebugLogEntry } from "../lib/debug-log-buffer";
8
9
  import { redactSecretString, redactSecrets } from "../lib/redact";
9
10
  import type { OcxUsage } from "../types";
@@ -42,6 +43,7 @@ export function truncateForDebug(text: string, max = USAGE_DEBUG_BODY_SAMPLE_BYT
42
43
 
43
44
  function ensureUsageDebugDir(): void {
44
45
  const dir = getConfigDir();
46
+ recordOwnedConfigPath(dir, usageDebugPath());
45
47
  mkdirSync(dir, { recursive: true, mode: 0o700 });
46
48
  try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
47
49
  }
@@ -144,16 +144,17 @@ export function findExpectedPriceOverlay(
144
144
  }
145
145
 
146
146
  /**
147
- * OpenAI service_tier "priority" (Fast) price multipliers by model slug.
148
- * Source: https://platform.openai.com/docs/models (2026-07-24).
149
- * Priority pricing applies uniformly to all token types (input, output, cache).
147
+ * OpenAI Fast mode (`service_tier=priority`) price multipliers by model slug.
148
+ * Source: https://openai.com/api-fast-mode/ (2026-07-31).
149
+ * Fast pricing applies uniformly to all token types (input, output, cache).
150
150
  * Models not listed here fall back to 1× (no multiplier).
151
151
  */
152
152
  export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
153
153
  "gpt-5.6-sol": 2,
154
- "gpt-5.6-terra": 2,
155
- "gpt-5.6-luna": 2,
154
+ "gpt-5.6-terra": 1.6,
155
+ "gpt-5.6-luna": 0.4,
156
156
  "gpt-5.5": 2.5,
157
+ "gpt-5.4-mini": 2,
157
158
  "gpt-5.4": 2,
158
159
  };
159
160
 
package/src/usage/log.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getConfigDir } from "../config";
4
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
4
5
  import { usageDisplayTotalTokens } from "./totals";
5
6
  import type { OcxUsage } from "../types";
6
7
 
@@ -11,6 +12,7 @@ export type AttemptRecoveryKind =
11
12
  | "connection-reset"
12
13
  | "oauth-401"
13
14
  | "key-429"
15
+ | "anthropic-oauth-429"
14
16
  | "image-413";
15
17
 
16
18
  export interface PersistedUsageAttempt {
@@ -126,6 +128,13 @@ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined
126
128
  return {
127
129
  inputTokens: usage.inputTokens,
128
130
  outputTokens: usage.outputTokens,
131
+ // Absolute active-context checkpoint (types.ts). Stateful providers such as Kiro report
132
+ // per-attempt usage only, so this field is the ONLY carrier of the cumulative context
133
+ // figure once the log records raw adapter usage instead of re-parsing the bridged wire
134
+ // (usageFromBridge, request-log.ts). Omitting it here silently dropped Kiro's context
135
+ // growth from every persisted row. It is deliberately NOT folded into totalTokens:
136
+ // a checkpoint is not a per-request total and must never be summed across requests.
137
+ ...(typeof usage.contextTotalTokens === "number" ? { contextTotalTokens: usage.contextTotalTokens } : {}),
129
138
  ...(typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {}),
130
139
  ...(typeof usage.cachedInputTokens === "number" ? { cachedInputTokens: usage.cachedInputTokens } : {}),
131
140
  ...(typeof usage.cacheReadInputTokens === "number" ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
@@ -140,6 +149,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
140
149
  "connection-reset",
141
150
  "oauth-401",
142
151
  "key-429",
152
+ "anthropic-oauth-429",
143
153
  "image-413",
144
154
  ]);
145
155
  const USAGE_STATUSES = new Set<UsageStatus>([
@@ -159,6 +169,7 @@ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
159
169
  if (!isNonNegativeFiniteNumber(usage.inputTokens)
160
170
  || !isNonNegativeFiniteNumber(usage.outputTokens)) return null;
161
171
  for (const key of [
172
+ "contextTotalTokens",
162
173
  "totalTokens",
163
174
  "cachedInputTokens",
164
175
  "cacheReadInputTokens",
@@ -315,6 +326,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
315
326
 
316
327
  function ensureUsageLogDir(): void {
317
328
  const dir = getConfigDir();
329
+ recordOwnedConfigPath(dir, usageLogPath());
318
330
  mkdirSync(dir, { recursive: true, mode: 0o700 });
319
331
  try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
320
332
  }
@@ -103,23 +103,63 @@ async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
103
103
  * replaying a bare toolCall 400s ("Expected `thinking` or `redacted_thinking`, but found
104
104
  * `tool_use`"). The signature validity gate stays in the anthropic adapter; other adapters
105
105
  * ignore or serialize the part harmlessly.
106
+ *
107
+ * Each signed block keeps its OWN signature and text, mirroring src/images/loop.ts: a signature
108
+ * authenticates the exact block it closed, so flattening two blocks under the last signature
109
+ * 400s on replay just as it does there.
110
+ *
111
+ * Raw reasoning (`reasoning_raw_delta`, what OpenAI-compatible providers emit instead of signed
112
+ * thinking) accumulates into a SEPARATE UNSIGNED part. It must never join a signed block: the
113
+ * anthropic serializer skips signature-less parts, while openai-chat serializes their text as
114
+ * `reasoning_content` — which DeepSeek V4 thinking mode requires back alongside the replayed
115
+ * tool_calls, and whose absence ended the turn as a provider 400 (issue #688).
116
+ *
117
+ * This assumes raw reasoning never interleaves INSIDE an unfinished signed block: Anthropic-family
118
+ * adapters emit thinking_delta/signature and OpenAI-compatible ones emit reasoning_raw_delta, and
119
+ * the two never share a stream. Honoring a genuinely mixed stream would need per-segment state,
120
+ * not another accumulator.
106
121
  */
107
- function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent | null {
122
+ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[] {
123
+ const parts: OcxThinkingContent[] = [];
108
124
  let thinking = "";
109
125
  let signature: string | undefined;
110
- const redacted: string[] = [];
126
+ let rawReasoning = "";
127
+
128
+ const flushVisible = () => {
129
+ if (!thinking && !signature) return;
130
+ parts.push({
131
+ type: "thinking",
132
+ thinking,
133
+ ...(signature ? { signature } : {}),
134
+ });
135
+ thinking = "";
136
+ signature = undefined;
137
+ };
138
+ const flushRaw = () => {
139
+ if (!rawReasoning) return;
140
+ parts.push({ type: "thinking", thinking: rawReasoning });
141
+ rawReasoning = "";
142
+ };
143
+
111
144
  for (const e of events) {
112
- if (e.type === "thinking_delta") thinking += e.thinking;
113
- else if (e.type === "thinking_signature") signature = e.signature;
114
- else if (e.type === "redacted_thinking") redacted.push(e.data);
145
+ if (e.type === "thinking_delta") {
146
+ flushRaw();
147
+ thinking += e.thinking;
148
+ } else if (e.type === "reasoning_raw_delta") {
149
+ flushVisible();
150
+ rawReasoning += e.text;
151
+ } else if (e.type === "thinking_signature") {
152
+ signature = e.signature;
153
+ flushVisible();
154
+ } else if (e.type === "redacted_thinking") {
155
+ flushVisible();
156
+ flushRaw();
157
+ parts.push({ type: "thinking", thinking: "", redacted: [e.data] });
158
+ }
115
159
  }
116
- if (!thinking && !signature && redacted.length === 0) return null;
117
- return {
118
- type: "thinking",
119
- thinking,
120
- ...(signature ? { signature } : {}),
121
- ...(redacted.length > 0 ? { redacted } : {}),
122
- };
160
+ flushVisible();
161
+ flushRaw();
162
+ return parts;
123
163
  }
124
164
 
125
165
  /** Normalize a query for failed-query de-duplication (case/whitespace-insensitive). */
@@ -406,7 +446,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
406
446
  // valid, and surface as ONE search cell carrying every attempted query. A real search (one that
407
447
  // hits the sidecar) shows the spinner WHILE the batch runs. Empty/limit/repeat placeholders never
408
448
  // emit a cell (matching the prior single-query behavior).
409
- async function* runSearchCall(call: WebSearchCall, precedingThinking?: OcxThinkingContent | null): AsyncGenerator<AdapterEvent> {
449
+ async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = []): AsyncGenerator<AdapterEvent> {
410
450
  const results: { query: string; outcome: SidecarOutcome }[] = [];
411
451
  let beganCell = false;
412
452
  if (call.queries.length === 0) {
@@ -465,8 +505,9 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
465
505
  messages.push({
466
506
  role: "assistant",
467
507
  content: [
468
- // Signed thinking must precede tool_use on replay (Anthropic extended thinking).
469
- ...(precedingThinking ? [precedingThinking] : []),
508
+ // Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
509
+ // unsigned raw reasoning has to ride along for providers that require it back (#688).
510
+ ...precedingThinking,
470
511
  { type: "toolCall" as const, id: call.id, name: WEB_SEARCH_TOOL_NAME, arguments: callArgs },
471
512
  ],
472
513
  timestamp: now,
@@ -559,7 +600,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
559
600
  // The thinking that led to the search belongs to the FIRST call's assistant replay turn.
560
601
  const iterationThinking = extractIterationThinking(split.passthrough);
561
602
  for (const [callIndex, call] of split.calls.entries()) {
562
- yield* runSearchCall(call, callIndex === 0 ? iterationThinking : null);
603
+ yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
563
604
  }
564
605
  } catch (e) {
565
606
  yield { type: "error", message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)) };