@coseung2/opencodex 2.8.0-cs.16 → 2.8.0-cs.17

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 (52) hide show
  1. package/gui/dist/assets/{index-BZGMtkmp.js → index-Ch-99jy3.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +12 -0
  5. package/src/adapters/kiro-calibration.ts +83 -0
  6. package/src/adapters/kiro-constants.ts +11 -2
  7. package/src/adapters/kiro-errors.ts +11 -0
  8. package/src/adapters/kiro-events.ts +19 -1
  9. package/src/adapters/kiro-thinking.ts +18 -2
  10. package/src/adapters/kiro-tools.ts +12 -3
  11. package/src/adapters/kiro.ts +300 -78
  12. package/src/adapters/openai-chat.ts +1 -42
  13. package/src/adapters/openai-responses.ts +93 -14
  14. package/src/adapters/xai-schema-analysis.ts +78 -0
  15. package/src/adapters/xai-tool-schema.ts +274 -0
  16. package/src/adapters/xai-web-search.ts +138 -0
  17. package/src/bridge.ts +61 -6
  18. package/src/codex/catalog/effort.ts +4 -2
  19. package/src/codex/catalog/metadata.ts +38 -9
  20. package/src/codex/catalog/parsing.ts +17 -2
  21. package/src/codex/catalog/provider-fetch.ts +9 -3
  22. package/src/codex/catalog/sync.ts +8 -5
  23. package/src/codex/data/upstream-models.json +169 -0
  24. package/src/grok/inject.ts +1 -1
  25. package/src/lib/token-estimate.ts +42 -38
  26. package/src/lib/translator-budget.ts +34 -0
  27. package/src/oauth/index.ts +10 -4
  28. package/src/oauth/kiro.ts +71 -6
  29. package/src/oauth/store.ts +3 -1
  30. package/src/oauth/types.ts +4 -0
  31. package/src/providers/derive.ts +7 -5
  32. package/src/providers/opencode-go-transport.ts +18 -0
  33. package/src/providers/registry.ts +34 -10
  34. package/src/providers/xai-transport.ts +10 -0
  35. package/src/responses/compaction.ts +8 -1
  36. package/src/responses/namespace-aliases.ts +56 -0
  37. package/src/responses/parser.ts +12 -0
  38. package/src/responses/reasoning-envelope.ts +9 -1
  39. package/src/responses/snapshot-policy.ts +108 -0
  40. package/src/responses/state.ts +23 -10
  41. package/src/responses/turn-termination.ts +108 -0
  42. package/src/responses/xai-custom-tool-compat.ts +237 -0
  43. package/src/server/grok-responses-snapshot-repair.ts +338 -0
  44. package/src/server/index.ts +2 -1
  45. package/src/server/relay-eager.ts +1 -0
  46. package/src/server/responses/core.ts +173 -13
  47. package/src/server/responses-image-gen-repair.ts +2 -2
  48. package/src/server/sse-payload-rewrite.ts +20 -3
  49. package/src/types.ts +10 -1
  50. package/src/usage/cost.ts +0 -0
  51. package/src/usage/expected-prices.ts +7 -0
  52. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
@@ -1,10 +1,11 @@
1
1
  import { decodeEventStream } from "../lib/eventstream-decoder";
2
2
  import { estimateTokens } from "../lib/token-estimate";
3
3
  import { debugProviderDiagnostic } from "../lib/debug";
4
- import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
4
+ import { resolveKiroApiRegion, resolveKiroRequestProfile } from "../oauth/kiro";
5
5
  import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
6
6
  import { modelRecordValue } from "../reasoning-effort";
7
7
  import { parseKiroEvent } from "./kiro-events";
8
+ import { calibrateKiroEstimate, recordKiroCalibration, rekeyKiroCalibration } from "./kiro-calibration";
8
9
  import {
9
10
  classifyKiroEventError,
10
11
  classifyKiroHttpError,
@@ -17,8 +18,11 @@ import { KiroThinkingParser } from "./kiro-thinking";
17
18
  import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
18
19
  import { createKiroToolNameRegistry, fallbackToolUseId, fingerprint, invocationId, isValidKiroConversationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
19
20
  import { namespacedToolName } from "../types";
21
+ import { hasRecordedTrailingDeliveredFinalAnswer } from "../responses/turn-termination";
20
22
  import {
21
23
  isTranslatorBudgetExceededError,
24
+ releaseTranslatedEvent,
25
+ retainTranslatedEvent,
22
26
  type TranslatorBudget,
23
27
  } from "../lib/translator-budget";
24
28
  import type {
@@ -42,6 +46,7 @@ import { convertKiroToolContext } from "./kiro-tools";
42
46
  import { neutralizeIdentity } from "./identity";
43
47
  import { buildNonOpenAIToolCatalogNudgeFromNames } from "./tool-catalog-nudge";
44
48
  import {
49
+ KIRO_ANSWER_DELIVERED_MESSAGE,
45
50
  KIRO_COMPLETION_INSTRUCTIONS,
46
51
  KIRO_COMPLETION_RETRY_MESSAGE,
47
52
  KIRO_COMPLETION_TOOL_NAME,
@@ -99,7 +104,11 @@ interface KiroUserInputMessage {
99
104
  }
100
105
  interface KiroHistoryEntry {
101
106
  userInputMessage?: KiroUserInputMessage;
102
- assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[] };
107
+ assistantResponseMessage?: {
108
+ content: string;
109
+ toolUses?: KiroToolUse[];
110
+ reasoningContent?: { redactedContent: string };
111
+ };
103
112
  }
104
113
 
105
114
  function kiroToolWireNames(tools: readonly unknown[]): string[] {
@@ -176,6 +185,11 @@ function estimateKiroTokens(text: string, modelId?: string): number {
176
185
  return estimateTokens(text, modelId ? `kiro/${modelId}` : "kiro");
177
186
  }
178
187
 
188
+ // Per-entry JSON/role framing is invisible to the text walker but grows with conversation length.
189
+ const KIRO_ENTRY_FRAMING_TOKENS = 12;
190
+ // Newlines, quotes, tabs and backslashes expand when serialized onto the Kiro JSON wire.
191
+ const KIRO_JSON_ESCAPE_EXPANSION = 1.12;
192
+
179
193
  function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelId: string): number {
180
194
  const conversationState = (payload as {
181
195
  conversationState?: {
@@ -206,7 +220,9 @@ function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelI
206
220
  if (assistant.toolUses?.length) parts.push(serializeForUsage(assistant.toolUses));
207
221
  }
208
222
  }
209
- return estimateKiroTokens(parts.join("\n"), modelId) + imageTokens;
223
+ return Math.ceil(estimateKiroTokens(parts.join("\n"), modelId) * KIRO_JSON_ESCAPE_EXPANSION)
224
+ + imageTokens
225
+ + entries.length * KIRO_ENTRY_FRAMING_TOKENS;
210
226
  }
211
227
 
212
228
  function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
@@ -312,21 +328,61 @@ function validateKiroCapabilities(parsed: OcxParsedRequest): void {
312
328
  if (choice !== undefined && choice !== "auto" && choice !== "none") {
313
329
  throw new Error("Kiro supports only automatic tool choice or tool_choice:none");
314
330
  }
315
- if (parsed.options.parallelToolCalls === true) {
316
- throw new Error("Kiro does not support parallel tool calls");
317
- }
318
331
  if (parsed.options.serviceTier !== undefined) {
319
332
  throw new Error("Kiro does not support service tiers");
320
333
  }
321
- const raw = parsed._rawBody as Record<string, unknown> | undefined;
322
- if (parsed._structuredOutput || raw?.text !== undefined) {
323
- throw new Error("Kiro does not support Responses text controls or structured output");
334
+ // Structured output is a real contract Kiro cannot honour: the wire has no
335
+ // schema-constrained response mode, so a caller expecting parseable JSON would receive
336
+ // prose and fail downstream. Refuse it.
337
+ //
338
+ // The rest of the Responses `text` object is not that. `text.verbosity` is a length
339
+ // preference and `text.format: {type:"text"}` is ordinary prose — the default output
340
+ // mode, which no capability flag governs and every correct client may send. Testing
341
+ // `_rawBody.text !== undefined` refused those turns for the mere PRESENCE of the key,
342
+ // the same mistake db040e70f removed one condition earlier where a permissive
343
+ // `parallel_tool_calls` hint was read as a requirement.
344
+ //
345
+ // Nothing needs stripping the way openai-responses strips a no-op verbosity:
346
+ // buildKiroPayload composes conversationState field by field from `parsed` and never
347
+ // spreads `_rawBody`, so a tolerated control is dropped by construction. The test
348
+ // asserts that absence so it stays true.
349
+ if (parsed._structuredOutput) {
350
+ throw new Error("Kiro does not support Responses structured output");
324
351
  }
325
352
  }
326
353
 
327
354
  type KiroTurn =
328
- | { kind: "user"; content: string; images: KiroImage[]; toolResults: KiroToolResult[] }
329
- | { kind: "assistant"; content: string; toolUses: KiroToolUse[] };
355
+ | {
356
+ kind: "user";
357
+ content: string;
358
+ images: KiroImage[];
359
+ toolResults: KiroToolResult[];
360
+ /** True only for the proxy-generated acknowledgement after a delivered final answer. */
361
+ answerDeliveredAck?: boolean;
362
+ }
363
+ | {
364
+ kind: "assistant";
365
+ content: string;
366
+ toolUses: KiroToolUse[];
367
+ redactedReasoning?: string;
368
+ /** A Responses final_answer already shown to the user; this turn must not be resumed. */
369
+ finalAnswer?: boolean;
370
+ };
371
+
372
+ /** True only when no later user/tool-result work follows the delivered final answer. */
373
+ function hasTrailingDeliveredFinalAnswer(messages: readonly OcxMessage[], parsed?: OcxParsedRequest): boolean {
374
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
375
+ const message = messages[index];
376
+ if (message.role !== "assistant") return false;
377
+ const assistant = message as OcxAssistantMessage;
378
+ if ((assistant.content ?? []).some(part => part.type === "toolCall")) return false;
379
+ const hasText = (assistant.content ?? []).some(part => part.type === "text" && part.text.trim());
380
+ if (!hasText) continue;
381
+ return assistant.phase === "final_answer"
382
+ || (parsed !== undefined && hasRecordedTrailingDeliveredFinalAnswer(parsed, messages));
383
+ }
384
+ return false;
385
+ }
330
386
 
331
387
  function appendTurnText(target: string, next: string): string {
332
388
  if (!next) return target;
@@ -381,23 +437,39 @@ function validateKiroConversationState(history: KiroHistoryEntry[], currentMessa
381
437
  function boundedInjectedInstruction(text: string, used: { value: number }): string | undefined {
382
438
  const remaining = MAX_KIRO_INJECTED_INSTRUCTION_CHARS - used.value;
383
439
  if (remaining <= 0 || !text) return undefined;
384
- const result = text.length <= remaining ? text : text.slice(0, remaining);
440
+ let result = text.length <= remaining ? text : text.slice(0, remaining);
441
+ // Never end the slice on a lone high surrogate: encoding it substitutes
442
+ // U+FFFD into the injected instruction. One step back keeps a valid pair
443
+ // out instead of a broken half.
444
+ if (result.length > 0) {
445
+ const last = result.charCodeAt(result.length - 1);
446
+ if (last >= 0xd800 && last <= 0xdbff) result = result.slice(0, -1);
447
+ }
385
448
  used.value += result.length;
386
- return result;
449
+ return result.length > 0 ? result : undefined;
450
+ }
451
+
452
+ /** Test-only: exercise the surrogate-safe instruction bound directly. */
453
+ export function boundedInjectedInstructionForTests(text: string, used: { value: number }): string | undefined {
454
+ return boundedInjectedInstruction(text, used);
387
455
  }
388
456
 
389
457
  function kiroCompletionTool(): Record<string, unknown> {
390
458
  return {
391
459
  toolSpecification: {
392
460
  name: KIRO_COMPLETION_TOOL_NAME,
393
- description: "Finish the task and return the complete user-facing final answer. Call only when no more work or tool calls are needed.",
461
+ // The shared catalog nudge treats every listed tool as if a result must return. This one is
462
+ // different: a valid call is the terminal itself. State that at the schema surface where the
463
+ // model chooses tools, otherwise a finished model can keep working while waiting for a result
464
+ // that will never exist.
465
+ description: "Terminal completion channel, not an ordinary work tool. When the task is fully complete and no more work or tool calls are needed, you must call this tool exactly once instead of providing the final answer as ordinary assistant text. Call it the same way when you cannot continue until the user supplies a decision, information, or a clarification that only they can give: the question itself is the answer. Put the complete user-facing final answer in `answer`. The call is complete when issued: it ends the turn, returns no tool result, and no text or tool call may follow it.",
394
466
  inputSchema: {
395
467
  json: {
396
468
  type: "object",
397
469
  properties: {
398
470
  answer: {
399
471
  type: "string",
400
- description: "The complete final answer to show the user.",
472
+ description: "The complete final answer to show the user, or the blocking question you need the user to answer before you can continue.",
401
473
  },
402
474
  },
403
475
  required: ["answer"],
@@ -423,8 +495,12 @@ export function buildKiroPayload(
423
495
  const registry = createKiroToolNameRegistry();
424
496
  const toolContext = convertKiroToolContext(parsed, registry);
425
497
  const ordinaryTools = toolContext.tools;
498
+ // A replay that already ends in a delivered final answer has nothing left to complete. Keeping
499
+ // the private completion tool enabled here reopens the closed task even if the trailing prompt is
500
+ // neutral, because the model is still instructed to produce another terminal answer.
501
+ const trailingDeliveredAnswer = hasTrailingDeliveredFinalAnswer(kiroPayloadMessages(parsed), parsed);
426
502
  const completionMode: KiroCompletionMode = forcedCompletionMode
427
- ?? (ordinaryTools.length > 0 ? "required" : "disabled");
503
+ ?? (ordinaryTools.length > 0 && !trailingDeliveredAnswer ? "required" : "disabled");
428
504
  const kiroTools = completionMode === "disabled"
429
505
  ? ordinaryTools
430
506
  : [...ordinaryTools, kiroCompletionTool()];
@@ -447,7 +523,7 @@ export function buildKiroPayload(
447
523
  }
448
524
  const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
449
525
  const turns: KiroTurn[] = [];
450
- const priorCalls = new Map<string, { wireName: string }>();
526
+ const priorCalls = new Map<string, { wireName: string; rawId: string }>();
451
527
  const pushUser = (content: string, images: KiroImage[] = [], toolResults: KiroToolResult[] = []): void => {
452
528
  const last = turns.at(-1);
453
529
  if (last?.kind === "user") {
@@ -458,14 +534,53 @@ export function buildKiroPayload(
458
534
  turns.push({ kind: "user", content, images: [...images], toolResults: [...toolResults] });
459
535
  }
460
536
  };
461
- const pushAssistant = (content: string, toolUses: KiroToolUse[]): void => {
537
+ const pushAssistant = (
538
+ content: string,
539
+ toolUses: KiroToolUse[],
540
+ redactedReasoning?: string,
541
+ finalAnswer?: boolean,
542
+ ): void => {
462
543
  const last = turns.at(-1);
463
544
  if (last?.kind === "assistant") {
464
545
  last.content = appendTurnText(last.content, content);
465
546
  last.toolUses.push(...toolUses);
547
+ // Merged turns keep the newest blob: it covers the reasoning up to the merged turn's end.
548
+ if (redactedReasoning) last.redactedReasoning = redactedReasoning;
549
+ // Finality follows the LAST merged component. Commentary after a final answer means work
550
+ // continued and therefore reopens the turn legitimately.
551
+ last.finalAnswer = finalAnswer === true;
466
552
  } else {
467
- turns.push({ kind: "assistant", content, toolUses: [...toolUses] });
553
+ turns.push({
554
+ kind: "assistant",
555
+ content,
556
+ toolUses: [...toolUses],
557
+ ...(redactedReasoning ? { redactedReasoning } : {}),
558
+ ...(finalAnswer ? { finalAnswer: true } : {}),
559
+ });
560
+ }
561
+ };
562
+
563
+ // Codex custom tools may emit several adjacent output items for one invocation (for example
564
+ // progress notifications followed by the final value). Kiro accepts one result per tool use, so
565
+ // coalesce only immediately adjacent outputs whose ORIGINAL ids are identical. The raw-id check
566
+ // is important: normalizeToolId is lossy (`|`, whitespace, truncation), and must never authorize a
567
+ // different result merely because two caller-controlled ids normalize to the same wire id.
568
+ let adjacentResult: {
569
+ rawId: string;
570
+ result: KiroToolResult;
571
+ texts: string[];
572
+ count: number;
573
+ hasImages: boolean;
574
+ } | undefined;
575
+ const finishAdjacentResult = (): void => {
576
+ if (adjacentResult && adjacentResult.count > 1) {
577
+ if (adjacentResult.texts.some(text => text.trim())) {
578
+ adjacentResult.result.content = adjacentResult.texts.map(text => ({ text }));
579
+ } else if (adjacentResult.hasImages || adjacentResult.result.status === "error") {
580
+ adjacentResult.result.content = [{ text: KIRO_EMPTY_TOOL_RESULT_MESSAGE }];
581
+ }
468
582
  }
583
+ adjacentResult = undefined;
469
584
  };
470
585
 
471
586
  const payloadMessages = kiroPayloadMessages(parsed);
@@ -476,6 +591,9 @@ export function buildKiroPayload(
476
591
  for (let messageIndex = 0; messageIndex < payloadMessages.length; messageIndex++) {
477
592
  const msg = payloadMessages[messageIndex];
478
593
  const isReplayedMessage = messageIndex < replayMessagePrefixLength;
594
+ // Preserve source-message adjacency even when the turn normalization below would collapse or
595
+ // skip a structural message.
596
+ if (msg.role !== "toolResult") finishAdjacentResult();
479
597
  if (msg.role === "user" || msg.role === "developer") {
480
598
  const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
481
599
  // Historical text/tool structure remains replayable, but image bytes are scoped to the turn
@@ -508,14 +626,19 @@ export function buildKiroPayload(
508
626
  if (priorCalls.has(toolUseId)) throw new Error(`Kiro history contains duplicate tool call id ${JSON.stringify(tc.id)}`);
509
627
  const wireName = namespacedToolName(tc.namespace, tc.name);
510
628
  const name = registry.alias(wireName);
511
- priorCalls.set(toolUseId, { wireName });
629
+ priorCalls.set(toolUseId, { wireName, rawId: tc.id });
512
630
  return { name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
513
631
  });
514
632
  if (!text && toolUses.length === 0) {
515
633
  const hasReasoning = aMsg.content.some(part => part.type === "thinking" && part.thinking.trim());
516
634
  if (hasReasoning || aMsg.phase === "commentary") continue;
517
635
  }
518
- pushAssistant(text, toolUses);
636
+ pushAssistant(
637
+ text,
638
+ toolUses,
639
+ aMsg.kiroRedactedReasoning,
640
+ aMsg.phase === "final_answer" && toolUses.length === 0,
641
+ );
519
642
  } else if (msg.role === "toolResult") {
520
643
  const tr = msg as OcxToolResultMessage;
521
644
  if (tr.containsEncryptedContent) {
@@ -525,30 +648,57 @@ export function buildKiroPayload(
525
648
  const resultText = text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE;
526
649
  const images = isReplayedMessage ? [] : extractKiroImages(tr.content);
527
650
  const toolUseId = normalizeToolId(tr.toolCallId);
528
- if (!priorCalls.has(toolUseId)) {
651
+ const call = priorCalls.get(toolUseId);
652
+ if (!call || call.rawId !== tr.toolCallId) {
529
653
  throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
530
654
  }
655
+ const last = turns.at(-1);
656
+ if (
657
+ adjacentResult?.rawId === tr.toolCallId
658
+ && last?.kind === "user"
659
+ && last.toolResults.at(-1) === adjacentResult.result
660
+ ) {
661
+ adjacentResult.count += 1;
662
+ adjacentResult.hasImages ||= images.length > 0;
663
+ if (text.length > 0) adjacentResult.texts.push(text);
664
+ last.images.push(...images);
665
+ if (tr.isError) adjacentResult.result.status = "error";
666
+ continue;
667
+ }
668
+ finishAdjacentResult();
531
669
  // Carrier text is a placeholder for an OTHERWISE EMPTY tool-result turn, not a prefix.
532
670
  // Passing it here would push proxy filler AHEAD of a human instruction that Claude Code
533
671
  // sends in the same turn (mid-turn steering / queued_command, issue #543), burying the
534
672
  // newest user intent behind boilerplate. Backfill below only when nothing else speaks.
535
- pushUser("", images, [{
673
+ const result: KiroToolResult = {
536
674
  content: [{ text: resultText }],
537
675
  status: tr.isError ? "error" : "success",
538
676
  toolUseId,
539
- }]);
677
+ };
678
+ pushUser("", images, [result]);
679
+ adjacentResult = {
680
+ rawId: tr.toolCallId,
681
+ result,
682
+ texts: text.length > 0 ? [text] : [],
683
+ count: 1,
684
+ hasImages: images.length > 0,
685
+ };
540
686
  }
541
687
  }
688
+ finishAdjacentResult();
542
689
 
543
690
  if (turns.length === 0 || turns[0].kind === "assistant") {
544
691
  turns.unshift({ kind: "user", content: KIRO_CONTINUATION_MESSAGE, images: [], toolResults: [] });
545
692
  }
546
- if (turns.at(-1)?.kind === "assistant") {
693
+ const trailingTurn = turns.at(-1);
694
+ if (trailingTurn?.kind === "assistant") {
695
+ const resumeText = completionMode === "text_fallback" ? KIRO_COMPLETION_RETRY_MESSAGE : KIRO_CONTINUATION_MESSAGE;
547
696
  turns.push({
548
697
  kind: "user",
549
- content: completionMode === "text_fallback" ? KIRO_COMPLETION_RETRY_MESSAGE : KIRO_CONTINUATION_MESSAGE,
698
+ content: trailingTurn.finalAnswer ? KIRO_ANSWER_DELIVERED_MESSAGE : resumeText,
550
699
  images: [],
551
700
  toolResults: [],
701
+ ...(trailingTurn.finalAnswer ? { answerDeliveredAck: true } : {}),
552
702
  });
553
703
  }
554
704
 
@@ -564,11 +714,15 @@ export function buildKiroPayload(
564
714
 
565
715
  const currentTurn = turns.pop();
566
716
  if (!currentTurn || currentTurn.kind !== "user") throw new Error("Kiro request must end with a user turn");
717
+ // Keep internal acknowledgement state separate from its text: a real user may quote the same
718
+ // sentence and must still receive ordinary thinking/completion behavior.
719
+ const answerDeliveredAck = currentTurn.answerDeliveredAck === true;
567
720
  const toEntry = (turn: KiroTurn): KiroHistoryEntry => turn.kind === "assistant"
568
721
  ? {
569
722
  assistantResponseMessage: {
570
723
  content: turn.content,
571
724
  ...(turn.toolUses.length > 0 ? { toolUses: turn.toolUses } : {}),
725
+ ...(turn.redactedReasoning ? { reasoningContent: { redactedContent: turn.redactedReasoning } } : {}),
572
726
  },
573
727
  }
574
728
  : {
@@ -593,10 +747,14 @@ export function buildKiroPayload(
593
747
  currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools };
594
748
  }
595
749
  if (completionMode === "text_fallback") {
596
- if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE) {
750
+ if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE && !answerDeliveredAck) {
597
751
  currentUim.content = appendTurnText(currentUim.content, KIRO_COMPLETION_RETRY_MESSAGE);
598
752
  }
599
- } else if (!currentUim.userInputMessageContext?.toolResults && currentUim.content !== KIRO_CONTINUATION_MESSAGE) {
753
+ } else if (
754
+ !currentUim.userInputMessageContext?.toolResults
755
+ && currentUim.content !== KIRO_CONTINUATION_MESSAGE
756
+ && !answerDeliveredAck
757
+ ) {
600
758
  currentUim.content = injectKiroThinkingTags(currentUim.content, parsed);
601
759
  }
602
760
 
@@ -628,8 +786,8 @@ export function buildKiroPayload(
628
786
 
629
787
  // Stream parsing (shared by parseStream + parseResponse)
630
788
  // CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
631
- // non-streaming mode), so both the streaming bridge and the non-streaming web-search sidecar loop
632
- // decode the same way — parseResponse just collects what parseStream yields.
789
+ // non-streaming wire mode), so the streaming bridge and non-streaming Responses path decode the
790
+ // same way — parseResponse just collects what parseStream yields.
633
791
  interface KiroAttemptParseResult {
634
792
  terminal?: AdapterEvent;
635
793
  needsFallback?: boolean;
@@ -835,6 +993,9 @@ async function* parseKiroAttempt(
835
993
  // the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
836
994
  const deferred: AdapterEvent[] = [];
837
995
  const retention = createKiroAttemptRetention(budget);
996
+ // The inner parser can observe Kiro's authoritative context checkpoint, but only this wrapper
997
+ // knows whether the attempt is terminal or will be followed by the bounded completion retry.
998
+ const attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } } = {};
838
999
  const attempt = parseKiroAttemptEvents(
839
1000
  response,
840
1001
  budget,
@@ -846,21 +1007,27 @@ async function* parseKiroAttempt(
846
1007
  conversationId,
847
1008
  deferred,
848
1009
  retention,
1010
+ attemptCalibration,
849
1011
  contextInputEstimate,
850
1012
  priorEmittedOutput,
851
1013
  );
852
1014
  let handedOff = false;
853
1015
  try {
854
- let next = await attempt.next();
855
- while (!next.done) {
856
- yield next.value;
857
- next = await attempt.next();
1016
+ const result = yield* attempt;
1017
+ const stagedCalibration = attemptCalibration.value;
1018
+ attemptCalibration.value = undefined;
1019
+ if (stagedCalibration && !result.needsFallback) {
1020
+ recordKiroCalibration(
1021
+ stagedCalibration.conversationId,
1022
+ stagedCalibration.estimated,
1023
+ stagedCalibration.charged,
1024
+ );
858
1025
  }
859
1026
  for (const event of deferred.splice(0)) {
860
1027
  try { yield event; } finally { retention.releaseEvent(event); }
861
1028
  }
862
1029
  handedOff = true;
863
- return { ...next.value, releaseRetained: () => retention.releaseAll() };
1030
+ return { ...result, releaseRetained: () => retention.releaseAll() };
864
1031
  } finally {
865
1032
  if (!handedOff) retention.releaseAll();
866
1033
  }
@@ -877,6 +1044,7 @@ async function* parseKiroAttemptEvents(
877
1044
  conversationId: string | undefined,
878
1045
  deferred: AdapterEvent[],
879
1046
  retention: KiroAttemptRetention,
1047
+ attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } },
880
1048
  contextInputEstimate?: number,
881
1049
  priorEmittedOutput = false,
882
1050
  ): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
@@ -889,6 +1057,12 @@ async function* parseKiroAttemptEvents(
889
1057
  }
890
1058
 
891
1059
  let open: { id: string; name: string; chunks: string[]; completion: boolean } | null = null;
1060
+ let openCallId: string | undefined;
1061
+ const closeOpenCall = () => {
1062
+ if (!openCallId) return;
1063
+ budget.closeCall(openCallId);
1064
+ openCallId = undefined;
1065
+ };
892
1066
  let outputChars = "";
893
1067
  let outputCharsBytes = 0;
894
1068
  let contextUsagePercentage: number | undefined;
@@ -916,6 +1090,21 @@ async function* parseKiroAttemptEvents(
916
1090
  try { yield event; } finally { retention.releaseEvent(event); }
917
1091
  }
918
1092
  };
1093
+ // A valid private completion supersedes prose staged during the SAME inference. Kiro sometimes
1094
+ // emits answer-shaped text and then calls the terminal tool; forwarding both makes Codex render
1095
+ // two near-identical assistant messages. Drop only staged text on this proven completion path,
1096
+ // preserve non-text events, and release every retained event either way.
1097
+ const consumeSupersededByCompletion = async function* (
1098
+ events: AdapterEvent[],
1099
+ ): AsyncGenerator<AdapterEvent> {
1100
+ for (const event of events.splice(0)) {
1101
+ try {
1102
+ if (event.type !== "text_delta") yield event;
1103
+ } finally {
1104
+ retention.releaseEvent(event);
1105
+ }
1106
+ }
1107
+ };
919
1108
 
920
1109
  const providerState = (): { kiro: { conversationId: string } } | undefined =>
921
1110
  returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
@@ -1101,7 +1290,7 @@ async function* parseKiroAttemptEvents(
1101
1290
  if (!open) return { events: [] };
1102
1291
  const tool = open;
1103
1292
  open = null;
1104
- budget.closeCall(tool.id);
1293
+ closeOpenCall();
1105
1294
  const input = tool.chunks.join("");
1106
1295
  if (!isCompleteKiroToolInput(input)) {
1107
1296
  return { events: [], terminal: protocolTerminal(kiroTruncationErrorMessage("incomplete tool input JSON"), tool.completion) };
@@ -1168,7 +1357,10 @@ async function* parseKiroAttemptEvents(
1168
1357
  if (ev.stopReason !== undefined) stopReason = ev.stopReason;
1169
1358
  break;
1170
1359
  case "message_metadata":
1171
- if (isValidKiroConversationId(ev.conversationId)) returnedConversationId = ev.conversationId;
1360
+ if (isValidKiroConversationId(ev.conversationId)) {
1361
+ rekeyKiroCalibration(returnedConversationId, ev.conversationId);
1362
+ returnedConversationId = ev.conversationId;
1363
+ }
1172
1364
  break;
1173
1365
  case "content":
1174
1366
  if (ev.modelId) {
@@ -1191,6 +1383,12 @@ async function* parseKiroAttemptEvents(
1191
1383
  if (ev.data) {
1192
1384
  yield* emitRetained(stage({ type: "reasoning_raw_delta", text: ev.data }));
1193
1385
  }
1386
+ if (ev.redactedContent) {
1387
+ yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: ev.redactedContent }));
1388
+ }
1389
+ break;
1390
+ case "context_usage":
1391
+ if (ev.contextUsagePercentage > 0) contextUsagePercentage = ev.contextUsagePercentage;
1194
1392
  break;
1195
1393
  case "tool": {
1196
1394
  for (const contentEvent of thinking.flush()) {
@@ -1207,11 +1405,12 @@ async function* parseKiroAttemptEvents(
1207
1405
  if (started.terminal) return { assistantText, sawReasoning, terminal: started.terminal };
1208
1406
  open = started.tool!;
1209
1407
  budget.openCall(open.id);
1408
+ openCallId = open.id;
1210
1409
  } else if (
1211
1410
  (ev.toolUseId && ev.toolUseId !== open.id)
1212
1411
  || (ev.name && open.name !== "unknown" && ev.name !== open.name)
1213
1412
  ) {
1214
- budget.closeCall(open.id);
1413
+ closeOpenCall();
1215
1414
  open = null;
1216
1415
  return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("tool input changed identity before stop")) };
1217
1416
  }
@@ -1290,6 +1489,20 @@ async function* parseKiroAttemptEvents(
1290
1489
  ...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
1291
1490
  });
1292
1491
  }
1492
+ // The percentage is an absolute post-response checkpoint. Remove generated output before
1493
+ // comparing it with the request-only estimate, then stage the observation for the outer parser
1494
+ // to commit only if this attempt is terminal (not the first half of a bounded fallback).
1495
+ const chargedTotal = contextUsageTotalFloor();
1496
+ if (chargedTotal !== undefined && contextInputEstimate !== undefined) {
1497
+ const chargedInput = chargedTotal - finalUsage.outputTokens;
1498
+ if (chargedInput > 0 && returnedConversationId) {
1499
+ attemptCalibration.value = {
1500
+ conversationId: returnedConversationId,
1501
+ estimated: contextInputEstimate,
1502
+ charged: chargedInput,
1503
+ };
1504
+ }
1505
+ }
1293
1506
  // Native stop metadata proves that this inference ended, but it does not prove that ordinary
1294
1507
  // text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
1295
1508
  // still require the private completion call to distinguish commentary from completion (#531).
@@ -1313,12 +1526,13 @@ async function* parseKiroAttemptEvents(
1313
1526
  });
1314
1527
 
1315
1528
  if (mode === "required") {
1316
- yield* emitRetained(deferred.splice(0));
1529
+ if (completionAnswer !== undefined) yield* consumeSupersededByCompletion(deferred);
1530
+ else yield* emitRetained(deferred.splice(0));
1317
1531
  }
1318
1532
 
1319
1533
  if (mode === "text_fallback") {
1320
1534
  if (completionAnswer !== undefined) {
1321
- yield* emitRetained(fallbackEvents);
1535
+ yield* consumeSupersededByCompletion(fallbackEvents);
1322
1536
  yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
1323
1537
  return {
1324
1538
  assistantText,
@@ -1475,7 +1689,7 @@ async function* parseKiroAttemptEvents(
1475
1689
  };
1476
1690
  } catch (err) {
1477
1691
  if (isTranslatorBudgetExceededError(err)) {
1478
- if (open) budget.closeCall(open.id);
1692
+ closeOpenCall();
1479
1693
  return {
1480
1694
  assistantText,
1481
1695
  sawReasoning,
@@ -1517,6 +1731,9 @@ async function* parseKiroAttemptEvents(
1517
1731
  usage: usage(),
1518
1732
  },
1519
1733
  };
1734
+ } finally {
1735
+ thinking.dispose();
1736
+ closeOpenCall();
1520
1737
  }
1521
1738
  }
1522
1739
 
@@ -1533,7 +1750,7 @@ export async function* parseKiroStream(
1533
1750
  contextInputEstimate?: number,
1534
1751
  ): AsyncGenerator<AdapterEvent> {
1535
1752
  const contextWindowState: KiroContextWindowState = { value: contextWindow };
1536
- const first = parseKiroAttempt(
1753
+ const firstResult = yield* parseKiroAttempt(
1537
1754
  response,
1538
1755
  budget,
1539
1756
  completionMode,
@@ -1545,12 +1762,6 @@ export async function* parseKiroStream(
1545
1762
  contextInputEstimate,
1546
1763
  false,
1547
1764
  );
1548
- let firstNext = await first.next();
1549
- while (!firstNext.done) {
1550
- yield firstNext.value;
1551
- firstNext = await first.next();
1552
- }
1553
- const firstResult = firstNext.value;
1554
1765
  try {
1555
1766
  if (!firstResult.needsFallback) {
1556
1767
  if (firstResult.terminal) yield firstResult.terminal;
@@ -1628,7 +1839,7 @@ export async function* parseKiroStream(
1628
1839
  return;
1629
1840
  }
1630
1841
 
1631
- const second = parseKiroAttempt(
1842
+ const secondResult = yield* parseKiroAttempt(
1632
1843
  fallback.response,
1633
1844
  budget,
1634
1845
  "text_fallback",
@@ -1642,12 +1853,6 @@ export async function* parseKiroStream(
1642
1853
  // A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
1643
1854
  priorEmittedOutput,
1644
1855
  );
1645
- let secondNext = await second.next();
1646
- while (!secondNext.done) {
1647
- yield secondNext.value;
1648
- secondNext = await second.next();
1649
- }
1650
- const secondResult = secondNext.value;
1651
1856
  try {
1652
1857
  if (!secondResult.terminal) {
1653
1858
  yield retryableKiroIncomplete(
@@ -1716,12 +1921,12 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1716
1921
  throw new Error("kiro token missing — run ocx login kiro");
1717
1922
  }
1718
1923
  const region = resolveKiroApiRegion(parsed._kiroAuthContext);
1719
- const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext);
1924
+ const requestProfile = resolveKiroRequestProfile(parsed._kiroAuthContext);
1720
1925
  const isApiKey = provider.apiKey.trim().startsWith("ksk_");
1721
- const profileArn = isApiKey ? undefined : resolvedProfileArn;
1722
- // Builder ID and Kiro API keys have no profile ARN and are accepted only on Kiro's CLI
1723
- // request path. Enterprise profiles retain the existing IDE-shaped request.
1724
- const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide";
1926
+ const profileArn = isApiKey ? undefined : requestProfile.profileArn;
1927
+ // A Builder ID service profile does not turn the account into an enterprise identity.
1928
+ // Use the same resolver verdict for the envelope, including legacy accountless calls.
1929
+ const wireClient: KiroWireClient = isApiKey || requestProfile.builderIdFallback || !profileArn ? "cli" : "ide";
1725
1930
  const fp = fingerprint().slice(0, 64);
1726
1931
  const headers: Record<string, string> = wireClient === "cli" ? {
1727
1932
  authorization: `Bearer ${provider.apiKey}`,
@@ -1748,7 +1953,8 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1748
1953
  if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
1749
1954
  const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
1750
1955
  await normalizeKiroImages(built.payload);
1751
- const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
1956
+ const rawContextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
1957
+ const contextInputEstimate = calibrateKiroEstimate(built.conversationId, rawContextInputEstimate);
1752
1958
  const body = JSON.stringify(built.payload);
1753
1959
  debugProviderDiagnostic("kiro", "request", {
1754
1960
  region,
@@ -1855,6 +2061,15 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1855
2061
 
1856
2062
  return {
1857
2063
  name: "kiro",
2064
+ // Replayed history that already ends in the answer the user saw is not a new inference turn.
2065
+ // This hook lets the server terminate locally before build/send and, critically, before the
2066
+ // empty-completion guard can reinterpret an outputless terminal as something to retry.
2067
+ localTerminal(parsed: OcxParsedRequest) {
2068
+ return hasTrailingDeliveredFinalAnswer(kiroPayloadMessages(parsed), parsed)
2069
+ ? { reason: "kiro_final_answer_already_delivered" }
2070
+ : undefined;
2071
+ },
2072
+
1858
2073
  async buildRequest(parsed: OcxParsedRequest, incoming) {
1859
2074
  const built = await build(parsed);
1860
2075
  modelId = parsed.modelId;
@@ -1897,25 +2112,32 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1897
2112
  return safeKiroHttpErrorMessage(status, headers, payloadText);
1898
2113
  },
1899
2114
 
1900
- // Non-streaming path used by the web-search sidecar loop (loop.ts runs each iteration
1901
- // non-streamed so it can inspect tool calls). CW only ever event-streams, so we drain the
1902
- // same decoder into an array. Without this, any Codex request that includes the web_search
1903
- // tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
2115
+ // Kiro always returns an event stream, including for non-streaming Responses requests. Drain
2116
+ // the decoder into a budget-owned batch so an upstream stream cannot grow this array without
2117
+ // bound while the caller waits for the complete JSON response.
1904
2118
  async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1905
2119
  const events: AdapterEvent[] = [];
1906
- for await (const e of parseKiroStream(
1907
- response,
1908
- budget,
1909
- modelId,
1910
- inputTokens,
1911
- contextWindow,
1912
- toolNameMap,
1913
- conversationId,
1914
- completionMode,
1915
- completionMode === "required" ? fallbackFactory : undefined,
1916
- contextInputEstimate,
1917
- )) events.push(e);
1918
- return events;
2120
+ try {
2121
+ for await (const e of parseKiroStream(
2122
+ response,
2123
+ budget,
2124
+ modelId,
2125
+ inputTokens,
2126
+ contextWindow,
2127
+ toolNameMap,
2128
+ conversationId,
2129
+ completionMode,
2130
+ completionMode === "required" ? fallbackFactory : undefined,
2131
+ contextInputEstimate,
2132
+ )) {
2133
+ retainTranslatedEvent(e, budget, events.at(-1));
2134
+ events.push(e);
2135
+ }
2136
+ return events;
2137
+ } catch (error) {
2138
+ for (const event of events) releaseTranslatedEvent(event, budget);
2139
+ throw error;
2140
+ }
1919
2141
  },
1920
2142
  };
1921
2143
  }