@bitkyc08/opencodex 2.7.33 → 2.7.34

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 (64) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +21 -10
  4. package/README.ru.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/gui/dist/assets/index-BkmJJgg6.js +52 -0
  7. package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic.ts +13 -6
  11. package/src/adapters/cursor/discovery.ts +39 -4
  12. package/src/adapters/cursor/exec-policy.ts +11 -13
  13. package/src/adapters/cursor/live-transport.ts +22 -4
  14. package/src/adapters/cursor/protobuf-events.ts +140 -8
  15. package/src/adapters/cursor/protobuf-request.ts +15 -0
  16. package/src/adapters/cursor/request-builder.ts +10 -5
  17. package/src/adapters/cursor/transport.ts +3 -2
  18. package/src/adapters/cursor/types.ts +14 -0
  19. package/src/adapters/kiro-constants.ts +12 -0
  20. package/src/adapters/kiro-errors.ts +111 -2
  21. package/src/adapters/kiro-events.ts +154 -35
  22. package/src/adapters/kiro-retry.ts +116 -32
  23. package/src/adapters/kiro-tools.ts +30 -20
  24. package/src/adapters/kiro-wire.ts +47 -6
  25. package/src/adapters/kiro.ts +891 -228
  26. package/src/adapters/openai-chat.ts +12 -5
  27. package/src/adapters/openai-responses.ts +7 -2
  28. package/src/bridge.ts +109 -26
  29. package/src/claude/outbound.ts +27 -4
  30. package/src/cli/index.ts +1 -1
  31. package/src/codex/catalog.ts +375 -33
  32. package/src/combos/index.ts +3 -0
  33. package/src/combos/request.ts +4 -4
  34. package/src/combos/resolve.ts +2 -2
  35. package/src/combos/types.ts +104 -2
  36. package/src/config.ts +70 -1
  37. package/src/lib/eventstream-decoder.ts +9 -0
  38. package/src/oauth/index.ts +3 -1
  39. package/src/oauth/kiro-credentials.ts +48 -20
  40. package/src/oauth/login-cli.ts +2 -0
  41. package/src/providers/derive.ts +8 -0
  42. package/src/providers/kiro-models.ts +2 -2
  43. package/src/providers/openai-sidecar.ts +28 -1
  44. package/src/providers/registry.ts +39 -2
  45. package/src/responses/parser.ts +22 -10
  46. package/src/responses/schema.ts +1 -0
  47. package/src/responses/state.ts +50 -10
  48. package/src/router.ts +15 -3
  49. package/src/server/auth-cors.ts +7 -0
  50. package/src/server/claude-messages.ts +6 -0
  51. package/src/server/index.ts +6 -3
  52. package/src/server/management-api.ts +187 -43
  53. package/src/server/ports.ts +4 -2
  54. package/src/server/request-log.ts +3 -2
  55. package/src/server/responses-item-id-repair.ts +281 -0
  56. package/src/server/responses.ts +274 -73
  57. package/src/types.ts +109 -16
  58. package/src/update/job.ts +81 -1
  59. package/src/vision/describe.ts +2 -1
  60. package/src/web-search/executor.ts +2 -1
  61. package/src/web-search/loop.ts +9 -1
  62. package/src/web-search/progress-stream.ts +12 -10
  63. package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
  64. package/gui/dist/assets/index-d63HMU0x.js +0 -52
@@ -2,12 +2,13 @@ import type { Server } from "bun";
2
2
  import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
3
3
  import {
4
4
  getConfigPath,
5
+ multiAgentGuidanceEnabled,
5
6
  resolveEnvValue,
6
7
  } from "../config";
7
8
  import { parseRequest } from "../responses/parser";
8
9
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../responses/compaction";
9
10
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../adapters/openai-responses";
10
- import { expandPreviousResponseInput, previousResponseConversationId, rememberResponseState } from "../responses/state";
11
+ import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../responses/state";
11
12
  import { routeModel } from "../router";
