@yansigit/opencodex 2.33.0 → 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 (196) 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 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  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 +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -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,6 +34,8 @@ 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
40
  kind?: "responses" | "live-sideband";
39
41
  liveUpstream?: WebSocket;
@@ -60,10 +62,20 @@ export interface WsData {
60
62
  * A test that only asserts "the socket opened" would still pass if the admission
61
63
  * were dropped from the payload, so the payload itself is what gets asserted.
62
64
  */
63
- 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 {
64
71
  // Auth is handshake-time only on this path: the per-frame contexts have no
65
72
  // request headers left to re-resolve from, so the decision rides along here.
66
- return { headers, admission, ...(admissionLease ? { admissionLease } : {}) };
73
+ return {
74
+ headers,
75
+ admission,
76
+ ...(admissionLease ? { admissionLease } : {}),
77
+ ...(sessionLaneId ? { sessionLaneId } : {}),
78
+ };
67
79
  }
68
80
 
69
81
  export class WsSendDroppedError extends Error {
@@ -0,0 +1,133 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ export interface SmokeCacheEntry {
7
+ fingerprint: string;
8
+ timestamp: number;
9
+ status: "passed" | "skipped" | "failed";
10
+ reason?: string;
11
+ modelsTested?: string[];
12
+ }
13
+
14
+ export interface SmokeCacheData {
15
+ version: number;
16
+ providers: Record<string, SmokeCacheEntry>;
17
+ }
18
+
19
+ const CACHE_VERSION = 1;
20
+
21
+ export function defaultSmokeCachePath(): string {
22
+ return join(homedir(), ".opencodex", "live-inference-cache.json");
23
+ }
24
+
25
+ function defaultCachePath(customPath?: string): string {
26
+ return customPath ?? defaultSmokeCachePath();
27
+ }
28
+
29
+ function emptyCache(): SmokeCacheData {
30
+ return { version: CACHE_VERSION, providers: {} };
31
+ }
32
+
33
+ function walkTs(root: string, relDir: string): string[] {
34
+ const paths: string[] = [];
35
+ const fullDir = join(root, relDir);
36
+ if (!existsSync(fullDir)) return paths;
37
+ for (const entry of readdirSync(fullDir, { withFileTypes: true })) {
38
+ const relPath = join(relDir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ paths.push(...walkTs(root, relPath));
41
+ continue;
42
+ }
43
+ if (entry.isFile() && entry.name.endsWith(".ts")) {
44
+ paths.push(relPath);
45
+ }
46
+ }
47
+ return paths;
48
+ }
49
+
50
+ function collectProviderSourcePaths(provider: string, root: string): string[] {
51
+ const smokePaths = ["src/smoke/runner.ts", "src/smoke/live-scenarios.ts"];
52
+ let paths: string[];
53
+ switch (provider) {
54
+ case "google":
55
+ case "google-antigravity":
56
+ case "google-aistudio":
57
+ paths = [...smokePaths, "src/adapters/google.ts", "src/adapters/google-wire-compiler.ts", "src/adapters/google-tool-schema.ts", "src/adapters/google-antigravity-wire.ts", "src/types/tools.ts", "src/server/responses-undeclared-tool-guard.ts"];
58
+ break;
59
+ case "cursor":
60
+ paths = [...smokePaths, "src/adapters/cursor.ts", ...walkTs(root, "src/adapters/cursor")];
61
+ break;
62
+ case "command-code":
63
+ paths = [...smokePaths, "src/adapters/command-code.ts", "src/adapters/command-code-project-context.ts", "src/providers/command-code-efforts.ts"];
64
+ break;
65
+ case "openai":
66
+ case "openai-responses":
67
+ paths = [...smokePaths, "src/adapters/openai-responses.ts", "src/adapters/responses-tool-schema.ts"];
68
+ break;
69
+ default:
70
+ paths = [...smokePaths, `src/adapters/${provider}.ts`];
71
+ break;
72
+ }
73
+ return [...new Set(paths)].filter(path => existsSync(join(root, path))).sort();
74
+ }
75
+
76
+ export async function computeProviderSourceFingerprint(
77
+ provider: string,
78
+ projectRoot?: string,
79
+ ): Promise<string> {
80
+ const root = resolve(projectRoot ?? process.cwd());
81
+ const hash = createHash("sha256");
82
+ const paths = collectProviderSourcePaths(provider, root);
83
+ if (paths.length === 0) return hash.update(`empty:${provider}`, "utf8").digest("hex");
84
+ for (const relPath of paths) {
85
+ hash.update(`${relPath}\0`, "utf8");
86
+ hash.update(readFileSync(join(root, relPath)));
87
+ hash.update("\0");
88
+ }
89
+ return hash.digest("hex");
90
+ }
91
+
92
+ export async function loadSmokeCache(customPath?: string): Promise<SmokeCacheData> {
93
+ const cachePath = defaultCachePath(customPath);
94
+ if (!existsSync(cachePath)) return emptyCache();
95
+ try {
96
+ const parsed = JSON.parse(readFileSync(cachePath, "utf8")) as SmokeCacheData;
97
+ if (typeof parsed !== "object" || parsed === null) return emptyCache();
98
+ return {
99
+ version: typeof parsed.version === "number" ? parsed.version : CACHE_VERSION,
100
+ providers: typeof parsed.providers === "object" && parsed.providers !== null ? parsed.providers : {},
101
+ };
102
+ } catch {
103
+ return emptyCache();
104
+ }
105
+ }
106
+
107
+ export async function saveSmokeCache(data: SmokeCacheData, customPath?: string): Promise<void> {
108
+ const cachePath = defaultCachePath(customPath);
109
+ mkdirSync(dirname(cachePath), { recursive: true });
110
+ writeFileSync(cachePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
111
+ }
112
+
113
+ export function shouldRunSmokeForProvider(
114
+ provider: string,
115
+ currentFingerprint: string,
116
+ options?: { force?: boolean; cache?: SmokeCacheData },
117
+ ): boolean {
118
+ if (options?.force) return true;
119
+ const entry = options?.cache?.providers[provider];
120
+ if (!entry) return true;
121
+ if (entry.fingerprint !== currentFingerprint) return true;
122
+ return entry.status !== "passed";
123
+ }
124
+
125
+ export async function recordSmokeResult(
126
+ provider: string,
127
+ entry: SmokeCacheEntry,
128
+ customPath?: string,
129
+ ): Promise<void> {
130
+ const cache = await loadSmokeCache(customPath);
131
+ cache.providers[provider] = entry;
132
+ await saveSmokeCache(cache, customPath);
133
+ }
@@ -0,0 +1,33 @@
1
+ export type SmokeLevel = 1 | 2 | 3;
2
+
3
+ export interface SmokeScenario {
4
+ level: SmokeLevel;
5
+ name: string;
6
+ buildRequest: (modelId: string, options?: { previousResponseId?: string; toolResult?: string }) => Record<string, unknown>;
7
+ }
8
+
9
+ export function buildSmokeScenarioRequest(
10
+ level: SmokeLevel,
11
+ modelId: string,
12
+ options: { previousResponseId?: string; toolCallId?: string; toolResult?: string } = {},
13
+ ): Record<string, unknown> {
14
+ const common = { model: modelId, stream: true };
15
+ if (level === 1) return {
16
+ ...common,
17
+ input: [{ role: "user", content: [{ type: "input_text", text: "Reply with exactly five words and include a brief reasoning process." }] }],
18
+ };
19
+ if (level === 2) return {
20
+ ...common,
21
+ input: [{ role: "user", content: [{ type: "input_text", text: "Use the provided exec_command tool to run echo \"smoke_test_123\"." }] }],
22
+ tools: [{ type: "function", name: "exec_command", description: "Execute a shell command.", parameters: { type: "object", properties: { cmd: { type: "string" } }, required: ["cmd"] } }],
23
+ };
24
+ return {
25
+ ...common,
26
+ ...(options.previousResponseId ? { previous_response_id: options.previousResponseId } : {}),
27
+ input: [{ type: "function_call_output", call_id: options.toolCallId ?? "smoke_call", output: options.toolResult ?? "smoke_test_123" }],
28
+ };
29
+ }
30
+
31
+ export const BasicStreamingTurn: SmokeScenario = { level: 1, name: "BasicStreamingTurn", buildRequest: (model, options) => buildSmokeScenarioRequest(1, model, options) };
32
+ export const ToolCallingTurn: SmokeScenario = { level: 2, name: "ToolCallingTurn", buildRequest: (model, options) => buildSmokeScenarioRequest(2, model, options) };
33
+ export const MultiTurnContinuity: SmokeScenario = { level: 3, name: "MultiTurnContinuity", buildRequest: (model, options) => buildSmokeScenarioRequest(3, model, options) };
@@ -0,0 +1,119 @@
1
+ import { loadConfig, resolveEnvValue } from "../config";
2
+ import { getCredential } from "../oauth/store";
3
+ import type { OAuthCredentials } from "../oauth/types";
4
+ import type { OcxProviderConfig } from "../types";
5
+ import { computeProviderSourceFingerprint, loadSmokeCache, recordSmokeResult, shouldRunSmokeForProvider } from "./fingerprint-cache";
6
+ import { buildSmokeScenarioRequest } from "./live-scenarios";
7
+
8
+ export interface ProviderSmokeResult {
9
+ provider: string;
10
+ modelId: string;
11
+ status: "passed" | "skipped" | "failed";
12
+ reason?: string;
13
+ level1Passed: boolean;
14
+ level2Passed: boolean;
15
+ level3Passed: boolean;
16
+ durationMs: number;
17
+ error?: string;
18
+ }
19
+
20
+ function result(provider: string, modelId: string, started: number, extra: Partial<ProviderSmokeResult>): ProviderSmokeResult {
21
+ return { provider, modelId, status: "failed", level1Passed: false, level2Passed: false, level3Passed: false, durationMs: Date.now() - started, ...extra };
22
+ }
23
+
24
+ export function providerHasSmokeCredential(
25
+ provider: Partial<Pick<OcxProviderConfig, "apiKey" | "authMode" | "headers">>,
26
+ credential?: OAuthCredentials | null,
27
+ ): boolean {
28
+ if (resolveEnvValue(provider.apiKey)?.trim()) return true;
29
+ if (Object.entries(provider.headers ?? {}).some(([name, value]) => /authorization|cookie|api[-_]?key/i.test(name) && typeof value === "string" && value.trim().length > 0)) return true;
30
+ if (provider.authMode === "forward") return true;
31
+ return provider.authMode === "oauth" && Boolean(credential?.access?.trim() || credential?.refresh?.trim());
32
+ }
33
+
34
+ function classifyHttp(status: number, body: string): "not_authenticated" | "quota_exhausted" | undefined {
35
+ if (status === 401 || status === 403) return "not_authenticated";
36
+ if (status === 429 || /credit|quota|insufficient.?balance|billing/i.test(body)) return "quota_exhausted";
37
+ return undefined;
38
+ }
39
+
40
+ export async function runProviderSmoke(options: { provider: string; modelId?: string; proxyUrl?: string; force?: boolean; cachePath?: string }): Promise<ProviderSmokeResult> {
41
+ const started = Date.now();
42
+ const config = loadConfig();
43
+ const providerConfig = config.providers[options.provider];
44
+ const modelId = options.modelId ?? config.subagentModels?.[0] ?? "gpt-5";
45
+ if (!providerConfig || providerConfig.disabled === true) return result(options.provider, modelId, started, { status: "skipped", reason: "not_authenticated" });
46
+ const credential = providerConfig.authMode === "oauth" ? getCredential(options.provider) : null;
47
+ if (!providerHasSmokeCredential(providerConfig, credential)) return result(options.provider, modelId, started, { status: "skipped", reason: "not_authenticated" });
48
+ const fingerprint = await computeProviderSourceFingerprint(options.provider);
49
+ const cache = await loadSmokeCache(options.cachePath);
50
+ if (!shouldRunSmokeForProvider(options.provider, fingerprint, { force: options.force, cache })) return result(options.provider, modelId, started, { status: "skipped", reason: "cached_pass" });
51
+
52
+ const baseUrl = options.proxyUrl ?? "http://127.0.0.1:10100/v1/responses";
53
+ let level1Passed = false, level2Passed = false, level3Passed = false;
54
+ let previousResponseId: string | undefined;
55
+ let emittedToolCallId: string | undefined;
56
+ try {
57
+ for (const level of [1, 2, 3] as const) {
58
+ const reqBody = buildSmokeScenarioRequest(level, modelId, {
59
+ previousResponseId,
60
+ toolCallId: emittedToolCallId,
61
+ toolResult: "smoke_test_123",
62
+ });
63
+ const response = await fetch(baseUrl, {
64
+ method: "POST",
65
+ headers: { "content-type": "application/json" },
66
+ body: JSON.stringify(reqBody),
67
+ signal: AbortSignal.timeout(30000),
68
+ });
69
+ const body = await response.text();
70
+ if (!response.ok) {
71
+ const reason = classifyHttp(response.status, body);
72
+ if (reason) {
73
+ const skipped = result(options.provider, modelId, started, { status: "skipped", reason, level1Passed, level2Passed, level3Passed });
74
+ await recordSmokeResult(options.provider, { fingerprint, timestamp: Date.now(), status: "skipped", reason, modelsTested: [modelId] }, options.cachePath);
75
+ return skipped;
76
+ }
77
+ throw new Error(`HTTP ${response.status}: ${body.slice(0, 200)}`);
78
+ }
79
+ const events = body.split(/\r?\n/).filter(line => line.startsWith("data: ")).flatMap(line => { try { return [JSON.parse(line.slice(6)) as Record<string, unknown>]; } catch { return []; } });
80
+ let jsonPayload: Record<string, unknown> | undefined;
81
+ try {
82
+ jsonPayload = JSON.parse(body) as Record<string, unknown>;
83
+ } catch {
84
+ jsonPayload = undefined;
85
+ }
86
+ const completed = events.find(event => event.type === "response.completed");
87
+ const responseData = (completed?.response as Record<string, unknown> | undefined) ?? (jsonPayload?.status === "completed" ? jsonPayload : undefined);
88
+ if (level === 1) level1Passed = events.some(event => event.type === "response.output_text.delta" || event.type === "response.output_item.added") || responseData?.status === "completed";
89
+ if (level === 2) {
90
+ const itemsFromEvents = events.flatMap(event => {
91
+ const item = event.item as Record<string, unknown> | undefined;
92
+ const output = (event.response as Record<string, unknown> | undefined)?.output;
93
+ return [item, ...(Array.isArray(output) ? output : [])].filter(value => value?.type === "function_call") as Record<string, unknown>[];
94
+ });
95
+ const itemsFromJson = Array.isArray(jsonPayload?.output) ? (jsonPayload!.output as Record<string, unknown>[]).filter(v => v?.type === "function_call") : [];
96
+ const calls = [...itemsFromEvents, ...itemsFromJson];
97
+ level2Passed = calls.some(call => {
98
+ try {
99
+ const rawArgs = typeof call.arguments === "string" ? JSON.parse(call.arguments) : call.arguments;
100
+ const args = rawArgs && typeof rawArgs === "object" ? rawArgs as Record<string, unknown> : {};
101
+ if (call.call_id || call.id) {
102
+ emittedToolCallId = String(call.call_id ?? call.id);
103
+ }
104
+ return typeof args.cmd === "string" && args.cmd.includes("echo") && args.cmd.includes("smoke_test_123");
105
+ } catch { return false; }
106
+ });
107
+ }
108
+ if (level === 3) level3Passed = (Boolean(completed) || responseData?.status === "completed") && !/400|thought.?signature|continuity/i.test(body);
109
+ if (responseData?.id) previousResponseId = String(responseData.id);
110
+ if (![level1Passed, level2Passed, level3Passed][level - 1]) throw new Error(`level ${level} assertion failed`);
111
+ }
112
+ await recordSmokeResult(options.provider, { fingerprint, timestamp: Date.now(), status: "passed", modelsTested: [modelId] }, options.cachePath);
113
+ return result(options.provider, modelId, started, { status: "passed", level1Passed, level2Passed, level3Passed });
114
+ } catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ await recordSmokeResult(options.provider, { fingerprint, timestamp: Date.now(), status: "failed", reason: message, modelsTested: [modelId] }, options.cachePath);
117
+ return result(options.provider, modelId, started, { status: "failed", level1Passed, level2Passed, level3Passed, error: message });
118
+ }
119
+ }
@@ -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
+ }