@bitkyc08/opencodex 2.6.1 → 2.6.2

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 (48) hide show
  1. package/README.md +8 -0
  2. package/bin/ocx.mjs +41 -9
  3. package/gui/dist/assets/index-LK87QnT7.js +9 -0
  4. package/gui/dist/index.html +1 -1
  5. package/package.json +1 -1
  6. package/src/abort.ts +22 -0
  7. package/src/adapters/base.ts +17 -4
  8. package/src/adapters/kiro-errors.ts +101 -0
  9. package/src/adapters/kiro-events.ts +48 -0
  10. package/src/adapters/kiro-images.ts +33 -0
  11. package/src/adapters/kiro-retry.ts +95 -0
  12. package/src/adapters/kiro-thinking.ts +82 -0
  13. package/src/adapters/kiro-tool-fallback.ts +36 -0
  14. package/src/adapters/kiro-tools.ts +44 -0
  15. package/src/adapters/kiro-truncation.ts +33 -0
  16. package/src/adapters/kiro-wire.ts +51 -0
  17. package/src/adapters/kiro.ts +527 -0
  18. package/src/adapters/openai-chat.ts +10 -1
  19. package/src/bridge.ts +1 -1
  20. package/src/cli.ts +25 -3
  21. package/src/codex-catalog.ts +97 -13
  22. package/src/codex-inject.ts +18 -0
  23. package/src/config.ts +52 -0
  24. package/src/crash-guard.ts +197 -9
  25. package/src/debug.ts +11 -0
  26. package/src/errors.ts +39 -3
  27. package/src/lib/eventstream-decoder.ts +244 -0
  28. package/src/lib/token-estimate.ts +43 -0
  29. package/src/oauth/anthropic.ts +1 -1
  30. package/src/oauth/index.ts +53 -6
  31. package/src/oauth/kiro-credentials.ts +256 -0
  32. package/src/oauth/kiro.ts +164 -0
  33. package/src/oauth/local-token-detect.ts +2 -1
  34. package/src/oauth/store.ts +36 -3
  35. package/src/oauth/types.ts +3 -0
  36. package/src/oauth/xai.ts +1 -1
  37. package/src/providers/kiro-models.ts +55 -0
  38. package/src/providers/registry.ts +15 -0
  39. package/src/redact.ts +71 -0
  40. package/src/server.ts +40 -22
  41. package/src/sidecar-tracker.ts +49 -0
  42. package/src/types.ts +3 -0
  43. package/src/usage-debug.ts +7 -4
  44. package/src/usage-log.ts +41 -3
  45. package/src/vision/describe.ts +11 -2
  46. package/src/web-search/executor.ts +10 -2
  47. package/src/web-search/loop.ts +27 -7
  48. package/gui/dist/assets/index-BmHrbTmO.js +0 -9
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Lightweight in-flight breadcrumb for sidecar work (web-search / vision), read by crash-guard.
3
+ *
4
+ * The proxy's unhandled rejections arrive with a native-only stack, so the throw site is invisible.
5
+ * To confirm or rule out the strong correlation with sidecar activity (the gpt-mini web-search /
6
+ * vision passes that run an EXTRA upstream fetch alongside the main turn), each sidecar call brackets
7
+ * itself with enter()/exit(). crash-guard then records whether any sidecar was in flight when a
8
+ * rejection fired — turning a guess into evidence without a debugger.
9
+ */
10
+ let inFlight = 0;
11
+ let lastLabel = "";
12
+ let lastEnterAt = 0;
13
+
14
+ export function sidecarEnter(label: string): () => void {
15
+ inFlight++;
16
+ lastLabel = label;
17
+ lastEnterAt = Date.now();
18
+ let exited = false;
19
+ return () => {
20
+ if (exited) return;
21
+ exited = true;
22
+ if (inFlight > 0) inFlight--;
23
+ };
24
+ }
25
+
26
+ export function sidecarBreadcrumb(): { inFlight: number; lastLabel: string; sinceMs: number } {
27
+ return {
28
+ inFlight,
29
+ lastLabel,
30
+ sinceMs: lastEnterAt ? Date.now() - lastEnterAt : 0,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Generic last-activity breadcrumb (any code path, not just sidecars). The proxy's native-only
36
+ * rejections carry no source location, so we record the most recent meaningful activity —
37
+ * request path + a short note — to correlate the next fault with what the daemon was doing.
38
+ */
39
+ let lastActivity = "";
40
+ let lastActivityAt = 0;
41
+
42
+ export function markActivity(note: string): void {
43
+ lastActivity = note;
44
+ lastActivityAt = Date.now();
45
+ }
46
+
47
+ export function activityBreadcrumb(): { note: string; sinceMs: number } {
48
+ return { note: lastActivity, sinceMs: lastActivityAt ? Date.now() - lastActivityAt : 0 };
49
+ }
package/src/types.ts CHANGED
@@ -175,6 +175,7 @@ export interface OcxRequestOptions {
175
175
  }
176
176
 
177
177
  export type AdapterEvent =
178
+ | { type: "heartbeat" }
178
179
  | { type: "text_delta"; text: string }
179
180
  | { type: "thinking_delta"; thinking: string }
180
181
  | { type: "reasoning_raw_delta"; text: string }
@@ -187,8 +188,10 @@ export type AdapterEvent =
187
188
  export interface OcxUsage {
188
189
  inputTokens: number;
189
190
  outputTokens: number;
191
+ totalTokens?: number;
190
192
  cachedInputTokens?: number;
191
193
  reasoningOutputTokens?: number;
194
+ estimated?: boolean;
192
195
  }
193
196
 
194
197
  export interface OcxConfig {
@@ -1,6 +1,7 @@
1
1
  import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getConfigDir } from "./config";
4
+ import { redactSecretString, redactSecrets } from "./redact";
4
5
  import type { OcxUsage } from "./types";
5
6
 
6
7
  export const USAGE_DEBUG_ENV = "OPENCODEX_USAGE_DEBUG";
@@ -31,9 +32,10 @@ export function usageDebugPath(): string {
31
32
  }
32
33
 
33
34
  export function truncateForDebug(text: string, max = USAGE_DEBUG_BODY_SAMPLE_BYTES): string {
34
- if (text.length <= max) return text;
35
- const cut = text.slice(0, max);
36
- const remaining = text.length - max;
35
+ const redacted = redactSecretString(text);
36
+ if (redacted.length <= max) return redacted;
37
+ const cut = redacted.slice(0, max);
38
+ const remaining = redacted.length - max;
37
39
  return `${cut}... [+${remaining} more]`;
38
40
  }
39
41
 
@@ -56,7 +58,8 @@ export function appendUsageDebug(record: UsageDebugRecord): void {
56
58
  try {
57
59
  ensureUsageDebugDir();
58
60
  const path = usageDebugPath();
59
- appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf-8", mode: 0o600 });
61
+ const safeRecord = redactSecrets(record) as UsageDebugRecord;
62
+ appendFileSync(path, `${JSON.stringify(safeRecord)}\n`, { encoding: "utf-8", mode: 0o600 });
60
63
  try { chmodSync(path, 0o600); } catch { /* best-effort */ }
61
64
  if (existsSync(path)) trimRollingFile(path);
62
65
  } catch {
package/src/usage-log.ts CHANGED
@@ -24,11 +24,49 @@ export function usageLogPath(): string {
24
24
 
25
25
  export function usageTotalTokens(usage: OcxUsage | undefined): number | undefined {
26
26
  if (!usage) return undefined;
27
- return usage.inputTokens + usage.outputTokens;
27
+ return usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
28
+ }
29
+
30
+ function isKiroLogProvider(provider: string): boolean {
31
+ return provider === "kiro" || provider.startsWith("kiro-");
32
+ }
33
+
34
+ export function usageForFinalLog(provider: string, usage: OcxUsage | undefined): OcxUsage | undefined {
35
+ if (!usage) return undefined;
36
+ if (usage.estimated || isKiroLogProvider(provider)) return { ...usage, estimated: true };
37
+ return usage;
28
38
  }
29
39
 
30
40
  export function usageStatusForFinalLog(usage: OcxUsage | undefined): UsageStatus {
31
- return usage ? "reported" : "unreported";
41
+ if (!usage) return "unreported";
42
+ return usage.estimated ? "estimated" : "reported";
43
+ }
44
+
45
+ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined {
46
+ if (!usage) return undefined;
47
+ return {
48
+ inputTokens: usage.inputTokens,
49
+ outputTokens: usage.outputTokens,
50
+ ...(typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {}),
51
+ ...(typeof usage.cachedInputTokens === "number" ? { cachedInputTokens: usage.cachedInputTokens } : {}),
52
+ ...(typeof usage.reasoningOutputTokens === "number" ? { reasoningOutputTokens: usage.reasoningOutputTokens } : {}),
53
+ ...(usage.estimated ? { estimated: true } : {}),
54
+ };
55
+ }
56
+
57
+ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
58
+ return {
59
+ requestId: entry.requestId,
60
+ timestamp: entry.timestamp,
61
+ provider: entry.provider,
62
+ model: entry.model,
63
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
64
+ status: entry.status,
65
+ durationMs: entry.durationMs,
66
+ usageStatus: entry.usageStatus,
67
+ ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
68
+ ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
69
+ };
32
70
  }
33
71
 
34
72
  function ensureUsageLogDir(): void {
@@ -40,7 +78,7 @@ function ensureUsageLogDir(): void {
40
78
  export function appendUsageEntry(entry: PersistedUsageEntry): void {
41
79
  ensureUsageLogDir();
42
80
  const path = usageLogPath();
43
- appendFileSync(path, `${JSON.stringify(entry)}\n`, { encoding: "utf-8", mode: 0o600 });
81
+ appendFileSync(path, `${JSON.stringify(normalizeUsageEntry(entry))}\n`, { encoding: "utf-8", mode: 0o600 });
44
82
  try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
45
83
  }
46
84
 
@@ -1,6 +1,7 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
2
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
3
- import { signalWithTimeout } from "../abort";
3
+ import { signalWithTimeout, cancelBodyOnAbort } from "../abort";
4
+ import { sidecarEnter } from "../sidecar-tracker";
4
5
  import { parseSidecarSSE } from "../web-search/parse";
5
6
  import type { SidecarOutcomeRecorder } from "../web-search/executor";
6
7
 
@@ -81,6 +82,7 @@ export async function describeImage(
81
82
  stream: true,
82
83
  };
83
84
  const linkedSignal = signalWithTimeout(settings.timeoutMs, abortSignal);
85
+ const sidecarExit = sidecarEnter("vision");
84
86
  try {
85
87
  const res = await fetch(`${forwardProvider.baseUrl}/responses`, {
86
88
  method: "POST",
@@ -93,7 +95,13 @@ export async function describeImage(
93
95
  const t = await res.text().catch(() => "");
94
96
  return { text: "", error: `vision sidecar HTTP ${res.status}: ${t.slice(0, 200)}` };
95
97
  }
96
- const parsed = await parseSidecarSSE(res);
98
+ const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
99
+ let parsed;
100
+ try {
101
+ parsed = await parseSidecarSSE(res);
102
+ } finally {
103
+ detachBodyGuard();
104
+ }
97
105
  // The backend can return HTTP 200 then stream a `response.failed`/`error` event with no text;
98
106
  // surface that as a describe error instead of an empty (silently-blank) description.
99
107
  if (!parsed.text.trim() && parsed.error) return { text: "", error: parsed.error };
@@ -102,6 +110,7 @@ export async function describeImage(
102
110
  recordOutcome?.(e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error");
103
111
  return { text: "", error: e instanceof Error ? e.message : String(e) };
104
112
  } finally {
113
+ sidecarExit();
105
114
  linkedSignal.cleanup();
106
115
  }
107
116
  }
@@ -1,6 +1,7 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
2
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
3
- import { signalWithTimeout } from "../abort";
3
+ import { signalWithTimeout, cancelBodyOnAbort } from "../abort";
4
+ import { sidecarEnter } from "../sidecar-tracker";
4
5
  import { parseSidecarSSE, type WebSearchResult } from "./parse";
5
6
  import type { CodexUpstreamOutcome } from "../codex-routing";
6
7
 
@@ -63,6 +64,7 @@ export async function runWebSearch(
63
64
  };
64
65
  const url = `${forwardProvider.baseUrl}/responses`;
65
66
  const linkedSignal = signalWithTimeout(settings.timeoutMs, abortSignal);
67
+ const sidecarExit = sidecarEnter("web-search");
66
68
  try {
67
69
  const res = await fetch(url, {
68
70
  method: "POST",
@@ -75,11 +77,17 @@ export async function runWebSearch(
75
77
  const t = await res.text().catch(() => "");
76
78
  return { text: "", sources: [], error: `sidecar HTTP ${res.status}: ${t.slice(0, 200)}` };
77
79
  }
78
- return await parseSidecarSSE(res);
80
+ const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
81
+ try {
82
+ return await parseSidecarSSE(res);
83
+ } finally {
84
+ detachBodyGuard();
85
+ }
79
86
  } catch (e) {
80
87
  recordOutcome?.(e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error");
81
88
  return { text: "", sources: [], error: e instanceof Error ? e.message : String(e) };
82
89
  } finally {
90
+ sidecarExit();
83
91
  linkedSignal.cleanup();
84
92
  }
85
93
  }
@@ -3,6 +3,7 @@ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig } fr
3
3
  import { namespacedToolName } from "../types";
4
4
  import { bridgeToResponsesSSE } from "../bridge";
5
5
  import { runWebSearch, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
6
+ import { cancelBodyOnAbort } from "../abort";
6
7
  import { formatWebSearchResult } from "./format-result";
7
8
  import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
8
9
 
@@ -132,12 +133,14 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
132
133
  const request = adapter.buildRequest(iterParsed, { headers: selectedForwardHeaders });
133
134
  let resp: Response;
134
135
  try {
135
- resp = await fetch(request.url, {
136
- method: request.method,
137
- headers: request.headers,
138
- body: request.body,
139
- signal: abortSignal,
140
- });
136
+ resp = adapter.fetchResponse
137
+ ? await adapter.fetchResponse(request, { abortSignal })
138
+ : await fetch(request.url, {
139
+ method: request.method,
140
+ headers: request.headers,
141
+ body: request.body,
142
+ signal: abortSignal,
143
+ });
141
144
  } catch (e) {
142
145
  return jsonError(502, `Provider unreachable: ${e instanceof Error ? e.message : String(e)}`);
143
146
  }
@@ -145,7 +148,24 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
145
148
  const t = await resp.text().catch(() => "");
146
149
  return jsonError(resp.status, `Provider error ${resp.status}: ${t.slice(0, 400)}`);
147
150
  }
148
- const events = await adapter.parseResponse(resp);
151
+ // The fetch above carries `abortSignal`; when the turn is superseded/cancelled, Bun aborts the
152
+ // response body stream. If parseResponse hasn't attached a reader yet, the body's pending read is
153
+ // orphaned off the awaited path and surfaces as `unhandledRejection: TypeError: null is not an
154
+ // object` (native-only stack). Proactively cancel the body on abort so WE settle it, and guard
155
+ // the drain so a mid-decode abort/stream error ends cleanly instead of throwing.
156
+ const detachBodyGuard = cancelBodyOnAbort(resp.body, abortSignal);
157
+ let events: AdapterEvent[];
158
+ try {
159
+ events = await adapter.parseResponse(resp);
160
+ } catch (e) {
161
+ await resp.body?.cancel().catch(() => {});
162
+ if (abortSignal?.aborted) {
163
+ return jsonError(499, "client closed request during web-search");
164
+ }
165
+ return jsonError(502, `Provider stream error: ${e instanceof Error ? e.message : String(e)}`);
166
+ } finally {
167
+ detachBodyGuard();
168
+ }
149
169
  const { calls, passthrough, hasRealToolCall } = scanEventsForWebSearch(events);
150
170
  // Loop (search + re-ask) ONLY when the model's actionable output is purely web_search. A real
151
171
  // tool call (e.g. shell/apply_patch) means this turn is terminal for Codex — finalize so those