@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-D_t6sCWs.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -7,14 +7,76 @@
7
7
  * streams CloudChatEvent into AdapterEvent.
8
8
  */
9
9
  import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxToolCall, OcxToolResultMessage, OcxUsage } from "../types";
10
+ import { namespacedToolName } from "../types";
10
11
  import type { IncomingMeta, ProviderAdapter } from "./base";
11
12
  import { streamChatEvents, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
12
13
  import { getCachedCatalog } from "./devin/cloud-direct/catalog";
14
+ import { collapseDevinModelUid } from "./devin/live-models";
15
+ import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
13
16
  import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
14
17
 
18
+ /**
19
+ * Combine two usage frames from one turn by keeping the larger count per field.
20
+ *
21
+ * Devin's counters are cumulative within a turn, so a frame that reports less
22
+ * than an earlier one is reporting a subset, not a correction.
23
+ */
24
+ export function mergeDevinUsage(previous: OcxUsage, next: OcxUsage): OcxUsage {
25
+ const keys = [
26
+ "inputTokens", "outputTokens",
27
+ "cachedInputTokens", "cacheReadInputTokens", "cacheCreationInputTokens",
28
+ "reasoningOutputTokens",
29
+ ] as const;
30
+ const merged: OcxUsage = { ...previous, ...next };
31
+ for (const key of keys) {
32
+ const a = previous[key];
33
+ const b = next[key];
34
+ if (typeof a === "number" && typeof b === "number") merged[key] = Math.max(a, b);
35
+ else if (typeof a === "number" && b === undefined) merged[key] = a;
36
+ }
37
+ // totalTokens is derived, not merged. Taking the max of two totals alongside
38
+ // per-field maxima can leave total !== input + output, and the cost and log
39
+ // paths read the total.
40
+ const total = (merged.inputTokens ?? 0) + (merged.outputTokens ?? 0);
41
+ if (total > 0) merged.totalTokens = total;
42
+ return merged;
43
+ }
44
+
45
+ /**
46
+ * The wording `isClientClosedMessage` recognises.
47
+ *
48
+ * "Devin turn was aborted." matched nothing, so a cancelled turn fell through to
49
+ * the default inference and was logged as a 502 upstream failure rather than as
50
+ * the client hanging up.
51
+ */
52
+ const DEVIN_CLIENT_CLOSED_MESSAGE = "client closed request";
53
+
54
+ /** Map a cloud-direct failure onto the structured fields the error event carries. */
55
+ export function devinErrorClassification(error: unknown): { status?: number; errorType?: string; retryable?: boolean } {
56
+ const status = error instanceof CloudChatError ? error.status : undefined;
57
+ if (status === undefined) return {};
58
+ if (status === 401) return { status, errorType: "authentication_error", retryable: false };
59
+ if (status === 403) return { status, errorType: "permission_error", retryable: false };
60
+ if (status === 429) return { status, errorType: "rate_limit_error", retryable: true };
61
+ // 501 is the one 5xx that will never succeed on a second attempt: the service
62
+ // does not implement the call. Marking it retryable put `retryable: true` on
63
+ // the SSE failure a client reads, inviting a retry that cannot change.
64
+ if (status === 501) return { status, retryable: false };
65
+ if (status >= 500) return { status, retryable: true };
66
+ return { status, retryable: false };
67
+ }
68
+
15
69
  export const DEVIN_API_SERVER = DEVIN_DEFAULT_API_SERVER;
16
70
 
17
- const EFFORT_SUFFIXES = new Set(["low", "medium", "high", "xhigh", "max", "none", "1m", "max-1m", "none-1m", "fast"]);
71
+ /**
72
+ * Reasoning-effort values a CALLER may name. Deliberately not the same set as
73
+ * the catalog suffix tokens: `priority` is a service tier that appears in a UID
74
+ * but is not something a caller asks for as effort, and `max-1m` / `none-1m`
75
+ * are compound values a caller can send that never appear as a trailing token.
76
+ * The two sets share most members and mean different things; merging them would
77
+ * both admit a tier as an effort and silently drop the compound values.
78
+ */
79
+ const CALLER_EFFORT_VALUES = new Set(["low", "medium", "high", "xhigh", "max", "none", "1m", "max-1m", "none-1m", "fast"]);
18
80
 
19
81
  /**
20
82
  * Cognition's catalog spells model ids with hyphens (`swe-1-7`), but the same
@@ -27,9 +89,52 @@ export function normalizeDevinModelId(modelId: string): string {
27
89
  return modelId.replace(/\./g, "-");
28
90
  }
29
91
 
92
+ /**
93
+ * Does this id already carry a catalog effort/variant suffix?
94
+ *
95
+ * Delegates to the collapser so there is one answer to "what is a suffix".
96
+ * The previous local set had drifted: it was missing `priority`, so
97
+ * `gpt-5-6-sol-medium-priority` read as unsuffixed and got a second suffix
98
+ * appended, producing a UID Cognition answers with an opaque permission_denied.
99
+ * Delegating also handles compound suffixes, which testing only the final
100
+ * hyphen-separated token never could.
101
+ */
30
102
  function hasEffortSuffix(modelId: string): boolean {
31
- const parts = modelId.split("-");
32
- return parts.length > 1 && EFFORT_SUFFIXES.has(parts[parts.length - 1]!);
103
+ return collapseDevinModelUid(modelId) !== modelId;
104
+ }
105
+
106
+ /**
107
+ * SWE-2 ships exactly three native lanes. Cognition spells them as the model id,
108
+ * not as a separate effort field, so an explicit caller effort has to be resolved
109
+ * to the UID before the suffix shortcut below accepts whatever the picker sent.
110
+ *
111
+ * Kept as a named table rather than an inline branch because the caller-effort
112
+ * set does not carry `ultra`, `off`, or `minimal`, so the two would drift apart
113
+ * silently.
114
+ * Values below Medium select Medium: SWE-2 has no lane under it, and rounding down
115
+ * to nothing would quietly disable its reasoning.
116
+ */
117
+ const SWE2_EFFORT: Record<string, "medium" | "high" | "max"> = {
118
+ none: "medium",
119
+ off: "medium",
120
+ minimal: "medium",
121
+ low: "medium",
122
+ medium: "medium",
123
+ high: "high",
124
+ xhigh: "max",
125
+ ultra: "max",
126
+ max: "max",
127
+ };
128
+
129
+ /**
130
+ * Resolve an explicit effort onto a SWE-2 lane, or undefined when this is not a
131
+ * SWE-2 id or the caller named no usable effort. Undefined leaves every existing
132
+ * path untouched, which is what keeps other model families on suffix precedence.
133
+ */
134
+ function resolveSwe2Variant(modelId: string, reasoningEffort?: string): string | undefined {
135
+ if (!/^swe-2(?:-(?:medium|high|max))?$/.test(modelId)) return undefined;
136
+ const mapped = reasoningEffort ? SWE2_EFFORT[reasoningEffort.toLowerCase()] : undefined;
137
+ return mapped ? `swe-2-${mapped}` : undefined;
33
138
  }
34
139
 
35
140
  /**
@@ -51,11 +156,16 @@ async function resolveWireModelUid(
51
156
  reasoningEffort?: string,
52
157
  ): Promise<string> {
53
158
  const modelId = normalizeDevinModelId(rawModelId);
159
+ // Explicit effort wins over a suffix the picker already baked into the id, so
160
+ // `swe-2-high` asked for at `medium` becomes `swe-2-medium` instead of ignoring
161
+ // the caller. Runs before the shortcut below, which would otherwise return early.
162
+ const swe2 = resolveSwe2Variant(modelId, reasoningEffort);
163
+ if (swe2) return swe2;
54
164
  if (hasEffortSuffix(modelId)) return modelId;
55
165
  const catalog = await getCachedCatalog(apiKey, host);
56
166
  if (catalog) {
57
167
  if (catalog.byUid.has(modelId)) return modelId;
58
- const effort = reasoningEffort && EFFORT_SUFFIXES.has(reasoningEffort) ? reasoningEffort : "medium";
168
+ const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
59
169
  const suffixed = `${modelId}-${effort}`;
60
170
  if (catalog.byUid.has(suffixed)) return suffixed;
61
171
  // Fall back to any enabled variant of this base model.
@@ -64,10 +174,17 @@ async function resolveWireModelUid(
64
174
  }
65
175
  }
66
176
  // Degraded mode: append the default effort suffix.
67
- const effort = reasoningEffort && EFFORT_SUFFIXES.has(reasoningEffort) ? reasoningEffort : "medium";
177
+ const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
68
178
  return `${modelId}-${effort}`;
69
179
  }
70
180
 
181
+ /**
182
+ * Test seam. The resolver stays module-private because it reaches the catalog;
183
+ * exporting it under its bare name would make an async network-touching helper
184
+ * part of the adapter public API. Mirrors sanitizeToolDescriptionForCognitionForTests.
185
+ */
186
+ export const resolveWireModelUidForTests = resolveWireModelUid;
187
+
71
188
  export class DevinMissingCredentialError extends Error {
72
189
  constructor() {
73
190
  super("Devin live transport requires a Devin API key. Run ocx login devin to sign in with your Cognition/Devin account.");
@@ -108,18 +225,60 @@ function assistantToolCalls(message: OcxAssistantMessage): Array<{ id: string; n
108
225
 
109
226
  function assistantText(message: OcxAssistantMessage): string {
110
227
  return message.content
111
- // Thinking stays out of the replayed content. Cognition has no reasoning
112
- // replay field, and folding chain-of-thought into assistant text sends it
113
- // back as visible prior output - which the model then treats as something
114
- // it said to the user.
228
+ // Thinking stays out of the replayed TEXT: folding chain-of-thought into
229
+ // assistant text sends it back as visible prior output, which the model
230
+ // then treats as something it said to the user. It is replayed in its own
231
+ // field instead — see assistantThinking below.
115
232
  .map((part) => (part.type === "text" ? part.text : ""))
116
233
  .filter(Boolean)
117
234
  .join("\n");
118
235
  }
119
236
 
237
+ /**
238
+ * The assistant turn's own reasoning, for replay in ChatMessagePrompt #11.
239
+ *
240
+ * This adapter previously asserted that Cognition has no reasoning-replay
241
+ * field and dropped the thinking outright, so a reasoning model restarted its
242
+ * chain on every turn of a tool loop. The field exists: two independent
243
+ * clients of the same service write #11 thinking with #12 signature and #18
244
+ * signature_type on the assistant prompt.
245
+ *
246
+ * The signature attests the thinking it was produced with, so a block without
247
+ * one contributes its text and nothing else rather than borrowing a neighbour's.
248
+ */
249
+ function assistantThinking(
250
+ message: OcxAssistantMessage,
251
+ ): { thinking?: string; signature?: string } {
252
+ const blocks = message.content.filter(
253
+ (part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
254
+ );
255
+ if (blocks.length === 0) return {};
256
+ const thinking = blocks.map(b => b.thinking).filter(Boolean).join("\n");
257
+ // Only one signature can ride the prompt, so take the last block that has
258
+ // one: that is the block the turn actually ended on.
259
+ const signature = blocks.filter(b => b.signature).at(-1)?.signature;
260
+ return {
261
+ ...(thinking ? { thinking } : {}),
262
+ ...(signature ? { signature } : {}),
263
+ };
264
+ }
265
+
120
266
  export function mapOcxMessagesToDevin(parsed: OcxParsedRequest): ChatHistoryItem[] {
121
267
  const items: ChatHistoryItem[] = [];
122
- const system = parsed.context.systemPrompt?.filter((line) => line.trim().length > 0).join("\n");
268
+ // Cognition is not an OpenAI host, and this adapter does advertise a real
269
+ // client tool catalog (proto #10 via `mapOcxToolsToDevin`), so the same
270
+ // contract paragraph the other non-OpenAI adapters inject belongs here. The
271
+ // wire name is the bare `tool.name` that encoder writes, not the namespaced
272
+ // form, so the nudge names exactly what the model is offered.
273
+ const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(
274
+ parsed.context.tools,
275
+ parsed.options.toolChoice,
276
+ (tool) => tool.name,
277
+ );
278
+ const systemPrompt = parsed.context.systemPrompt?.filter((line) => line.trim().length > 0).join("\n");
279
+ const system = [systemPrompt, toolCatalogNudge]
280
+ .filter((part): part is string => typeof part === "string" && part.length > 0)
281
+ .join("\n\n");
123
282
  if (system) items.push({ role: "system", content: system });
124
283
 
125
284
  for (const message of parsed.context.messages) {
@@ -138,11 +297,15 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
138
297
  if (message.role === "assistant") {
139
298
  const toolCalls = assistantToolCalls(message);
140
299
  const text = assistantText(message);
141
- if (!text && toolCalls.length === 0) return undefined;
300
+ const reasoning = assistantThinking(message);
301
+ // A turn that produced only reasoning is still worth replaying: dropping it
302
+ // is what makes the next turn re-derive the same chain.
303
+ if (!text && toolCalls.length === 0 && !reasoning.thinking) return undefined;
142
304
  return {
143
305
  role: "assistant",
144
306
  content: text || "",
145
307
  ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
308
+ ...reasoning,
146
309
  };
147
310
  }
148
311
  if (message.role === "toolResult") {
@@ -164,13 +327,87 @@ export function mapOcxToolsToDevin(tools: OcxTool[] | undefined): ToolDef[] | un
164
327
  }));
165
328
  }
166
329
 
330
+ /**
331
+ * Devin's request mapper advertises the local tool name, so a namespaced Codex tool such as
332
+ * `mcp__cua_repl__js` is sent upstream as `js`. Restore a returned bare name to its canonical request
333
+ * identity only when exactly one advertised tool owns it. A null owner is an ambiguous catalog and
334
+ * must fail before dispatch; an absent owner remains unchanged for the shared undeclared-tool guard
335
+ * to reject.
336
+ *
337
+ * Canonical names are registered as aliases of themselves because the adapter accepts them on return
338
+ * too. Tracking only local names let one tool's canonical identity collide with another tool's local
339
+ * name and resolve to the wrong owner: with `{ namespace: "a", name: "x" }` and
340
+ * `{ namespace: "b", name: "a__x" }`, a returned `a__x` is both the first tool's canonical identity
341
+ * and the second tool's advertised name, and it used to map to `b__a__x` — so the bridge dispatched
342
+ * the call to the wrong client tool. That case is genuinely ambiguous and now fails closed.
343
+ */
344
+ function buildDevinReturnedToolNameMap(
345
+ tools: OcxTool[] | undefined,
346
+ ): ReadonlyMap<string, string | null> {
347
+ const names = new Map<string, string | null>();
348
+ const addOwner = (alias: string, canonical: string) => {
349
+ if (!names.has(alias)) {
350
+ names.set(alias, canonical);
351
+ } else if (names.get(alias) !== canonical) {
352
+ names.set(alias, null);
353
+ }
354
+ };
355
+ for (const tool of tools ?? []) {
356
+ const canonical = namespacedToolName(tool.namespace, tool.name);
357
+ addOwner(tool.name, canonical);
358
+ addOwner(canonical, canonical);
359
+ }
360
+ return names;
361
+ }
362
+
363
+ function restoreDevinReturnedToolName(
364
+ name: string,
365
+ names: ReadonlyMap<string, string | null>,
366
+ ): string | null {
367
+ return names.has(name) ? names.get(name)! : name;
368
+ }
369
+
370
+ type DevinMappedToolCallStart =
371
+ | Extract<AdapterEvent, { type: "tool_call_start" }>
372
+ | Extract<AdapterEvent, { type: "error" }>;
373
+
374
+ function mapDevinToolCallStart(
375
+ id: string,
376
+ name: string,
377
+ names: ReadonlyMap<string, string | null>,
378
+ ): DevinMappedToolCallStart {
379
+ const restoredName = restoreDevinReturnedToolName(name, names);
380
+ if (restoredName === null) {
381
+ return {
382
+ type: "error",
383
+ message: "Devin emitted a bare client tool name that maps to multiple request-declared tools.",
384
+ status: 502,
385
+ retryable: false,
386
+ };
387
+ }
388
+ return { type: "tool_call_start", id, name: restoredName };
389
+ }
390
+
391
+ /** Test seam for the request-scoped tool-call event mapping used by runTurn. */
392
+ export function mapDevinToolCallStartForTests(
393
+ id: string,
394
+ name: string,
395
+ tools: OcxTool[] | undefined,
396
+ ): DevinMappedToolCallStart {
397
+ return mapDevinToolCallStart(id, name, buildDevinReturnedToolNameMap(tools));
398
+ }
399
+
167
400
  export function createDevinAdapter(
168
401
  provider: OcxProviderConfig,
169
402
  context: { providerId?: string } = {},
170
403
  ): ProviderAdapter {
171
- // Which credential slot holds this row's tenant. Defaults to `devin` so every
172
- // existing caller — including the tests that construct this adapter directly —
173
- // behaves exactly as before.
404
+ // Which credential slot holds this row's tenant. The key is the configured
405
+ // provider id verbatim: `devin-cli` is a deprecated alias for the one merged
406
+ // `devin` provider, and the startup migration rekeys the config row and the
407
+ // credential slot together, so normalizing here would only misread a row that
408
+ // has not been migrated yet. Defaults to `devin` so every existing caller —
409
+ // including the tests that construct this adapter directly — behaves exactly
410
+ // as before.
174
411
  const credentialProviderId = context.providerId ?? "devin";
175
412
  const cascadeIds = new Map<string, string>();
176
413
  const CASCADE_ID_MAX = 256;
@@ -196,7 +433,7 @@ export function createDevinAdapter(
196
433
 
197
434
  async runTurn(parsed: OcxParsedRequest, incoming: IncomingMeta, emit: (event: AdapterEvent) => void) {
198
435
  if (incoming.abortSignal?.aborted) {
199
- emit({ type: "error", message: "Devin turn was aborted before start." });
436
+ emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false });
200
437
  return;
201
438
  }
202
439
  let apiKey: string;
@@ -225,6 +462,7 @@ export function createDevinAdapter(
225
462
  // every RPC to the US server it is not provisioned on.
226
463
  const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId);
227
464
  const modelUid = await resolveWireModelUid(rawModelId, apiKey, host, parsed.options.reasoning);
465
+ const returnedToolNames = buildDevinReturnedToolNameMap(parsed.context.tools);
228
466
  let openToolId: string | undefined;
229
467
  let usage: OcxUsage | undefined;
230
468
  let stopReason: string | undefined;
@@ -258,7 +496,7 @@ export function createDevinAdapter(
258
496
  // Say what happened instead, the way the other runTurn-only adapter
259
497
  // does, and carry any usage already seen.
260
498
  closeOpenTool();
261
- emit({ type: "error", message: "Devin turn was aborted.", ...(usage ? { usage } : {}) });
499
+ emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
262
500
  return;
263
501
  }
264
502
  if (event.kind === "text") {
@@ -270,10 +508,21 @@ export function createDevinAdapter(
270
508
  if (event.text) emit({ type: "thinking_delta", thinking: event.text });
271
509
  continue;
272
510
  }
511
+ if (event.kind === "reasoning_signature") {
512
+ // Carried back out so the next turn can replay it in the prompt's
513
+ // signature field; an unsigned replay is what the service ignores.
514
+ emit({ type: "thinking_signature", signature: event.signature });
515
+ continue;
516
+ }
273
517
  if (event.kind === "tool_call_start") {
274
518
  closeOpenTool();
519
+ const mapped = mapDevinToolCallStart(event.id, event.name, returnedToolNames);
520
+ if (mapped.type === "error") {
521
+ emit({ ...mapped, ...(usage ? { usage } : {}) });
522
+ return;
523
+ }
275
524
  openToolId = event.id;
276
- emit({ type: "tool_call_start", id: event.id, name: event.name });
525
+ emit(mapped);
277
526
  continue;
278
527
  }
279
528
  if (event.kind === "tool_call_args") {
@@ -291,7 +540,7 @@ export function createDevinAdapter(
291
540
  }
292
541
  if (event.kind === "usage") {
293
542
  const total = event.totalTokens ?? ((event.promptTokens ?? 0) + (event.completionTokens ?? 0));
294
- usage = {
543
+ const next: OcxUsage = {
295
544
  inputTokens: event.promptTokens ?? 0,
296
545
  outputTokens: event.completionTokens ?? 0,
297
546
  ...(total > 0 ? { totalTokens: total } : {}),
@@ -299,19 +548,24 @@ export function createDevinAdapter(
299
548
  ...(event.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: event.cacheCreationInputTokens } : {}),
300
549
  ...(event.reasoningTokens !== undefined ? { reasoningOutputTokens: event.reasoningTokens } : {}),
301
550
  };
551
+ // Merge rather than replace. A turn can carry more than one usage
552
+ // frame, and the counters are cumulative, so a later partial frame
553
+ // that omits a field used to zero a count the earlier frame had
554
+ // already reported.
555
+ usage = usage ? mergeDevinUsage(usage, next) : next;
302
556
  continue;
303
557
  }
304
558
  }
305
559
  closeOpenTool();
306
560
  if (incoming.abortSignal?.aborted) {
307
- emit({ type: "error", message: "Devin turn was aborted.", ...(usage ? { usage } : {}) });
561
+ emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
308
562
  } else {
309
563
  emit({ type: "done", ...(usage ? { usage } : {}), ...(stopReason ? { stopReason } : {}) });
310
564
  }
311
565
  } catch (error) {
312
566
  closeOpenTool();
313
567
  if (incoming.abortSignal?.aborted) {
314
- emit({ type: "error", message: "Devin turn was aborted.", ...(usage ? { usage } : {}) });
568
+ emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
315
569
  return;
316
570
  }
317
571
  const message = error instanceof CloudChatError
@@ -319,7 +573,13 @@ export function createDevinAdapter(
319
573
  : error instanceof Error ? error.message : String(error);
320
574
  // Usage that already arrived is still real; dropping it loses the
321
575
  // accounting for a turn that did most of its work before failing.
322
- emit({ type: "error", message, ...(usage ? { usage } : {}) });
576
+ emit({
577
+ type: "error",
578
+ message,
579
+ ...devinErrorClassification(error),
580
+ ...(error instanceof CloudChatError && error.code ? { code: error.code } : {}),
581
+ ...(usage ? { usage } : {}),
582
+ });
323
583
  }
324
584
  },
325
585
  };
@@ -130,12 +130,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
130
130
  ))].slice(0, 5);
131
131
  if (stopSequences.length > 0) out.stopSequences = stopSequences;
132
132
  }
133
- if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") {
134
- const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
135
- const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw)
136
- ? raw
137
- : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
138
- if (thinkingLevel) out.thinkingConfig = { thinkingLevel };
133
+ if (isObject(value.thinkingConfig)) {
134
+ const thinking: JsonObject = {};
135
+ if (typeof value.thinkingConfig.thinkingLevel === "string") {
136
+ const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
137
+ const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw)
138
+ ? raw
139
+ : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
140
+ if (thinkingLevel) thinking.thinkingLevel = thinkingLevel;
141
+ }
142
+ // The one key that makes Google return `thought: true` text. Cloud Code Assist serves
143
+ // thinking either way (thoughtsTokenCount stays non-zero) but withholds the text unless the
144
+ // request opts in, so dropping it here silently reinstates the missing-thinking behavior.
145
+ if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true;
146
+ if (Object.keys(thinking).length > 0) out.thinkingConfig = thinking;
139
147
  }
