@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830

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 (155) hide show
  1. package/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
  2. package/gui/dist/assets/index-DPl4nBMA.js +112 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +2 -1
  5. package/src/AGENTS.md +2 -1
  6. package/src/adapters/agentrouter.ts +50 -0
  7. package/src/adapters/anthropic.ts +1 -51
  8. package/src/adapters/cursor/call-id.ts +76 -8
  9. package/src/adapters/cursor/checkpoint-store.ts +6 -1
  10. package/src/adapters/cursor/cursor-errors.ts +44 -0
  11. package/src/adapters/cursor/native-exec.ts +13 -0
  12. package/src/adapters/cursor/protobuf-request.ts +651 -29
  13. package/src/adapters/cursor/tool-result-normalize.ts +3 -3
  14. package/src/adapters/cursor/transport-retry.ts +5 -1
  15. package/src/adapters/cursor.ts +15 -1
  16. package/src/adapters/empty-tool-output-annotation.ts +43 -0
  17. package/src/adapters/exec-tool-result-normalize.ts +70 -5
  18. package/src/adapters/google.ts +22 -2
  19. package/src/adapters/kiro.ts +26 -2
  20. package/src/adapters/ollama-native-url.ts +111 -0
  21. package/src/adapters/ollama-native.ts +1131 -0
  22. package/src/adapters/openai-chat.ts +30 -7
  23. package/src/adapters/openai-responses.ts +72 -4
  24. package/src/adapters/registry.ts +7 -0
  25. package/src/adapters/xai-web-search.ts +58 -0
  26. package/src/claude/desktop-3p.ts +21 -1
  27. package/src/claude/desktop-policy.ts +149 -0
  28. package/src/cli/account.ts +16 -2
  29. package/src/cli/claude-desktop.ts +13 -3
  30. package/src/cli/combo.ts +8 -5
  31. package/src/cli/doctor.ts +77 -11
  32. package/src/cli/help.ts +1 -1
  33. package/src/cli/index.ts +16 -0
  34. package/src/cli/models.ts +20 -3
  35. package/src/cli/registry.ts +2 -1
  36. package/src/cli/status.ts +140 -2
  37. package/src/cli/storage.ts +10 -1
  38. package/src/codex/account-runtime-state.ts +39 -5
  39. package/src/codex/account-store.ts +393 -13
  40. package/src/codex/account-usability.ts +11 -4
  41. package/src/codex/app-server-processes.ts +46 -5
  42. package/src/codex/auth-context.ts +160 -32
  43. package/src/codex/catalog/bundled.ts +7 -5
  44. package/src/codex/catalog/metadata.ts +1 -1
  45. package/src/codex/catalog/parsing.ts +57 -1
  46. package/src/codex/catalog/provider-fetch.ts +61 -4
  47. package/src/codex/catalog/sync.ts +4 -3
  48. package/src/codex/convergence.ts +3 -2
  49. package/src/codex/data/upstream-models.json +40 -8
  50. package/src/codex/inject-coordination.ts +111 -14
  51. package/src/codex/integration-record.ts +12 -2
  52. package/src/codex/main-account.ts +225 -1
  53. package/src/codex/model-entitlements.ts +339 -27
  54. package/src/codex/prompt-layers.ts +346 -7
  55. package/src/codex/prompt-text-probe.ts +272 -21
  56. package/src/codex/routing.ts +693 -132
  57. package/src/codex/runtime.ts +12 -0
  58. package/src/codex/subagent-model-fallback.ts +62 -24
  59. package/src/codex/user-identity.ts +33 -25
  60. package/src/combos/index.ts +1 -0
  61. package/src/combos/reset-window.ts +46 -0
  62. package/src/combos/resolve.ts +84 -2
  63. package/src/combos/types.ts +5 -2
  64. package/src/config/atomic-write.ts +104 -22
  65. package/src/config/provider-validation.ts +11 -0
  66. package/src/config.ts +75 -3
  67. package/src/generated/compatibility-version.json +207 -131
  68. package/src/generated/model-metadata.ts +1 -1
  69. package/src/grok/catalog.ts +71 -0
  70. package/src/grok/effort.ts +83 -0
  71. package/src/grok/inject.ts +952 -127
  72. package/src/grok/models.ts +56 -0
  73. package/src/grok/status.ts +21 -8
  74. package/src/grok/sync.ts +10 -18
  75. package/src/images/loop.ts +6 -3
  76. package/src/integrations/native/ownership-preflight.ts +4 -1
  77. package/src/lab/fabric/producer-isolate.ts +36 -3
  78. package/src/lib/destination-policy.ts +93 -7
  79. package/src/lib/redact.ts +6 -1
  80. package/src/lib/shadow-call.ts +38 -3
  81. package/src/lib/test-home-guard.ts +18 -3
  82. package/src/lib/upstream-retry.ts +43 -6
  83. package/src/lib/windows-secret-acl.ts +66 -0
  84. package/src/lib/windows-text.ts +28 -2
  85. package/src/lib/windows-user-principal.ts +35 -23
  86. package/src/oauth/account-quota-rank.ts +107 -0
  87. package/src/oauth/anthropic-routing.ts +125 -30
  88. package/src/oauth/chatgpt.ts +5 -1
  89. package/src/oauth/generic-account-failover.ts +114 -7
  90. package/src/oauth/index.ts +15 -8
  91. package/src/oauth/store.ts +16 -0
  92. package/src/providers/account-quota-disk.ts +79 -0
  93. package/src/providers/command-code-efforts.ts +24 -0
  94. package/src/providers/derive.ts +6 -0
  95. package/src/providers/key-failover.ts +33 -1
  96. package/src/providers/kiro-usage.ts +272 -0
  97. package/src/providers/ollama-show.ts +311 -0
  98. package/src/providers/openai-sidecar.ts +5 -0
  99. package/src/providers/quota-routing-cache.ts +32 -0
  100. package/src/providers/quota-types.ts +36 -0
  101. package/src/providers/quota-wire.ts +102 -0
  102. package/src/providers/quota.ts +208 -147
  103. package/src/providers/registry.ts +68 -8
  104. package/src/providers/slug-codec.ts +12 -4
  105. package/src/providers/vercel-gateway-routing.ts +108 -0
  106. package/src/router.ts +22 -12
  107. package/src/server/auth-cors.ts +26 -0
  108. package/src/server/catalog-download.ts +73 -0
  109. package/src/server/chat-native.ts +12 -2
  110. package/src/server/gui-static.ts +4 -1
  111. package/src/server/index.ts +132 -9
  112. package/src/server/management/agent-settings-routes.ts +38 -5
  113. package/src/server/management/codex-prompt-routes.ts +7 -1
  114. package/src/server/management/combo-routes.ts +10 -1
  115. package/src/server/management/config-routes.ts +9 -1
  116. package/src/server/management/context.ts +5 -0
  117. package/src/server/management/model-routes.ts +16 -6
  118. package/src/server/management/native-integration-routes.ts +12 -17
  119. package/src/server/management/oauth-account-routes.ts +13 -0
  120. package/src/server/management/provider-routes.ts +32 -5
  121. package/src/server/management/routing-profile-routes.ts +15 -0
  122. package/src/server/management/shadow-call-validation.ts +29 -0
  123. package/src/server/management-api.ts +7 -3
  124. package/src/server/request-log.ts +3 -5
  125. package/src/server/responses/agent-task-recovery-cache.ts +8 -0
  126. package/src/server/responses/agent-task-recovery.ts +52 -20
  127. package/src/server/responses/codex-auth-error.ts +26 -0
  128. package/src/server/responses/compact.ts +345 -10
  129. package/src/server/responses/core.ts +736 -108
  130. package/src/server/responses/empty-completion-guard.ts +16 -0
  131. package/src/server/responses/fetch-helpers.ts +42 -0
  132. package/src/server/responses/policy-fallback.ts +11 -6
  133. package/src/server/responses-undeclared-tool-guard.ts +16 -3
  134. package/src/server/startup-health-cache.ts +59 -13
  135. package/src/service-manager-probe.ts +115 -9
  136. package/src/service.ts +139 -40
  137. package/src/storage/cleanup.ts +10 -0
  138. package/src/storage/storage-mutation-coordinator.ts +14 -3
  139. package/src/tray/windows-tray.ps1 +10 -4
  140. package/src/tray/windows.ts +30 -2
  141. package/src/types/config.ts +27 -14
  142. package/src/types/provider.ts +54 -0
  143. package/src/types/tools.ts +13 -3
  144. package/src/types.ts +4 -0
  145. package/src/usage/summary.ts +421 -177
  146. package/src/vision/anthropic-describe.ts +3 -3
  147. package/src/vision/describe.ts +5 -3
  148. package/src/web-search/anthropic-executor.ts +9 -2
  149. package/src/web-search/exa-executor.ts +3 -3
  150. package/src/web-search/executor.ts +8 -3
  151. package/src/web-search/gemini-executor.ts +3 -3
  152. package/src/web-search/loop.ts +11 -3
  153. package/src/web-search/xai-executor.ts +3 -3
  154. package/gui/dist/assets/index-DNdRKXK9.js +0 -112
  155. package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
