@bitkyc08/opencodex 2.69.0 → 2.70.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 (147) hide show
  1. package/gui/dist/assets/App-_MrbTrBe.js +51 -0
  2. package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
  3. package/gui/dist/assets/index-DKrO2eF3.js +86 -0
  4. package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-model-contract.ts +119 -0
  8. package/src/adapters/anthropic.ts +14 -83
  9. package/src/adapters/base.ts +2 -0
  10. package/src/adapters/command-code-project-context.ts +542 -0
  11. package/src/adapters/command-code-tool-text.ts +99 -67
  12. package/src/adapters/command-code.ts +5 -1
  13. package/src/adapters/cursor/catalog.ts +11 -0
  14. package/src/adapters/cursor/effort-map.ts +3 -0
  15. package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
  16. package/src/adapters/devin/cloud-direct/chat.ts +78 -48
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
  18. package/src/adapters/devin/context-overflow.ts +75 -0
  19. package/src/adapters/devin/live-models.ts +174 -13
  20. package/src/adapters/devin/reasoning-signature.ts +100 -0
  21. package/src/adapters/devin.ts +310 -117
  22. package/src/adapters/kiro/payload.ts +10 -10
  23. package/src/adapters/kiro-images.ts +58 -7
  24. package/src/adapters/openai-chat/passthrough.ts +3 -1
  25. package/src/adapters/openai-chat.ts +2 -1
  26. package/src/adapters/openai-responses/passthrough.ts +2 -1
  27. package/src/bridge/response-json.ts +15 -1
  28. package/src/bridge/sse.ts +3 -1
  29. package/src/claude/intercept/model-bindings.ts +1 -0
  30. package/src/cli/account-extended.ts +31 -1
  31. package/src/cli/account.ts +3 -0
  32. package/src/cli/capabilities.ts +13 -0
  33. package/src/cli/client-start-fence.ts +36 -0
  34. package/src/cli/dispatch.ts +3 -2
  35. package/src/cli/index.ts +98 -117
  36. package/src/cli/provider-runtime.ts +23 -2
  37. package/src/cli/provider.ts +3 -0
  38. package/src/cli/resolve.ts +18 -0
  39. package/src/cli/start-owner-fence.ts +30 -0
  40. package/src/cli/stop-approval.ts +13 -1
  41. package/src/cli/stop-restore.ts +73 -0
  42. package/src/cli/tray-proxy.ts +221 -18
  43. package/src/cli/version-skew.ts +25 -23
  44. package/src/client/runtime.ts +8 -4
  45. package/src/codex/auth-api/account-list.ts +11 -5
  46. package/src/codex/auth-api/login-flow.ts +5 -3
  47. package/src/codex/auth-api/main-account-probe.ts +186 -110
  48. package/src/codex/auth-api/pool-mode-gate.ts +61 -7
  49. package/src/codex/auth-api/pool-quota-probe.ts +142 -90
  50. package/src/codex/auth-api/reset-credit-service.ts +1 -1
  51. package/src/codex/catalog/gather-capture.ts +1 -0
  52. package/src/codex/catalog/model-hints.ts +3 -3
  53. package/src/codex/catalog/provider-models.ts +2 -0
  54. package/src/codex/low-quota-events.ts +28 -0
  55. package/src/codex/low-quota-observer.ts +23 -0
  56. package/src/codex/low-quota-protection.ts +272 -0
  57. package/src/codex/quota-query-backoff.ts +148 -0
  58. package/src/codex/quota.ts +7 -2
  59. package/src/config/diagnostics.ts +10 -1
  60. package/src/config/load-degrade.ts +17 -0
  61. package/src/config/mutation-lock.ts +3 -4
  62. package/src/config/process-state.ts +1 -1
  63. package/src/config/provider-validation.ts +22 -0
  64. package/src/config/schema/blocked-model-redirects.ts +17 -0
  65. package/src/config/schema/config-schema.ts +4 -1
  66. package/src/config/schema/leaf-validators.ts +33 -2
  67. package/src/config/serving-runtimes.ts +412 -0
  68. package/src/generated/compatibility-version.json +211 -115
  69. package/src/generated/model-metadata.ts +3 -3
  70. package/src/images/loop.ts +3 -0
  71. package/src/lib/errors.ts +2 -1
  72. package/src/lib/handled-signal-exit.ts +23 -0
  73. package/src/lib/request-failure-model.ts +1 -0
  74. package/src/lib/self-launch-argv.ts +9 -0
  75. package/src/lib/shadow-call.ts +5 -2
  76. package/src/lib/strict-semver.ts +26 -0
  77. package/src/lib/transient-service-path.ts +16 -0
  78. package/src/lib/windows-service-wrappers.ts +10 -6
  79. package/src/lib/winsw.ts +3 -1
  80. package/src/oauth/anthropic-model-routes.ts +105 -0
  81. package/src/oauth/anthropic-routing.ts +85 -48
  82. package/src/oauth/devin.ts +135 -5
  83. package/src/oauth/index.ts +20 -3
  84. package/src/oauth/kiro-terminal-failover.ts +23 -9
  85. package/src/oauth/pool-settings-capability.ts +10 -2
  86. package/src/oauth/store.ts +1 -1
  87. package/src/protocols/encoders/adapter-events.ts +3 -1
  88. package/src/providers/github-copilot-context.ts +27 -0
  89. package/src/providers/kiro-models.ts +4 -0
  90. package/src/providers/model-rename-fields.ts +3 -0
  91. package/src/providers/quota/account-cache.ts +8 -3
  92. package/src/providers/quota/devin.ts +286 -0
  93. package/src/providers/quota/vendor-probes-oauth.ts +6 -2
  94. package/src/providers/quota.ts +1 -1
  95. package/src/providers/registry/entries-core.ts +1 -1
  96. package/src/providers/registry/model-seeds.ts +5 -2
  97. package/src/responses/compaction-images.ts +27 -0
  98. package/src/router.ts +104 -16
  99. package/src/routing/analytics.ts +1 -0
  100. package/src/server/auth-cors.ts +9 -1
  101. package/src/server/background-lifecycle.ts +21 -10
  102. package/src/server/chat-completions.ts +2 -1
  103. package/src/server/chat-native.ts +1 -0
  104. package/src/server/index.ts +4 -8
  105. package/src/server/inference/client-encoder-delivery.ts +3 -0
  106. package/src/server/management/context.ts +3 -0
  107. package/src/server/management/low-quota-routes.ts +14 -0
  108. package/src/server/management/oauth-account-routes.ts +21 -1
  109. package/src/server/management/provider-overwrite-carry.ts +79 -6
  110. package/src/server/management/provider-routes.ts +41 -28
  111. package/src/server/management/route-registry.ts +1 -0
  112. package/src/server/management-api.ts +7 -0
  113. package/src/server/responses/adapter-continuation.ts +3 -0
  114. package/src/server/responses/adapter-delivery.ts +3 -0
  115. package/src/server/responses/adapter-dispatch.ts +20 -4
  116. package/src/server/responses/antigravity-validation-refusal.ts +20 -0
  117. package/src/server/responses/core-auth.ts +1 -0
  118. package/src/server/responses/core-codex-account.ts +1 -0
  119. package/src/server/responses/core-normalize.ts +4 -0
  120. package/src/server/responses/passthrough-dispatch.ts +8 -1
  121. package/src/server/responses/request-sidecar-auth.ts +2 -0
  122. package/src/server/responses/request-transport.ts +27 -10
  123. package/src/server/responses/response-effects.ts +4 -1
  124. package/src/server/responses/run-turn-execution.ts +126 -42
  125. package/src/server/responses/sidecar-execution.ts +4 -1
  126. package/src/service/diagnostics.ts +3 -1
  127. package/src/service/guarded-manager-target.ts +39 -12
  128. package/src/service/launchd.ts +14 -6
  129. package/src/service/repair.ts +6 -1
  130. package/src/service/service-child-ownership.ts +168 -0
  131. package/src/service/state.ts +22 -4
  132. package/src/service/systemd.ts +2 -2
  133. package/src/service/windows-guarded-manager.ts +530 -0
  134. package/src/service/windows-taskxml.ts +2 -2
  135. package/src/service.ts +2 -0
  136. package/src/types/config.ts +26 -0
  137. package/src/types/provider.ts +12 -0
  138. package/src/types/request.ts +2 -0
  139. package/src/types.ts +1 -0
  140. package/src/usage/expected-prices.ts +13 -0
  141. package/src/usage/telemetry-contract.ts +1 -0
  142. package/src/vision/anthropic-describe.ts +2 -1
  143. package/src/web-search/anthropic-executor.ts +2 -1
  144. package/src/web-search/loop.ts +1 -0
  145. package/gui/dist/assets/App-DDbgOTh6.js +0 -51
  146. package/gui/dist/assets/index-CuaBJkG4.js +0 -86
  147. package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
