@bitkyc08/opencodex 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -10,6 +10,7 @@
10
10
  * Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
11
11
  */
12
12
  import { loadConfig, readAlivePid, readRuntimePort, verifyPidIdentity } from "../config";
13
+ import { directLocalHttpFetch } from "./direct-local-http";
13
14
 
14
15
  export interface HealthzIdentity {
15
16
  service?: unknown;
@@ -97,7 +98,7 @@ export async function proxyIdentityAt(
97
98
  opts: { hostname?: string; expectedPid?: number } = {},
98
99
  io: LivenessIo = {},
99
100
  ): Promise<{ pid: number | null } | null> {
100
- const fetchFn = io.fetchFn ?? fetch;
101
+ const fetchFn = io.fetchFn ?? directLocalHttpFetch;
101
102
  const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
102
103
  const nowFn = io.nowFn ?? Date.now;
103
104
  const baseTimeoutMs = io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
@@ -307,7 +308,7 @@ export async function probeReadiness(
307
308
  opts: { hostname?: string; expectedPid?: number } = {},
308
309
  io: ReadinessProbeIo = {},
309
310
  ): Promise<ReadinessProbeResult | null> {
310
- const fetchFn = io.fetchFn ?? fetch;
311
+ const fetchFn = io.fetchFn ?? directLocalHttpFetch;
311
312
  try {
312
313
  const res = await fetchFn(`http://${probeHostname(opts.hostname)}:${port}/readyz`, {
313
314
  signal: AbortSignal.timeout(io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS),
@@ -180,12 +180,18 @@ export function decodeRequestBody(
180
180
  return assertBodySizeWithinLimit(decoded, maxBytes);
181
181
  }
182
182
 
183
- /** Parse a bounded JSON request body, transparently decoding compressed payloads. */
183
+ /**
184
+ * Parse a bounded JSON request body, transparently decoding compressed payloads.
185
+ *
186
+ * `options.signal`, when provided, replaces `req.signal` rather than composing with
187
+ * it. Callers that need both (for example request disconnect plus a deadline) must
188
+ * merge them into one AbortSignal before calling.
189
+ */
184
190
  export async function readBoundedJsonRequestBody(
185
191
  req: Request,
186
192
  maxBytes: number,
187
193
  budget?: TranslatorBudget,
188
- options?: { emptyBodyFallback?: unknown },
194
+ options?: { emptyBodyFallback?: unknown; signal?: AbortSignal },
189
195
  ): Promise<unknown> {
190
196
  const encoding = req.headers.get("content-encoding");
191
197
  const declaredLength = declaredBodyLength(req);
@@ -201,7 +207,7 @@ export async function readBoundedJsonRequestBody(
201
207
  : undefined;
202
208
  let raw: Uint8Array;
203
209
  try {
204
- raw = await readRequestBodyBytesCapped(req.body, maxBytes, req.signal);
210
+ raw = await readRequestBodyBytesCapped(req.body, maxBytes, options?.signal ?? req.signal);
205
211
  } finally {
206
212
  releaseReservation?.();
207
213
  }
@@ -2,8 +2,10 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import type { ResponsesTerminalStatus } from "../bridge";
3
3
  import {
4
4
  classifyError,
5
+ CYBER_POLICY_ERROR_CODE,
5
6
  httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
6
7
  isClientClosedMessage,
8
+ isCyberPolicyCode,
7
9
  } from "../lib/errors";
8
10
  import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
9
11
  import { readCodexCatalogPath } from "../codex/catalog";
@@ -16,6 +18,7 @@ import {
16
18
  isKnownAdmissionKind,
17
19
  isKnownInboundProtocol,
18
20
  isKnownUsageSurface,
21
+ isCodexUsageAccountLogLabel,
19
22
  isValidReasoningWireValue,
20
23
  readRecentUsageEntries,
21
24
  usageForFinalLog,
@@ -54,6 +57,8 @@ export interface RequestLogContext {
54
57
  * product: widening that enum would merge Responses and Chat Completions,
55
58
  * since both leave it undefined. */
56
59
  inboundProtocol?: "responses" | "chat" | "messages";
60
+ /** Stable non-PII Codex Pool account identity for durable usage attribution. */
61
+ accountLogLabel?: string;
57
62
  requestedModel?: string;
58
63
  /** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
59
64
  comboId?: string;
@@ -94,6 +99,8 @@ export interface RequestLogContext {
94
99
  upstreamError?: string;
95
100
  /** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
96
101
  terminalHttpStatus?: number;
102
+ /** Recognized structured terminal code whose exact identity must survive status mapping. */
103
+ terminalErrorCode?: typeof CYBER_POLICY_ERROR_CODE;
97
104
  /** Structured reason from `response.incomplete`; internal-only input to log classification. */
98
105
  terminalIncompleteReason?: string;
99
106
  affinity?: "reused" | "new_bind" | "rebound" | "cleared";
@@ -121,6 +128,7 @@ export interface RequestLogEntry {
121
128
  * product: widening that enum would merge Responses and Chat Completions,
122
129
  * since both leave it undefined. */
123
130
  inboundProtocol?: "responses" | "chat" | "messages";
131
+ accountLogLabel?: string;
124
132
  /** Best-effort chat/session correlation for Logs grouping (#330). */
125
133
  conversationId?: string;
126
134
  requestedModel?: string;
@@ -231,6 +239,9 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
231
239
  ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
232
240
  ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
233
241
  ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
242
+ ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
243
+ ? { accountLogLabel: entry.accountLogLabel }
244
+ : {}),
234
245
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
235
246
  ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
236
247
  ...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
@@ -327,6 +338,9 @@ export function addRequestLog(entry: RequestLogEntry) {
327
338
  ...(entry.apiKeyId ? { apiKeyId: entry.apiKeyId } : {}),
328
339
  ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
329
340
  ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
341
+ ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
342
+ ? { accountLogLabel: entry.accountLogLabel }
343
+ : {}),
330
344
  ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
331
345
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
332
346
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
@@ -446,12 +460,26 @@ export function recordAdapterReasoning(
446
460
  }
447
461
  }
448
462
 
449
- export function requestLogErrorCode(status: number, upstreamError?: string): string | undefined {
463
+ export function requestLogErrorCode(
464
+ status: number,
465
+ upstreamError?: string,
466
+ terminalErrorCode?: string,
467
+ ): string | undefined {
450
468
  if (status >= 200 && status < 400) return undefined;
469
+ // A structured terminal code is authoritative even when the provider message is localized,
470
+ // generic, or absent. Only preserve the one narrowly recognized policy code here: broadly
471
+ // forwarding arbitrary upstream codes would change unrelated request-log taxonomy.
472
+ if (isCyberPolicyCode(terminalErrorCode)) return CYBER_POLICY_ERROR_CODE;
473
+ const classifiedCode = upstreamError?.trim()
474
+ ? classifyError(status, "upstream_error", upstreamError).code
475
+ : undefined;
451
476
  // Defense in depth: mid-stream web-search aborts used to land as 502 with this message.
452
- if (status === 499 || (upstreamError?.trim() && classifyError(status, "upstream_error", upstreamError).code === "client_closed_request")) {
477
+ if (status === 499 || classifiedCode === "client_closed_request") {
453
478
  return "client_closed_request";
454
479
  }
480
+ // Keep the high-confidence message fallback for runtimes/providers that stripped the
481
+ // structured code before emitting response.failed.
482
+ if (classifiedCode === CYBER_POLICY_ERROR_CODE) return CYBER_POLICY_ERROR_CODE;
455
483
  if (status === 400 || status === 409) return "invalid_request_error";
456
484
  if (status === 401) return "invalid_api_key";
457
485
  if (status === 403) {
@@ -719,9 +747,17 @@ function captureTerminalHttpStatus(
719
747
  if (json.type !== "response.failed") return;
720
748
  const error = json.response?.error;
721
749
  if (!error || typeof error !== "object") return;
750
+ const terminalCode = error.code === null || typeof error.code === "string"
751
+ ? error.code
752
+ : undefined;
753
+ if (isCyberPolicyCode(terminalCode)) {
754
+ logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
755
+ } else {
756
+ delete logCtx.terminalErrorCode;
757
+ }
722
758
  logCtx.terminalHttpStatus = httpStatusFromTerminalError({
723
759
  type: typeof error.type === "string" ? error.type : undefined,
724
- code: error.code === null || typeof error.code === "string" ? error.code : undefined,
760
+ code: terminalCode,
725
761
  message: typeof error.message === "string" ? error.message : undefined,
726
762
  });
727
763
  }
@@ -777,7 +813,11 @@ export function addFinalRequestLog(
777
813
  const effectiveStatus = status >= 500 && logCtx.upstreamError && isClientClosedMessage(logCtx.upstreamError)
778
814
  ? 499
779
815
  : status;
780
- const errorCode = requestLogErrorCode(effectiveStatus, logCtx.upstreamError);
816
+ const errorCode = requestLogErrorCode(
817
+ effectiveStatus,
818
+ logCtx.upstreamError,
819
+ logCtx.terminalErrorCode,
820
+ );
781
821
  // A response.failed whose classified status is 499 is still a client cancel, not an upstream
782
822
  // terminal failure — keep /api/logs closeReason aligned with that.
783
823
  const closeReason = effectiveStatus === 499
@@ -790,6 +830,10 @@ export function addFinalRequestLog(
790
830
  Date.now() - (logCtx.activeAttemptStartedAt ?? start),
791
831
  logCtx.usage,
792
832
  );
833
+ // The final row and its active physical attempt describe the same terminal. Preserve the
834
+ // semantic code on both so detailed attempt telemetry cannot regress to a generic status code.
835
+ if (errorCode) logCtx.activeAttempt.errorCode = errorCode;
836
+ else delete logCtx.activeAttempt.errorCode;
793
837
  }
794
838
  const existing = finalizedUsage(
795
839
  logCtx.providerAdapter ?? logCtx.provider,
@@ -815,6 +859,9 @@ export function addFinalRequestLog(
815
859
  ...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
816
860
  ...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
817
861
  ...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
862
+ ...(isCodexUsageAccountLogLabel(logCtx.accountLogLabel)
863
+ ? { accountLogLabel: logCtx.accountLogLabel }
864
+ : {}),
818
865
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
819
866
  ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
820
867
  ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
@@ -965,10 +1012,12 @@ export function sealRequestAttemptIdentity(
965
1012
  attempt: PersistedUsageAttempt | undefined,
966
1013
  provider: string,
967
1014
  adapter: string,
1015
+ accountLogLabel?: string,
968
1016
  ): void {
969
1017
  if (!attempt) return;
970
1018
  attempt.provider = provider;
971
1019
  attempt.adapter = adapter;
1020
+ if (isCodexUsageAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel;
972
1021
  }
973
1022
 
974
1023
  export function noteAttemptSend(
@@ -249,9 +249,24 @@ export async function multiAgentGuidanceText(
249
249
  // Codex cannot actually spawn makes spawn_agent reject the override, so
250
250
  // suppress positive model claims while the state is stale or unknown.
251
251
  const catalogState = await (deps.collectCatalogState ?? defaultCollectCatalogState)();
252
+ // #1354 / #1395: `collectCodexAppServerCatalogState()` folds every app-server
253
+ // owned by the current user into ONE global observation, and the inbound
254
+ // request carries no sender PID or catalog fingerprint. So a stale process A
255
+ // makes the global state `stale` even when this request came from a fresh
256
+ // process B, and `unknown` can be reached by a process-enumeration failure
257
+ // that says nothing about any particular server.
258
+ //
259
+ // Emitting "do not set model or reasoning_effort overrides" off that global
260
+ // observation prohibits options the active `spawn_agent` tool legitimately
261
+ // advertises, for a request we cannot attribute to the stale process. The
262
+ // safe behaviour is to withhold OpenCodex's own disk-derived claims —
263
+ // preferred model, roster, fallback, custom guidance — and stay silent about
264
+ // overrides, leaving the active tool schema authoritative.
265
+ //
266
+ // `fresh` and `not_running` are unchanged: there we can positively describe
267
+ // the catalog, so the guidance below still applies.
252
268
  if (catalogState.state === "stale" || catalogState.state === "unknown") {
253
- return "<multi_agent_mode>The model catalog changed after Codex started; do not set "
254
- + "model or reasoning_effort overrides until Codex restarts.</multi_agent_mode>";
269
+ return null;
255
270
  }
256
271
  // codex-rs supplies the Proactive text on v2; the proxy only adds model-designation
257
272
  // guidance, and only when there is something concrete to designate: a configured
@@ -381,32 +396,93 @@ function isRecord(value: unknown): value is Record<string, unknown> {
381
396
  return !!value && typeof value === "object" && !Array.isArray(value);
382
397
  }
383
398
 
384
- function isGeneratedDeveloperItem(item: unknown, text: string): boolean {
385
- if (!isRecord(item) || item.type !== "message" || item.role !== "developer") return false;
386
- if (!Array.isArray(item.content) || item.content.length !== 1) return false;
399
+ function generatedDeveloperText(item: unknown): string | undefined {
400
+ if (!isRecord(item) || item.type !== "message" || item.role !== "developer") return undefined;
401
+ if (!Array.isArray(item.content) || item.content.length !== 1) return undefined;
387
402
  const [part] = item.content;
388
- return isRecord(part) && part.type === "input_text" && part.text === text;
403
+ return isRecord(part) && part.type === "input_text" && typeof part.text === "string"
404
+ ? part.text
405
+ : undefined;
406
+ }
407
+
408
+ function isGeneratedDeveloperItem(item: unknown, text: string): boolean {
409
+ return generatedDeveloperText(item) === text;
410
+ }
411
+
412
+ function isDeveloperPrefixItem(item: unknown): boolean {
413
+ if (!isRecord(item)) return false;
414
+ if (item.type === "additional_tools") return item.role === "developer";
415
+ const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
416
+ return type === "message" && (item.role === "system" || item.role === "developer");
417
+ }
418
+
419
+ function leadingDeveloperPrefixLength(items: readonly unknown[]): number {
420
+ let index = 0;
421
+ while (index < items.length && isDeveloperPrefixItem(items[index])) index += 1;
422
+ return index;
423
+ }
424
+
425
+ function isConversationalItem(item: unknown): boolean {
426
+ if (!isRecord(item)) return false;
427
+ if (item.type === "agent_message") return true;
428
+ const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
429
+ return type === "message" && (item.role === "user" || item.role === "assistant");
430
+ }
431
+
432
+ function statefulRawInsertionIndex(items: readonly unknown[], replayPrefixLen: number): number {
433
+ for (let index = replayPrefixLen; index < items.length; index += 1) {
434
+ if (isConversationalItem(items[index])) return index;
435
+ }
436
+ const last = items[items.length - 1];
437
+ return isRecord(last) && last.type === "compaction_trigger"
438
+ ? items.length - 1
439
+ : items.length;
389
440
  }
390
441
 
391
442
  export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string): void {
392
443
  const raw = parsed._rawBody as { input?: unknown } | undefined;
444
+ const rawInput = raw && Array.isArray(raw.input) ? raw.input : undefined;
445
+ const replayPrefixLen = rawInput
446
+ ? Math.min(parsed._replayPrefixLen ?? 0, rawInput.length)
447
+ : 0;
393
448
  const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] };
394
- if (raw && Array.isArray(raw.input)) {
395
- const replayPrefixLen = Math.min(parsed._replayPrefixLen ?? 0, raw.input.length);
396
- if (raw.input.slice(0, replayPrefixLen).some(item => isGeneratedDeveloperItem(item, text))) {
449
+ if (rawInput) {
450
+ const replayPrefix = rawInput.slice(0, replayPrefixLen);
451
+ const taggedGuidance = text.startsWith("<multi_agent_mode>") && text.endsWith("</multi_agent_mode>");
452
+ const lastTaggedGuidance = taggedGuidance
453
+ ? replayPrefix.map(generatedDeveloperText)
454
+ .filter(item => item?.startsWith("<multi_agent_mode>") && item.endsWith("</multi_agent_mode>"))
455
+ .at(-1)
456
+ : undefined;
457
+ if (taggedGuidance ? lastTaggedGuidance === text : replayPrefix.some(item => isGeneratedDeveloperItem(item, text))) {
397
458
  return;
398
459
  }
399
460
  }
400
461
 
401
- parsed.context.messages.push({ role: "developer", content: text, timestamp: Date.now() });
402
- if (raw && Array.isArray(raw.input)) {
403
- // compaction_trigger must remain the final input item (codex-rs + ChatGPT backend both
404
- // validate this). Insert the developer message BEFORE the trigger when present.
405
- const last = raw.input[raw.input.length - 1];
406
- if (last && typeof last === "object" && (last as { type?: string }).type === "compaction_trigger") {
407
- raw.input.splice(raw.input.length - 1, 0, devItem);
462
+ const statefulContinuation = parsed.previousResponseId !== undefined;
463
+ const message = { role: "developer" as const, content: text, timestamp: Date.now() };
464
+ const statefulRawIndex = statefulContinuation && rawInput
465
+ ? statefulRawInsertionIndex(rawInput, replayPrefixLen)
466
+ : undefined;
467
+
468
+ // A previous_response_id delta can begin with tool/protocol items. Keep those first, then place
469
+ // changed guidance before the current conversation. Stateless requests keep guidance in the prefix.
470
+ if (statefulContinuation) {
471
+ const index = Math.min(
472
+ parsed._continuationConversationMessageIndex ?? parsed.context.messages.length,
473
+ parsed.context.messages.length,
474
+ );
475
+ parsed.context.messages.splice(index, 0, message);
476
+ } else {
477
+ const prefixLen = parsed.context.messages.findIndex(item => item.role !== "developer");
478
+ parsed.context.messages.splice(prefixLen < 0 ? parsed.context.messages.length : prefixLen, 0, message);
479
+ }
480
+
481
+ if (rawInput) {
482
+ if (statefulContinuation) {
483
+ rawInput.splice(statefulRawIndex!, 0, devItem);
408
484
  } else {
409
- raw.input.push(devItem);
485
+ rawInput.splice(leadingDeveloperPrefixLength(rawInput), 0, devItem);
410
486
  }
411
487
  }
412
488
  }
@@ -120,6 +120,7 @@ import {
120
120
  } from "../relay";
121
121
  import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
122
122
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
123
+ import { codexAuthContextLogLabel } from "../../codex/account-label";
123
124
 
124
125
  import {
125
126
  decodeRequestErrorResponse,
@@ -362,6 +363,7 @@ export async function handleResponsesCompact(
362
363
  modelId: selectedModelId,
363
364
  beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
364
365
  });
366
+ logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
365
367
  const selected = headersForCodexAuthContext(req.headers, authCtx);
366
368
  compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
367
369
  for (const name of FORWARD_HEADERS) {
@@ -590,6 +592,7 @@ export async function handleResponsesCompact(
590
592
  });
591
593
  await upstream.body?.cancel().catch(() => undefined);
592
594
  outcomeCtx = alternate.authCtx;
595
+ logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
593
596
  try {
594
597
  upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single");
595
598
  } catch (err) {