140
148
  if (Array.isArray(value.responseModalities)) {
141
149
  const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
@@ -568,13 +568,15 @@ function googleToolCallMetadataFromPart(
568
568
  * Keep that provider visibility bit authoritative here so the streaming and buffered parsers
569
569
  * cannot accidentally expose the same hidden reasoning through different event types.
570
570
  */
571
- function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
571
+ function googlePartTextEvent(part: GoogleResponsePart, thoughtSummary = false): AdapterEvent | undefined {
572
572
  // A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
573
573
  // only this optional field preserves the rest of the part without inventing assistant output by
574
574
  // coercion; an empty string keeps its existing no-event behavior.
575
575
  if (typeof part.text !== "string" || part.text.length === 0) return undefined;
576
576
  return part.thought === true
577
- ? { type: "reasoning_raw_delta", text: part.text }
577
+ ? thoughtSummary
578
+ ? { type: "thinking_delta", thinking: part.text }
579
+ : { type: "reasoning_raw_delta", text: part.text }
578
580
  : { type: "text_delta", text: part.text };
579
581
  }
580
582
 
@@ -721,6 +723,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
721
723
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
722
724
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
723
725
  let antigravityModel: string | undefined;
726
+ let returnsThoughtSummaries = false;
724
727
  let antigravitySession: string | undefined;
725
728
  // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
726
729
  // must stay transport-scoped: a signature minted by one Google backend must never be sent to
@@ -795,6 +798,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
795
798
  : provider.googleMode === "vertex"
796
799
  ? parsed.modelId
797
800
  : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
801
+ returnsThoughtSummaries = provider.googleMode === "cloud-code-assist"
802
+ && /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId);
798
803
  // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
799
804
  const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
800
805
  const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist"
@@ -866,11 +871,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
866
871
  );