@@ -39,6 +39,7 @@ import { getCachedCatalog, ModelNotAvailableError, type CacheEntry } from './cat
39
39
  import { anySignal, cancelBodyOnAbort } from '../../../lib/abort.js';
40
40
  import { parseRetryAfterFromMessage } from '../../../lib/retry-delay.js';
41
41
  import { resolveDevinApiBaseUrl } from '../../../oauth/devin/api-base.js';
42
+ import { normalizeDevinToolParameters } from './tool-schema.js';
42
43
 
43
44
  /**
44
45
  * Connect-RPC streaming inactivity timeout. If the cloud sends zero bytes
@@ -175,6 +176,7 @@ export function allocateCascadeId(): string {
175
176
  * #3 prompt: string (text content)
176
177
  * #4 num_tokens: int (rough estimate)
177
178
  * #5 safe_for_code_telemetry: bool (1 = ok to log)
179
+ * #9 tool_result_is_error: bool (tool prompts only)
178
180
  * #10 images: repeated ImageData (multimodal)
179
181
  * #11 thinking: string (assistant reasoning, replayed)
180
182
  * #12 signature: string (opaque attestation for #11)
@@ -218,6 +220,7 @@ function encodeChatMessagePrompt(
218
220
  thinking?: string;
219
221
  signature?: string;
220
222
  signatureType?: string;
223
+ isError?: boolean;
221
224
  },
222
225
  ): Buffer {
223
226
  const textParts = content.filter((p): p is { type: 'text'; text: string } => p.type === 'text');
@@ -234,6 +237,9 @@ function encodeChatMessagePrompt(
234
237
  if (opts?.toolCallId) {
235
238
  parts.push(encodeString(7, opts.toolCallId));
236
239
  }
240
+ // Accepted live on a tool prompt. Only some models act on it, so the adapter
241
+ // also keeps an in-band marker in the text.
242
+ if (opts?.isError) parts.push(encodeVarintField(9, 1));
237
243
  // Assistant message with tool_calls: encode each as a ChatToolCall.
238
244
  if (opts?.toolCalls && opts.toolCalls.length > 0) {
239
245
  for (const tc of opts.toolCalls) {
@@ -257,30 +263,26 @@ function encodeChatMessagePrompt(
257
263
  const SOURCE_BY_ROLE: Record<string, number> = {
258
264
  user: 1,
259
265
  assistant: 2,
260
- // NOTE: do not send source=3 (SYSTEM) directly — the Codeium chat backend
261
- // returns "third-party model provider is experiencing issues" when any
262
- // ChatMessagePrompt has source=SYSTEM. The captured LS upstream traffic
263
- // shows the IDE inlines system context into the *user* prompt (source=1)
264
- // wrapped in <additional_metadata>...</additional_metadata>. We collapse
265
- // role:'system' messages into the next user turn before building the
266
- // proto — see `collapseSystemIntoUser` below.
266
+ // Never sent as a prompt source: the leading system text goes in request #2,
267
+ // and a later system message is collapsed into the next user turn below.
267
268
  system: 1,
268
269
  tool: 4,
269
270
  };
270
271
 
271
272
  /**
272
- * Collapse OpenAI-style messages so all `role:'system'` entries are inlined
273
- * into the immediately-following user message, matching the wire format the
274
- * IDE uses. Cognition's chat backend rejects raw role=system entries.
273
+ * Collapse `role:'system'` entries that follow the conversation start into the
274
+ * immediately-following user message. The leading run of system messages never
275
+ * reaches here; it is the request's #2 system prompt. With S0 already sent as #2:
275
276
  *
276
- * [{system: "S1"}, {system: "S2"}, {user: "U1"}, {assistant: "A1"}, {user: "U2"}]
277
+ * [{user: "U1"}, {assistant: "A1"}, {system: "S1"}, {system: "S2"}, {user: "U2"}]
277
278
  *
278
279
  * becomes
279
280
  *
280
- * [{user: "<system>\nS1\nS2\n</system>\nU1"}, {assistant: "A1"}, {user: "U2"}]
281
+ * [{user: "U1"}, {assistant: "A1"}, {user: "<system>\nS1\n\nS2\n</system>\nU2"}]
281
282
  *
282
283
  * If there's no following user message, the trailing system messages get
283
- * appended as a synthesized user turn.
284
+ * appended as a synthesized user turn. A request made only of system messages
285
+ * keeps no #2 and comes through here whole, so its prompt list is never empty.
284
286
  */