@@ -13,7 +13,7 @@ import {
13
13
  EMPTY_EXEC_OUTPUT_MESSAGE,
14
14
  EMPTY_EXEC_OUTPUT_REGEX,
15
15
  FAILED_EXEC_OUTPUT_MESSAGE,
16
- FAILED_EXEC_OUTPUT_REGEX,
16
+ isFailedEmptyExecWrapper,
17
17
  isCodexExecBridgeTool,
18
18
  } from "../exec-tool-result-normalize";
19
19
 
@@ -23,7 +23,7 @@ import {
23
23
  * `Script failed`, so restore that arm here rather than widening the shared one.
24
24
  */
25
25
  function isEmptyOrFailedExecWrapper(text: string): boolean {
26
- return EMPTY_EXEC_OUTPUT_REGEX.test(text) || FAILED_EXEC_OUTPUT_REGEX.test(text);
26
+ return EMPTY_EXEC_OUTPUT_REGEX.test(text) || isFailedEmptyExecWrapper(text);
27
27
  }
28
28
 
29
29
  const COMPUTER_USE_TOOL_NAMES = new Set([
@@ -99,7 +99,7 @@ export function normalizeCursorToolResultText(
99
99
  // A `Script failed` wrapper is empty but NOT a success: reporting it as an empty success
100
100
  // would erase the only failure signal. Text classification stays separate from Cursor's
101
101
  // isError policy, which the Computer Use branch above owns.
102
- text: FAILED_EXEC_OUTPUT_REGEX.test(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE,
102
+ text: isFailedEmptyExecWrapper(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE,
103
103
  isError: false,
104
104
  changed: true,
105
105
  };
@@ -2,7 +2,7 @@ import type { CursorRunRequest, CursorServerMessage } from "./types";
2
2
  import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
3
3
  import { abortError, retryBackoffDelayMs, sleepWithAbort } from "../../lib/upstream-retry";
4
4
  import { debugProviderDiagnostic } from "../../lib/debug";
5
- import { safeCursorErrorMessage } from "./cursor-errors";
5
+ import { isCursorRootEnvelopeError, safeCursorErrorMessage } from "./cursor-errors";
6
6
 
7
7
  // Compat: historical name for the shared abortable sleep, kept for external callers.
8
8
  export { sleepWithAbort as abortAwareSleep } from "../../lib/upstream-retry";
@@ -18,6 +18,10 @@ export const CURSOR_RETRY_MAX_MS = 2_000;
18
18
  * never replay a turn the Cursor server might already have accepted.
19
19
  */
20
20
  export function isRetryableCursorError(err: unknown): boolean {
21
+ // Local envelope failures are deterministic: the same request produces the same rejection.
22
+ // Checked before the text heuristics below, which would otherwise have to infer this from
23
+ // wording.
24
+ if (isCursorRootEnvelopeError(err)) return false;
21
25
  const code = typeof err === "object" && err && "code" in err ? String((err as { code?: unknown }).code ?? "") : "";
22
26
  const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
23
27
  const haystack = `${code} ${message}`.toLowerCase();
@@ -3,7 +3,7 @@ import type { AdapterEvent, OcxProviderConfig } from "../types";
3
3
  import type { ProviderAdapter } from "./base";
4
4
  import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
5
5
  import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
6
- import { isCursorBenignCancelError, isCursorInvalidArgumentError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
6
+ import { isCursorBenignCancelError, isCursorInvalidArgumentError, isCursorRootEnvelopeError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
7
7
  import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
8
8
  import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
9
9
  import { mapCursorServerMessage } from "./cursor/message-mapper";
@@ -70,6 +70,14 @@ function safeCursorTransportError(err: unknown, sizeContext?: CursorSizeContext)
70
70
  if (err instanceof CursorMissingCredentialError) {
71
71
  return "Cursor live transport is enabled, but no Cursor access token is configured. Set provider.apiKey or OPENCODEX_CURSOR_TEST_TOKEN.";
72
72
  }
73
+ // A locally raised envelope rejection is already safe, specific, and actionable: it was composed
74
+ // here from our own measurements and contains no upstream text. Passing it through
75
+ // `safeCursorErrorMessage` would collapse it to the bare label "Cursor invalid request" (it
76
+ // matches the "invalid"/"exceeds" keyword branch) and discard the counts that tell the operator
77
+ // which limit was hit and by how much.
78
+ if (isCursorRootEnvelopeError(err)) {
79
+ return err instanceof Error ? `Cursor invalid request: ${err.message}` : "Cursor invalid request";
80
+ }
73
81
  const message = err instanceof Error ? err.message : typeof err === "string" ? err : undefined;
74
82
  if (message) return safeCursorErrorMessage(message, sizeContext);
75
83
  return "Cursor upstream error: transport failed before completion.";
@@ -485,6 +493,12 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
485
493
  ...(isTranslatorBudgetExceededError(err)
486
494
  ? { status: 502, errorType: "upstream_error", code: "translation_buffer_limit" }
487
495
  : {}),
496
+ // A local envelope rejection is a client error with a stable code, and the caller needs
497
+ // that code to distinguish "this conversation cannot be sent" from a transient upstream
498
+ // fault. Without this the class was flattened to a bare message and the code was lost.
499
+ ...(isCursorRootEnvelopeError(err)
500
+ ? { status: 400, errorType: "invalid_request_error", code: "cursor_root_envelope_limit", retryable: false }
501
+ : {}),
488
502
  ...(partialUsage ? { usage: partialUsage } : {}),
489
503
  });
490
504
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared wire text and emptiness contract for present-but-empty tool outputs.
3
+ *
4
+ * Both the OpenAI Chat and Responses adapters use this module so the two wires
5
+ * cannot drift again: only a pure text/refusal part array whose joined content
6
+ * trims empty is "present but empty". Image, file, encrypted-content and any
7
+ * other non-text part is real output and is never replaced by the annotation.
8
+ */
9
+
10
+ /** Wire text used when a present-but-empty tool output must stay visible to the model. */
11
+ export const EMPTY_TOOL_OUTPUT_ANNOTATION =
12
+ "[ocx] empty tool output: the tool ran but produced no stdout or return value; do not treat this as success, failure, or user-provided input.";
13
+
14
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ /** Part types that carry wire text on either adapter (Chat uses `text`, Responses uses `input_text`/`output_text`). */
19
+ const TEXT_PART_TYPES = new Set(["text", "input_text", "output_text"]);
20
+
21
+ /**
22
+ * True when every part is text/refusal and the joined text/refusal content trims
23
+ * empty. An empty array is the array twin of a blank string. Any image, file,
24
+ * encrypted-content or other non-text part makes the array non-empty so the
25
+ * model still receives the real payload.
26
+ */
27
+ export function isWhitespaceOnlyTextPartArray(parts: readonly unknown[]): boolean {
28
+ if (parts.length === 0) return true;
29
+ let joined = "";
30
+ for (const part of parts) {
31
+ if (!isPlainObject(part)) return false;
32
+ if (typeof part.type === "string" && TEXT_PART_TYPES.has(part.type) && typeof part.text === "string") {
33
+ joined += part.text;
34
+ continue;
35
+ }
36
+ if (part.type === "refusal" && typeof part.refusal === "string") {
37
+ joined += part.refusal;
38
+ continue;
39
+ }
40
+ return false;
41
+ }
42
+ return joined.trim() === "";
43
+ }
@@ -17,13 +17,78 @@
17
17
  * `Script failed` is deliberately NOT in this set. A failed cell with no captured output is still
18
18
  * a FAILURE, and the success guidance below ("not a blocked tool", "do not re-run") would erase the
19
19
  * only signal that anything went wrong — reachable through Responses history, where
20
- * `function_call_output` is parsed with `isError: false`. Cursor keeps its own broader regex for
21
- * Computer Use, where a failed wrapper is separately marked `isError`.
20
+ * `function_call_output` is parsed with `isError: false`. Cursor combines this set with
21
+ * `isFailedEmptyExecWrapper` below for Computer Use, where a failed wrapper is separately marked
22
+ * `isError`.
22
23
  */
23
24
  export const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
24
25
 
25
- /** Wrapper for a cell that FAILED without emitting output: empty, but not a success. */
26
- export const FAILED_EXEC_OUTPUT_REGEX = /^Script failed[^\n]*\n*(?:Wall time[^\n]*\n*)?(?:Output:\s*)?(?:<empty>)?\s*$/;
26
+ function skipFailedWrapperBlankSeparators(text: string, start: number): number {
27
+ let index = start;
28
+ while (index < text.length) {
29
+ if (text[index] === "\n") {
30
+ index += 1;
31
+ continue;
32
+ }
33
+ // A CRLF blank line is one separator, not a stray carriage return. The regex this replaced
34
+ // matched only `\n`, so a Windows-produced wrapper never classified and the failure guidance
35
+ // was silently replaced by the empty-SUCCESS message on that platform.
36
+ if (text[index] === "\r" && text[index + 1] === "\n") {
37
+ index += 2;
38
+ continue;
39
+ }
40
+ break;
41
+ }
42
+ return index;
43
+ }
44
+
45
+ function skipFailedWrapperWhitespace(text: string, start: number): number {
46
+ let index = start;
47
+ while (index < text.length && text[index]!.trim() === "") index += 1;
48
+ return index;
49
+ }
50
+
51
+ function skipFailedWrapperLine(text: string, start: number): number {
52
+ const newline = text.indexOf("\n", start);
53
+ return newline === -1 ? text.length : skipFailedWrapperBlankSeparators(text, newline + 1);
54
+ }
55
+
56
+ /**
57
+ * Wrapper for a cell that FAILED without emitting output: empty, but not a success.
58
+ *
59
+ * A single forward scan, replacing a regex whose `\n*` and `\s*` runs sat adjacent over the same
60
+ * span and could be made to backtrack on a long whitespace run followed by one non-matching
61
+ * character. Every loop here advances an index monotonically and the token checks are fixed-length,
62
+ * so the work is bounded by the input length with no path that retries a prefix.
63
+ *
64
+ * Two boundaries are load-bearing and both were divergences in an earlier attempt at this rewrite:
65
+ *
66
+ * - Whitespace after `Output:` may precede the marker, so an INDENTED `<empty>` still classifies.
67
+ * Rejecting it would leave the wrapper unnormalized and the failure unexplained.
68
+ * - Only whitespace may follow the marker, so a DUPLICATE `<empty>` still does not classify.
69
+ * Accepting it would erase a real payload as an empty failed wrapper — the damaging direction.
70
+ *
71
+ * Behaviour is otherwise identical to the regex; the CRLF separators above are the only
72
+ * intentional change, verified against a 63-shape differential corpus.
73
+ */
74
+ export function isFailedEmptyExecWrapper(trimmed: string): boolean {
75
+ if (!trimmed.startsWith("Script failed")) return false;
76
+
77
+ const firstNewline = trimmed.indexOf("\n", "Script failed".length);
78
+ if (firstNewline === -1) return true;
79
+
80
+ let index = skipFailedWrapperBlankSeparators(trimmed, firstNewline + 1);
81
+ if (trimmed.startsWith("Wall time", index)) {
82
+ index = skipFailedWrapperLine(trimmed, index);
83
+ }
84
+ if (trimmed.startsWith("Output:", index)) {
85
+ index = skipFailedWrapperWhitespace(trimmed, index + "Output:".length);
86
+ }
87
+ if (trimmed.startsWith("<empty>", index)) {
88
+ index += "<empty>".length;
89
+ }
90
+ return skipFailedWrapperWhitespace(trimmed, index) === trimmed.length;
91
+ }
27
92
 
28
93
  /** Guidance for a failed cell whose output was empty: the failure must survive normalization. */
29
94
  export const FAILED_EXEC_OUTPUT_MESSAGE =
@@ -94,6 +159,6 @@ export function normalizeEmptyExecToolResultText(
94
159
  if (!isCodexExecBridgeTool(options.toolName, options.toolNamespace)) return undefined;
95
160
  const trimmed = text.trim();
96
161
  // Failure first: a failed wrapper must never be described as an empty success.
97
- if (FAILED_EXEC_OUTPUT_REGEX.test(trimmed)) return FAILED_EXEC_OUTPUT_MESSAGE;
162
+ if (isFailedEmptyExecWrapper(trimmed)) return FAILED_EXEC_OUTPUT_MESSAGE;
98
163
  return EMPTY_EXEC_OUTPUT_REGEX.test(trimmed) ? EMPTY_EXEC_OUTPUT_MESSAGE : undefined;
99
164
  }
@@ -54,6 +54,16 @@ const GOOGLE_BREVITY_INSTRUCTION = [
54
54
  "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
55
55
  ].join("\n");
56
56
 
57
+ const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
58
+ "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
59
+
60
+ function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string {
61
+ return systemText
62
+ .split("\n\n")
63
+ .filter(paragraph => paragraph !== ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH)
64
+ .join("\n\n");
65
+ }
66
+
57
67
  /**
58
68
  * Documented output ceiling for a Google-surface model, or `undefined` when the id is not
59
69
  * recognized.
@@ -227,15 +237,19 @@ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
227
237
  function messagesToGeminiFormat(
228
238
  parsed: OcxParsedRequest,
229
239
  identityModelId: string,
240
+ stripRejectedClaudeSdkParagraph = false,
230
241
  ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
231
242
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
232
243
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
233
244
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
234
- const systemText = identifyRoutedModel([
245
+ const identifiedSystemText = identifyRoutedModel([
235
246
  ...(parsed.context.systemPrompt ?? []),
236
247
  ...(toolCatalogNudge ? [toolCatalogNudge] : []),
237
248
  GOOGLE_BREVITY_INSTRUCTION,
238
249
  ].join("\n\n"), identityModelId);
250
+ const systemText = stripRejectedClaudeSdkParagraph
251
+ ? stripAntigravityRejectedClaudeSdkParagraph(identifiedSystemText)
252
+ : identifiedSystemText;
239
253
  const systemInstruction = { parts: [{ text: systemText }] };
240
254
 
241
255
  const contents: unknown[] = [];
@@ -733,7 +747,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
733
747
  : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
734
748
  // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
735
749
  const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
736
- const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(parsed, identityModelId);
750
+ const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist"
751
+ && parsed.modelId === "gemini-3.7-flash";
752
+ const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
753
+ parsed,
754
+ identityModelId,
755
+ stripRejectedClaudeSdkParagraph,
756
+ );
737
757
  lastInjectedCallIds = [...replayedCallIds];
738
758
  lastReasoningReplayScope = parsed._reasoningReplayScope;
739
759
  const tools = toolsToGeminiFormat(parsed);
@@ -1062,6 +1062,27 @@ async function* parseKiroAttemptEvents(
1062
1062
  try { yield event; } finally { retention.releaseEvent(event); }
1063
1063
  }
1064
1064
  };
1065
+ // A valid private completion answer supersedes the progress prose staged during the SAME
1066
+ // inference: Kiro emits answer-like text and then calls the completion tool, so releasing both
1067
+ // makes the bridge close the commentary message and open a second one with near-identical text
1068
+ // (#2819 follow-up). Consume the collection instead — drop the redundant text, keep every
1069
+ // non-text event, and release retention either way.
1070
+ //
1071
+ // This is deliberately the ONLY suppression site. The outer drain in `parseKiroAttempt` is also
1072
+ // the leftover flush for early terminal returns (stream, protocol, and provider failures), so
1073
+ // teaching it to discard text would hide the only commentary a failed turn ever produced.
1074
+ // Splicing here leaves that drain empty on the completion path and untouched everywhere else.
1075
+ const consumeSupersededByCompletion = async function* (
1076
+ events: AdapterEvent[],
1077
+ ): AsyncGenerator<AdapterEvent> {
1078
+ for (const event of events.splice(0)) {
1079
+ try {
1080
+ if (event.type !== "text_delta") yield event;
1081
+ } finally {
1082
+ retention.releaseEvent(event);
1083
+ }
1084
+ }
1085
+ };
1065
1086
 
1066
1087
  const providerState = (): { kiro: { conversationId: string } } | undefined =>
1067
1088
  returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
@@ -1466,12 +1487,15 @@ async function* parseKiroAttemptEvents(
1466
1487
  });
1467
1488
 
1468
1489
  if (mode === "required") {
1469
- yield* emitRetained(deferred.splice(0));
1490
+ // A valid completion answer makes this inference's staged prose redundant; anything else
1491
+ // still flushes exactly as before (bounded fallback, explicit stops, real tool calls).
1492
+ if (completionAnswer !== undefined) yield* consumeSupersededByCompletion(deferred);
1493
+ else yield* emitRetained(deferred.splice(0));
1470
1494
  }
1471
1495
 
1472
1496
  if (mode === "text_fallback") {
1473
1497
  if (completionAnswer !== undefined) {
1474
- yield* emitRetained(fallbackEvents);
1498
+ yield* consumeSupersededByCompletion(fallbackEvents);
1475
1499
  yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
1476
1500
  return {
1477
1501
  assistantText,
@@ -0,0 +1,111 @@
1
+ /**
2
+ * URL policy for Ollama's native REST API.
3
+ *
4
+ * The built-in Ollama provider historically stored an OpenAI-compatible `/v1` base URL. The
5
+ * native adapter deliberately canonicalizes that compatibility spelling only for Ollama's known
6
+ * local/cloud hosts. An arbitrary custom host with a `/v1` path is never silently rewritten.
7
+ */
8
+
9
+ export type OllamaNativeEndpointKind = "local" | "cloud" | "custom";
10
+
11
+ const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
12
+ const CLOUD_HOSTNAMES = new Set(["ollama.com"]);
13
+ const REJECTED_CLOUD_ALIAS_HOSTNAMES = new Set(["www.ollama.com"]);
14
+
15
+ function normalizedHostname(url: URL): string {
16
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
17
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
18
+ }
19
+
20
+ function normalizedPath(url: URL): string {
21
+ const path = url.pathname.replace(/\/+$/, "");
22
+ return path === "/" ? "" : path;
23
+ }
24
+
25
+ function endpointKind(url: URL): OllamaNativeEndpointKind {
26
+ // WHATWG URL keeps IPv6 brackets in `hostname` on Bun/Node (`[::1]`), while the
27
+ // loopback policy is stored in its canonical host form (`::1`).
28
+ const hostname = normalizedHostname(url);
29
+ if (REJECTED_CLOUD_ALIAS_HOSTNAMES.has(hostname)) {
30
+ throw new Error("ollama-native requires canonical Ollama Cloud host ollama.com; www.ollama.com is rejected");
31
+ }
32
+ if (LOCAL_HOSTNAMES.has(hostname)) return "local";
33
+ if (CLOUD_HOSTNAMES.has(hostname)) return "cloud";
34
+ return "custom";
35
+ }
36
+
37
+ function assertCloudTransport(url: URL, kind: OllamaNativeEndpointKind): void {
38
+ if (kind !== "cloud") return;
39
+ if (url.protocol !== "https:") {
40
+ throw new Error("ollama-native canonical Ollama Cloud requires HTTPS");
41
+ }
42
+ if (url.port) {
43
+ throw new Error("ollama-native canonical Ollama Cloud rejects non-default ports");
44
+ }
45
+ }
46
+
47
+ function parseBaseUrl(baseUrl: string): URL {
48
+ const trimmed = baseUrl.trim();
49
+ if (!trimmed) throw new Error("ollama-native requires a non-empty baseUrl");
50
+
51
+ let url: URL;
52
+ try {
53
+ url = new URL(trimmed);
54
+ } catch {
55
+ throw new Error("ollama-native requires an absolute http(s) baseUrl");
56
+ }
57
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
58
+ throw new Error("ollama-native only supports http(s) base URLs");
59
+ }
60
+ if (url.username || url.password || url.search || url.hash) {
61
+ throw new Error("ollama-native baseUrl must not contain credentials, a query, or a fragment");
62
+ }
63
+ return url;
64
+ }
65
+
66
+ /** Return the endpoint family used by native authentication policy. */
67
+ export function ollamaNativeEndpointKind(baseUrl: string): OllamaNativeEndpointKind {
68
+ const url = parseBaseUrl(baseUrl);
69
+ const kind = endpointKind(url);
70
+ assertCloudTransport(url, kind);
71
+ return kind;
72
+ }
73
+
74
+ /** True when the base URL points at canonical Ollama Cloud (not a self-hosted destination). */
75
+ export function isCanonicalOllamaCloudUrl(baseUrl: string): boolean {
76
+ return ollamaNativeEndpointKind(baseUrl) === "cloud";
77
+ }
78
+
79
+ /**
80
+ * Build the native chat endpoint from a configured base URL.
81
+ *
82
+ * Recognized compatibility forms on canonical Ollama hosts:
83
+ * - `/`, `/api`, `/api/chat`
84
+ * - legacy `/v1` and `/v1/chat/completions`
85
+ *
86
+ * For an unrelated custom host only `/`, `/api`, and `/api/chat` are accepted. In particular,
87
+ * `/v1` is rejected instead of being stripped or guessed at.
88
+ */
89
+ export function ollamaNativeChatUrl(baseUrl: string): string {
90
+ const url = parseBaseUrl(baseUrl);
91
+ const kind = endpointKind(url);
92
+ assertCloudTransport(url, kind);
93
+ const path = normalizedPath(url);
94
+ const canonicalPaths = new Set(["", "/api", "/api/chat", "/v1", "/v1/chat/completions"]);
95
+ const customPaths = new Set(["", "/api", "/api/chat"]);
96
+
97
+ if (kind === "custom" && !customPaths.has(path)) {
98
+ throw new Error(
99
+ `ollama-native refuses custom baseUrl path "${path || "/"}"; use a native root, /api, or /api/chat`,
100
+ );
101
+ }
102
+ if (kind !== "custom" && !canonicalPaths.has(path)) {
103
+ throw new Error(
104
+ `ollama-native refuses Ollama baseUrl path "${path || "/"}"; use root, /v1, /api, or /api/chat`,
105
+ );
106
+ }
107
+
108
+ if (kind === "cloud") url.hostname = normalizedHostname(url);
109
+ url.pathname = "/api/chat";
110
+ return url.toString();
111
+ }