@bitkyc08/opencodex 2.10.0 → 2.10.1
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.
- package/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
* up to the drain window.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { buildFailedTailPayload } from "./relay";
|
|
27
|
+
import { buildFailedTailPayload, createSseTerminalOutputBoundary } from "./relay";
|
|
28
28
|
import {
|
|
29
29
|
nextSseBlock,
|
|
30
|
+
payloadRewriteAsBlockRewrite,
|
|
30
31
|
replaceSseDataPayload,
|
|
31
32
|
sseDataPayload,
|
|
33
|
+
type SseBlockRewrite,
|
|
32
34
|
type SsePayloadRewrite,
|
|
33
35
|
} from "./sse-payload-rewrite";
|
|
34
36
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
@@ -43,6 +45,12 @@ export type EagerRelayHooks = {
|
|
|
43
45
|
* Bun#32111-unsafe tee()+JS-pull chain (#864).
|
|
44
46
|
*/
|
|
45
47
|
rewritePayload?: SsePayloadRewrite;
|
|
48
|
+
/**
|
|
49
|
+
* Optional block-level rewrite (zero or more blocks out per upstream
|
|
50
|
+
* block) for lifecycle event injection (#893). Takes precedence over
|
|
51
|
+
* rewritePayload when both are set.
|
|
52
|
+
*/
|
|
53
|
+
rewriteBlocks?: SseBlockRewrite;
|
|
46
54
|
/** Flush inspection at upstream end (createSseInspector.finish). */
|
|
47
55
|
finishInspection: () => void;
|
|
48
56
|
/** Drop inspector-owned frame/item state during producer teardown. */
|
|
@@ -92,9 +100,11 @@ export function relaySseEagerBounded(
|
|
|
92
100
|
const now = opts?.now ?? Date.now;
|
|
93
101
|
|
|
94
102
|
const reader = body.getReader();
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
103
|
+
const terminalBoundary = createSseTerminalOutputBoundary();
|
|
104
|
+
const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
|
|
105
|
+
?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
|
|
106
|
+
const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
|
|
107
|
+
const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
|
|
98
108
|
const rewriteBudget = opts?.rewriteBudget;
|
|
99
109
|
let frameBuffer = "";
|
|
100
110
|
let frameBufferBytes = 0;
|
|
@@ -121,15 +131,9 @@ export function relaySseEagerBounded(
|
|
|
121
131
|
for (;;) {
|
|
122
132
|
const next = nextSseBlock(frameBuffer);
|
|
123
133
|
if (!next) break;
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// multi-data-line events and normalizes newline style even when the
|
|
128
|
-
// payload is identical, which corrupts valid streams.
|
|
129
|
-
const block = payload !== null && rewrittenPayload !== payload
|
|
130
|
-
? replaceSseDataPayload(next.block, rewrittenPayload!)
|
|
131
|
-
: next.block;
|
|
132
|
-
out += block + next.delimiter;
|
|
134
|
+
for (const outBlock of activeRewrite!(next.block)) {
|
|
135
|
+
out += outBlock + next.delimiter;
|
|
136
|
+
}
|
|
133
137
|
frameBuffer = next.rest;
|
|
134
138
|
}
|
|
135
139
|
if (rewriteBudget) {
|
|
@@ -143,14 +147,13 @@ export function relaySseEagerBounded(
|
|
|
143
147
|
};
|
|
144
148
|
/** Flush any trailing partial block at upstream end (rewrite applied, matching the pull relay). */
|
|
145
149
|
const flushRewriteTail = (): Uint8Array => {
|
|
146
|
-
if (!
|
|
150
|
+
if (!activeRewrite) return new Uint8Array(0);
|
|
147
151
|
// Decoder-flushed bytes logically follow everything already decoded.
|
|
148
152
|
let tail = frameBuffer + rewriteDecoder!.decode();
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
153
|
+
const rewritten = activeRewrite(tail);
|
|
154
|
+
// Multiple emitted blocks must stay separately framed (#893 review);
|
|
155
|
+
// join places the delimiter only between blocks, never after the last.
|
|
156
|
+
tail = rewritten.join(tail.includes("\r\n") ? "\r\n\r\n" : "\n\n");
|
|
154
157
|
frameBuffer = "";
|
|
155
158
|
if (rewriteBudget && frameBufferBytes > 0) {
|
|
156
159
|
rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" });
|
|
@@ -161,6 +164,7 @@ export function relaySseEagerBounded(
|
|
|
161
164
|
let queuedBytes = 0;
|
|
162
165
|
let cancelled = false;
|
|
163
166
|
let done = false;
|
|
167
|
+
const terminalSentinel = new TextEncoder().encode("data: [DONE]\n\n");
|
|
164
168
|
// Pause gate: resolved by client pull, client cancel, or upstream abort so a
|
|
165
169
|
// paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
|
|
166
170
|
// turn unregistration stay reachable, drainAndShutdown never hangs).
|
|
@@ -194,33 +198,45 @@ export function relaySseEagerBounded(
|
|
|
194
198
|
let syntheticKind: "incomplete" | "failed" | null = null;
|
|
195
199
|
// reader.read() is not intrinsically tied to the upstream AbortController
|
|
196
200
|
// (a fetch body usually rejects on abort, but that coupling is the fetch
|
|
197
|
-
// implementation's, not the stream's)
|
|
198
|
-
//
|
|
199
|
-
// loop
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
201
|
+
// implementation's, not the stream's), so abort must break a parked read on
|
|
202
|
+
// a silent upstream. Cancelling the reader does that: the pending read
|
|
203
|
+
// settles and the loop observes the abort. This is deliberately NOT a
|
|
204
|
+
// shared `Promise.race([reader.read(), aborted])` companion — racing every
|
|
205
|
+
// read against one never-settled promise retains a reaction per chunk, and
|
|
206
|
+
// that is the exact retention class relay.ts avoids at its own drain.
|
|
207
|
+
const wakeParkedRead = () => { reader.cancel(upstream.signal.reason).catch(() => {}); };
|
|
208
|
+
if (upstream.signal.aborted) wakeParkedRead();
|
|
209
|
+
else upstream.signal.addEventListener("abort", wakeParkedRead, { once: true });
|
|
204
210
|
try {
|
|
205
211
|
for (;;) {
|
|
206
|
-
const result = await
|
|
207
|
-
if (result === "aborted") break;
|
|
212
|
+
const result = await reader.read();
|
|
208
213
|
const { done: upstreamDone, value } = result;
|
|
214
|
+
// A chunk that already settled is INSPECTED before abort is honored. A read
|
|
215
|
+
// can settle with a real chunk in the same tick the signal fires (post-cancel
|
|
216
|
+
// drain: the terminal frame arrives, then the drain timer aborts upstream).
|
|
217
|
+
// Checking the signal first discarded that frame, so the terminal was never
|
|
218
|
+
// recorded and the turn was accounted as a plain cancel.
|
|
219
|
+
if (!upstreamDone && value !== undefined) hooks.inspectChunk(value);
|
|
220
|
+
if (upstream.signal.aborted) break;
|
|
209
221
|
if (upstreamDone) {
|
|
210
222
|
hooks.finishInspection();
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
const boundedTail = terminalBoundary.finish();
|
|
224
|
+
if (activeRewrite) {
|
|
225
|
+
const rewritten = rewriteOutbound(boundedTail);
|
|
226
|
+
const tail = joinUint8Arrays(rewritten, flushRewriteTail());
|
|
213
227
|
if (tail.byteLength > 0 && !cancelled) {
|
|
214
228
|
queuedBytes += tail.byteLength;
|
|
215
229
|
try { controllerRef?.enqueue(tail); } catch { /* client already gone */ }
|
|
216
230
|
}
|
|
231
|
+
} else if (boundedTail.byteLength > 0 && !cancelled) {
|
|
232
|
+
queuedBytes += boundedTail.byteLength;
|
|
233
|
+
try { controllerRef?.enqueue(boundedTail); } catch { /* client already gone */ }
|
|
217
234
|
}
|
|
218
235
|
if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
|
|
219
236
|
syntheticKind = "incomplete";
|
|
220
237
|
}
|
|
221
238
|
break;
|
|
222
239
|
}
|
|
223
|
-
hooks.inspectChunk(value);
|
|
224
240
|
if (cancelled) {
|
|
225
241
|
// Discard-drain: inspection only, nothing queued. Stop at terminal
|
|
226
242
|
// or when the bounded window expires.
|
|
@@ -230,17 +246,30 @@ export function relaySseEagerBounded(
|
|
|
230
246
|
}
|
|
231
247
|
continue;
|
|
232
248
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
const terminalBounded = terminalBoundary.feed(value);
|
|
250
|
+
const outbound = activeRewrite ? rewriteOutbound(terminalBounded) : terminalBounded;
|
|
251
|
+
if (outbound.byteLength > 0) {
|
|
252
|
+
queuedBytes += outbound.byteLength;
|
|
253
|
+
try {
|
|
254
|
+
controllerRef?.enqueue(outbound);
|
|
255
|
+
} catch {
|
|
256
|
+
// Controller already torn down (client went away without cancel()).
|
|
257
|
+
cancelled = true;
|
|
258
|
+
drainDeadline = now() + drainMs;
|
|
259
|
+
armDrainTimer();
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (terminalBoundary.terminalSeen()) {
|
|
264
|
+
// The Responses terminal event ends the turn even when a compatible
|
|
265
|
+
// gateway keeps its HTTP connection alive. Add the conventional
|
|
266
|
+
// sentinel and stop the single-reader relay at that protocol boundary.
|
|
267
|
+
if (!terminalBoundary.doneSeen()) {
|
|
268
|
+
queuedBytes += terminalSentinel.byteLength;
|
|
269
|
+
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
270
|
+
}
|
|
271
|
+
reader.cancel("Responses terminal event received").catch(() => {});
|
|
272
|
+
break;
|
|
244
273
|
}
|
|
245
274
|
while (queuedBytes > maxQueueBytes && !cancelled && !upstream.signal.aborted) {
|
|
246
275
|
await paused();
|
|
@@ -272,6 +301,7 @@ export function relaySseEagerBounded(
|
|
|
272
301
|
try { rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" }); } catch { /* teardown must not throw */ }
|
|
273
302
|
frameBufferBytes = 0;
|
|
274
303
|
}
|
|
304
|
+
terminalBoundary.dispose();
|
|
275
305
|
if (syntheticKind) hooks.onSynthetic(syntheticKind);
|
|
276
306
|
if (cancelled && !hooks.sawTerminal()) {
|
|
277
307
|
hooks.onClientCancel();
|
|
@@ -284,6 +314,7 @@ export function relaySseEagerBounded(
|
|
|
284
314
|
try { controllerRef?.close(); } catch { /* already closed/errored */ }
|
|
285
315
|
}
|
|
286
316
|
try { hooks.disposeInspection?.(); } catch { /* inspection teardown must not block lifecycle cleanup */ }
|
|
317
|
+
try { activeRewrite?.dispose?.(); } catch { /* rewrite teardown must not block lifecycle cleanup */ }
|
|
287
318
|
fireDone();
|
|
288
319
|
}
|
|
289
320
|
};
|
|
@@ -311,3 +342,12 @@ export function relaySseEagerBounded(
|
|
|
311
342
|
},
|
|
312
343
|
});
|
|
313
344
|
}
|
|
345
|
+
|
|
346
|
+
function joinUint8Arrays(first: Uint8Array, second: Uint8Array): Uint8Array {
|
|
347
|
+
if (first.byteLength === 0) return second;
|
|
348
|
+
if (second.byteLength === 0) return first;
|
|
349
|
+
const joined = new Uint8Array(first.byteLength + second.byteLength);
|
|
350
|
+
joined.set(first);
|
|
351
|
+
joined.set(second, first.byteLength);
|
|
352
|
+
return joined;
|
|
353
|
+
}
|
package/src/server/relay.ts
CHANGED
|
@@ -95,6 +95,79 @@ export function buildFailedTailPayload(err: unknown): string {
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export type SseTerminalOutputBoundary = {
|
|
99
|
+
feed(chunk: Uint8Array): Uint8Array;
|
|
100
|
+
finish(): Uint8Array;
|
|
101
|
+
terminalSeen(): boolean;
|
|
102
|
+
doneSeen(): boolean;
|
|
103
|
+
dispose(): void;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Frame-aware client output boundary shared by both native Responses relays.
|
|
108
|
+
* It buffers only the current incomplete SSE block, forwards complete blocks
|
|
109
|
+
* through the first Responses terminal, and drops every later block/byte.
|
|
110
|
+
*/
|
|
111
|
+
export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
112
|
+
let decoder: TextDecoder | null = new TextDecoder();
|
|
113
|
+
const encoder = new TextEncoder();
|
|
114
|
+
let buffer = "";
|
|
115
|
+
let terminal = false;
|
|
116
|
+
let done = false;
|
|
117
|
+
let disposed = false;
|
|
118
|
+
|
|
119
|
+
const process = (flush: boolean): Uint8Array => {
|
|
120
|
+
if (disposed || terminal) return new Uint8Array(0);
|
|
121
|
+
let output = "";
|
|
122
|
+
let responsesTerminal = false;
|
|
123
|
+
for (;;) {
|
|
124
|
+
const next = nextSseBlock(buffer);
|
|
125
|
+
if (!next) break;
|
|
126
|
+
buffer = next.rest;
|
|
127
|
+
const payload = sseDataPayload(next.block);
|
|
128
|
+
if (!responsesTerminal) output += next.block + next.delimiter;
|
|
129
|
+
if (payload === "[DONE]") {
|
|
130
|
+
done = true;
|
|
131
|
+
if (responsesTerminal) output += next.block + next.delimiter;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!responsesTerminal && payload && terminalStatusFromSsePayload(payload)) {
|
|
135
|
+
responsesTerminal = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (responsesTerminal) {
|
|
139
|
+
terminal = true;
|
|
140
|
+
buffer = "";
|
|
141
|
+
}
|
|
142
|
+
if (flush && !terminal && buffer.length > 0) {
|
|
143
|
+
output += buffer;
|
|
144
|
+
buffer = "";
|
|
145
|
+
}
|
|
146
|
+
return encoder.encode(output);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
feed(chunk) {
|
|
151
|
+
if (disposed || terminal) return new Uint8Array(0);
|
|
152
|
+
buffer += decoder!.decode(chunk, { stream: true });
|
|
153
|
+
return process(false);
|
|
154
|
+
},
|
|
155
|
+
finish() {
|
|
156
|
+
if (disposed || terminal) return new Uint8Array(0);
|
|
157
|
+
buffer += decoder!.decode();
|
|
158
|
+
return process(true);
|
|
159
|
+
},
|
|
160
|
+
terminalSeen: () => terminal,
|
|
161
|
+
doneSeen: () => done,
|
|
162
|
+
dispose() {
|
|
163
|
+
if (disposed) return;
|
|
164
|
+
disposed = true;
|
|
165
|
+
decoder = null;
|
|
166
|
+
buffer = "";
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
98
171
|
/**
|
|
99
172
|
* Relay a passthrough SSE body like relayWithAbort, but convert a MID-STREAM failure (upstream
|
|
100
173
|
* reset after headers) into a clean terminal: any partial block is closed off, then a synthetic
|
|
@@ -110,18 +183,57 @@ export function relaySseWithFailedTail(
|
|
|
110
183
|
): ReadableStream<Uint8Array> {
|
|
111
184
|
const reader = body.getReader();
|
|
112
185
|
const encoder = new TextEncoder();
|
|
186
|
+
const terminalBoundary = createSseTerminalOutputBoundary();
|
|
187
|
+
let closed = false;
|
|
188
|
+
const relayChunk = (
|
|
189
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
190
|
+
value: Uint8Array,
|
|
191
|
+
): "terminal" | "output" | "buffered" => {
|
|
192
|
+
const outbound = terminalBoundary.feed(value);
|
|
193
|
+
if (outbound.byteLength > 0) controller.enqueue(outbound);
|
|
194
|
+
if (!terminalBoundary.terminalSeen()) return outbound.byteLength > 0 ? "output" : "buffered";
|
|
195
|
+
|
|
196
|
+
// A Responses terminal frame is the protocol boundary. Some compatible
|
|
197
|
+
// gateways leave the HTTP connection open after response.completed, which
|
|
198
|
+
// otherwise leaves Codex waiting forever even though the model turn is done.
|
|
199
|
+
// Preserve through the terminal block only, add the conventional sentinel
|
|
200
|
+
// when there was no real [DONE] data event, then stop reading upstream.
|
|
201
|
+
if (!terminalBoundary.doneSeen()) {
|
|
202
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
203
|
+
}
|
|
204
|
+
closed = true;
|
|
205
|
+
controller.close();
|
|
206
|
+
const reason = "Responses terminal event received";
|
|
207
|
+
// Notify the tee inspection branch as well. It has already received the
|
|
208
|
+
// same terminal-bearing upstream chunk, so its bounded drain records the
|
|
209
|
+
// real terminal and then releases the turn/upstream keep-alive connection.
|
|
210
|
+
onClientGone?.(reason);
|
|
211
|
+
reader.cancel(reason).catch(() => {});
|
|
212
|
+
terminalBoundary.dispose();
|
|
213
|
+
return "terminal";
|
|
214
|
+
};
|
|
113
215
|
return new ReadableStream<Uint8Array>({
|
|
114
216
|
async pull(controller) {
|
|
115
217
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
218
|
+
for (;;) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
if (done) {
|
|
221
|
+
const tail = terminalBoundary.finish();
|
|
222
|
+
if (tail.byteLength > 0) controller.enqueue(tail);
|
|
223
|
+
terminalBoundary.dispose();
|
|
224
|
+
controller.close();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const result = relayChunk(controller, value);
|
|
228
|
+
if (result !== "buffered") return;
|
|
120
229
|
}
|
|
121
|
-
controller.enqueue(value);
|
|
122
230
|
} catch (err) {
|
|
231
|
+
const partial = terminalBoundary.finish();
|
|
232
|
+
terminalBoundary.dispose();
|
|
233
|
+
if (closed) return;
|
|
123
234
|
const payload = buildFailedTailPayload(err);
|
|
124
235
|
try {
|
|
236
|
+
if (partial.byteLength > 0) controller.enqueue(partial);
|
|
125
237
|
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
126
238
|
controller.enqueue(encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\ndata: [DONE]\n\n`));
|
|
127
239
|
controller.close();
|
|
@@ -130,6 +242,7 @@ export function relaySseWithFailedTail(
|
|
|
130
242
|
}
|
|
131
243
|
},
|
|
132
244
|
cancel(reason) {
|
|
245
|
+
terminalBoundary.dispose();
|
|
133
246
|
if (onClientGone) onClientGone(reason);
|
|
134
247
|
else upstream.abort(reason);
|
|
135
248
|
reader.cancel(reason).catch(() => {});
|
|
@@ -137,11 +250,12 @@ export function relaySseWithFailedTail(
|
|
|
137
250
|
});
|
|
138
251
|
}
|
|
139
252
|
|
|
140
|
-
export function nextSseBlock(buffer: string): { block: string; rest: string } | null {
|
|
253
|
+
export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
|
|
141
254
|
const match = buffer.match(/\r?\n\r?\n/);
|
|
142
255
|
if (!match || match.index === undefined) return null;
|
|
143
256
|
return {
|
|
144
257
|
block: buffer.slice(0, match.index),
|
|
258
|
+
delimiter: match[0],
|
|
145
259
|
rest: buffer.slice(match.index + match[0].length),
|
|
146
260
|
};
|
|
147
261
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
9
9
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
10
10
|
import type { OcxUsage } from "../types";
|
|
11
|
+
import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
|
|
11
12
|
import type { AdapterRequest } from "../adapters/base";
|
|
12
13
|
import { redactSecretString } from "../lib/redact";
|
|
13
14
|
import {
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
isKnownAdmissionKind,
|
|
16
17
|
isKnownInboundProtocol,
|
|
17
18
|
isKnownUsageSurface,
|
|
19
|
+
isValidReasoningWireValue,
|
|
18
20
|
readRecentUsageEntries,
|
|
19
21
|
usageForFinalLog,
|
|
20
22
|
usageStatusForFinalLog,
|
|
@@ -58,7 +60,7 @@ export interface RequestLogContext {
|
|
|
58
60
|
requestedEffort?: string;
|
|
59
61
|
effectiveEffort?: string;
|
|
60
62
|
reasoningWireField?: string;
|
|
61
|
-
reasoningWireValue?: string | number;
|
|
63
|
+
reasoningWireValue?: string | number | boolean;
|
|
62
64
|
requestedServiceTier?: string;
|
|
63
65
|
requestedSpeedLabel?: string;
|
|
64
66
|
configuredServiceTier?: string;
|
|
@@ -95,6 +97,8 @@ export interface RequestLogContext {
|
|
|
95
97
|
affinity?: "reused" | "new_bind" | "rebound" | "cleared";
|
|
96
98
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
97
99
|
terminalSource?: "upstream" | "synthetic";
|
|
100
|
+
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
101
|
+
routeDecision?: RouteDecisionTraceV1;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
export interface RequestLogEntry {
|
|
@@ -121,7 +125,7 @@ export interface RequestLogEntry {
|
|
|
121
125
|
requestedEffort?: string;
|
|
122
126
|
effectiveEffort?: string;
|
|
123
127
|
reasoningWireField?: string;
|
|
124
|
-
reasoningWireValue?: string | number;
|
|
128
|
+
reasoningWireValue?: string | number | boolean;
|
|
125
129
|
requestedServiceTier?: string;
|
|
126
130
|
requestedSpeedLabel?: string;
|
|
127
131
|
configuredServiceTier?: string;
|
|
@@ -146,6 +150,8 @@ export interface RequestLogEntry {
|
|
|
146
150
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
147
151
|
/** Whether the terminal came from a real upstream SSE event or a proxy synthetic tail. */
|
|
148
152
|
terminalSource?: "upstream" | "synthetic";
|
|
153
|
+
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
154
|
+
routeDecision?: RouteDecisionTraceV1;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
const requestLog: RequestLogEntry[] = [];
|
|
@@ -214,6 +220,7 @@ function asCloseReason(value: string | undefined): RequestLogEntry["closeReason"
|
|
|
214
220
|
export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): RequestLogEntry {
|
|
215
221
|
const terminalStatus = asTerminalStatus(entry.terminalStatus);
|
|
216
222
|
const closeReason = asCloseReason(entry.closeReason);
|
|
223
|
+
const routeDecision = normalizeRouteDecisionTraceForLog(entry.routeDecision);
|
|
217
224
|
return {
|
|
218
225
|
requestId: entry.requestId,
|
|
219
226
|
timestamp: entry.timestamp,
|
|
@@ -246,9 +253,21 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
246
253
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
247
254
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
248
255
|
...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
|
|
256
|
+
...(routeDecision ? { routeDecision } : {}),
|
|
249
257
|
};
|
|
250
258
|
}
|
|
251
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Hydration guard: persisted traces are re-normalized before they enter the
|
|
262
|
+
* in-memory ring buffer so a hand-edited or corrupt row cannot poison the DTO.
|
|
263
|
+
* A row that fails validation is dropped, never forwarded unvalidated.
|
|
264
|
+
*/
|
|
265
|
+
function normalizeRouteDecisionTraceForLog(
|
|
266
|
+
entry: RouteDecisionTraceV1 | undefined,
|
|
267
|
+
): RouteDecisionTraceV1 | null {
|
|
268
|
+
return entry ? normalizeRouteDecisionTrace(entry) : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
252
271
|
/**
|
|
253
272
|
* Seed the in-memory Logs ring buffer from usage.jsonl so GUI /api/logs survives
|
|
254
273
|
* `ocx stop` / `ocx start` (process restart). Idempotent per process; no-ops when
|
|
@@ -329,6 +348,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
329
348
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
330
349
|
...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
|
|
331
350
|
...failureDiagnostics,
|
|
351
|
+
...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
|
|
332
352
|
});
|
|
333
353
|
} catch {
|
|
334
354
|
/* request logging must never fail a user request */
|
|
@@ -399,12 +419,11 @@ export function recordAdapterReasoning(
|
|
|
399
419
|
const reasoning = raw as Record<string, unknown>;
|
|
400
420
|
if (typeof reasoning.effectiveEffort !== "string" || !reasoning.effectiveEffort
|
|
401
421
|
|| (reasoning.wireField !== "reasoning_effort"
|
|
422
|
+
&& reasoning.wireField !== "reasoning.enabled"
|
|
423
|
+
&& reasoning.wireField !== "reasoning.effort"
|
|
402
424
|
&& reasoning.wireField !== "thinking_budget"
|
|
403
425
|
&& reasoning.wireField !== "thinking.type")
|
|
404
|
-
||
|
|
405
|
-
&& !(typeof reasoning.wireValue === "number"
|
|
406
|
-
&& Number.isFinite(reasoning.wireValue)
|
|
407
|
-
&& reasoning.wireValue >= 0))) {
|
|
426
|
+
|| !isValidReasoningWireValue(reasoning.wireField, reasoning.wireValue)) {
|
|
408
427
|
return;
|
|
409
428
|
}
|
|
410
429
|
|
|
@@ -817,6 +836,7 @@ export function addFinalRequestLog(
|
|
|
817
836
|
...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
|
|
818
837
|
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
819
838
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
839
|
+
...(logCtx.routeDecision ? { routeDecision: logCtx.routeDecision } : {}),
|
|
820
840
|
});
|
|
821
841
|
if (isUsageDebugEnabled()) {
|
|
822
842
|
appendUsageDebug({
|
|
@@ -96,7 +96,7 @@ import {
|
|
|
96
96
|
sanitizePassthroughHeaders,
|
|
97
97
|
} from "../relay";
|
|
98
98
|
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
|
|
99
|
-
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
99
|
+
import type { EffectiveSubagentModel, EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
100
100
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
101
101
|
|
|
102
102
|
|
|
@@ -169,6 +169,7 @@ export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
|
|
|
169
169
|
|
|
170
170
|
export interface MultiAgentGuidanceOptions {
|
|
171
171
|
multiAgentGuidanceEnabled?: boolean;
|
|
172
|
+
codexAccountNamespace?: string;
|
|
172
173
|
injectionModel?: string;
|
|
173
174
|
injectionEffort?: string;
|
|
174
175
|
subagentModels?: string[];
|
|
@@ -207,6 +208,16 @@ export async function resolveEffectiveSubagentRoster(
|
|
|
207
208
|
return effectiveSubagentRoster(configuredModels, surface);
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
/** Reuse one parsed catalog snapshot across every roster projection for this request. */
|
|
212
|
+
async function createRequestScopedSubagentRosterResolver(): Promise<NonNullable<
|
|
213
|
+
MultiAgentGuidanceDeps["resolveEffectiveSubagentRoster"]
|
|
214
|
+
>> {
|
|
215
|
+
const { effectiveSubagentRoster, readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
|
|
216
|
+
const catalogEntries = readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
217
|
+
return (configuredModels, surface) =>
|
|
218
|
+
effectiveSubagentRoster(configuredModels, surface, catalogEntries);
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
|
|
211
222
|
|
|
212
223
|
export async function multiAgentGuidanceText(
|
|
@@ -218,10 +229,14 @@ export async function multiAgentGuidanceText(
|
|
|
218
229
|
const {
|
|
219
230
|
injectionModel,
|
|
220
231
|
injectionEffort,
|
|
232
|
+
codexAccountNamespace,
|
|
221
233
|
subagentModels,
|
|
222
234
|
subagentModelFallback,
|
|
223
235
|
injectionPrompt,
|
|
224
236
|
} = options;
|
|
237
|
+
const activeAccountNamespace = codexAccountNamespace?.length
|
|
238
|
+
? codexAccountNamespace
|
|
239
|
+
: undefined;
|
|
225
240
|
const surface = collabSurface(parsed);
|
|
226
241
|
if (surface === null) return null;
|
|
227
242
|
|
|
@@ -242,15 +257,51 @@ export async function multiAgentGuidanceText(
|
|
|
242
257
|
...(subagentModels ?? []),
|
|
243
258
|
...(injectionModel ? [injectionModel] : []),
|
|
244
259
|
];
|
|
245
|
-
const resolveRoster = deps.resolveEffectiveSubagentRoster
|
|
260
|
+
const resolveRoster = deps.resolveEffectiveSubagentRoster
|
|
261
|
+
?? await createRequestScopedSubagentRosterResolver();
|
|
246
262
|
const effective = await resolveRoster(configuredForGuidance, "v2");
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
// Resolve the roster and preferred roles independently so a bare native can project onto its
|
|
264
|
+
// generated account rows without making an unrelated provider/gpt-* row look equivalent.
|
|
265
|
+
// The intersection keeps both projections inside Codex's one global five-model window.
|
|
266
|
+
const candidateModels = new Set(effective.candidates.map(candidate => candidate.model));
|
|
267
|
+
const withinCandidateWindow = (candidate: EffectiveSubagentModel): boolean =>
|
|
268
|
+
candidateModels.has(candidate.model);
|
|
269
|
+
const configuredSubagents = subagentModels ?? [];
|
|
270
|
+
const subagentEffective = configuredSubagents.length > 0
|
|
271
|
+
? injectionModel
|
|
272
|
+
? await resolveRoster(configuredSubagents, "v2")
|
|
273
|
+
: effective
|
|
274
|
+
: undefined;
|
|
275
|
+
const preferredEffective = injectionModel
|
|
276
|
+
? configuredSubagents.length > 0
|
|
277
|
+
? await resolveRoster([injectionModel], "v2")
|
|
278
|
+
: effective
|
|
279
|
+
: undefined;
|
|
280
|
+
const explicitlyConfigured = (candidate: EffectiveSubagentModel): boolean =>
|
|
281
|
+
configuredSubagents.some(model =>
|
|
282
|
+
model.includes("/") && slugsEquivalent(model, candidate.model)
|
|
283
|
+
);
|
|
284
|
+
const allowedForCurrentRoute = (candidate: EffectiveSubagentModel): boolean =>
|
|
285
|
+
explicitlyConfigured(candidate)
|
|
286
|
+
|| !candidate.model.includes("/")
|
|
287
|
+
|| (activeAccountNamespace !== undefined
|
|
288
|
+
&& candidate.model.startsWith(`${activeAccountNamespace}/`));
|
|
289
|
+
const rosterModels = (subagentEffective?.advertised ?? [])
|
|
290
|
+
.filter(withinCandidateWindow)
|
|
291
|
+
.filter(allowedForCurrentRoute);
|
|
250
292
|
const roster = subagentRosterText(rosterModels);
|
|
251
|
-
const
|
|
252
|
-
|
|
293
|
+
const preferredCandidates = (preferredEffective?.advertised ?? []).filter(withinCandidateWindow);
|
|
294
|
+
const soleBarePreferred = preferredCandidates.length === 1
|
|
295
|
+
&& !preferredCandidates[0]!.model.includes("/")
|
|
296
|
+
? preferredCandidates[0]
|
|
253
297
|
: undefined;
|
|
298
|
+
const preferred = injectionModel?.includes("/")
|
|
299
|
+
? preferredCandidates[0]
|
|
300
|
+
: activeAccountNamespace
|
|
301
|
+
? preferredCandidates.find(candidate =>
|
|
302
|
+
candidate.model.startsWith(`${activeAccountNamespace}/`)
|
|
303
|
+
) ?? soleBarePreferred
|
|
304
|
+
: soleBarePreferred;
|
|
254
305
|
|
|
255
306
|
if (isInjectionDebugEnabled() && effective.excluded.length > 0) {
|
|
256
307
|
injectionDebugLog(`[opencodex] multi-agent guidance excluded: ${effective.excluded
|
|
@@ -260,7 +311,11 @@ export async function multiAgentGuidanceText(
|
|
|
260
311
|
const fallbackGuidance = subagentFallbackGuidanceText({ subagentModelFallback } as OcxConfig);
|
|
261
312
|
if (!injectionModel && roster === "" && fallbackGuidance === "") return null;
|
|
262
313
|
if (injectionPrompt) {
|
|
263
|
-
|
|
314
|
+
// Bare ids must resolve to a unique/current-route candidate. Preserve the legacy raw
|
|
315
|
+
// fallback only for explicit routed/account-qualified ids.
|
|
316
|
+
const promptModel = preferred?.model
|
|
317
|
+
?? (injectionModel?.includes("/") ? injectionModel : undefined);
|
|
318
|
+
return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, promptModel, injectionEffort, roster, fallbackGuidance)}</multi_agent_mode>`;
|
|
264
319
|
}
|
|
265
320
|
if (!preferred && roster === "" && fallbackGuidance === "") return null;
|
|
266
321
|
let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "
|