285
287
  function collapseSystemIntoUser(messages: ChatHistoryItem[]): ChatHistoryItem[] {
286
288
  const out: ChatHistoryItem[] = [];
@@ -332,10 +334,15 @@ function collapseSystemIntoUser(messages: ChatHistoryItem[]): ChatHistoryItem[]
332
334
  * CompletionConfiguration — mirrors the LS-shipped defaults, lets the caller
333
335
  * override the obvious knobs.
334
336
  */
335
- /** Output cap when the caller named none. */
337
+ /** Output cap when neither the caller nor the catalog named one. */
336
338
  const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
337
- /** Context window when the caller named none. */
338
- const DEFAULT_CONTEXT_WINDOW = 128_000;
339
+ /**
340
+ * CompletionConfiguration #3 is `max_newlines`, not a token count and not an
341
+ * input ceiling: live, a value of 5 did not truncate a 25-line answer. It is
342
+ * still sent, at the value every turn has carried, so the request shape the
343
+ * service accepts does not change.
344
+ */
345
+ const MAX_NEWLINES = 128_000;
339
346
 
340
347
  /**
341
348
  * Cognition rejects a temperature of exactly 0 with the same opaque internal
@@ -353,7 +360,6 @@ function safeTemperature(value: number | undefined): number {
353
360
 
354
361
  function encodeCompletionConfiguration(opts: {
355
362
  maxOutputTokens?: number;
356
- maxInputTokens?: number;
357
363
  temperature?: number;
358
364
  topK?: number;
359
365
  topP?: number;
@@ -365,15 +371,15 @@ function encodeCompletionConfiguration(opts: {
365
371
  };
366
372
  // Tag map, verified by building the same turn with a working client and
367
373
  // diffing the encoded messages field by field: #2 is the OUTPUT cap and #3 is
368
- // the context window. This layout had those two swapped, so a caller asking
369
- // for 32 output tokens put 32 into the context-window field and the request
370
- // came back as an opaque "an internal error occurred" — for every turn, on
371
- // every account, which is why free and paid failed identically. #6 and #11
372
- // are not part of the message the service accepts.
374
+ // max_newlines. This layout once had those two swapped, so a caller's output
375
+ // cap landed in #3 and a large value in #2, and the request came back as an
376
+ // opaque "an internal error occurred" — for every turn, on every account,
377
+ // which is why free and paid failed identically. #6 and #11 are not part of
378
+ // the message the service accepts.
373
379
  return Buffer.concat([
374
380
  encodeVarintField(1, 1),
375
381
  encodeVarintField(2, opts.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS),
376
- encodeVarintField(3, opts.maxInputTokens ?? DEFAULT_CONTEXT_WINDOW),
382
+ encodeVarintField(3, MAX_NEWLINES),
377
383
  enc64(5, safeTemperature(opts.temperature)),
378
384
  encodeVarintField(7, opts.topK ?? 40),
379
385
  enc64(8, opts.topP ?? 1.0),
@@ -424,6 +430,8 @@ export interface ChatHistoryItem {
424
430
  thinking?: string;
425
431
  signature?: string;
426
432
  signature_type?: string;
433
+ /** For `role: 'tool'` only — the tool failed. Encoded as ChatMessagePrompt #9. */
434
+ is_error?: boolean;
427
435
  }