867
872
  antigravityModel = wireModelId;
868
873
  antigravitySession = sessionId;
874
+ // CCA Gemini exposes provider-authored thought summaries with includeThoughts.
875
+ // Other CCA model families do not share this request contract.
876
+ const includeThoughts = provider.showThinkingSummary === true
877
+ && parsed.options.hideThinkingSummary !== true
878
+ && /^gemini-/.test(wireModelId)
879
+ && !isImageCapableModel(parsed.modelId);
869
880
  // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
870
881
  // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
871
- if (thinkingLevel) {
882
+ if (thinkingLevel || includeThoughts) {
872
883
  const gc = (body.generationConfig ?? {}) as Record<string, unknown>;
873
- gc.thinkingConfig = { thinkingLevel };
884
+ gc.thinkingConfig = {
885
+ ...(thinkingLevel ? { thinkingLevel } : {}),
886
+ ...(includeThoughts ? { includeThoughts: true } : {}),
887
+ };
874
888
  body.generationConfig = gc;
875
889
  }
876
890
  // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
@@ -1139,7 +1153,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1139
1153
  if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1140
1154
  pendingStreamThoughtSig = sig;
1141
1155
  }
1142
- const textEvent = googlePartTextEvent(part);
1156
+ const textEvent = googlePartTextEvent(part, returnsThoughtSummaries);
1143
1157
  if (textEvent) {
1144
1158
  emittedContentEvent = true;
1145
1159
  yield textEvent;
@@ -1311,7 +1325,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1311
1325
  bytesReservation.commitRetained();
1312
1326
  budget.releaseRetained(total, { kind: "retained_collectors" });
1313
1327
  rawText = new TextDecoder().decode(bytes);
1314
- rawTextBytes = new TextEncoder().encode(rawText).byteLength;
1328
+ rawTextBytes = Buffer.byteLength(rawText, "utf8");
1315
1329
  const textReservation = budget.reserveTransient(rawTextBytes, { kind: "retained_collectors" });
1316
1330
  textReservation.commitRetained();
1317
1331
  budget.releaseRetained(total, { kind: "retained_collectors" });
@@ -1333,7 +1347,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1333
1347
  return [{ type: "error", message: `google response was not a JSON object (${valueType})` }];
1334
1348
  }
1335
1349
  raw = parsedRaw;
1336
- rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
1350
+ rawBytes = Buffer.byteLength(JSON.stringify(raw), "utf8");
1337
1351
  const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
1338
1352
  rawReservation.commitRetained();
1339
1353
  budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
@@ -1415,7 +1429,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1415
1429
  if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1416
1430
  pendingThoughtSig = sig;
1417
1431
  }
1418
- const textEvent = googlePartTextEvent(part);
1432
+ const textEvent = googlePartTextEvent(part, returnsThoughtSummaries);
1419
1433
  if (textEvent) events.push(textEvent);
1420
1434
  const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
1421
1435
  if (inline && typeof inline.data === "string") {