@gajae-code/coding-agent 0.15.5 → 0.16.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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
package/src/tools/gh.ts CHANGED
@@ -2806,6 +2806,14 @@ export interface PrDiffFile {
2806
2806
  changeType: "modified" | "added" | "deleted" | "renamed" | "binary";
2807
2807
  /** Pre-image path for renames/deletes; same as `path` otherwise. */
2808
2808
  oldPath?: string;
2809
+ /**
2810
+ * Set when `path` kept its C-style escaped source form because the quoted
2811
+ * token held invalid UTF-8 or a malformed escape; distinguishes it from a
2812
+ * valid path whose decoded text looks identical.
2813
+ */
2814
+ pathEscaped?: boolean;
2815
+ /** Same as {@link PrDiffFile.pathEscaped}, for `oldPath`. */
2816
+ oldPathEscaped?: boolean;
2809
2817
  /** Byte offset of the section's `diff --git` line in the unified diff. */
2810
2818
  startOffset: number;
2811
2819
  /** Byte offset of the next section (or end-of-text). */
@@ -2861,6 +2869,8 @@ export function parsePrUnifiedDiff(text: string): PrDiffPayload {
2861
2869
  interface ParsedDiffHeaderToken {
2862
2870
  value: string;
2863
2871
  nextIndex: number;
2872
+ /** Set when the token kept its C-style escaped source form. */
2873
+ escaped?: boolean;
2864
2874
  }
2865
2875
 
2866
2876
  function skipDiffHeaderSpaces(text: string, index: number): number {
@@ -2869,63 +2879,87 @@ function skipDiffHeaderSpaces(text: string, index: number): number {
2869
2879
  return i;
2870
2880
  }
2871
2881
 
2872
- function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffHeaderToken {
2873
- const next = text.charAt(slashIndex + 1);
2874
- if (next === "") return { value: "\\", nextIndex: slashIndex + 1 };
2882
+ interface ParsedDiffQuotedEscape {
2883
+ byte: number;
2884
+ nextIndex: number;
2885
+ }
2875
2886
 
2876
- if (next >= "0" && next <= "7") {
2877
- let end = slashIndex + 1;
2878
- while (end < text.length && end < slashIndex + 4) {
2879
- const digit = text.charAt(end);
2880
- if (digit < "0" || digit > "7") break;
2881
- end += 1;
2882
- }
2883
- return {
2884
- value: String.fromCharCode(Number.parseInt(text.slice(slashIndex + 1, end), 8)),
2885
- nextIndex: end,
2886
- };
2887
- }
2887
+ const DIFF_PATH_ENCODER = new TextEncoder();
2888
+ const DIFF_PATH_DECODER = new TextDecoder("utf-8", { fatal: true });
2888
2889
 
2889
- switch (next) {
2890
- case "a":
2891
- return { value: "\x07", nextIndex: slashIndex + 2 };
2892
- case "b":
2893
- return { value: "\b", nextIndex: slashIndex + 2 };
2894
- case "f":
2895
- return { value: "\f", nextIndex: slashIndex + 2 };
2896
- case "n":
2897
- return { value: "\n", nextIndex: slashIndex + 2 };
2898
- case "r":
2899
- return { value: "\r", nextIndex: slashIndex + 2 };
2900
- case "t":
2901
- return { value: "\t", nextIndex: slashIndex + 2 };
2902
- case "v":
2903
- return { value: "\v", nextIndex: slashIndex + 2 };
2904
- case "\\":
2905
- case '"':
2906
- return { value: next, nextIndex: slashIndex + 2 };
2907
- default:
2908
- return { value: next, nextIndex: slashIndex + 2 };
2890
+ const DIFF_NAMED_ESCAPE_BYTES: Record<string, number> = {
2891
+ a: 0x07,
2892
+ b: 0x08,
2893
+ f: 0x0c,
2894
+ n: 0x0a,
2895
+ r: 0x0d,
2896
+ t: 0x09,
2897
+ v: 0x0b,
2898
+ "\\": 0x5c,
2899
+ '"': 0x22,
2900
+ };
2901
+
2902
+ /**
2903
+ * Git quotes paths C-style: its named escapes plus exactly three octal digits
2904
+ * per byte (leading digit 0-3), so a multi-byte UTF-8 character arrives as
2905
+ * consecutive octal escapes. Anything else is malformed for this grammar.
2906
+ */
2907
+ function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffQuotedEscape | undefined {
2908
+ const first = text.charAt(slashIndex + 1);
2909
+ if (first >= "0" && first <= "3") {
2910
+ const second = text.charAt(slashIndex + 2);
2911
+ const third = text.charAt(slashIndex + 3);
2912
+ if (second < "0" || second > "7" || third < "0" || third > "7") return undefined;
2913
+ return { byte: Number.parseInt(text.slice(slashIndex + 1, slashIndex + 4), 8), nextIndex: slashIndex + 4 };
2909
2914
  }
2915
+ const named = DIFF_NAMED_ESCAPE_BYTES[first];
2916
+ if (named === undefined) return undefined;
2917
+ return { byte: named, nextIndex: slashIndex + 2 };
2910
2918
  }
2911
2919
 
2920
+ /**
2921
+ * Escaped bytes are decoded as strict UTF-8. A token holding invalid UTF-8 or
2922
+ * a malformed escape keeps its escaped source form, so distinct non-UTF-8
2923
+ * byte sequences never collapse into one replacement-character path.
2924
+ */
2912
2925
  function parseDiffQuotedToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
2913
2926
  if (text.charAt(startIndex) !== '"') return undefined;
2914
- let value = "";
2915
- for (let i = startIndex + 1; i < text.length; i += 1) {
2916
- const ch = text.charAt(i);
2917
- if (ch === '"') return { value, nextIndex: i + 1 };
2927
+ const bytes: number[] = [];
2928
+ let malformed = false;
2929
+ for (let i = startIndex + 1; i < text.length; ) {
2930
+ const codePoint = text.codePointAt(i) as number;
2931
+ const ch = String.fromCodePoint(codePoint);
2932
+ if (ch === '"') {
2933
+ if (!malformed) {
2934
+ try {
2935
+ return { value: DIFF_PATH_DECODER.decode(Uint8Array.from(bytes)), nextIndex: i + 1 };
2936
+ } catch {}
2937
+ }
2938
+ return { value: text.slice(startIndex + 1, i), nextIndex: i + 1, escaped: true };
2939
+ }
2918
2940
  if (ch !== "\\") {
2919
- value += ch;
2941
+ bytes.push(...DIFF_PATH_ENCODER.encode(ch));
2942
+ i += ch.length;
2920
2943
  continue;
2921
2944
  }
2922
2945
  const escaped = parseDiffQuotedEscape(text, i);
2923
- value += escaped.value;
2924
- i = escaped.nextIndex - 1;
2946
+ if (escaped === undefined) {
2947
+ malformed = true;
2948
+ i += 2;
2949
+ continue;
2950
+ }
2951
+ bytes.push(escaped.byte);
2952
+ i = escaped.nextIndex;
2925
2953
  }
2926
2954
  return undefined;
2927
2955
  }
2928
2956
 
2957
+ function parseDiffPathLine(line: string, prefix: string): { value: string; escaped?: boolean } {
2958
+ const rest = line.slice(prefix.length);
2959
+ const token = parseDiffQuotedToken(rest, 0);
2960
+ return token ? { value: token.value, escaped: token.escaped } : { value: rest };
2961
+ }
2962
+
2929
2963
  function parseDiffHeaderToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
2930
2964
  const start = skipDiffHeaderSpaces(text, startIndex);
2931
2965
  if (start >= text.length) return undefined;
@@ -2940,7 +2974,14 @@ function stripPrDiffPathPrefix(value: string, prefix: "a/" | "b/"): string | und
2940
2974
  return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
2941
2975
  }
2942
2976
 
2943
- function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: string } {
2977
+ interface PrDiffHeaderPaths {
2978
+ oldPath?: string;
2979
+ newPath?: string;
2980
+ oldPathEscaped?: boolean;
2981
+ newPathEscaped?: boolean;
2982
+ }
2983
+
2984
+ function parsePrDiffHeaderPaths(header: string): PrDiffHeaderPaths {
2944
2985
  const trail = header.slice("diff --git ".length);
2945
2986
  if (trail.startsWith('"')) {
2946
2987
  const oldToken = parseDiffQuotedToken(trail, 0);
@@ -2950,6 +2991,8 @@ function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: s
2950
2991
  return {
2951
2992
  oldPath: stripPrDiffPathPrefix(oldToken.value, "a/"),
2952
2993
  newPath: stripPrDiffPathPrefix(newToken.value, "b/"),
2994
+ oldPathEscaped: oldToken.escaped,
2995
+ newPathEscaped: newToken.escaped,
2953
2996
  };
2954
2997
  }
2955
2998
 
@@ -2980,6 +3023,8 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
2980
3023
  const headerPaths = parsePrDiffHeaderPaths(header);
2981
3024
  let oldPath = headerPaths.oldPath;
2982
3025
  let newPath = headerPaths.newPath;
3026
+ let oldPathEscaped = headerPaths.oldPathEscaped;
3027
+ let newPathEscaped = headerPaths.newPathEscaped;
2983
3028
 
2984
3029
  let changeType: PrDiffFile["changeType"] = "modified";
2985
3030
  let isBinary = false;
@@ -2999,11 +3044,11 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
2999
3044
  }
3000
3045
  if (line.startsWith("rename from ")) {
3001
3046
  changeType = "renamed";
3002
- oldPath = line.slice("rename from ".length);
3047
+ ({ value: oldPath, escaped: oldPathEscaped } = parseDiffPathLine(line, "rename from "));
3003
3048
  continue;
3004
3049
  }
3005
3050
  if (line.startsWith("rename to ")) {
3006
- newPath = line.slice("rename to ".length);
3051
+ ({ value: newPath, escaped: newPathEscaped } = parseDiffPathLine(line, "rename to "));
3007
3052
  continue;
3008
3053
  }
3009
3054
  if (line.startsWith("Binary files ") && line.endsWith(" differ")) {
@@ -3028,8 +3073,15 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
3028
3073
  deletions = 0;
3029
3074
  }
3030
3075
 
3031
- const displayPath =
3032
- changeType === "deleted" ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
3076
+ const preferOld = changeType === "deleted";
3077
+ const displayPath = preferOld ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
3078
+ const displayEscaped = preferOld
3079
+ ? oldPath !== undefined
3080
+ ? oldPathEscaped
3081
+ : newPathEscaped
3082
+ : newPath !== undefined
3083
+ ? newPathEscaped
3084
+ : oldPathEscaped;
3033
3085
  const file: PrDiffFile = {
3034
3086
  path: displayPath,
3035
3087
  additions,
@@ -3038,8 +3090,12 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
3038
3090
  startOffset,
3039
3091
  endOffset,
3040
3092
  };
3041
- if (oldPath && oldPath !== displayPath) {
3093
+ if (displayEscaped) {
3094
+ file.pathEscaped = true;
3095
+ }
3096
+ if (oldPath && (oldPath !== displayPath || oldPathEscaped !== displayEscaped)) {
3042
3097
  file.oldPath = oldPath;
3098
+ if (oldPathEscaped) file.oldPathEscaped = true;
3043
3099
  }
3044
3100
  return file;
3045
3101
  }
@@ -103,7 +103,7 @@ export function openDb(): Database | null {
103
103
  // than running an in-place ALTER dance.
104
104
  const userVersion = (db.prepare("PRAGMA user_version").get() as { user_version?: number } | undefined)
105
105
  ?.user_version;
106
- if (userVersion !== undefined && userVersion < 3) {
106
+ if (userVersion !== undefined && userVersion < 4) {
107
107
  db.run("DROP TABLE IF EXISTS github_view_cache");
108
108
  }
109
109
  db.run(`
@@ -120,7 +120,7 @@ export function openDb(): Database | null {
120
120
  PRIMARY KEY (auth_key, repo, kind, number, include_comments)
121
121
  );
122
122
  CREATE INDEX IF NOT EXISTS idx_github_view_cache_fetched ON github_view_cache(fetched_at);
123
- PRAGMA user_version = 3;
123
+ PRAGMA user_version = 4;
124
124
  `);
125
125
  protectDbFiles(dbPath);
126
126
  cachedDb = db;
@@ -17,6 +17,7 @@ import type {
17
17
  } from "../session/agent-session";
18
18
  import type { ArtifactManager } from "../session/artifacts";
19
19
  import type { ClientBridge } from "../session/client-bridge";
20
+ import type { FoldAdapter } from "../session/fold-coordinator";
20
21
  import type { CustomMessage } from "../session/messages";
21
22
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
22
23
  import type { SkillActiveEntry } from "../skill-state/active-state";
@@ -235,12 +236,13 @@ export interface ToolSession {
235
236
  assertEvalExecutionAllowed?: () => void;
236
237
  /** Track tool-owned eval work so session disposal can await/abort it like direct session eval runs. */
237
238
  trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
238
- /** Register a safe request handler that asks a managed foreground bash call to fold into a background job. */
239
- registerForegroundBashBackgroundRequestHandler?: (handler: () => void) => () => void;
240
- /** Whether a managed foreground bash call is currently foldable into a background job. */
239
+ /** Register a foldable foreground wait so the fold chord can move it into a background job. */
240
+ registerForegroundFoldParticipant?: (adapter: FoldAdapter) => () => void;
241
+ /** Whether a foreground wait is currently foldable into a background job. */
241
242
  hasForegroundBashBackgroundRequestHandler?: () => boolean;
242
- /** Request that the active managed foreground bash call fold into a background job, if supported. */
243
- requestForegroundBashBackground?: () => boolean;
243
+ /** Request that the active foreground wait fold into a background job, if supported. */
244
+ requestForegroundBashBackground?: () => Promise<boolean>;
245
+
244
246
  /** Get the session-owned or inherited async job manager. */
245
247
  getAsyncJobManager?: () => AsyncJobManager | undefined;
246
248
  /** Resolves when the session queues user steering (or `signal` aborts) without consuming it; wait-style tools use this to end their observation early. */
@@ -269,6 +271,10 @@ export interface ToolSession {
269
271
  bashAllowedPrefixes?: string[];
270
272
  /** Restriction policy for sessions that deliberately expose a narrow bash surface. */
271
273
  bashRestrictionProfile?: BashRestrictionProfile;
274
+ /** Process-local master capability injected into direct Bash children only. */
275
+ getMasterBashCapability?: () => string | undefined;
276
+ /** Stable master lineage identity used across session switches and branches. */
277
+ getMasterOwnerSessionId?: () => string | undefined;
272
278
  /** Optional per-session allowlist for tools exposed through search_tool_bm25. */
273
279
  discoverableToolAllowedNames?: readonly string[];
274
280
  /** Throw instead of warn when toolNames contains an unknown name. */
@@ -2319,7 +2319,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2319
2319
  "job": {
2320
2320
  "name": "job",
2321
2321
  "label": "Job",
2322
- "description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, supported managed foreground bash can be folded into a background job by pressing `Ctrl+B` twice while it is running. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
2322
+ "description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
2323
2323
  "parameters": {
2324
2324
  "type": "object",
2325
2325
  "properties": {
@@ -208,7 +208,15 @@ function chainCacheKey(
208
208
  preferred ?? null,
209
209
  fallbacks,
210
210
  ctx
211
- ? [ctx.provider, ctx.modelId, ctx.wireModelId ?? null, ctx.api, ctx.baseUrl ?? null, ctx.webSearch ?? null]
211
+ ? [
212
+ ctx.provider,
213
+ ctx.modelId,
214
+ ctx.wireModelId ?? null,
215
+ ctx.api,
216
+ ctx.baseUrl ?? null,
217
+ ctx.webSearch ?? null,
218
+ ctx.ownerAuthOverride ?? false,
219
+ ]
212
220
  : null,
213
221
  ]);
214
222
  }
@@ -278,7 +286,17 @@ function isAnthropicWire(api: string): boolean {
278
286
  }
279
287
 
280
288
  function isGoogleWire(api: string): boolean {
281
- return api === "google-generative-ai" || api === "google-vertex" || api === "google-gemini-cli";
289
+ return (
290
+ api === "google-generative-ai" ||
291
+ api === "google-generative-language" ||
292
+ api === "gemini-wire" ||
293
+ api === "google-vertex" ||
294
+ api === "google-gemini-cli"
295
+ );
296
+ }
297
+
298
+ function isGenerativeLanguageWire(api: string): boolean {
299
+ return api === "google-generative-ai" || api === "google-generative-language" || api === "gemini-wire";
282
300
  }
283
301
 
284
302
  function isOpenAICompatWire(api: string): boolean {
@@ -305,6 +323,52 @@ function looksXaiFamilyModelId(ctx: ActiveSearchModelContext): boolean {
305
323
  return looksXaiModelId(ctx.wireModelId) || looksXaiModelId(ctx.modelId);
306
324
  }
307
325
 
326
+ const OFFICIAL_GEMINI_HOSTS = new Set([
327
+ "cloudcode-pa.googleapis.com",
328
+ "daily-cloudcode-pa.googleapis.com",
329
+ "daily-cloudcode-pa.sandbox.googleapis.com",
330
+ "generativelanguage.googleapis.com",
331
+ ]);
332
+
333
+ function normalizedHost(baseUrl: string): string | undefined {
334
+ try {
335
+ return new URL(baseUrl).hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
336
+ } catch {
337
+ return undefined;
338
+ }
339
+ }
340
+
341
+ function isOfficialGeminiBaseUrl(baseUrl: string | undefined): boolean {
342
+ if (!baseUrl?.trim()) return true;
343
+ const host = normalizedHost(baseUrl);
344
+ return host !== undefined && OFFICIAL_GEMINI_HOSTS.has(host);
345
+ }
346
+
347
+ function isOfficialXaiBaseUrl(baseUrl: string | undefined): boolean {
348
+ if (!baseUrl?.trim()) return true;
349
+ return normalizedHost(baseUrl) === "api.x.ai";
350
+ }
351
+
352
+ const CANONICAL_GEMINI_PROVIDERS = new Set(["google", "gemini", "google-gemini-cli", "google-antigravity"]);
353
+
354
+ function hasCustomGeminiSearchContext(ctx: ActiveSearchModelContext): boolean {
355
+ if (!isGoogleWire(ctx.api)) return false;
356
+ if (ctx.ownerAuthOverride) return true;
357
+ if (!isOfficialGeminiBaseUrl(ctx.baseUrl)) return true;
358
+ const provider = ctx.provider.toLowerCase();
359
+ return Boolean(ctx.resolveCredentials && !CANONICAL_GEMINI_PROVIDERS.has(provider));
360
+ }
361
+
362
+ function hasCustomXaiSearchContext(ctx: ActiveSearchModelContext): boolean {
363
+ if (!isOpenAICompatWire(ctx.api)) return false;
364
+ if (ctx.ownerAuthOverride) return true;
365
+ const provider = ctx.provider.toLowerCase();
366
+ if (provider === "xai" && !isOfficialXaiBaseUrl(ctx.baseUrl)) return true;
367
+ if (!looksXaiFamilyModelId(ctx)) return false;
368
+ if (!isOfficialXaiBaseUrl(ctx.baseUrl)) return true;
369
+ return provider !== "xai";
370
+ }
371
+
308
372
  export function isLocalBaseUrl(baseUrl: string | undefined): boolean {
309
373
  if (!baseUrl) return false;
310
374
  let url: URL;
@@ -369,12 +433,29 @@ export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | und
369
433
  if (!ctx || ctx.webSearch === "off") return undefined;
370
434
  const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
371
435
  if (modelId.startsWith("claude-") && isAnthropicWire(ctx.api)) return "anthropic";
372
- if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api)) return "gemini";
373
- if (looksXaiFamilyModelId(ctx) && isOpenAICompatWire(ctx.api)) return "xai";
436
+ if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api) && !hasCustomGeminiSearchContext(ctx)) return "gemini";
437
+ // A custom proxy may expose a Grok model through an OpenAI wire while the
438
+ // process also has canonical xAI credentials. Do not infer the canonical
439
+ // xAI provider from the model family: that would silently send the search to
440
+ // api.x.ai with another registry's credentials. Custom active contexts are
441
+ // dispatched below by activeContextNativeId, where the provider can consume
442
+ // the active owner-bound resolver instead.
443
+ if (
444
+ looksXaiFamilyModelId(ctx) &&
445
+ isOpenAICompatWire(ctx.api) &&
446
+ ctx.provider.toLowerCase() === "xai" &&
447
+ !hasCustomXaiSearchContext(ctx)
448
+ )
449
+ return "xai";
374
450
  // `codex` hits the ChatGPT backend with local Codex OAuth, so only infer it
375
451
  // for genuine OpenAI endpoints. Custom/proxy OpenAI-compatible models fall
376
- // through to `activeContextNativeId` → `openai-compatible` (their own creds).
377
- if (looksOpenAIFamilyModelId(ctx) && isOpenAICompatWire(ctx.api) && isOpenAIOfficialBaseUrl(ctx.baseUrl)) {
452
+ // through to `activeContextNativeId` and reuse their own credentials.
453
+ if (
454
+ !ctx.ownerAuthOverride &&
455
+ looksOpenAIFamilyModelId(ctx) &&
456
+ isOpenAICompatWire(ctx.api) &&
457
+ isOpenAIOfficialBaseUrl(ctx.baseUrl)
458
+ ) {
378
459
  return "codex";
379
460
  }
380
461
  return undefined;
@@ -382,7 +463,10 @@ export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | und
382
463
 
383
464
  function canUseDirectProviderMapping(ctx: ActiveSearchModelContext, id: SearchProviderId): boolean {
384
465
  if (ctx.webSearch === "off") return false;
466
+ if (id === "gemini" && hasCustomGeminiSearchContext(ctx)) return false;
467
+ if (id === "xai" && hasCustomXaiSearchContext(ctx)) return false;
385
468
  if (id !== "codex") return true;
469
+ if (ctx.ownerAuthOverride) return false;
386
470
  // Same constraint as inference: the ChatGPT-backed codex provider is valid
387
471
  // only for official OpenAI endpoints, not custom/proxy base URLs.
388
472
  return isOpenAIOfficialBaseUrl(ctx.baseUrl);
@@ -395,6 +479,10 @@ export async function canUseGenericCredentials(
395
479
  signal?: AbortSignal,
396
480
  ): Promise<boolean> {
397
481
  if (!ctx) return false;
482
+ if (ctx.resolveCredentials) {
483
+ const resolved = await ctx.resolveCredentials({ sessionId, signal });
484
+ return Boolean(resolved.apiKey) || hasActiveCredentialHeader(resolved.headers);
485
+ }
398
486
  const key = await authStorage.getApiKey(ctx.provider, sessionId, {
399
487
  baseUrl: ctx.baseUrl,
400
488
  modelId: ctx.modelId,
@@ -403,6 +491,13 @@ export async function canUseGenericCredentials(
403
491
  return Boolean(key);
404
492
  }
405
493
 
494
+ function hasActiveCredentialHeader(headers: Record<string, string> | undefined): boolean {
495
+ return Object.keys(headers ?? {}).some(key => {
496
+ const normalized = key.toLowerCase();
497
+ return normalized === "authorization" || normalized === "x-api-key" || normalized === "x-goog-api-key";
498
+ });
499
+ }
500
+
406
501
  /**
407
502
  * Native web-search provider to attempt by reusing the ACTIVE model's own
408
503
  * credentials + baseUrl, dispatched by the model's wire protocol.
@@ -424,13 +519,20 @@ export function activeContextNativeId(ctx: ActiveSearchModelContext | undefined)
424
519
  if (!ctx || ctx.webSearch === "off") return undefined;
425
520
  const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
426
521
  // Dispatch must match exactly what each provider can service by reusing the
427
- // active credential: the OpenAI-compatible adapter only speaks the two plain
428
- // OpenAI wires (not azure), and the Gemini active path only speaks the public
429
- // Generative Language wire (not vertex/cloud-code). Returning an id the
430
- // provider would reject just wastes a guaranteed-fail attempt before DuckDuckGo.
522
+ // active credential: xAI-family models use the xAI search tools, the
523
+ // OpenAI-compatible adapter only speaks the two plain OpenAI wires (not
524
+ // azure), and the Gemini active path only speaks the public Generative
525
+ // Language wire (not vertex/cloud-code). Returning an id the provider would
526
+ // reject just wastes a guaranteed-fail attempt before DuckDuckGo.
431
527
  if (isAnthropicWire(ctx.api) && modelId.startsWith("claude-")) return "anthropic";
528
+ if (isGenerativeLanguageWire(ctx.api) && modelId.startsWith("gemini-")) return "gemini";
529
+ if (
530
+ isOpenAICompatWire(ctx.api) &&
531
+ hasCustomXaiSearchContext(ctx) &&
532
+ (looksXaiFamilyModelId(ctx) || ctx.provider.toLowerCase() === "xai")
533
+ )
534
+ return "xai";
432
535
  if (ctx.api === "openai-responses" || ctx.api === "openai-completions") return "openai-compatible";
433
- if (ctx.api === "google-generative-ai" && modelId.startsWith("gemini-")) return "gemini";
434
536
  return undefined;
435
537
  }
436
538
 
@@ -445,6 +547,13 @@ export async function resolveProviderChain(options: ResolveProviderChainOptions)
445
547
  settings,
446
548
  } = options;
447
549
 
550
+ // Resolve the active model before consulting the chain cache. The registry
551
+ // resolver refreshes rotating apiKeyEnv credentials and updates generated
552
+ // headers, which also advances AuthStorage's generation when credentials
553
+ // rotate or disappear.
554
+ if (preferredProvider === "auto" && activeModelContext?.resolveCredentials) {
555
+ await activeModelContext.resolveCredentials({ sessionId, signal });
556
+ }
448
557
  const cacheKey = chainCacheKey(preferredProvider, fallbackProviders, activeModelContext, authStorage, settings);
449
558
  const perStorage = chainCache.get(authStorage);
450
559
  const now = Date.now();
@@ -18,6 +18,7 @@ import {
18
18
  } from "@gajae-code/ai/utils/anthropic-auth";
19
19
  import { $credentialEnv, $env } from "@gajae-code/utils";
20
20
  import type {
21
+ ActiveSearchModelCredentials,
21
22
  AnthropicApiResponse,
22
23
  AnthropicCitation,
23
24
  SearchCitation,
@@ -73,6 +74,28 @@ function buildSystemBlocks(
73
74
  });
74
75
  }
75
76
 
77
+ function mergeHeadersPreservingExplicit(
78
+ baseHeaders: Record<string, string>,
79
+ explicitHeaders: Record<string, string> | undefined,
80
+ ): Record<string, string> {
81
+ const merged = { ...baseHeaders };
82
+ for (const [key, value] of Object.entries(explicitHeaders ?? {})) {
83
+ for (const existingKey of Object.keys(merged)) {
84
+ if (existingKey.toLowerCase() === key.toLowerCase()) delete merged[existingKey];
85
+ }
86
+ merged[key] = value;
87
+ }
88
+ return merged;
89
+ }
90
+
91
+ function explicitCredentialHeader(headers: Record<string, string> | undefined): string | undefined {
92
+ for (const [key, value] of Object.entries(headers ?? {})) {
93
+ const normalized = key.toLowerCase();
94
+ if (normalized === "authorization" || normalized === "x-api-key") return value;
95
+ }
96
+ return undefined;
97
+ }
98
+
76
99
  /**
77
100
  * Calls the Anthropic API with web search tool enabled.
78
101
  * @param auth - Authentication configuration (API key or OAuth)
@@ -93,7 +116,7 @@ async function callSearch(
93
116
  extraHeaders?: Record<string, string>,
94
117
  ): Promise<AnthropicApiResponse> {
95
118
  const url = buildAnthropicUrl(auth);
96
- const headers = { ...(extraHeaders ?? {}), ...buildAnthropicSearchHeaders(auth) };
119
+ const headers = mergeHeadersPreservingExplicit(buildAnthropicSearchHeaders(auth), extraHeaders);
97
120
 
98
121
  const systemBlocks = buildSystemBlocks(auth, model, systemPrompt);
99
122
 
@@ -284,29 +307,78 @@ export async function searchAnthropic(
284
307
  // proxy), prefer its wire model id and carry its request headers through.
285
308
  let modelOverride: string | undefined;
286
309
  let extraHeaders: Record<string, string> | undefined;
287
-
288
- if (searchApiKey) {
289
- auth = buildAnthropicAuthConfig(searchApiKey, searchBaseUrl);
290
- } else if ("authStorage" in params) {
291
- const apiKey = await params.authStorage.getApiKey("anthropic", params.sessionId, {
310
+ const activeContext = "authStorage" in params ? params.activeModelContext : undefined;
311
+ const activeSessionId = "authStorage" in params ? params.sessionId : undefined;
312
+ const activeCustomContext =
313
+ activeContext?.api === "anthropic-messages" &&
314
+ (activeContext.ownerAuthOverride || activeContext.provider.toLowerCase() !== "anthropic");
315
+
316
+ if (activeCustomContext && activeContext?.resolveCredentials) {
317
+ const activeCredentials = await activeContext.resolveCredentials({
318
+ sessionId: activeSessionId,
292
319
  signal: params.signal,
293
320
  });
294
- if (apiKey) auth = buildAnthropicAuthConfig(apiKey);
295
-
296
- // Fall back to the active model's own credentials + baseUrl when no
297
- // canonical Anthropic key exists but the active model speaks the
298
- // Anthropic wire (e.g. Claude served through a proxy).
299
- const ctx = params.activeModelContext;
300
- if (!auth && ctx && ctx.api === "anthropic-messages") {
301
- const ctxKey = await params.authStorage.getApiKey(ctx.provider, params.sessionId, {
302
- baseUrl: ctx.baseUrl,
303
- modelId: ctx.modelId,
321
+ const ctxKey =
322
+ activeCredentials.apiKey ?? explicitCredentialHeader(activeCredentials.headers ?? activeContext.headers);
323
+ if (ctxKey) {
324
+ auth = buildAnthropicAuthConfig(ctxKey, activeContext.baseUrl);
325
+ modelOverride = activeContext.wireModelId ?? activeContext.modelId;
326
+ extraHeaders = activeCredentials.headers ?? activeContext.headers;
327
+ }
328
+ } else if (searchApiKey) {
329
+ auth = buildAnthropicAuthConfig(searchApiKey, searchBaseUrl);
330
+ } else if ("authStorage" in params) {
331
+ const ctx = activeContext;
332
+ const resolveActiveCredentials = ctx?.resolveCredentials;
333
+ const activeAnthropicWire = ctx?.api === "anthropic-messages";
334
+ const activeModelOwnsProvider =
335
+ ctx !== undefined &&
336
+ resolveActiveCredentials !== undefined &&
337
+ activeAnthropicWire &&
338
+ ctx.provider.toLowerCase() === "anthropic";
339
+ const activeCustomModel =
340
+ ctx !== undefined &&
341
+ resolveActiveCredentials !== undefined &&
342
+ activeAnthropicWire &&
343
+ ctx.provider.toLowerCase() !== "anthropic";
344
+ if (activeModelOwnsProvider || activeCustomModel) {
345
+ const activeCredentials: ActiveSearchModelCredentials = await resolveActiveCredentials({
346
+ sessionId: params.sessionId,
304
347
  signal: params.signal,
305
348
  });
349
+ const ctxKey = activeCredentials.apiKey ?? explicitCredentialHeader(activeCredentials.headers ?? ctx.headers);
306
350
  if (ctxKey) {
307
351
  auth = buildAnthropicAuthConfig(ctxKey, ctx.baseUrl);
308
352
  modelOverride = ctx.wireModelId ?? ctx.modelId;
309
- extraHeaders = ctx.headers;
353
+ extraHeaders = activeCredentials.headers ?? ctx.headers;
354
+ }
355
+ } else {
356
+ const apiKey = await params.authStorage.getApiKey("anthropic", params.sessionId, {
357
+ signal: params.signal,
358
+ });
359
+ if (apiKey) auth = buildAnthropicAuthConfig(apiKey);
360
+
361
+ // Fall back to the active model's own credentials + baseUrl when no
362
+ // canonical Anthropic key exists but the active model speaks the
363
+ // Anthropic wire (e.g. Claude served through a proxy).
364
+ if (!auth && ctx && ctx.api === "anthropic-messages") {
365
+ const activeCredentials: ActiveSearchModelCredentials = ctx.resolveCredentials
366
+ ? await ctx.resolveCredentials({ sessionId: params.sessionId, signal: params.signal })
367
+ : {
368
+ apiKey: await params.authStorage.getApiKey(ctx.provider, params.sessionId, {
369
+ baseUrl: ctx.baseUrl,
370
+ modelId: ctx.modelId,
371
+ signal: params.signal,
372
+ }),
373
+ headers: ctx.headers,
374
+ };
375
+ const ctxKey =
376
+ activeCredentials.apiKey ?? explicitCredentialHeader(activeCredentials.headers ?? ctx.headers);
377
+ if (ctxKey) {
378
+ auth = buildAnthropicAuthConfig(ctxKey, ctx.baseUrl);
379
+ modelOverride = ctx.wireModelId ?? ctx.modelId;
380
+ extraHeaders = activeCredentials.headers ?? ctx.headers;
381
+ }
310
382
  }
311
383
  }
312
384
  }