@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
package/src/utils/git.ts CHANGED
@@ -197,12 +197,14 @@ const GIT_NON_INTERACTIVE_ENV = {
197
197
  GIT_ASKPASS: "true",
198
198
  GIT_EDITOR: "true",
199
199
  GIT_TERMINAL_PROMPT: "0",
200
+ LC_ALL: undefined,
201
+ LC_MESSAGES: "C",
200
202
  SSH_ASKPASS: "/usr/bin/false",
201
- } satisfies Record<string, string>;
203
+ } satisfies Record<string, string | undefined>;
202
204
  const GH_NON_INTERACTIVE_ENV = {
203
205
  ...GIT_NON_INTERACTIVE_ENV,
204
206
  GH_PROMPT_DISABLED: "1",
205
- } satisfies Record<string, string>;
207
+ } satisfies Record<string, string | undefined>;
206
208
 
207
209
  /** Default deadline for git and gh subprocesses spawned by the coding agent. */
208
210
  export const GIT_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
@@ -215,6 +217,14 @@ export const GIT_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
215
217
  export const GIT_NETWORK_TIMEOUT_MS = 30 * 60 * 1000;
216
218
  /** Maximum captured stdout or stderr bytes retained from git and gh subprocesses. */
217
219
  export const GIT_COMMAND_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;
220
+ /**
221
+ * Deadline for synchronous git plumbing commands launched via
222
+ * {@link gitSpawnSyncText}. These run on the render path (e.g. reftable HEAD
223
+ * resolution), so the deadline is short: a command that has not exited by then
224
+ * is killed and reported as {@link GIT_COMMAND_TIMEOUT_EXIT_CODE} so the caller
225
+ * degrades instead of freezing the UI indefinitely.
226
+ */
227
+ export const GIT_SPAWN_SYNC_TIMEOUT_MS = 5_000;
218
228
 
219
229
  const GIT_COMMAND_TIMEOUT_EXIT_CODE = 124;
220
230
  // Exit code returned when the `git` binary cannot be launched at all (spawn
@@ -378,16 +388,35 @@ function normalizeStdin(input: CommandOptions["stdin"]): "ignore" | Uint8Array {
378
388
  return new Uint8Array(input);
379
389
  }
380
390
 