12
13
  import {
13
14
  advanceComboAfterFailure,
@@ -26,7 +27,7 @@ import {
26
27
  import { isInjectionDebugEnabled } from "../lib/debug-settings";
27
28
  import { injectionDebugLog } from "../lib/injection-debug-log";
28
29
  import { modelInList, namespacedToolName } from "../types";
29
- import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
30
+ import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../types";
30
31
  import {
31
32
  forceRefreshOAuthAccessSnapshot,
32
33
  getOAuthCredentialApiBaseUrl,
@@ -58,6 +59,8 @@ import {
58
59
  import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../lib/upstream-retry";
59
60
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
60
61
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
62
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
63
+ import { slugsEquivalent } from "../providers/slug-codec";
61
64
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../providers/openai-virtual-models";
62
65
  import { isUsageDebugEnabled } from "../usage/debug";
63
66
  import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "./request-decompress";
@@ -91,6 +94,7 @@ import {
91
94
  relayWithAbort,
92
95
  sanitizePassthroughHeaders,
93
96
  } from "./relay";
97
+ import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "./responses-item-id-repair";
94
98
 
95
99
  export function buildToolBridgeMaps(parsed: OcxParsedRequest): {
96
100
  toolNsMap: Map<string, { namespace: string; name: string }>;
@@ -182,13 +186,9 @@ export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
182
186
  * model/effort overrides on a full-history fork (multi_agents_v2/spawn.rs
183
187
  * reject_full_fork_spawn_overrides), so the prompt mandates fork_turns "none" or a
184
188
  * partial fork plus a self-contained task message.
185
- * The published spawn_agent schema HIDES model/reasoning_effort by
186
- * default (hide_spawn_agent_metadata=true upstream and it must STAY hidden: the
187
- * ChatGPT backend treats collaboration.spawn_agent as a reserved function and
188
- * rejects any request whose declared schema deviates, "Invalid Value: 'tools'").
189
- * That is prompt-workable: SpawnAgentArgs always parses model/reasoning_effort
190
- * regardless of the flag (spawn.rs), so the prompt tells the model to pass the
191
- * arguments even though the schema does not list them.
189
+ * Current Codex surfaces can expose model/reasoning_effort overrides directly or
190
+ * omit them. The proxy wording therefore stays schema-agnostic and advertises only
191
+ * the effective candidates described for this collaboration surface.
192
192
  *
193
193
  * The v2 body is budgeted to <= 700 chars (V2_GUIDANCE_CHAR_BUDGET): rules first,
194
194
  * then the preferred model, then the compact roster of configured `subagentModels`
@@ -197,7 +197,43 @@ export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
197
197
  * v2 body with {{model}}/{{effort}}/{{roster}} placeholder substitution (own length,
198
198
  * user-owned); firing gates are unchanged.
199
199
  */
200
- export async function multiAgentGuidanceText(parsed: OcxParsedRequest, injectionModel?: string, injectionEffort?: string, subagentModels?: string[], injectionPrompt?: string): Promise<string | null> {
200
+ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../codex/catalog";
201
+
202
+ export interface MultiAgentGuidanceOptions {
203
+ multiAgentGuidanceEnabled?: boolean;
204
+ injectionModel?: string;
205
+ injectionEffort?: string;
206
+ subagentModels?: string[];
207
+ injectionPrompt?: string;
208
+ }
209
+
210
+ export interface MultiAgentGuidanceDeps {
211
+ resolveEffectiveSubagentRoster?: (
212
+ configuredModels: readonly string[],
213
+ surface: SpawnAgentSurface,
214
+ ) => EffectiveSubagentRoster | Promise<EffectiveSubagentRoster>;
215
+ }
216
+
217
+ async function resolveEffectiveSubagentRoster(
218
+ configuredModels: readonly string[],
219
+ surface: SpawnAgentSurface,
220
+ ): Promise<EffectiveSubagentRoster> {
221
+ const { effectiveSubagentRoster } = await import("../codex/catalog");
222
+ return effectiveSubagentRoster(configuredModels, surface);
223
+ }
224
+
225
+ export async function multiAgentGuidanceText(
226
+ parsed: OcxParsedRequest,
227
+ options: MultiAgentGuidanceOptions = {},
228
+ deps: MultiAgentGuidanceDeps = {},
229
+ ): Promise<string | null> {
230
+ if (options.multiAgentGuidanceEnabled === false) return null;
231
+ const {
232
+ injectionModel,
233
+ injectionEffort,
234
+ subagentModels,
235
+ injectionPrompt,
236
+ } = options;
201
237
  const surface = collabSurface(parsed);
202
238
  if (surface === null) return null;
203
239
 
@@ -205,17 +241,37 @@ export async function multiAgentGuidanceText(parsed: OcxParsedRequest, injection
205
241
  // codex-rs supplies the Proactive text on v2; the proxy only adds model-designation
206
242
  // guidance, and only when there is something concrete to designate: a configured
207
243
  // injectionModel and/or a roster entry that resolves in the injected catalog.
208
- const roster = await subagentRosterText(subagentModels);
244
+ const configuredForGuidance = [
245
+ ...(subagentModels ?? []),
246
+ ...(injectionModel ? [injectionModel] : []),
247
+ ];
248
+ const resolveRoster = deps.resolveEffectiveSubagentRoster ?? resolveEffectiveSubagentRoster;
249
+ const effective = await resolveRoster(configuredForGuidance, "v2");
250
+ const rosterModels = effective.advertised.filter(candidate =>
251
+ (subagentModels ?? []).some(model => slugsEquivalent(model, candidate.model))
252
+ );
253
+ const roster = subagentRosterText(rosterModels);
254
+ const preferred = injectionModel
255
+ ? effective.candidates.find(candidate => slugsEquivalent(injectionModel, candidate.model))
256
+ : undefined;
257
+
258
+ if (isInjectionDebugEnabled() && effective.excluded.length > 0) {
259
+ injectionDebugLog(`[opencodex] multi-agent guidance excluded: ${effective.excluded
260
+ .map(item => `${item.configured}:${item.reason}`)
261
+ .join(", ")}`);
262
+ }
209
263
  if (!injectionModel && roster === "") return null;
210
264
  if (injectionPrompt) {
211
265
  return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster)}</multi_agent_mode>`;
212
266
  }
213
- let text = "spawn_agent also accepts hidden \"model\" and \"reasoning_effort\" string arguments "
214
- + "(not in the schema, but parsed and applied) never claim sub-agent models cannot be selected. "
215
- + "When setting either, set fork_turns to \"none\" (or e.g. \"3\"; full-history forks reject overrides) "
216
- + "and make the message self-contained.";
217
- if (injectionModel) {
218
- text += ` Preferred sub-agent: model "${injectionModel}"`
267
+ if (!preferred && roster === "") return null;
268
+ let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "
269
+ + "use only models listed for this collaboration surface. "
270
+ + "When setting either override, set fork_turns to \"none\" "
271
+ + "(or a positive turn count such as \"3\"; full-history forks reject overrides) "
272
+ + "and make the task message self-contained.";
273
+ if (preferred) {
274
+ text += ` Preferred sub-agent: model "${preferred.model}"`
219
275
  + (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "")
220
276
  + " — use it unless the user names another.";
221
277
  }
@@ -246,25 +302,21 @@ function applyInjectionPlaceholders(prompt: string, model?: string, effort?: str
246
302
  }
247
303
 
248
304
  /**
249
- * Compact one-line roster of configured sub-agent models, or "" when no configured
250
- * model resolves to a catalog entry. Efforts come from the injected catalog
251
- * (catalogModelEfforts) so only rungs codex-rs will actually accept are advertised.
305
+ * Compact one-line roster of effective sub-agent candidates, or "" when empty.
306
+ * Efforts come from the injected catalog so only rungs codex-rs will actually
307
+ * accept are advertised.
252
308
  */
253
- async function subagentRosterText(subagentModels?: string[]): Promise<string> {
254
- const featured = (subagentModels ?? []).filter(id => typeof id === "string" && id.trim().length > 0);
255
- if (featured.length === 0) return "";
256
- const { catalogModelEfforts } = await import("../codex/catalog");
257
- const efforts = catalogModelEfforts(featured);
258
- const resolved = featured.filter(id => efforts.has(id));
259
- if (resolved.length === 0) return "";
260
- const ladders = new Set(resolved.map(id => efforts.get(id)!.join("/")));
261
- if (ladders.size === 1) {
262
- // Shared ladder (the common case: the injected catalog advertises one rung set)
263
- // -> state it once instead of per model, keeping the roster inside the budget.
264
- const ids = resolved.map(id => `"${id}"`).join(", ");
265
- return ` Available models (reasoning_effort ${[...ladders][0]}): ${ids}.`;
309
+ function subagentRosterText(models: Array<{ model: string; efforts: string[] }>): string {
310
+ if (models.length === 0) return "";
311
+ const ladders = new Set(models.map(model => model.efforts.join("/")));
312
+ if (!ladders.has("") && ladders.size === 1) {
313
+ return ` Available models (reasoning_effort ${[...ladders][0]}): ${models
314
+ .map(model => `"${model.model}"`)
315
+ .join(", ")}.`;
266
316
  }
267
- const entries = resolved.map(id => `"${id}" (${efforts.get(id)!.join("/")})`);
317
+ const entries = models.map(model => model.efforts.length > 0
318
+ ? `"${model.model}" (${model.efforts.join("/")})`
319
+ : `"${model.model}"`);
268
320
  return ` Available models (valid reasoning_effort): ${entries.join(", ")}.`;
269
321
  }
270
322
 
@@ -311,6 +363,57 @@ function looksLikeBackendCiphertext(payload: string): boolean {
311
363
  */
312
364
  const FERNET_TOKEN_RUN = /gAAAA[A-Za-z0-9_-]{60,}={0,2}/g;
313
365
 
366
+ const AGENT_MESSAGE_ROUTING_ENVELOPE = /(?:^|\n)Message Type\s*:\s*NEW_TASK[^\n]*\nTask name\s*:[^\n]*\nSender\s*:[^\n]*\nPayload\s*:\s*(?:\n|$)/gi;
367
+ const AGENT_MESSAGE_CONTROL_PREAMBLE = /(?:^|\n)\[CXC-(?:LEAF-GUARD|SKILL-AFFORDANCE)\][\s\S]*?(?=\n{2,}|$)/g;
368
+
369
+ /**
370
+ * True when a V2 agent message contains a backend-minted Fernet task but no
371
+ * provider-readable task text. The routing envelope and hook-added control
372
+ * preambles are metadata, not actionable work. Inspect this before spawn-message
373
+ * sanitization splits mixed encrypted slots into plaintext and ciphertext parts.
374
+ */
375
+ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
376
+ if (!Array.isArray(input)) return false;
377
+
378
+ return input.some(item => {
379
+ if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
380
+ return false;
381
+ }
382
+
383
+ const content = (item as { content?: unknown }).content;
384
+ if (!Array.isArray(content)) return false;
385
+
386
+ let hasFernetTask = false;
387
+ const readableParts: string[] = [];
388
+ for (const part of content) {
389
+ if (!part || typeof part !== "object") continue;
390
+ const record = part as { type?: unknown; text?: unknown; encrypted_content?: unknown };
391
+ if (
392
+ (record.type === "input_text" || record.type === "text" || record.type === "output_text")
393
+ && typeof record.text === "string"
394
+ ) {
395
+ readableParts.push(record.text);
396
+ continue;
397
+ }
398
+ if (record.type !== "encrypted_content" || typeof record.encrypted_content !== "string") {
399
+ continue;
400
+ }
401
+
402
+ const withoutFernet = record.encrypted_content.replace(FERNET_TOKEN_RUN, "\n\n");
403
+ if (withoutFernet !== record.encrypted_content) hasFernetTask = true;
404
+ readableParts.push(withoutFernet);
405
+ }
406
+
407
+ if (!hasFernetTask) return false;
408
+ const readableTask = readableParts
409
+ .join("\n\n")
410
+ .replace(AGENT_MESSAGE_ROUTING_ENVELOPE, "\n")
411
+ .replace(AGENT_MESSAGE_CONTROL_PREAMBLE, "\n")
412
+ .trim();
413
+ return readableTask.length === 0;
414
+ });
415
+ }
416
+
314
417
  /**
315
418
  * Split a non-ciphertext encrypted slot into ordered parts: prose becomes input_text,
316
419
  * embedded Fernet blobs stay encrypted_content so the backend can still decrypt the
@@ -607,10 +710,14 @@ async function handleComboResponses(
607
710
  logCtx: RequestLogContext,
608
711
  options: HandleResponsesOptions,
609
712
  ): Promise<Response> {
713
+ const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
714
+ ? (rawBody as { model: string }).model
715
+ : `combo/${comboId}`;
610
716
  Object.assign(logCtx, {
611
- requestedModel: `combo/${comboId}`,
612
- model: `combo/${comboId}`,
717
+ requestedModel,
718
+ model: requestedModel,
613
719
  provider: "combo",
720
+ comboId,
614
721
  });
615
722
  const combo = getCombo(config, comboId);
616
723
  if (!combo) {
@@ -713,9 +820,10 @@ async function handleComboResponses(
713
820
  attemptRetained = true;
714
821
  noteComboSuccess(comboId, combo, pick.target);
715
822
  Object.assign(logCtx, childLog, {
716
- requestedModel: `combo/${comboId}`,
717
- model: `combo/${comboId}`,
823
+ requestedModel,
824
+ model: requestedModel,
718
825
  provider: "combo",
826
+ comboId,
719
827
  attempts: logCtx.attempts,
720
828
  activeAttempt: attempt,
721
829
  activeAttemptStartedAt: started,
@@ -763,9 +871,10 @@ async function handleComboResponses(
763
871
  lastFailure = failure.response;
764
872
  if (comboFailureDecision(response.status, failure.classificationText) === "stop") {
765
873
  Object.assign(logCtx, childLog, {
766
- requestedModel: `combo/${comboId}`,
767
- model: `combo/${comboId}`,
874
+ requestedModel,
875
+ model: requestedModel,
768
876
  provider: "combo",
877
+ comboId,
769
878
  attempts: logCtx.attempts,
770
879
  activeAttempt: undefined,
771
880
  activeAttemptStartedAt: undefined,
@@ -795,13 +904,16 @@ export async function handleResponses(
795
904
  } catch (err) {
796
905
  return decodeRequestErrorResponse(err, "responses");
797
906
  }
798
- const comboId = !options.comboAttempt ? comboIdFromRawBody(body) : null;
907
+ const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
799
908
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
800
909
  return handleComboResponses(req, body, comboId, config, logCtx, options);
801
910
  }
802
911
  const originalBody = body;
803
912
  body = expandPreviousResponseInput(body);
804
913
  const previousResponseInputExpanded = body !== originalBody;
914
+ const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
915
+ (body as { input?: unknown } | undefined)?.input,
916
+ );
805
917
 
806
918
  // Spawn-message compatibility (both directions): agent_message task payloads ride in
807
919
  // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
@@ -822,7 +934,8 @@ export async function handleResponses(
822
934
  try {
823
935
  parsed = parseRequest(body);
824
936
  if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
825
- parsed._cursorConversationId = previousResponseConversationId(parsed.previousResponseId);
937
+ parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
938
+ parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
826
939
  } catch (err) {
827
940
  return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
828
941
  }
@@ -859,6 +972,17 @@ export async function handleResponses(
859
972
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
860
973
  }
861
974
 
975
+ // The canonical ChatGPT backend can decrypt its V2 Fernet task tokens; routed
976
+ // providers cannot. Reject the raw-input classification before adapter construction
977
+ // or provider dispatch so an unreadable worker task cannot trigger a cost storm.
978
+ if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
979
+ return formatErrorResponse(
980
+ 400,
981
+ "invalid_request_error",
982
+ "Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.",
983
+ );
984
+ }
985
+
862
986
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace
863
987
  // (e.g. "opencode-go/deepseek-v4-pro" → "deepseek-v4-pro"). Adapters read parsed.modelId,
864
988
  // and the passthrough adapter serializes _rawBody, so rewrite both.
@@ -888,17 +1012,23 @@ export async function handleResponses(
888
1012
  }
889
1013
 
890
1014
  // Multi-agent guidance shim: codex-rs emits its Proactive delegation developer
891
- // message only on the v2 surface. The proxy fills both gaps: the Proactive text
892
- // for v1 collab surfaces at the top tier, and the sub-agent model designation on
893
- // BOTH surfaces when an injectionModel is configured (v2 additionally gets the
894
- // fork_turns override rules). The surface is judged from the request's own tool list.
895
- // Runs BEFORE the mock-max clamp below so the synthetic top tier (ultra arrives
896
- // as max on the codex wire) is still visible. Both request shapes are rewritten.
1015
+ // message only on the v2 surface. The proxy fills the gaps: the Proactive text
1016
+ // for v1 collab surfaces at the top tier (no model designation on v1), and the
1017
+ // sub-agent model/roster designation plus fork_turns override rules on v2.
1018
+ // The surface is judged from the request's own tool list. Runs BEFORE the
1019
+ // mock-max clamp below so the synthetic top tier (ultra arrives as max on the
1020
+ // codex wire) is still visible. Both request shapes are rewritten.
897
1021
  {
898
- const guidance = await multiAgentGuidanceText(parsed, config.injectionModel, config.injectionEffort, config.subagentModels, config.injectionPrompt);
1022
+ const guidance = await multiAgentGuidanceText(parsed, {
1023
+ multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
1024
+ injectionModel: config.injectionModel,
1025
+ injectionEffort: config.injectionEffort,
1026
+ subagentModels: config.subagentModels,
1027
+ injectionPrompt: config.injectionPrompt,
1028
+ });
899
1029
  if (guidance) {
900
1030
  injectDeveloperMessage(parsed, guidance);
901
- if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, ${guidance.length} chars)`);
1031
+ if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
902
1032
  } else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
903
1033
  injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
904
1034
  }
@@ -935,16 +1065,17 @@ export async function handleResponses(
935
1065
  // receive `max` when the user picks Ultra (codex converts ultra->max client-side).
936
1066
  // Clamp to the model's highest real effort BEFORE any adapter — the ChatGPT
937
1067
  // passthrough serializes _rawBody verbatim, so both shapes must be rewritten.
938
- // GUARD: judge nativeness by the ORIGINALLY REQUESTED id (logCtx.requestedModel),
939
- // never by route.modelId routing strips the "<provider>/" namespace, so a routed
940
- // model (anthropic/claude-opus-4-6, real max) would masquerade as an off-snapshot
941
- // bare native and get wrongly clamped. Routed efforts belong to their adapters.
1068
+ // GUARD: judge nativeness by BOTH the originally requested id (logCtx.requestedModel)
1069
+ // and the resolved provider identity. Routing strips the "<provider>/" namespace, and
1070
+ // some third-party providers expose bare `defaultModel` selectors, so route.modelId
1071
+ // alone can make a routed model masquerade as an off-snapshot native. Only the
1072
+ // canonical built-in ChatGPT forward provider should receive the native clamp.
942
1073
  {
943
1074
  const requestedModelId = logCtx.requestedModel ?? route.modelId;
944
- const { nativeEffortClamp } = await import("../codex/catalog");
945
- const clamped = requestedModelId.includes("/")
946
- ? null
947
- : nativeEffortClamp(route.modelId, parsed.options.reasoning);
1075
+ const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../codex/catalog");
1076
+ const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, requestedModelId)
1077
+ ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
1078
+ : null;
948
1079
  if (clamped) {
949
1080
  parsed.options.reasoning = clamped;
950
1081
  const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
@@ -999,7 +1130,7 @@ export async function handleResponses(
999
1130
 
1000
1131
  // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
1001
1132
  // existing openai-chat / anthropic adapters authenticate with no change.
1002
- const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot")
1133
+ const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
1003
1134
  && route.provider.authMode === "oauth";
1004
1135
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
1005
1136
  if (route.provider.authMode === "oauth") {
@@ -1036,6 +1167,14 @@ export async function handleResponses(
1036
1167
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
1037
1168
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
1038
1169
 
1170
+ if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
1171
+ return formatErrorResponse(
1172
+ 400,
1173
+ "invalid_request_error",
1174
+ "Kiro continuation state is missing; start a new session instead of reusing this previous_response_id.",
1175
+ );
1176
+ }
1177
+
1039
1178
  let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
1040
1179
  const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
1041
1180
  const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
@@ -1074,6 +1213,30 @@ export async function handleResponses(
1074
1213
 
1075
1214
  const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
1076
1215
 
1216
+ const continuationStateForResponse = (
1217
+ emitted?: OcxProviderContinuationState,
1218
+ ): OcxProviderContinuationState | undefined => {
1219
+ const cursorConversationId = parsed._cursorConversationId;
1220
+ const inherited = parsed._providerContinuation;
1221
+ if (!emitted && !inherited && !cursorConversationId) return undefined;
1222
+ return {
1223
+ ...(inherited ?? {}),
1224
+ ...(emitted ?? {}),
1225
+ ...((inherited?.kiro || emitted?.kiro)
1226
+ ? { kiro: { ...(inherited?.kiro ?? {}), ...(emitted?.kiro ?? {}) } }
1227
+ : {}),
1228
+ ...(cursorConversationId
1229
+ ? {
1230
+ cursor: {
1231
+ ...(inherited?.cursor ?? {}),
1232
+ ...(emitted?.cursor ?? {}),
1233
+ conversationId: cursorConversationId,
1234
+ },
1235
+ }
1236
+ : {}),
1237
+ };
1238
+ };
1239
+
1077
1240
  // Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
1078
1241
  // one `{type:"compaction"}` output item (codex-rs compact_remote_v2.rs). Passthrough handles it
1079
1242
  // natively upstream; here we run the routed model as a plain summarizer — no tools, no web-search
@@ -1215,6 +1378,7 @@ export async function handleResponses(
1215
1378
  // background for terminal-outcome/quota inspection only.
1216
1379
  if (upstreamResponse.ok && isEventStream && upstreamResponse.body) {
1217
1380
  const [nativeBody, inspectBody] = upstreamResponse.body.tee();
1381
+ const repairConfig = route.provider.responsesItemIdRepair;
1218
1382
  const turnAc = new AbortController();
1219
1383
  linkAbortSignal(upstream, turnAc.signal);
1220
1384
  registerTurn(turnAc);
@@ -1251,9 +1415,12 @@ export async function handleResponses(
1251
1415
  // win32 must keep the pure native relay (Bun#32111 JS-sink segfault); elsewhere a JS pull
1252
1416
  // relay is established practice (relayWithAbort, relaySseWithHeartbeat) and lets a
1253
1417
  // mid-stream reset end with a clean response.failed terminal instead of a raw socket error.
1254
- const clientBody = process.platform === "win32"
1418
+ const repairedBody = hasResponsesItemIdRepair(repairConfig)
1419
+ ? relaySseWithResponsesItemIdRepair(nativeBody, repairConfig!)
1420
+ : nativeBody;
1421
+ const clientBody = process.platform === "win32" && !hasResponsesItemIdRepair(repairConfig)
1255
1422
  ? nativeBody
1256
- : relaySseWithFailedTail(nativeBody, upstream);
1423
+ : relaySseWithFailedTail(repairedBody, upstream);
1257
1424
  return markNativePassthroughSseResponse(new Response(clientBody, {
1258
1425
  status: upstreamResponse.status,
1259
1426
  headers,
@@ -1335,7 +1502,15 @@ export async function handleResponses(
1335
1502
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1336
1503
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
1337
1504
  ...(routedCompaction ? { compaction: true } : {}),
1338
- ...(routedCompaction ? {} : { onCompletedResponse: (response: Record<string, unknown>) => rememberResponseState(parsed._rawBody, response, parsed._cursorConversationId) }),
1505
+ ...(routedCompaction ? {} : {
1506
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
1507
+ rememberResponseState(
1508
+ parsed._rawBody,
1509
+ response,
1510
+ continuationStateForResponse(providerState),
1511
+ adapter.name === "kiro" ? { force: true } : undefined,
1512
+ ),
1513
+ }),
1339
1514
  },
1340
1515
  );
1341
1516
  const bridgeTurnAc = new AbortController();
@@ -1356,14 +1531,23 @@ export async function handleResponses(
1356
1531
  return formatErrorResponse(502, "upstream_error", redactSecretString(message));
1357
1532
  }
1358
1533
  }
1534
+ let providerState: OcxProviderContinuationState | undefined;
1359
1535
  const json = buildResponseJSON(events, parsed.modelId, {
1360
1536
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1361
1537
  toolNsMap,
1362
1538
  freeformToolNames,
1363
1539
  toolSearchToolNames,
1364
1540
  ...(routedCompaction ? { compaction: true } : {}),
1541
+ onProviderState: state => { providerState = state; },
1365
1542
  });
1366
- if (!routedCompaction) rememberResponseState(parsed._rawBody, json, parsed._cursorConversationId);
1543
+ if (!routedCompaction) {
1544
+ rememberResponseState(
1545
+ parsed._rawBody,
1546
+ json,
1547
+ continuationStateForResponse(providerState),
1548
+ adapter.name === "kiro" ? { force: true } : undefined,
1549
+ );
1550
+ }
1367
1551
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
1368
1552
  }
1369
1553
 
@@ -1420,17 +1604,18 @@ export async function handleResponses(
1420
1604
  const upstream = new AbortController();
1421
1605
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
1422
1606
  const connectMs = config.connectTimeoutMs ?? 200_000;
1607
+ let activeAdapter = adapter;
1423
1608
 
1424
- const request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
1609
+ const request = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders });
1425
1610
  const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
1426
1611
  ? request.usageLog.inputTokens
1427
1612
  : undefined;
1428
1613
  if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
1429
1614
  let upstreamResponse: Response;
1430
1615
  try {
1431
- if (adapter.fetchResponse) {
1616
+ if (activeAdapter.fetchResponse) {
1432
1617
  noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
1433
- upstreamResponse = await adapter.fetchResponse(request, {
1618
+ upstreamResponse = await activeAdapter.fetchResponse(request, {
1434
1619
  abortSignal: upstream.signal,
1435
1620
  timeoutMs: connectMs,
1436
1621
  stream: parsed.stream,
@@ -1464,7 +1649,6 @@ export async function handleResponses(
1464
1649
  // both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
1465
1650
  // adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
1466
1651
  // 413→429 rotation cannot silently undo the tightening.
1467
- let activeAdapter = adapter;
1468
1652
  let imageTierBias = 0;
1469
1653
  let imageRetryAttempted = false;
1470
1654
  let oauth401ReplayAttempted = false;
@@ -1591,7 +1775,7 @@ export async function handleResponses(
1591
1775
  }
1592
1776
 
1593
1777
  if (parsed.stream) {
1594
- const eventStream = adapter.parseStream(upstreamResponse);
1778
+ const eventStream = activeAdapter.parseStream(upstreamResponse);
1595
1779
  const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
1596
1780
  const sseStream = bridgeToResponsesSSE(
1597
1781
  eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
@@ -1605,7 +1789,15 @@ export async function handleResponses(
1605
1789
  // Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
1606
1790
  // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
1607
1791
  // giant stale chain Codex just replaced.
1608
- ...(routedCompaction ? {} : { onCompletedResponse: (response: Record<string, unknown>) => rememberResponseState(parsed._rawBody, response, parsed._cursorConversationId) }),
1792
+ ...(routedCompaction ? {} : {
1793
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
1794
+ rememberResponseState(
1795
+ parsed._rawBody,
1796
+ response,
1797
+ continuationStateForResponse(providerState),
1798
+ activeAdapter.name === "kiro" ? { force: true } : undefined,
1799
+ ),
1800
+ }),
1609
1801
  },
1610
1802
  );
1611
1803
  const bridgeTurnAc = new AbortController();
@@ -1615,27 +1807,36 @@ export async function handleResponses(
1615
1807
  });
1616
1808
  }
1617
1809
 
1618
- if (adapter.parseResponse) {
1810
+ if (activeAdapter.parseResponse) {
1619
1811
  let events: AdapterEvent[];
1620
1812
  try {
1621
- events = await adapter.parseResponse(upstreamResponse);
1813
+ events = await activeAdapter.parseResponse(upstreamResponse);
1622
1814
  } finally {
1623
1815
  cleanupUpstreamAbort();
1624
1816
  }
1625
1817
  const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
1818
+ let providerState: OcxProviderContinuationState | undefined;
1626
1819
  const json = buildResponseJSON(events, parsed.modelId, {
1627
1820
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1628
1821
  toolNsMap,
1629
1822
  freeformToolNames,
1630
1823
  toolSearchToolNames,
1631
1824
  ...(routedCompaction ? { compaction: true } : {}),
1825
+ onProviderState: state => { providerState = state; },
1632
1826
  });
1633
1827
  // See the streaming branch: compaction turns skip the continuation cache.
1634
- if (!routedCompaction) rememberResponseState(parsed._rawBody, json, parsed._cursorConversationId);
1828
+ if (!routedCompaction) {
1829
+ rememberResponseState(
1830
+ parsed._rawBody,
1831
+ json,
1832
+ continuationStateForResponse(providerState),
1833
+ activeAdapter.name === "kiro" ? { force: true } : undefined,
1834
+ );
1835
+ }
1635
1836
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
1636
1837
  }
1637
1838
 
1638
- return formatErrorResponse(500, "internal_error", "Non-streaming not supported by this adapter");
1839
+ return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
1639
1840
  }
1640
1841
 
1641
1842
  export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal): () => void {