@yansigit/opencodex 2.33.1 → 2.35.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 (177) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -1,3 +1,5 @@
1
+ import { isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
2
+
1
3
  export const MAX_CLIENT_SSE_FRAME_BYTES = 4 * 1024 * 1024;
2
4
 
3
5
  const LF_LF = Uint8Array.of(10, 10);
@@ -106,10 +108,35 @@ function isResponsesTerminalFrame(block: Uint8Array): boolean {
106
108
  const payload = data.join("\n");
107
109
  if (payload === "[DONE]") return false;
108
110
  try {
109
- const parsed = JSON.parse(payload) as { type?: unknown };
110
- return parsed.type === "response.completed"
111
+ const parsed = JSON.parse(payload) as {
112
+ type?: unknown;
113
+ code?: unknown;
114
+ message?: unknown;
115
+ error?: unknown;
116
+ last_error?: unknown;
117
+ response?: { error?: unknown; incomplete_details?: unknown };
118
+ };
119
+ if (parsed.type === "response.completed"
111
120
  || parsed.type === "response.failed"
112
- || parsed.type === "response.incomplete";
121
+ || parsed.type === "response.incomplete") return true;
122
+ if (parsed.type !== "error") return false;
123
+ // A top-level error is terminal for this boundary only when it carries the
124
+ // same high-confidence cyber-policy evidence used by relay.ts. Ordinary
125
+ // upstream errors remain transport failures and still become a bounded 502.
126
+ const candidates = [
127
+ parsed,
128
+ parsed.error,
129
+ parsed.last_error,
130
+ parsed.response?.error,
131
+ parsed.response?.incomplete_details,
132
+ ];
133
+ for (const candidate of candidates) {
134
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
135
+ const record = candidate as { code?: unknown; message?: unknown };
136
+ if (isCyberPolicyCode(typeof record.code === "string" ? record.code : undefined)) return true;
137
+ if (typeof record.message === "string" && isCyberPolicyMessage(record.message)) return true;
138
+ }
139
+ return false;
113
140
  } catch {
114
141
  return false;
115
142
  }
@@ -289,4 +316,4 @@ export function joinSseFrameBytes(parts: readonly Uint8Array[]): Uint8Array {
289
316
  offset += part.byteLength;
290
317
  }
291
318
  return joined;
292
- }
319
+ }
@@ -34,9 +34,10 @@ export interface WsData {
34
34
  authContext?: CodexAuthContext; // last resolved account decision for observability/registry cleanup
35
35
  cancel?: () => void; // cancels the in-flight stream reader/fetch
36
36
  turnId?: number; // monotonically increasing per socket; prevents stale frames after replacement turns
37
+ /** Fixed-size logical session lane derived at the HTTP upgrade boundary. */
38
+ sessionLaneId?: string;
37
39
  /** Discriminator: Responses reframing vs transparent live/realtime sideband relay. */
38
- kind?: "responses" | "live-sideband" | "aistudio-relay";
39
- aistudioSessionId?: string;
40
+ kind?: "responses" | "live-sideband";
40
41
  liveUpstream?: WebSocket;
41
42
  liveUpstreamUrl?: string;
42
43
  liveUpstreamHeaders?: Record<string, string>;
@@ -61,10 +62,20 @@ export interface WsData {
61
62
  * A test that only asserts "the socket opened" would still pass if the admission
62
63
  * were dropped from the payload, so the payload itself is what gets asserted.
63
64
  */
64
- export function buildResponsesWsData(headers: Headers, admission: DataPlaneAdmission, admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>): WsData {
65
+ export function buildResponsesWsData(
66
+ headers: Headers,
67
+ admission: DataPlaneAdmission,
68
+ admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>,
69
+ sessionLaneId?: string,
70
+ ): WsData {
65
71
  // Auth is handshake-time only on this path: the per-frame contexts have no
66
72
  // request headers left to re-resolve from, so the decision rides along here.
67
- return { headers, admission, ...(admissionLease ? { admissionLease } : {}) };
73
+ return {
74
+ headers,
75
+ admission,
76
+ ...(admissionLease ? { admissionLease } : {}),
77
+ ...(sessionLaneId ? { sessionLaneId } : {}),
78
+ };
68
79
  }
69
80
 
70
81
  export class WsSendDroppedError extends Error {
@@ -0,0 +1,44 @@
1
+ import type { LedgerRecord } from "./types";
2
+
3
+ export type GhCommandRunner = (args: string[]) => Promise<string>;
4
+
5
+ export interface AutonomousRemediationConfig {
6
+ autonomousRemediation?: { enabled?: boolean; instanceId?: string };
7
+ }
8
+
9
+ export type DispatchResult =
10
+ | { status: "skipped"; reason: "unauthorized" | "invalid-record" }
11
+ | { status: "dispatched"; issueNumber?: number; issueUrl?: string; existing: boolean };
12
+
13
+ const defaultRunner: GhCommandRunner = async args => {
14
+ const proc = Bun.spawn(["gh", ...args], { stdout: "pipe", stderr: "pipe" });
15
+ return await new Response(proc.stdout).text();
16
+ };
17
+
18
+ export async function dispatchAutonomousFix(
19
+ record: LedgerRecord,
20
+ config: AutonomousRemediationConfig,
21
+ run: GhCommandRunner = defaultRunner,
22
+ ): Promise<DispatchResult> {
23
+ const authorization = config.autonomousRemediation;
24
+ const eventInstance = typeof record.details?.instanceId === "string" ? record.details.instanceId : undefined;
25
+ if (authorization?.enabled !== true || !authorization.instanceId || (eventInstance !== undefined && eventInstance !== authorization.instanceId)) {
26
+ return { status: "skipped", reason: "unauthorized" };
27
+ }
28
+ if (!record.fingerprint) return { status: "skipped", reason: "invalid-record" };
29
+
30
+ const search = await run(["issue", "list", "--repo", "yansigit/opencodex", "--state", "open", "--search", `fingerprint:${record.fingerprint}`]);
31
+ const existing = search.match(/(?:^|\D)(\d+)(?:\D|$)/);
32
+ if (existing) {
33
+ const issueNumber = Number(existing[1]);
34
+ return { status: "dispatched", issueNumber, existing: true };
35
+ }
36
+
37
+ const metadata = { fingerprint: record.fingerprint, instanceId: authorization.instanceId, firstSeen: record.firstSeen, lastSeen: record.lastSeen, count: record.count, ...(record.details ?? {}) };
38
+ const body = `<!-- opencodex-failure-telemetry ${JSON.stringify(metadata)} -->\n\nFailure fingerprint: ${record.fingerprint}\n\nThis issue was dispatched by the verified local telemetry instance.`;
39
+ const title = `Autonomous fix: ${String(record.details?.failureKind ?? "failure")} (${record.fingerprint})`;
40
+ const created = await run(["issue", "create", "--repo", "yansigit/opencodex", "--title", title, "--body", body, "--label", "agent:jules,autonomous-fix,instance:verified"]);
41
+ const url = created.match(/https?:\/\/\S+/)?.[0];
42
+ const issueNumber = url?.match(/\/issues\/(\d+)/)?.[1];
43
+ return { status: "dispatched", existing: false, ...(url ? { issueUrl: url } : {}), ...(issueNumber ? { issueNumber: Number(issueNumber) } : {}) };
44
+ }
@@ -0,0 +1,24 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { FailureEvent, FailureFingerprint } from "./types";
3
+
4
+ const EPHEMERAL_KEYS = /^(timestamp|.*(?:request|session)[_-]?id|line|column|col)$/i;
5
+ const EPHEMERAL_TEXT = [
6
+ /\b(?:request|session)[_-]?id\s*[:=]\s*[^\s,;]+/gi,
7
+ /\b(?:timestamp|time)\s*[:=]\s*[^\s,;]+/gi,
8
+ /:\d+\s*:\s*\d+(?=\b|\D)/g,
9
+ /\b(?:line|col(?:umn)?)\s*[:=]?\s*\d+/gi,
10
+ /\b\d{10,13}\b/g,
11
+ ];
12
+
13
+ function canonical(value: unknown): unknown {
14
+ if (typeof value === "string") return EPHEMERAL_TEXT.reduce((text, pattern) => text.replace(pattern, ""), value).replace(/\s+/g, " ").trim();
15
+ if (Array.isArray(value)) return value.map(canonical);
16
+ if (value && typeof value === "object") {
17
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !EPHEMERAL_KEYS.test(key)).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)]));
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export function computeFailureFingerprint(event: FailureEvent): FailureFingerprint {
23
+ return createHash("sha256").update(JSON.stringify(canonical(event))).digest("hex");
24
+ }
@@ -0,0 +1,43 @@
1
+ import { resolveAutonomousRemediationConfig, type AutonomousRemediationConfig } from "../config/autonomous-remediation";
2
+ import { TelemetryLedger } from "./ledger";
3
+ import type { LedgerRecord } from "./types";
4
+
5
+ export type RuntimeFailureCategory = "websocket_1006" | "tool_repetition_loop" | "upstream_wire_error";
6
+
7
+ export interface RuntimeFailureContext {
8
+ category?: RuntimeFailureCategory;
9
+ provider?: string;
10
+ model?: string;
11
+ requestId?: string;
12
+ sessionId?: string;
13
+ timestamp?: number;
14
+ config?: AutonomousRemediationConfig | unknown;
15
+ ledger?: TelemetryLedger;
16
+ }
17
+
18
+ let defaultLedger: TelemetryLedger | undefined;
19
+
20
+ function categoryFor(error: unknown, requested?: RuntimeFailureCategory): RuntimeFailureCategory {
21
+ if (requested) return requested;
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ if (/1006|abnormal closure|websocket/i.test(message)) return "websocket_1006";
24
+ if (/tool.{0,32}(repeat|loop)|(?:repeat|loop).{0,32}tool/i.test(message)) return "tool_repetition_loop";
25
+ return "upstream_wire_error";
26
+ }
27
+
28
+ export function interceptRuntimeFailure(error: unknown, context: RuntimeFailureContext = {}): LedgerRecord | null {
29
+ const config = resolveAutonomousRemediationConfig(context.config);
30
+ if (!config.enabled) return null;
31
+ const ledger = context.ledger ?? (defaultLedger ??= new TelemetryLedger());
32
+ const category = categoryFor(error, context.category);
33
+ const signature = error instanceof Error ? error.message : String(error);
34
+ return ledger.recordFailure({
35
+ failureKind: category,
36
+ signature,
37
+ ...(context.provider ? { provider: context.provider } : {}),
38
+ ...(context.model ? { model: context.model } : {}),
39
+ ...(context.requestId ? { requestId: context.requestId } : {}),
40
+ ...(context.sessionId ? { sessionId: context.sessionId } : {}),
41
+ ...(context.timestamp !== undefined ? { timestamp: context.timestamp } : {}),
42
+ }, config.rollingWindowMs);
43
+ }
@@ -0,0 +1,54 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { computeFailureFingerprint } from "./fingerprint";
6
+ import type { FailureEvent, FailureFingerprint, LedgerRecord, RemediationStatus } from "./types";
7
+
8
+ type Stored = LedgerRecord & { occurrences: number[] };
9
+
10
+ export class TelemetryLedger {
11
+ private readonly db: Database;
12
+
13
+ constructor(path = join(homedir(), ".opencodex", "telemetry-issues.sqlite")) {
14
+ if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
15
+ this.db = new Database(path, { create: true });
16
+ this.db.run("CREATE TABLE IF NOT EXISTS failure_events (fingerprint TEXT PRIMARY KEY, first_seen INTEGER NOT NULL, last_seen INTEGER NOT NULL, count INTEGER NOT NULL, status TEXT NOT NULL, details TEXT, occurrences TEXT NOT NULL)");
17
+ }
18
+
19
+ recordFailure(event: FailureEvent, windowMs: number): LedgerRecord {
20
+ const fingerprint = computeFailureFingerprint(event);
21
+ const timestamp = event.timestamp ?? Date.now();
22
+ const old = this.db.query("SELECT * FROM failure_events WHERE fingerprint = ?").get(fingerprint) as Stored | null;
23
+ const occurrences = [...(old ? JSON.parse((old as unknown as { occurrences: string }).occurrences) : []), timestamp].filter((seen) => seen >= timestamp - windowMs).sort((a, b) => a - b);
24
+ const record: LedgerRecord = { fingerprint, firstSeen: old?.firstSeen ?? timestamp, lastSeen: timestamp, count: occurrences.length, status: old?.status ?? "monitoring", ...(old?.details ? { details: JSON.parse(old.details as unknown as string) } : {}) };
25
+ this.db.query("INSERT INTO failure_events (fingerprint, first_seen, last_seen, count, status, details, occurrences) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(fingerprint) DO UPDATE SET first_seen=excluded.first_seen, last_seen=excluded.last_seen, count=excluded.count, status=excluded.status, details=excluded.details, occurrences=excluded.occurrences").run(fingerprint, record.firstSeen, timestamp, record.count, record.status, record.details ? JSON.stringify(record.details) : null, JSON.stringify(occurrences));
26
+ return record;
27
+ }
28
+
29
+ getRecord(fingerprint: FailureFingerprint): LedgerRecord | null {
30
+ const row = this.db.query("SELECT fingerprint, first_seen, last_seen, count, status, details FROM failure_events WHERE fingerprint = ?").get(fingerprint) as Record<string, unknown> | null;
31
+ if (!row) return null;
32
+ return { fingerprint: row.fingerprint as string, firstSeen: row.first_seen as number, lastSeen: row.last_seen as number, count: row.count as number, status: row.status as RemediationStatus, ...(row.details ? { details: JSON.parse(row.details as string) } : {}) };
33
+ }
34
+
35
+ listRecords(): LedgerRecord[] {
36
+ return this.db.query("SELECT fingerprint, first_seen, last_seen, count, status, details FROM failure_events ORDER BY last_seen DESC").all().map(row => {
37
+ const value = row as Record<string, unknown>;
38
+ return { fingerprint: value.fingerprint as string, firstSeen: value.first_seen as number, lastSeen: value.last_seen as number, count: value.count as number, status: value.status as RemediationStatus, ...(value.details ? { details: JSON.parse(value.details as string) } : {}) };
39
+ });
40
+ }
41
+
42
+ updateStatus(fingerprint: FailureFingerprint, status: RemediationStatus, details?: Record<string, unknown>): void {
43
+ this.db.query("UPDATE failure_events SET status = ?, details = ? WHERE fingerprint = ?").run(status, details ? JSON.stringify(details) : null, fingerprint);
44
+ }
45
+
46
+ shouldDispatch(fingerprint: FailureFingerprint, threshold: number, windowMs: number): boolean {
47
+ const row = this.db.query("SELECT status, last_seen, occurrences FROM failure_events WHERE fingerprint = ?").get(fingerprint) as { status: RemediationStatus; last_seen: number; occurrences: string } | null;
48
+ if (!row || row.status !== "monitoring") return false;
49
+ const count = (JSON.parse(row.occurrences) as number[]).filter((seen) => seen >= row.last_seen - windowMs).length;
50
+ return count >= threshold;
51
+ }
52
+
53
+ close(): void { this.db.close(); }
54
+ }
@@ -0,0 +1,23 @@
1
+ export type RemediationStatus = "monitoring" | "dispatched" | "fixed" | "ignored";
2
+
3
+ export interface FailureEvent {
4
+ failureKind: string;
5
+ provider?: string;
6
+ model?: string;
7
+ signature: string;
8
+ timestamp?: number;
9
+ requestId?: string;
10
+ sessionId?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export type FailureFingerprint = string;
15
+
16
+ export interface LedgerRecord {
17
+ fingerprint: FailureFingerprint;
18
+ firstSeen: number;
19
+ lastSeen: number;
20
+ count: number;
21
+ status: RemediationStatus;
22
+ details?: Record<string, unknown>;
23
+ }
@@ -255,8 +255,19 @@ export interface OcxSubagentRole {
255
255
  enabled?: boolean;
256
256
  }
257
257
 
258
+ export interface OcxConfigRebaseProvenance {
259
+ version: 1;
260
+ deletedTopLevelKeys: string[];
261
+ }
262
+
258
263
  export interface OcxConfig {
259
264
  port: number;
265
+ autonomousRemediation?: {
266
+ enabled?: boolean;
267
+ instanceId?: string;
268
+ threshold?: number;
269
+ rollingWindowMs?: number;
270
+ };
260
271
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
261
272
  emptyCompletionRetry?: boolean;
262
273
  /**
@@ -276,6 +287,22 @@ export interface OcxConfig {
276
287
  managementUsageMaxReadBytes?: number;
277
288
  providers: Record<string, OcxProviderConfig>;
278
289
  defaultProvider: string;
290
+ /** Persisted state for newly discovered provider models (#2464). Absent keeps legacy "on" behavior. */
291
+ modelDiscovery?: {
292
+ newModelPolicy?: "on" | "off";
293
+ knownModels?: Record<string, {
294
+ ids: string[];
295
+ removed: string[];
296
+ updatedAt: string;
297
+ /** Consecutive successful discoveries in which an active id was absent. */
298
+ missing?: Record<string, number>;
299
+ }>;
300
+ recentArrivals?: Record<string, Array<{ id: string; at: string }>>;
301
+ };
302
+ /** Enable the shipped model alias patterns for providers without an override. */
303
+ defaultModelAliases?: boolean;
304
+ /** Explicit top-level deletion intent used by stale whole-config rebases. */
305
+ configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
279
306
  /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
280
307
  openaiProviderTierVersion?: 1 | 2;
281
308
  /** One-time migration marker for Antigravity's static-catalog defaults. */
@@ -294,12 +321,7 @@ export interface OcxConfig {
294
321
  * into a selector-qualified group; Codex still advertises only the first 5 visible rows.
295
322
  */
296
323
  subagentModels?: string[];
297
- /**
298
- * Named specialist roles the parent may spawn: id, when-to-use description,
299
- * model, optional effort, and child developer instructions. Max 8 roles and
300
- * 5 unique enabled models (the spawn picker window). Do not store
301
- * `model_fallback` here — use `subagentModelFallbackByModel`.
302
- */
324
+ /** Named specialist roles the parent may spawn by id. */
303
325
  subagentRoles?: OcxSubagentRole[];
304
326
  /**
305
327
  * Project enabled roles into marker-owned `$CODEX_HOME/agents/ocx-<id>.toml`.
@@ -338,6 +360,10 @@ export interface OcxConfig {
338
360
  * reject the whole role file as an unknown field (#1190).
339
361
  */
340
362
  subagentModelFallbackByModel?: Record<string, string[]>;
363
+ /**
364
+ * Ordered candidates for spawned sub-agents, globally or keyed by role/model.
365
+ */
366
+ subagentCandidates?: string[] | Record<string, string[]>;
341
367
  /**
342
368
  * TTL (ms) for cached sub-agent model availability probes. Default 60_000.
343
369
  */
@@ -402,9 +428,7 @@ export interface OcxConfig {
402
428
  * remain unchanged),
403
429
  * `{{effort}}` -> injectionEffort, `{{roster}}` -> the resolved sub-agent roster
404
430
  * block ("" when nothing resolves), `{{fallback}}` -> the configured subagent
405
- * model fallback guidance block ("" when unset), `{{roles}}` -> the compact
406
- * enabled role catalog ("" when none). A custom prompt replaces the built-in
407
- * body; include `{{roles}}` to keep the catalog.
431
+ * model fallback guidance block ("" when unset).
408
432
  */
409
433
  injectionPrompt?: string;
410
434
  /**
@@ -468,12 +492,12 @@ export interface OcxConfig {
468
492
  * Routed parents get v2 tools; Sol/Terra can still spawn Grok/Claude (issue #92).
469
493
  */
470
494
  keepNativeChatGptOnV1?: boolean;
471
- /** Experimental routed target for eligible native V2 root parents. */
495
+ /** Optional v2-native parent override for spawn_agent routing. */
472
496
  v2NativeParentOverride?: { enabled?: boolean; model?: string };
473
497
  /** Experimental, default-off ChatGPT recovery for encrypted V2 routed tasks. */
474
498
  agentTaskRecovery?: {
475
499
  enabled?: boolean;
476
- /** ChatGPT model used by the recovery request. Default: gpt-5.6-luna. */
500
+ /** ChatGPT model used by the recovery request. Default: gpt-5.6-sol. */
477
501
  model?: string;
478
502
  /** Recovery request timeout in milliseconds. Default: 45000. */
479
503
  timeoutMs?: number;
@@ -516,6 +540,13 @@ export interface OcxConfig {
516
540
  * those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
517
541
  */
518
542
  proxy?: string;
543
+ /**
544
+ * Hosts that bypass `proxy` for OpenCodex's own outbound provider calls, merged into
545
+ * NO_PROXY at startup. Accepts a comma-separated string (NO_PROXY syntax) or an array.
546
+ * Loopback is always excluded regardless of this setting, and an inherited NO_PROXY is
547
+ * preserved — this ADDS entries, it never replaces the environment.
548
+ */
549
+ noProxy?: string | string[];
519
550
  /**
520
551
  * Upstream stall timeout (seconds). After this many seconds of no upstream data, emits
521
552
  * response.incomplete. Default 300. Min 1.
@@ -587,6 +618,15 @@ export interface OcxConfig {
587
618
  * selector map remains visible for compatibility with hand-written configurations.
588
619
  */
589
620
  codexAccountPickerEnabled?: boolean;
621
+ /**
622
+ * Show the GPT-5.3-Codex-Spark weekly window on Codex quota surfaces. Default false.
623
+ *
624
+ * Spark is a single-model window that reads 0% for most operators, and on a multi-account
625
+ * pool it doubles the bar count for information almost nobody acts on. Hidden by default and
626
+ * revealed by an explicit `true`; a malformed value reads as hidden rather than rejecting the
627
+ * whole config.
628
+ */
629
+ showCodexSparkQuota?: boolean;
590
630
  /** Active pool account id for next session. undefined = main (passthrough as-is). */
591
631
  activeCodexAccountId?: string;
592
632
  /** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
@@ -617,10 +657,22 @@ export interface OcxConfig {
617
657
  stickyLimit?: number;
618
658
  };
619
659
  /**
620
- * Opt-in Cursor OAuth account pool. Default OFF.
621
- * Sticky `_clientThreadId` affinity + bounded 429/auth failover when ≥2 OAuth accounts exist.
622
- * Does not wire weighted-round-robin `CursorCredentialRouter`.
660
+ * Generic OAuth multi-account 429 failover (#2568). Presence-driven by default.
661
+ *
662
+ * Rotates to another logged-in account of the SAME provider when one is rate-limited, for
663
+ * OAuth providers that have no pool of their own — xAI, Cursor, Kimi, GitHub Copilot,
664
+ * Antigravity, Nous. The Codex pool and the Anthropic pool own their own rotation and are
665
+ * excluded; this setting changes neither.
666
+ *
667
+ * With the key absent, rotation activates when a provider has 2 or more eligible stored
668
+ * accounts — the same consent rule API-key pools already apply to a 2+ key pool (#2568d). A
669
+ * single account is a strict no-op. Set `false` to keep strict single-account behaviour;
670
+ * `providers.<name>.oauthAccountFailover` overrides this per provider.
623
671
  */
672
+ oauthAccountFailover?: {
673
+ enabled?: boolean;
674
+ };
675
+ /** Cursor OAuth account pool rotation for api2.cursor.sh. */
624
676
  cursorAccountPool?: {
625
677
  enabled?: boolean;
626
678
  };
@@ -119,6 +119,15 @@ export interface TierObservationContext {
119
119
  fastWire: FastWire | null;
120
120
  demandDecision: "force-fast" | "force-default" | "inherit";
121
121
  callerTier?: string;
122
+ /**
123
+ * Whether the destination's echoed `service_tier` is authoritative about Fast scheduling.
124
+ *
125
+ * The ChatGPT-internal Codex backend returns `service_tier: "default"` on turns that were in
126
+ * fact scheduled as priority, so treating its echo as a downgrade produced a false
127
+ * `response-declined` on every Fast request (#2558). Absent means "assume authoritative",
128
+ * preserving the behaviour for the public API where the echo does mean what it says.
129
+ */
130
+ responseTierAuthoritative?: boolean;
122
131
  }
123
132
 
124
133
  export type TierDecision =
@@ -131,6 +140,12 @@ export type TierDecision =
131
140
  * retries are allowed; OAuth/forward credentials and local runtimes are never replayed.
132
141
  */
133
142
  export interface OcxProviderConfig {
143
+ /** Optional short provider namespace used only at request/catalog presentation time. */
144
+ alias?: string;
145
+ /** Native model id -> short, slash-free request alias. */
146
+ modelAliases?: Record<string, string>;
147
+ /** Override the global built-in model-alias switch for this provider. */
148
+ defaultAliases?: boolean;
134
149
  adapter: string;
135
150
  /**
136
151
  * Codex tool calling mode for routed models.
@@ -227,6 +242,19 @@ export interface OcxProviderConfig {
227
242
  * link-local, or unique-local upstreams. Metadata endpoints remain blocked.
228
243
  */
229
244
  allowPrivateNetwork?: boolean;
245
+ /**
246
+ * ChatGPT Codex backend WebSocket upstream transport.
247
+ * Defaults to false (routes streaming turns over standard HTTP/SSE).
248
+ * Set `true` to opt into the faster responses_websockets transport.
249
+ * `OCX_CODEX_WS_UPSTREAM=true` or `1` also enables it when this is omitted;
250
+ * `false` and `0` disable it. Invalid or absent values default to HTTP/SSE.
251
+ */
252
+ wsUpstream?: boolean;
253
+ /**
254
+ * Maximum WebSocket request frame size in bytes before routing over standard HTTP/SSE.
255
+ * Defaults to CODEX_WS_CREATE_FRAME_LIMIT_BYTES (~16 MiB minus margin).
256
+ */
257
+ maxWsFrameBytes?: number;
230
258
  /**
231
259
  * Pin the HTTP version used for upstream provider requests. Bun's fetch negotiates
232
260
  * HTTP/2 via TLS ALPN by default; some Cloudflare-fronted SSE endpoints hang on
@@ -284,6 +312,33 @@ export interface OcxProviderConfig {
284
312
  * full set so the user can pick). See devlog issue_052_provider-model-allowlist.
285
313
  */
286
314
  selectedModels?: string[];
315
+ /** Override for newly discovered models. Absent/"inherit" uses the install policy. */
316
+ newModelPolicy?: "on" | "off" | "inherit";
317
+ /**
318
+ * Model-preset marker for `selectedModels` (#2465). Absent means "all", exactly today's
319
+ * semantics — an existing provider is never narrowed by an upgrade.
320
+ *
321
+ * The preset is a SEED, not a lock: `selectedModels` holds concrete ids materialized from
322
+ * the shipped rules, so every existing consumer and older binaries keep working against a
323
+ * plain allowlist. Divergence is detected at the WRITE path rather than by diffing — any user
324
+ * edit while the mode is "preset" flips it to "custom", after which the proxy never
325
+ * re-materializes. That collapses upgrade reconciliation to a version compare.
326
+ *
327
+ * Deliberately distinct from `deriveProviderPresets`, which curates WHICH PROVIDERS to offer.
328
+ * This curates which MODELS a provider exposes; the code says "model preset" throughout.
329
+ */
330
+ modelPreset?: {
331
+ mode: "preset" | "all" | "custom";
332
+ /** MODEL_PRESETS version materialized into `selectedModels`. */
333
+ appliedVersion?: number;
334
+ appliedAt?: string;
335
+ /**
336
+ * Set when materialization matched nothing and the provider fell back to "all". A preset
337
+ * must never write an empty allowlist, because empty means ALL and would silently
338
+ * un-curate; the fallback marker lets the next convergence retry.
339
+ */
340
+ fallback?: "preset-empty";
341
+ };
287
342
  /** Provider-wide fallback when context metadata is absent; otherwise caps the reported window. */
288
343
  contextWindow?: number;
289
344
  /** Per-model fallback when context metadata is absent; otherwise caps the reported window. */
@@ -327,6 +382,16 @@ export interface OcxProviderConfig {
327
382
  * providers whose registry entry declares authKind "local" (management API enforces).
328
383
  */
329
384
  authMode?: "key" | "forward" | "oauth" | "local";
385
+ /**
386
+ * Per-provider override for generic OAuth multi-account 429 failover (#2568).
387
+ *
388
+ * Rotation is presence-driven by default — 2+ logged-in accounts activate it — so this exists
389
+ * for the operator who accepts rotation on one provider and refuses it on another. An explicit
390
+ * boolean here beats the global `oauthAccountFailover` and beats presence.
391
+ */
392
+ oauthAccountFailover?: {
393
+ enabled?: boolean;
394
+ };
330
395
  /** Allow an explicitly key/oauth provider to run without a credential (for keyless local proxies). */
331
396
  keyOptional?: boolean;
332
397
  /**
@@ -359,6 +424,18 @@ export interface OcxProviderConfig {
359
424
  * Responses backend rejects Codex summary-delivery fields for that model.
360
425
  */
361
426
  modelSupportsReasoningSummaries?: Record<string, boolean>;
427
+ /**
428
+ * Model-specific Codex Responses verbosity capability. Set false when the upstream ignores
429
+ * `text.verbosity`; the catalog hides the no-op picker and the Responses adapter strips stale
430
+ * or caller-supplied values while preserving other `text` fields.
431
+ */
432
+ modelSupportsVerbosity?: Record<string, boolean>;
433
+ /**
434
+ * Provider-wide Codex Responses verbosity capability, applied to models the per-model map
435
+ * does not enumerate (a live-discovered id, for example). Materialized from the registry at
436
+ * seed/enrich time so the catalog hint pass never has to read PROVIDER_REGISTRY.
437
+ */
438
+ supportsVerbosity?: boolean;
362
439
  /**
363
440
  * Per-model wire value for Responses `stream_options.reasoning_summary_delivery`.
364
441
  * Presence also advertises reasoning-summary support for that routed model.
@@ -72,6 +72,8 @@ export interface OcxParsedRequest {
72
72
  _promptCacheKeyIsSharedCohort?: boolean;
73
73
  /** Provider-private Command Code session affinity id, stable across in-process request mutation. */
74
74
  _commandCodeSessionId?: string;
75
+ /** Cursor-only thread owner; may be an opaque process-local Desktop session/thread identity. */
76
+ _cursorClientThreadId?: string;
75
77
  /** Conversation/provider/account/model-bound namespace for reasoning replay state. */
76
78
  _reasoningReplayScope?: OcxReasoningReplayScopeRef;
77
79
  /**
@@ -90,7 +92,7 @@ export interface OcxParsedRequest {
90
92
  */
91
93
  _cursorIsolateConversation?: boolean;
92
94
  /** Account-scoped, non-secret Kiro request metadata selected with the OAuth access token. */
93
- _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
95
+ _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
94
96
  /** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
95
97
  _providerContinuation?: OcxProviderContinuationState;
96
98
  /** Persisted continuation considered only after the final physical route is known. */
@@ -198,8 +200,14 @@ export interface OcxImageContent {
198
200
  detail?: string;
199
201
  }
200
202
 
201
- /** A user/developer message content part: text or an image (vision). */
202
- export type OcxContentPart = OcxTextContent | OcxImageContent;
203
+ export interface OcxVideoContent {
204
+ type: "video";
205
+ /** A base64 `data:` URL from an OpenAI-compatible `video_url` part. */
206
+ videoUrl: string;
207
+ }
208
+
209
+ /** A user/developer message content part: text or native media. */
210
+ export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent;
203
211
 
204
212
  export interface OcxThinkingContent {
205
213
  type: "thinking";
@@ -260,13 +268,9 @@ export interface OcxRequestOptions {
260
268
  /**
261
269
  * Responses `text.format` (json_schema / json_object), preserved for adapters whose
262
270
  * upstream wire has an equivalent. The openai-chat adapter re-nests it as chat
263
- * `response_format`, the exact inverse of responseFormatToText in src/chat/inbound.ts; the
264
- * Google adapter lowers supported requests to Gemini JSON mode (`responseMimeType` /
265
- * `responseSchema`) but skips requests with tools, Claude models, or image-capable models.
266
- * The `openai-chat` adapter can omit it for models in `noStructuredOutputModels`; Kiro
267
- * rejects structured output via `_structuredOutput`; and Cursor has no structured-output
268
- * wire field and rejects the request before transport.
269
- * Native passthrough does not consume this option and forwards `_rawBody.text` verbatim.
271
+ * `response_format`, the exact inverse of responseFormatToText in src/chat/inbound.ts.
272
+ * The native passthrough ignores it (it forwards `_rawBody.text` verbatim) and Kiro
273
+ * keeps rejecting structured output via `_structuredOutput`.
270
274
  */
271
275
  textFormat?: {
272
276
  type: "json_schema" | "json_object";
@@ -292,10 +296,12 @@ export type GoogleSafetyThreshold =
292
296
  | "BLOCK_ONLY_HIGH"
293
297
  | "BLOCK_NONE"
294
298
  | "OFF";
299
+
295
300
  export interface GoogleSafetySetting {
296
301
  category: GoogleSafetyCategory;
297
302
  threshold: GoogleSafetyThreshold;
298
303
  }
304
+
299
305
  export interface GoogleProviderOptions {
300
306
  thinkingBudget?: number;
301
307
  includeThoughts?: boolean;