428
436
 
429
437
  /**
@@ -486,7 +494,7 @@ export type CloudChatEvent =
486
494
  * turn produced. Without decoding it there is nothing to put in the prompt's
487
495
  * #12 on the next turn, so the replay would always be unsigned.
488
496
  */
489
- | { kind: 'reasoning_signature'; signature: string }
497
+ | { kind: 'reasoning_signature'; signature: string; signatureType?: string }
490
498
  | { kind: 'tool_call_start'; id: string; name: string }
491
499
  | {
492
500
  kind: 'tool_call_args';
@@ -531,8 +539,8 @@ interface BuildArgs {
531
539
  messages: ChatHistoryItem[];
532
540
  cascadeId: string;
533
541
  /**
534
- * GetChatMessageRequest #22. Optional because it is omitted on a first turn;
535
- * the working client only reuses one across a later tool loop.
542
+ * GetChatMessageRequest #17 prompt_id. Optional because it is omitted on a
543
+ * first turn; the working client only reuses one across a later tool loop.
536
544
  */
537
545
  promptId?: string;
538
546
  sessionId: string;
@@ -543,7 +551,6 @@ interface BuildArgs {
543
551
  requestType?: number;
544
552
  completionOpts?: {
545
553
  maxOutputTokens?: number;
546
- maxInputTokens?: number;
547
554
  temperature?: number;
548
555
  topK?: number;
549
556
  topP?: number;
@@ -636,16 +643,20 @@ export function sanitizeToolDescriptionForCognitionForTests(description: string)
636
643
  return sanitizeToolDescriptionForCognition(description);
637
644
  }
638
645
 
639
- function encodeToolDef(tool: ToolDef): Buffer {
640
- const rawDesc = sanitizeToolDescriptionForCognition(tool.description ?? '');
641
- const desc =
642
- rawDesc.length > MAX_TOOL_DESC_LEN
643
- ? rawDesc.slice(0, MAX_TOOL_DESC_LEN - 24) + '\n…(truncated for cloud)'
644
- : rawDesc;
646
+ /** Description as transmitted on the Cognition wire, also used by overflow estimation. */
647
+ export function prepareToolDescriptionForCognition(description: string): string {
648
+ const rawDesc = sanitizeToolDescriptionForCognition(description);
649
+ return rawDesc.length > MAX_TOOL_DESC_LEN
650
+ ? rawDesc.slice(0, MAX_TOOL_DESC_LEN - 24) + '\n…(truncated for cloud)'
651
+ : rawDesc;
652
+ }
653
+
654
+ function encodeToolDef(tool: ToolDef, modelUid: string): Buffer {
655
+ const desc = prepareToolDescriptionForCognition(tool.description ?? '');
645
656
  return Buffer.concat([
646
657
  encodeString(1, tool.name),
647
658
  encodeString(2, desc),
648
- encodeString(3, JSON.stringify(tool.parameters ?? {})),
659
+ encodeString(3, JSON.stringify(normalizeDevinToolParameters(modelUid, tool.parameters ?? {}))),
649
660
  ]);
650
661
  }
651
662
 
@@ -664,9 +675,22 @@ function buildGetChatMessageRequest(args: BuildArgs): Buffer {
664
675
  cloudChatShape: true,
665
676
  });
666
677
 
667
- // System messages must be inlined into the user turn (Cognition cloud
668
- // rejects source=3). See `collapseSystemIntoUser` for the format.
669
- const collapsed = collapseSystemIntoUser(args.messages);
678
+ // The leading system messages become request #2. Measured live on swe-1-6
679
+ // with a ~6.7k-token system prompt: turn 2 read 6688 of 6715 prompt tokens
680
+ // from cache in #2, against 7072 of 7097 when the same text was collapsed
681
+ // into the first user prompt, so the cache ratio is unchanged and the prompt
682
+ // is smaller. The model obeyed an instruction given only in #2.
683
+ // A request with only system text keeps it as a user prompt: #2 alone would
684
+ // leave the request with no prompt at all.
685
+ const firstNonSystem = args.messages.findIndex((m) => m.role !== 'system');
686
+ const leadingSystem = firstNonSystem === -1 ? [] : args.messages.slice(0, firstNonSystem);
687
+ const systemPrompt = leadingSystem
688
+ .map((m) => normalizeContent(m.content)
689
+ .filter((p): p is { type: 'text'; text: string } => p.type === 'text')
690
+ .map((p) => p.text).join('\n'))
691
+ .filter(Boolean)
692
+ .join('\n\n');
693
+ const collapsed = collapseSystemIntoUser(args.messages.slice(leadingSystem.length));
670
694
  const promptParts = collapsed.map((m) =>
671
695
  encodeMessage(
672
696
  3,
@@ -683,6 +707,7 @@ function buildGetChatMessageRequest(args: BuildArgs): Buffer {
683
707
  thinking: m.role === 'assistant' ? m.thinking : undefined,
684
708
  signature: m.role === 'assistant' ? m.signature : undefined,
685
709
  signatureType: m.role === 'assistant' ? m.signature_type : undefined,
710
+ isError: m.role === 'tool' ? m.is_error : undefined,
686
711
  },
687
712
  ),
688
713
  ),
@@ -691,7 +716,7 @@ function buildGetChatMessageRequest(args: BuildArgs): Buffer {
691
716
  const completion = encodeCompletionConfiguration(args.completionOpts ?? {});
692
717
 
693
718
  const toolParts: Buffer[] = (args.tools ?? []).map((t) =>
694
- encodeMessage(10, encodeToolDef(t)),
719
+ encodeMessage(10, encodeToolDef(t, args.modelUid)),
695
720
  );
696
721
 
697
722
  // Field layout from mitm capture of the LS:
@@ -701,15 +726,15 @@ function buildGetChatMessageRequest(args: BuildArgs): Buffer {
701
726
  // #8 completion_configuration
702
727
  // #10 tools (repeated ChatToolDefinition)
703
728
  // #13 prompt_cache_options
729
+ // #15 CortexTrajectoryReference
704
730
  // #16 cascade_id (string)
731
+ // #17 prompt_id (string)
705
732
  // #21 chat_model_uid (string)
706
- // #22 prompt_id (string)
733
+ // #22 execution_id (string)
707
734
  return Buffer.concat([
708
735
  encodeMessage(1, metadata),
709
- // #2 system_prompt is always written, empty when the caller had none. The
710
- // system turn is separately collapsed into the first user message because
711
- // source=SYSTEM is refused; this field is the one the wire expects here.
712
- encodeString(2, ''),
736
+ // #2 system_prompt is always written, empty when the caller had none.
737
+ encodeString(2, systemPrompt),
713
738
  ...promptParts,
714
739
  encodeVarintField(7, args.requestType ?? 5),
715
740
  encodeMessage(8, completion),
@@ -721,7 +746,7 @@ function buildGetChatMessageRequest(args: BuildArgs): Buffer {
721
746
  // it and records real savings; sending it unconditionally matches both the
722
747
  // native client and CLIProxyAPIPlus, which places it outside its tools gate.
723
748
  encodeMessage(13, encodeVarintField(1, PROMPT_CACHE_EPHEMERAL)),
724
- // #15 session model config: { id, turn, 4 }. Present on every verified
749
+ // #15 CortexTrajectoryReference: { id, 1, 4 }. Present on every verified
725
750
  // request.
726
751
  encodeMessage(15, Buffer.concat([
727
752
  encodeString(1, crypto.randomUUID()),
@@ -731,9 +756,9 @@ function buildGetChatMessageRequest(args: BuildArgs): Buffer {
731
756
  encodeString(16, args.cascadeId),
732
757
  encodeVarintField(20, 1),
733
758
  encodeString(21, args.modelUid),
734
- // #22 is deliberately omitted. It is a user-exchange id that only appears
735
- // from the second turn onward and is reused across that turn's tool loop; a
736
- // fresh per-request uuid matches neither shape.
759
+ // #17 prompt_id is deliberately omitted. It is a user-exchange id that only
760
+ // appears from the second turn onward and is reused across that turn's tool
761
+ // loop; a fresh per-request uuid matches neither shape.
737
762
  ]);
738
763
  }
739
764
 
@@ -792,6 +817,10 @@ export function* decodeChatFrame(proto: Buffer): Generator<CloudChatEvent> {
792
817
  }
793
818
  }
794
819
  if (authoritativeUsage) yield authoritativeUsage;
820
+ let signatureType: string | undefined;
821
+ for (const f of iterFields(proto)) {
822
+ if (f.num === 21 && f.wire === 2 && Buffer.isBuffer(f.value)) signatureType = (f.value as Buffer).toString('utf8') || undefined;
823
+ }
795
824
  for (const f of iterFields(proto)) {
796
825
  if (f.num === 3 && f.wire === 2 && Buffer.isBuffer(f.value)) {
797
826
  // Visible delta_text — what the user should SEE in the chat.
@@ -817,7 +846,8 @@ export function* decodeChatFrame(proto: Buffer): Generator<CloudChatEvent> {
817
846
  if (s) yield { kind: 'reasoning', text: s };
818
847
  } else if (f.num === 10 && f.wire === 2 && Buffer.isBuffer(f.value)) {
819
848
  const s = (f.value as Buffer).toString('utf8');
820
- if (s) yield { kind: 'reasoning_signature', signature: s };
849
+ // #21 delta_signature_type arrives in the same frame; the prompt replays it as #18.
850
+ if (s) yield { kind: 'reasoning_signature', signature: s, ...(signatureType ? { signatureType } : {}) };
821
851
  } else if (f.num === 6 && f.wire === 2 && Buffer.isBuffer(f.value)) {
822
852
  let id: string | undefined;
823
853
  let name: string | undefined;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Tool-schema compatibility for Gemini models behind GetChatMessage.
3
+ *
4
+ * Measured live on gemini-3-8-flash-medium: any schema node whose `type` is a
5
+ * JSON-Schema type array (`["string", "null"]`) is refused with an opaque
6
+ * `invalid_argument` on every turn, while the same union spelled as
7
+ * `anyOf: [{type: "string"}, {type: "null"}]` is accepted, as are `$schema`,
8
+ * `additionalProperties: false`, `const`, and `$ref`/`$defs`. Claude models
9
+ * accept type arrays, so only the Gemini family is rewritten, and only that one
10
+ * keyword: the full Google subset sanitizer would strip keywords this backend
11
+ * accepts.
12
+ */
13
+
14
+ export function isDevinGeminiModelUid(modelUid: string): boolean {
15
+ return /^gemini-/i.test(modelUid) || /^MODEL_GOOGLE_GEMINI_/i.test(modelUid);
16
+ }
17
+
18
+ /** Keys whose values are instance data, not subschemas. */
19
+ const DATA_KEYS = new Set(['enum', 'const', 'default', 'examples', 'example']);
20
+ /** Keys whose values map arbitrary names (which may be "enum" or "default") to subschemas. */
21
+ const SCHEMA_MAP_KEYS = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies']);
22
+ /** Keys that describe the whole node and stay on it rather than moving into each branch. */
23
+ const ANNOTATION_KEYS = new Set(['description', 'title', 'default', 'examples', 'example', '$comment', 'deprecated']);
24
+
25
+ function rewriteMap(map: unknown): unknown {
26
+ if (!map || typeof map !== 'object' || Array.isArray(map)) return map;
27
+ // A draft-7 `dependencies` entry may be a list of property names, which is data.
28
+ return Object.fromEntries(Object.entries(map).map(([name, schema]) => [name, Array.isArray(schema) ? schema : rewrite(schema)]));
29
+ }
30
+
31
+ type Schema = Record<string, unknown>;
32
+ const isSchema = (value: unknown): value is Schema => !!value && typeof value === 'object' && !Array.isArray(value);
33
+
34
+ /**
35
+ * `{type: [T, "null"], ...rest}` becomes `{anyOf: [{...rest, type: T}, {type: "null"}]}`,
36
+ * so type-specific keywords (items, properties, ...) stay attached to their type.
37
+ * An existing anyOf is folded in branch by branch when no branch contradicts an
38
+ * outer keyword, and kept beside the split under allOf when one does.
39
+ */
40
+ function splitTypeArray(node: Schema, types: unknown[]): Schema {
41
+ const annotations: Schema = {};
42
+ const rest: Schema = {};
43
+ for (const [key, value] of Object.entries(node)) {
44
+ if (key === 'type' || key === 'anyOf') continue;
45
+ Object.defineProperty(ANNOTATION_KEYS.has(key) ? annotations : rest, key, { value, enumerable: true, writable: true, configurable: true });
46
+ }
47
+ const concrete = types.filter((type) => type !== 'null');
48
+ // An outer enum/const is still binding after the type array is split.
49
+ const allowsNull = concrete.length < types.length
50
+ && (!Array.isArray(rest.enum) || rest.enum.includes(null))
51
+ && (!Object.hasOwn(rest, 'const') || rest.const === null);
52
+ const existing = Array.isArray(node.anyOf) ? node.anyOf : undefined;
53
+ // Boolean constraints apply to every type, including null. A bare null branch
54
+ // would bypass them, so keep the type split and the outer schema conjunctive.
55
+ if (allowsNull && ['not', 'allOf', 'oneOf', 'if', 'then', 'else'].some((key) => key in rest)) {
56
+ return {
57
+ ...annotations,
58
+ allOf: [splitTypeArray({ type: types }, types), existing ? { ...rest, anyOf: existing } : rest],
59
+ };
60
+ }
61
+ // Folding merges each branch into the outer keywords, which is only exact when
62
+ // they never disagree: `{maxLength: 5, anyOf: [{maxLength: 50}]}` folded would
63
+ // loosen the outer limit. On a disagreement keep both constraints under allOf,
64
+ // which this backend accepts (live: gemini-3-8-flash-medium).
65
+ if (existing?.some((branch) => isSchema(branch) && (
66
+ Object.keys(branch).some((key) => key !== 'type' && key in rest && JSON.stringify(branch[key]) !== JSON.stringify(rest[key]))
67
+ // Keep an existing null branch's own constraints; folding it to {type:"null"}
68
+ // would admit values that its enum, const, or nested schema rejects.
69
+ || (allowsNull && branch.type === 'null' && Object.keys(branch).some((key) => key !== 'type'))
70
+ || (allowsNull && branch.type === undefined && ['enum', 'const', 'not', 'allOf', 'oneOf'].some((key) => key in branch))
71
+ ))) {
72
+ return { ...annotations, allOf: [splitTypeArray({ ...rest, type: types }, types), { anyOf: existing }] };
73
+ }
74
+ const branches: unknown[] = [];
75
+ let nullReachable = allowsNull && !existing;
76
+ if (!existing) {
77
+ for (const type of concrete) branches.push({ ...rest, type });
78
+ } else {
79
+ for (const branch of existing) {
80
+ if (!isSchema(branch)) continue;
81
+ if (branch.type === undefined) {
82
+ for (const type of concrete) branches.push({ ...rest, ...branch, type });
83
+ if (allowsNull) nullReachable = true;
84
+ } else if (branch.type === 'null') {
85
+ if (allowsNull) nullReachable = true;
86
+ } else if (concrete.includes(branch.type)) {
87
+ branches.push({ ...rest, ...branch });
88
+ }
89
+ }
90
+ }
91
+ if (nullReachable) branches.push({ type: 'null' });
92
+ // The two unions are disjoint. Keep both constraints rather than drop the type union,
93
+ // which would let the anyOf branches admit types the node never allowed.
94
+ if (branches.length === 0) return { ...annotations, allOf: [splitTypeArray({ ...rest, type: types }, types), { anyOf: existing }] };
95
+ if (branches.length === 1 && isSchema(branches[0])) return { ...annotations, ...branches[0] };
96
+ return { ...annotations, anyOf: branches };
97
+ }
98
+
99
+ function rewrite(node: unknown): unknown {
100
+ if (Array.isArray(node)) return node.map(rewrite);
101
+ if (!isSchema(node)) return node;
102
+ // fromEntries defines own properties, so a "__proto__" key stays data.
103
+ const out: Schema = Object.fromEntries(
104
+ Object.entries(node).map(([key, value]) => [
105
+ key,
106
+ DATA_KEYS.has(key) ? value : SCHEMA_MAP_KEYS.has(key) ? rewriteMap(value) : rewrite(value),
107
+ ]),
108
+ );
109
+ if (!Array.isArray(out.type)) return out;
110
+ const types = out.type as unknown[];
111
+ if (types.length === 0) {
112
+ delete out.type;
113
+ return out;
114
+ }
115
+ return splitTypeArray(out, types);
116
+ }
117
+
118
+ /** Rewrite type arrays for Gemini uids; every other model gets the schema unchanged. */
119
+ export function normalizeDevinToolParameters(modelUid: string, parameters: unknown): unknown {
120
+ return isDevinGeminiModelUid(modelUid) ? rewrite(parameters) : parameters;
121
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Recognise an oversized Devin history behind Cognition's opaque refusal.
3
+ *
4
+ * Measured live on swe-1-6 (200k context): a 1.2 MB or 3 MB history is refused
5
+ * after ~11s with `invalid_argument` and no output, the same code a bad tool
6
+ * schema gets. Passed through as a plain 400, Codex never learns its context is
7
+ * full and never compacts, so every later turn dead-ends the same way. Only a
8
+ * request large against the model's window is reclassified; a small request
9
+ * with the same code stays an ordinary 400.
10
+ *
11
+ * Boundary, binary-searched live on swe-1-6 (200k catalog window, 8192 output):
12
+ * accepted at 199,186 prompt tokens of prose and 200,345 of JSON, refused at
13
+ * ~203k of either. Raising the output cap to 32000 did not move it (195,571
14
+ * input tokens still accepted), so the window is not reduced by the output
15
+ * reservation and the threshold is the window itself.
16
+ */
17
+ import type { AdapterEvent } from "../../types";
18
+ import type { ChatHistoryItem, ToolDef } from "./cloud-direct";
19
+ import { prepareToolDescriptionForCognition } from "./cloud-direct/chat";
20
+
21
+ /**
22
+ * Share of the window the estimate must reach. Characters per real token ran
23
+ * from 1.33 (Korean) through 2.31 (JSON) and 4.23 (source code) to 5.53
24
+ * (prose), so no character ratio separates "over the window" from "60% of it".
25
+ * The word-piece count below read 1.00x to 1.54x of the real count on those
26
+ * samples: at 0.95 every sample at the window is caught and none at 60% is.
27
+ */
28
+ const WINDOW_SHARE = 0.95;
29
+ /** One token per short letter run, 1-3 digit group, or other visible character. */
30
+ const WORD_PIECE = /[A-Z]?[a-z]{1,8}|[A-Z]{1,8}(?![a-z])|\d{1,3}|\S/g;
31
+ /** Text size that counts as oversized when the model's window is unknown. */
32
+ const UNKNOWN_WINDOW_CHARS = 512 * 1024;
33
+
34
+ export const DEVIN_CONTEXT_OVERFLOW_MESSAGE =
35
+ "Devin rejected this turn and its history is at or past the model's context window. Compact the conversation or start a new session.";
36
+
37
+ /** Text the model would tokenize; image bytes are excluded so a screenshot cannot look like a long history. */
38
+ function requestText(messages: ChatHistoryItem[], tools: ToolDef[] | undefined): string {
39
+ const parts: string[] = [];
40
+ for (const m of messages) {
41
+ if (typeof m.content === "string") parts.push(m.content);
42
+ else for (const p of m.content) if (p.type === "text") parts.push(p.text);
43
+ for (const call of m.tool_calls ?? []) parts.push(call.arguments);
44
+ if (m.thinking) parts.push(m.thinking);
45
+ }
46
+ for (const tool of tools ?? []) parts.push(prepareToolDescriptionForCognition(tool.description ?? ""), JSON.stringify(tool.parameters ?? {}));
47
+ return parts.join("\n");
48
+ }
49
+
50
+ export function isDevinHistoryOverflow(input: {
51
+ code: string | undefined;
52
+ producedOutput: boolean;
53
+ contextWindow: number | undefined;
54
+ messages: ChatHistoryItem[];
55
+ tools: ToolDef[] | undefined;
56
+ }): boolean {
57
+ if (input.code !== "invalid_argument" || input.producedOutput) return false;
58
+ const text = requestText(input.messages, input.tools);
59
+ if (!input.contextWindow) return text.length >= UNKNOWN_WINDOW_CHARS;
60
+ let pieces = 0;
61
+ for (const _ of text.matchAll(WORD_PIECE)) pieces++;
62
+ return pieces >= input.contextWindow * WINDOW_SHARE;
63
+ }
64
+
65
+ /** Same terminal shape the Kiro adapter uses, which Codex reads as "context full, compact". */
66
+ export function devinContextOverflowEvent(): Extract<AdapterEvent, { type: "error" }> {
67
+ return {
68
+ type: "error",
69
+ message: DEVIN_CONTEXT_OVERFLOW_MESSAGE,
70
+ status: 400,
71
+ errorType: "invalid_request_error",
72
+ code: "context_length_exceeded",
73
+ retryable: false,
74
+ };
75
+ }