381
- function buildGitEnv(overrides?: Record<string, string | undefined>): Record<string, string | undefined> {
391
+ function buildNonInteractiveEnv(
392
+ env: Record<string, string | undefined>,
393
+ pinnedEnv: Record<string, string | undefined>,
394
+ ): Record<string, string | undefined> {
395
+ const preservedCharacterLocale =
396
+ env.LC_ALL !== undefined && /(?:^|[._-])utf-?8(?:$|[.@_-])/i.test(env.LC_ALL) ? env.LC_ALL : undefined;
382
397
  return {
383
- ...process.env,
384
- GIT_OPTIONAL_LOCKS: "0",
385
- ...AMBIENT_GIT_ENV,
386
- ...overrides,
387
- ...GIT_NON_INTERACTIVE_ENV,
398
+ ...env,
399
+ ...(preservedCharacterLocale === undefined ? {} : { LC_CTYPE: preservedCharacterLocale }),
400
+ ...pinnedEnv,
388
401
  };
389
402
  }
390
403
 
404
+ function buildGitEnv(overrides?: Record<string, string | undefined>): Record<string, string | undefined> {
405
+ return buildNonInteractiveEnv(
406
+ {
407
+ ...process.env,
408
+ GIT_OPTIONAL_LOCKS: "0",
409
+ ...AMBIENT_GIT_ENV,
410
+ ...overrides,
411
+ },
412
+ GIT_NON_INTERACTIVE_ENV,
413
+ );
414
+ }
415
+
416
+ function buildGhEnv(): Record<string, string | undefined> {
417
+ return buildNonInteractiveEnv({ ...process.env }, GH_NON_INTERACTIVE_ENV);
418
+ }
419
+
391
420
  function ensureAvailable(): void {
392
421
  if (!$which("git")) {
393
422
  throw new Error("git is not installed.");
@@ -399,8 +428,17 @@ function ensureAvailable(): void {
399
428
  * exit code plus trimmed stdout; a missing `git` binary (spawn ENOENT) is
400
429
  * reported as {@link GIT_SPAWN_ENOENT_EXIT_CODE} so sync read-only callers
401
430
  * degrade to `null` instead of throwing an uncaught error during rendering.
431
+ *
432
+ * A deadline ({@link GIT_SPAWN_SYNC_TIMEOUT_MS}) is enforced so a pathological
433
+ * git invocation (lock contention, NFS stall, …) cannot hang the render path
434
+ * indefinitely: a child killed by the deadline is reported as
435
+ * {@link GIT_COMMAND_TIMEOUT_EXIT_CODE} rather than a successful exit.
402
436
  */
403
- function gitSpawnSyncText(cwd: string, args: readonly string[]): { exitCode: number; stdout: string } {
437
+ function gitSpawnSyncText(
438
+ cwd: string,
439
+ args: readonly string[],
440
+ timeoutMs: number = GIT_SPAWN_SYNC_TIMEOUT_MS,
441
+ ): { exitCode: number; stdout: string } {
404
442
  const commandArgs = withShortLivedGitConfig(withNoOptionalLocks(args));
405
443
  try {
406
444
  const result = Bun.spawnSync(["git", ...commandArgs], {
@@ -409,8 +447,14 @@ function gitSpawnSyncText(cwd: string, args: readonly string[]): { exitCode: num
409
447
  stdout: "pipe",
410
448
  stderr: "pipe",
411
449
  windowsHide: true,
450
+ timeout: timeoutMs,
412
451
  });
413
- return { exitCode: result.exitCode ?? 0, stdout: new TextDecoder().decode(result.stdout).trim() };
452
+ // Bun's timeout marker is authoritative even when process cleanup reports
453
+ // exit code zero, so render-path callers never trust partial output.
454
+ const exitCode = result.exitedDueToTimeout
455
+ ? GIT_COMMAND_TIMEOUT_EXIT_CODE
456
+ : (result.exitCode ?? GIT_COMMAND_TIMEOUT_EXIT_CODE);
457
+ return { exitCode, stdout: new TextDecoder().decode(result.stdout).trim() };
414
458
  } catch (err) {
415
459
  if (isEnoent(err)) return { exitCode: GIT_SPAWN_ENOENT_EXIT_CODE, stdout: "" };
416
460
  throw err;
@@ -1683,6 +1727,12 @@ export const revList = {
1683
1727
  async range(cwd: string, base: string, head: string, signal?: AbortSignal): Promise<string[]> {
1684
1728
  return splitLines(await runText(cwd, ["rev-list", "--reverse", `${base}..${head}`], { readOnly: true, signal }));
1685
1729
  },
1730
+ /** Commits reachable from `ref` that touched `file`, newest first, capped at `limit`. */
1731
+ async touching(cwd: string, ref: string, file: string, limit: number, signal?: AbortSignal): Promise<string[]> {
1732
+ return splitLines(
1733
+ await runText(cwd, ["rev-list", `--max-count=${limit}`, ref, "--", file], { readOnly: true, signal }),
1734
+ );
1735
+ },
1686
1736
  };
1687
1737
 
1688
1738
  // ════════════════════════════════════════════════════════════════════════════
@@ -2374,10 +2424,7 @@ export const github = {
2374
2424
  try {
2375
2425
  const child = Bun.spawn(["gh", ...args], {
2376
2426
  cwd,
2377
- env: {
2378
- ...process.env,
2379
- ...GH_NON_INTERACTIVE_ENV,
2380
- },
2427
+ env: buildGhEnv(),
2381
2428
  stdin: "ignore",
2382
2429
  stdout: "pipe",
2383
2430
  stderr: "pipe",
package/src/utils/jj.ts CHANGED
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { $which } from "@oh-my-pi/pi-utils";
4
4
  import { LRUCache } from "lru-cache/raw";
5
+ import { withTimeoutSignal } from "./fetch-timeout";
5
6
  import * as git from "./git";
6
7
 
7
8
  // ════════════════════════════════════════════════════════════════════════════
@@ -27,19 +28,24 @@ export interface JjRepository {
27
28
  }
28
29
 
29
30
  /** Options for `jj diff` invocations. */
30
- export interface DiffOptions {
31
+ export interface DiffOptions extends JjCommandOptions {
31
32
  /** Optional file paths to restrict the diff with `-- <files>`. */
32
33
  readonly files?: readonly string[];
33
34
  /** Return only changed file names instead of Git-format diff text. */
34
35
  readonly nameOnly?: boolean;
35
- /** Optional abort signal passed to the spawned `jj` process. */
36
- readonly signal?: AbortSignal;
37
36
  }
38
37
 
39
- interface CommandOptions {
38
+ /** Options for a bounded `jj` subprocess query. */
39
+ export interface JjCommandOptions {
40
+ /** Optional cancellation signal for the subprocess. */
40
41
  readonly signal?: AbortSignal;
42
+ /** Deadline in milliseconds. Defaults to {@link JJ_COMMAND_TIMEOUT_MS}. */
43
+ readonly timeoutMs?: number;
41
44
  }
42
45
 
46
+ /** Default finite deadline for local jj subprocesses. */
47
+ export const JJ_COMMAND_TIMEOUT_MS = 5_000;
48
+
43
49
  // ════════════════════════════════════════════════════════════════════════════
44
50
  // Error
45
51
  // ════════════════════════════════════════════════════════════════════════════
@@ -83,10 +89,10 @@ function formatCommandFailure(
83
89
  return `jj ${args.join(" ")} failed with exit code ${result.exitCode}`;
84
90
  }
85
91
 
86
- async function jj(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<JjCommandResult> {
92
+ async function jj(cwd: string, args: readonly string[], options: JjCommandOptions = {}): Promise<JjCommandResult> {
87
93
  const child = Bun.spawn(["jj", "--no-pager", "--color=never", ...args], {
88
94
  cwd,
89
- signal: options.signal,
95
+ signal: withTimeoutSignal(options.timeoutMs ?? JJ_COMMAND_TIMEOUT_MS, options.signal),
90
96
  stdin: "ignore",
91
97
  stdout: "pipe",
92
98
  stderr: "pipe",
@@ -109,7 +115,7 @@ async function jj(cwd: string, args: readonly string[], options: CommandOptions
109
115
  async function runChecked(
110
116
  cwd: string,
111
117
  args: readonly string[],
112
- options: CommandOptions = {},
118
+ options: JjCommandOptions = {},
113
119
  ): Promise<JjCommandResult> {
114
120
  ensureAvailable();
115
121
  const result = await jj(cwd, args, options);
@@ -119,14 +125,14 @@ async function runChecked(
119
125
  return result;
120
126
  }
121
127
 
122
- async function runText(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<string> {
128
+ async function runText(cwd: string, args: readonly string[], options: JjCommandOptions = {}): Promise<string> {
123
129
  return (await runChecked(cwd, args, options)).stdout;
124
130
  }
125
131
 
126
132
  async function runOptionalText(
127
133
  cwd: string,
128
134
  args: readonly string[],
129
- options: CommandOptions = {},
135
+ options: JjCommandOptions = {},
130
136
  ): Promise<string | null> {
131
137
  try {
132
138
  const result = await jj(cwd, args, options);
@@ -294,7 +300,7 @@ export const workingCopy = {
294
300
  * Label `@` with its nearest bookmark, falling back to its short change ID.
295
301
  * Returns `null` when `jj` is unavailable or the query fails.
296
302
  */
297
- async label(cwd: string, signal?: AbortSignal): Promise<string | null> {
303
+ async label(cwd: string, options?: JjCommandOptions): Promise<string | null> {
298
304
  const raw = await runOptionalText(
299
305
  cwd,
300
306
  [
@@ -306,7 +312,7 @@ export const workingCopy = {
306
312
  "-T",
307
313
  WORKING_COPY_LABEL_TEMPLATE,
308
314
  ],
309
- { signal },
315
+ options,
310
316
  );
311
317
  return raw === null ? null : parseWorkingCopyLabel(raw);
312
318
  },
@@ -325,8 +331,8 @@ export const status = {
325
331
  * Count changes in `@` relative to its parent using the Git status shape.
326
332
  * Jujutsu has no index, so `staged` is always zero.
327
333
  */
328
- async summary(cwd: string, signal?: AbortSignal): Promise<git.GitStatusSummary | null> {
329
- const raw = await runOptionalText(cwd, ["diff", "-r", "@", "--summary", "--ignore-working-copy"], { signal });
334
+ async summary(cwd: string, options?: JjCommandOptions): Promise<git.GitStatusSummary | null> {
335
+ const raw = await runOptionalText(cwd, ["diff", "-r", "@", "--summary", "--ignore-working-copy"], options);
330
336
  return raw === null ? null : parseStatusSummary(raw);
331
337
  },
332
338
 
@@ -114,7 +114,28 @@ function getDefaultModelCandidates(): CodexModelCandidate[] {
114
114
  return fallbackModel ? [{ modelId: fallbackModel.id, catalogModel: fallbackModel }] : [{ modelId: FALLBACK_MODEL }];
115
115
  }
116
116
 
117
+ /**
118
+ * Raised when Codex produced an answer without invoking the hosted `web_search`
119
+ * tool. GPT-5.6 Responses-Lite models receive `tool_choice: "auto"` (the forced
120
+ * hosted choice is invalid under the lite shape — see #5771 / #5772), so the
121
+ * model may skip searching and return a plain completion. A search command must
122
+ * not present that as a successful, search-backed result (#6988); this advances
123
+ * the candidate chain to a model that will search, or surfaces a clear failure
124
+ * when the model was explicitly configured.
125
+ */
126
+ class CodexNoWebSearchError extends SearchProviderError {
127
+ constructor() {
128
+ super(
129
+ "codex",
130
+ "Codex returned a completion without running web search (no web_search_call event); refusing to treat a non-search answer as a search result",
131
+ 502,
132
+ );
133
+ this.name = "CodexNoWebSearchError";
134
+ }
135
+ }
136
+
117
137
  function shouldRetryWithNextDefaultModel(error: unknown): boolean {
138
+ if (error instanceof CodexNoWebSearchError) return true;
118
139
  if (!(error instanceof SearchProviderError)) return false;
119
140
  if (error.provider !== "codex" || error.status !== 400) return false;
120
141
  return /model is not supported|requested model is not supported|not supported when using codex with a chatgpt account/i.test(
@@ -472,11 +493,19 @@ async function callCodexSearch(
472
493
  let model = requestedModel;
473
494
  let requestId = "";
474
495
  let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
496
+ // Evidence that the hosted web_search tool actually ran. Lite models get
497
+ // `tool_choice: "auto"` and may answer without searching (#6988); a search
498
+ // command must reject that rather than return a non-search completion.
499
+ let webSearchInvoked = false;
475
500
 
476
501
  for await (const rawEvent of readSseJson<Record<string, unknown>>(response.body, options.signal)) {
477
502
  const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
478
503
  if (!eventType) continue;
479
504
 
505
+ if (eventType.startsWith("response.web_search_call")) {
506
+ webSearchInvoked = true;
507
+ }
508
+
480
509
  if (eventType === "response.output_text.delta") {
481
510
  const delta = typeof rawEvent.delta === "string" ? rawEvent.delta : "";
482
511
  if (delta) {
@@ -485,6 +514,7 @@ async function callCodexSearch(
485
514
  } else if (eventType === "response.output_item.done") {
486
515
  const item = rawEvent.item as CodexResponseItem | undefined;
487
516
  if (!item) continue;
517
+ if (item.type === "web_search_call") webSearchInvoked = true;
488
518
 
489
519
  // Handle text message content and extract sources from annotations
490
520
  if (item.type === "message" && item.content) {
@@ -538,6 +568,10 @@ async function callCodexSearch(
538
568
  }
539
569
  }
540
570
 
571
+ if (!webSearchInvoked) {
572
+ throw new CodexNoWebSearchError();
573
+ }
574
+
541
575
  const finalAnswer = answerParts.join("\n\n").trim();
542
576
  const streamedAnswer = streamedAnswerParts.join("").trim();
543
577
  // Throw to advance the chain whenever Codex emitted nothing but image
@@ -32,7 +32,7 @@ export const SEARCH_PROVIDER_OPTIONS = [
32
32
  },
33
33
  { value: "xai", label: "xAI", description: "Grok web search via xAI Responses API (requires XAI_API_KEY)" },
34
34
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
35
- { value: "exa", label: "Exa", description: "Uses Exa API when EXA_API_KEY is set; falls back to Exa MCP" },
35
+ { value: "exa", label: "Exa", description: "API via /login exa or EXA_API_KEY; explicit keyless fallback via MCP" },
36
36
  { value: "tinyfish", label: "TinyFish", description: "Requires TINYFISH_API_KEY" },
37
37
  { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
38
38
  { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },