-
Notifications
You must be signed in to change notification settings - Fork 751
feat(responses): guard empty completions with one identical-turn retry #1655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,7 +77,7 @@ import { | |
| import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search"; | ||
| import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images"; | ||
| import { describeImagesInPlace, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision"; | ||
| import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue"; | ||
| import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue"; | ||
| import { | ||
| applyCodexAuthContextToProvider, | ||
| CodexAccountCooldownError, | ||
|
|
@@ -233,6 +233,10 @@ import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-t | |
| import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair"; | ||
| import { responsesJsonToSseStream } from "../responses-json-events"; | ||
| import { guardTerminalEventStream } from "./terminal-guard"; | ||
| import { | ||
| emptyCompletionRetryEnabled, | ||
| guardEmptyCompletionEventStream, | ||
| } from "./empty-completion-guard"; | ||
|
|
||
| /** | ||
| * Adapters whose continuation state must survive Codex's store:false requests. | ||
|
|
@@ -3055,22 +3059,37 @@ async function handleResponsesInner( | |
| return wsResponse; | ||
| } | ||
|
|
||
| // Empty-completion guard (codex-router PR #145 port): a 200 that completes with no output | ||
| // text and no tool call is a failure the client cannot see — it silently records the turn as | ||
| // done. The guard holds pre-content adapter events, suppresses the terminal of an empty | ||
| // turn, retries the IDENTICAL request once, and surfaces a stated error when the retry is | ||
| // empty or fails. Kill switch: OCX_EMPTY_COMPLETION_RETRY=0. Compaction turns and combo | ||
| // attempts keep their own machinery (the combo preflight already handles empty streams). | ||
| const emptyCompletionGuardEnabled = | ||
| emptyCompletionRetryEnabled() | ||
| && !options.comboAttempt | ||
| && !routedCompaction; | ||
|
|
||
| if (adapter.runTurn) { | ||
| const runTurnAbort = new AbortController(); | ||
| linkAbortSignal(runTurnAbort, options.abortSignal); | ||
| const queue = createAdapterEventQueue({ | ||
| onBacklogExceeded: () => runTurnAbort.abort(), | ||
| }); | ||
| const runTurn = async (): Promise<void> => { | ||
| // One attempt of the runTurn transport, against an explicit queue. The | ||
| // empty-completion guard re-invokes the IDENTICAL turn (same parsed request, | ||
| // same forwarded headers, same abort signal) through a fresh queue, so the | ||
| // attempt body must not capture the first queue. | ||
| const runTurnAttempt = async (targetQueue: AdapterEventQueue): Promise<void> => { | ||
| try { | ||
| noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens); | ||
| await adapter.runTurn?.( | ||
| parsed, | ||
| { headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal, translatorBudget }, | ||
| queue.push, | ||
| targetQueue.push, | ||
| ); | ||
|
Comment on lines
+3083
to
3090
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect the AttemptRecoveryKind union and every recovery label in use.
set -euo pipefail
echo "=== AttemptRecoveryKind definition ==="
rg -n --type=ts -C6 'AttemptRecoveryKind\s*=' src
echo
echo "=== recovery labels passed to noteAttemptSend ==="
ast-grep run --pattern 'noteAttemptSend($$$)' --lang typescript src
echo
echo "=== where recoveryKinds is rendered or persisted ==="
rg -n --type=ts 'recoveryKinds' srcRepository: lidge-jun/opencodex Length of output: 3828 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== usage/log.ts: recovery kinds and validation ==='
sed -n '1,75p' src/usage/log.ts
sed -n '1010,1050p' src/server/request-log.ts
echo
echo '=== core.ts: empty-completion flow and nearby retry paths ==='
sed -n '3045,3160p' src/server/responses/core.ts
sed -n '3380,3575p' src/server/responses/core.ts
echo
echo '=== all recovery-kind references and user-facing mappings ==='
rg -n --type=ts --glob '!src/server/responses/core.ts' \
'AttemptRecoveryKind|ATTEMPT_RECOVERY_KINDS|transient-5xx|connection-reset|oauth-401|key-429|rate-limit-429|anthropic-oauth-429|image-413|recoveryKinds' src
rg -n -S 'transient-5xx|connection-reset|oauth-401|key-429|rate-limit-429|anthropic-oauth-429|image-413|recoveryKinds' . \
--glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -250Repository: lidge-jun/opencodex Length of output: 44559 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== GUI recovery mapping and fallback ==='
sed -n '80,115p' gui/src/pages/Logs.tsx
sed -n '300,330p' gui/src/pages/Logs.tsx
rg -n -S 'logs\.detail\.attempt\.recovery\.(transient5xx|connectionReset|oauth401|key429|rateLimit429|anthropicOauth429|image413)' gui src \
--glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -120
echo
echo '=== empty-completion guard contract ==='
rg -n --type=ts -C8 'function guardEmptyCompletionEventStream|guardEmptyCompletionEventStream|emptyCompletionRetryEnabled' src tests
echo
echo '=== focused static consistency probe ==='
python3 - <<'PY'
from pathlib import Path
import re
usage = Path("src/usage/log.ts").read_text()
gui = Path("gui/src/pages/Logs.tsx").read_text()
core = Path("src/server/responses/core.ts").read_text()
def quoted_values(pattern, text):
m = re.search(pattern, text, re.S)
if not m:
raise SystemExit(f"pattern not found: {pattern}")
return set(re.findall(r'"([^"]+)"', m.group(1)))
usage_union = quoted_values(
r"export type AttemptRecoveryKind\s*=\s*(.*?);", usage
)
allow_list = quoted_values(
r"const ATTEMPT_RECOVERY_KINDS[^=]*=\s*new Set<AttemptRecoveryKind>\(\[(.*?)\]\);",
usage,
)
gui_union = quoted_values(
r"type AttemptRecoveryKind\s*=\s*(.*?);", gui
)
gui_map = quoted_values(
r"const RECOVERY_KIND_KEYS[^=]*=\s*\{(.*?)\}\s*as const;",
gui,
)
print("usage union:", sorted(usage_union))
print("usage allow-list:", sorted(allow_list))
print("GUI union:", sorted(gui_union))
print("GUI mapped keys:", sorted(gui_map))
print("usage union == allow-list:", usage_union == allow_list)
print("usage union == GUI union:", usage_union == gui_union)
print("usage union subset of GUI map:", usage_union <= gui_map)
print("retry invocation present:", "void runTurnAttempt(retryQueue)" in core)
print("unlabeled send present:", "noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);" in core)
PYRepository: lidge-jun/opencodex Length of output: 38561 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== GUI recovery mapping and fallback ==='
sed -n '80,115p' gui/src/pages/Logs.tsx
sed -n '300,330p' gui/src/pages/Logs.tsx
rg -n -S 'logs\.detail\.attempt\.recovery\.(transient5xx|connectionReset|oauth401|key429|rateLimit429|anthropicOauth429|image413)' gui src \
--glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -120
echo
echo '=== empty-completion guard contract ==='
rg -n --type=ts -C8 'function guardEmptyCompletionEventStream|guardEmptyCompletionEventStream|emptyCompletionRetryEnabled' src tests
echo
echo '=== focused static consistency probe ==='
python3 - <<'PY'
from pathlib import Path
import re
usage = Path("src/usage/log.ts").read_text()
gui = Path("gui/src/pages/Logs.tsx").read_text()
core = Path("src/server/responses/core.ts").read_text()
def quoted_values(pattern, text):
m = re.search(pattern, text, re.S)
if not m:
raise SystemExit(f"pattern not found: {pattern}")
return set(re.findall(r'"([^"]+)"', m.group(1)))
usage_union = quoted_values(r"export type AttemptRecoveryKind\s*=\s*(.*?);", usage)
allow_list = quoted_values(
r"const ATTEMPT_RECOVERY_KINDS[^=]*=\s*new Set<AttemptRecoveryKind>\(\[(.*?)\]\);",
usage,
)
gui_union = quoted_values(r"type AttemptRecoveryKind\s*=\s*(.*?);", gui)
gui_map = quoted_values(
r"const RECOVERY_KIND_KEYS[^=]*=\s*\{(.*?)\}\s*as const;",
gui,
)
print("usage union:", sorted(usage_union))
print("usage allow-list:", sorted(allow_list))
print("GUI union:", sorted(gui_union))
print("GUI mapped keys:", sorted(gui_map))
print("usage union == allow-list:", usage_union == allow_list)
print("usage union == GUI union:", usage_union == gui_union)
print("usage union subset of GUI map:", usage_union <= gui_map)
print("retry invocation present:", "void runTurnAttempt(retryQueue)" in core)
print("unlabeled send present:", "noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);" in core)
PYRepository: lidge-jun/opencodex Length of output: 38561 Record the empty-completion retry as
🤖 Prompt for AI Agents |
||
| } catch (err) { | ||
| queue.push({ | ||
| targetQueue.push({ | ||
| type: "error", | ||
| message: err instanceof Error ? err.message : String(err), | ||
| }); | ||
|
|
@@ -3080,9 +3099,20 @@ async function handleResponsesInner( | |
| if (!logCtx.conversationId && parsed._cursorConversationId) { | ||
| logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId); | ||
| } | ||
| queue.close(); | ||
| targetQueue.close(); | ||
| } | ||
| }; | ||
| const runTurn = async (): Promise<void> => runTurnAttempt(queue); | ||
| // The empty-completion retry re-runs the turn against a fresh queue: the | ||
| // first queue is closed once its attempt settles, and pushing into it after | ||
| // close is a silent no-op. | ||
| const runTurnRetrySource = (): AsyncIterable<AdapterEvent> => { | ||
| const retryQueue = createAdapterEventQueue({ | ||
| onBacklogExceeded: () => runTurnAbort.abort(), | ||
| }); | ||
| void runTurnAttempt(retryQueue); | ||
| return retryQueue.stream(); | ||
| }; | ||
|
|
||
| const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps; | ||
| if (parsed.stream) { | ||
|
|
@@ -3098,8 +3128,16 @@ async function handleResponsesInner( | |
| } | ||
| eventSource = preflight.stream; | ||
| } | ||
| const guardedSource = emptyCompletionGuardEnabled | ||
| ? guardEmptyCompletionEventStream({ | ||
| firstEvents: eventSource, | ||
| // Identical-turn retry: same parsed request, same headers, same | ||
| // signal — run the adapter transport again against a fresh queue. | ||
| continuation: runTurnRetrySource, | ||
| }) | ||
| : eventSource; | ||
| const sseStream = bridgeToResponsesSSE( | ||
| eventSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames, | ||
| guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames, | ||
|
Comment on lines
+3131
to
+3140
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Establish whether every runTurn adapter emits heartbeats, and what resets the bridge stall timer.
set -euo pipefail
echo "=== adapters implementing runTurn ==="
ast-grep run --pattern 'runTurn: $_' --lang typescript src
rg -n --type=ts -C2 '\brunTurn\s*[:(]' src/adapters
echo
echo "=== heartbeat emission per adapter ==="
rg -n --type=ts -C2 '"heartbeat"' src/adapters
echo
echo "=== what feeds/resets the bridge stall watchdog ==="
fd -t f 'bridge.ts' src --exec rg -n -C6 'stallTimeoutSec|stall|lastEventAt|upstream_stall_timeout|heartbeat' {}Repository: lidge-jun/opencodex Length of output: 195 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== relevant source files ==="
fd -t f '(empty-completion-guard|bridge|run-turn-queue|responses.*core|adapter)' src | sort
echo
echo "=== guard implementation and bridge call sites ==="
fd -t f 'empty-completion-guard.ts' src --exec sh -c 'cat -n "$1"' sh {}
rg -n -C8 'bridgeToResponsesSSE|stallTimeoutSec|runTurnRetrySource|guardEmptyCompletionEventStream' src/server/responses/core.ts
echo
echo "=== bridge implementation candidates ==="
rg -l --type=ts 'upstream_stall_timeout|stallTimeoutSec|lastEventAt|stall timer|stall watchdog' src | sortRepository: lidge-jun/opencodex Length of output: 22377 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== bridge stall-watchdog implementation ==="
ast-grep outline src/bridge.ts
rg -n -C12 'stallTimeoutSec|upstream_stall_timeout|heartbeat|setTimeout|last.*Event|last.*Activity|stall' src/bridge.ts src/stall-timeout.ts
echo
echo "=== all runTurn implementations and heartbeat producers ==="
rg -n --type=ts -C5 '\brunTurn\b|type:\s*"heartbeat"|type:\s*'\''heartbeat'\''' src/adapters src | head -n 1200
echo
echo "=== adapter event type and heartbeat-related helpers ==="
rg -n -C8 'AdapterEvent|heartbeat|emitHeartbeat|heartbeatInterval|stallTimeout' src/types.ts src/adapters src/lib src/server | head -n 1600Repository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== runTurn declarations ==="
rg -n --type=ts -C4 '(async\s+)?runTurn\s*[\(:]' src/adapters src | grep -vE 'node_modules|dist|build'
echo
echo "=== adapters selected by runTurn branches ==="
rg -n --type=ts -C8 'adapter\.runTurn|runTurn\?' src/server src/images src/adapters
echo
echo "=== Cursor runTurn event emission ==="
sed -n '1,180p' src/adapters/cursor.ts
rg -n -C8 'type:\s*"heartbeat"|push\(\{ type:\s*"heartbeat"|emit\(\{ type:\s*"heartbeat"' src/adapters/cursor src/adapters/cursor.ts
echo
echo "=== Kiro adapter transport shape ==="
rg -n -C5 'runTurn|fetchResponse|parseStream' src/adapters/kiro.tsRepository: lidge-jun/opencodex Length of output: 32339 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Cursor progress classification and mapping ==="
rg -n -C15 'function isCursorProgressFrame|const isCursorProgressFrame|isCursorProgressFrame|case "thinking"|thinking_delta|thinking' src/adapters/cursor/live-transport.ts src/adapters/cursor/message-mapper.ts
echo
echo "=== Cursor transport event loop around progress heartbeats ==="
sed -n '1010,1130p' src/adapters/cursor/live-transport.ts
sed -n '1,70p' src/adapters/cursor/message-mapper.ts
echo
echo "=== tests covering empty guard, heartbeat, and stall timeout ==="
rg -n -C6 'empty.?completion|upstream_stall_timeout|stallTicks|heartbeat.*reason|reason.*heartbeat|thinking_delta' tests src/server/responses | head -n 1600Repository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Cursor protobuf mapping for token and reasoning frames ==="
rg -n -C20 'function mapCursorProtobufServerMessage|mapCursorProtobufServerMessage|tokenDelta|thinking' src/adapters/cursor/live-transport.ts src/adapters/cursor | head -n 1200
echo
echo "=== Cursor server heartbeat/progress intervals ==="
rg -n -C8 'HEARTBEAT_MS|serverHeartbeat|heartbeat|setInterval' src/adapters/cursor/live-transport.ts src/adapters/cursor | head -n 800Repository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
guard = Path("src/server/responses/empty-completion-guard.ts").read_text()
bridge = Path("src/bridge.ts").read_text()
transport = Path("src/adapters/cursor/live-transport.ts").read_text()
events = Path("src/adapters/cursor/protobuf-events.ts").read_text()
checks = {
"guard passes heartbeat": 'if (event.type === "heartbeat")' in guard and "yield event" in guard,
"guard buffers thinking": 'held.push(event)' in guard and 'case "thinking_delta":' not in guard.split("export async function* guardEmptyCompletionEventStream", 1)[1].split("while (true)", 1)[1].split("for await", 1)[0],
"bridge resets on every event": "upstreamActivity = true" in bridge and "stallTicks = 0" in bridge,
"thinking delta is mapped": 'case "thinkingDelta":' in events and '{ type: "thinking", thinking: update.value.text }' in events,
"heartbeat fallback only for unmapped progress": "if (mapped.length > 0)" in transport and 'push({ type: "heartbeat" })' in transport,
"thinking delta not classified as swallowed progress": 'case "tokenDelta":' in transport and 'case "thinkingDelta":' not in transport.split("function isCursorProgressFrame", 1)[1].split("}", 1)[0],
}
for name, ok in checks.items():
print(f"{name}: {'PASS' if ok else 'FAIL'}")
if not all(checks.values()):
raise SystemExit(1)
PYRepository: lidge-jun/opencodex Length of output: 391 Emit liveness while buffering reasoning events
Only Emit a heartbeat when the guard buffers a reasoning event, or add equivalent heartbeat emission for mapped Cursor reasoning deltas. Add a regression test for a heartbeat-free reasoning prefix longer than 🤖 Prompt for AI Agents |
||
| () => { | ||
| runTurnAbort.abort(); | ||
| queue.close(); | ||
|
|
@@ -3142,7 +3180,17 @@ async function handleResponsesInner( | |
| } | ||
|
|
||
| await runTurn(); | ||
| const events = await queue.collect(); | ||
| const firstAttemptEvents = await queue.collect(); | ||
| let events: AdapterEvent[]; | ||
| if (emptyCompletionGuardEnabled) { | ||
| events = []; | ||
| for await (const event of guardEmptyCompletionEventStream({ | ||
| firstEvents: (async function* () { yield* firstAttemptEvents; })(), | ||
| continuation: runTurnRetrySource, | ||
| })) events.push(event); | ||
| } else { | ||
| events = firstAttemptEvents; | ||
| } | ||
| if (options.comboAttempt) { | ||
| const firstMeaningful = events.find(event => event.type !== "heartbeat"); | ||
| if (!firstMeaningful || firstMeaningful.type === "error") { | ||
|
|
@@ -3848,9 +3896,19 @@ async function handleResponsesInner( | |
| continuation: fetchTerminalGuardContinuation, | ||
| }) | ||
| : initialEventStream; | ||
| // The empty-completion guard sits OUTSIDE the terminal guard: a completed | ||
| // turn with no text and no tool call is retried with the IDENTICAL request | ||
| // (fetchTerminalGuardContinuation(parsed) replays the cached byte-identical | ||
| // request — same body, same headers, same signal). | ||
| const guardedEventStream = emptyCompletionGuardEnabled | ||
| ? guardEmptyCompletionEventStream({ | ||
| firstEvents: eventStream, | ||
| continuation: () => fetchTerminalGuardContinuation(parsed), | ||
| }) | ||
| : eventStream; | ||
|
Comment on lines
+3903
to
+3908
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win The empty-completion retry bypasses the Anthropic terminal guard on both response paths. At both sites the first attempt is wrapped in
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps; | ||
| const sseStream = bridgeToResponsesSSE( | ||
| eventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames, | ||
| guardedEventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames, | ||
| () => upstream.abort(), 2_000, | ||
| { | ||
| translatorBudget, | ||
|
|
@@ -3895,17 +3953,27 @@ async function handleResponsesInner( | |
| let events: AdapterEvent[]; | ||
| try { | ||
| const initialEvents = await activeAdapter.parseResponse(upstreamResponse, translatorBudget); | ||
| let guardedEvents: AdapterEvent[]; | ||
| if (terminalGuardEnabled) { | ||
| events = []; | ||
| guardedEvents = []; | ||
| for await (const event of guardTerminalEventStream({ | ||
| parsed, | ||
| firstEvents: (async function* () { yield* initialEvents; })(), | ||
| adapterName: activeAdapter.name, | ||
| maxAutoContinuations: 1, | ||
| continuation: fetchTerminalGuardContinuation, | ||
| })) guardedEvents.push(event); | ||
| } else { | ||
| guardedEvents = initialEvents; | ||
| } | ||
| if (emptyCompletionGuardEnabled) { | ||
| events = []; | ||
| for await (const event of guardEmptyCompletionEventStream({ | ||
| firstEvents: (async function* () { yield* guardedEvents; })(), | ||
| continuation: () => fetchTerminalGuardContinuation(parsed), | ||
| })) events.push(event); | ||
| } else { | ||
| events = initialEvents; | ||
| events = guardedEvents; | ||
| } | ||
| } finally { | ||
| cleanupUpstreamAbort(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 192
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50376
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50375
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50375
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 38226
Route the empty-completion setting through
OcxConfigAt
src/server/responses/core.ts:3069,emptyCompletionRetryEnabled()readsOCX_EMPTY_COMPLETION_RETRYfromprocess.envon every request. Comparable kill switches, such ascodexShimAutoRestoreEnabled()insrc/config.ts:3213-3217, acceptOcxConfigand then apply an environment override.Add the setting to
src/types.ts,src/config.ts, and the defaults. Pass it to the helper, withOCX_EMPTY_COMPLETION_RETRY=0taking precedence. Add config-driven tests. A top-level field provides global config control; provider/model scoping requires a separate provider-level design.🤖 Prompt for AI Agents
Source: Path instructions