@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.
- package/README.md +8 -0
- package/bin/ocx.mjs +41 -9
- package/gui/dist/assets/index-LK87QnT7.js +9 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/abort.ts +22 -0
- package/src/adapters/base.ts +17 -4
- package/src/adapters/kiro-errors.ts +101 -0
- package/src/adapters/kiro-events.ts +48 -0
- package/src/adapters/kiro-images.ts +33 -0
- package/src/adapters/kiro-retry.ts +95 -0
- package/src/adapters/kiro-thinking.ts +82 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +44 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +51 -0
- package/src/adapters/kiro.ts +527 -0
- package/src/adapters/openai-chat.ts +10 -1
- package/src/bridge.ts +1 -1
- package/src/cli.ts +25 -3
- package/src/codex-catalog.ts +97 -13
- package/src/codex-inject.ts +18 -0
- package/src/config.ts +52 -0
- package/src/crash-guard.ts +197 -9
- package/src/debug.ts +11 -0
- package/src/errors.ts +39 -3
- package/src/lib/eventstream-decoder.ts +244 -0
- package/src/lib/token-estimate.ts +43 -0
- package/src/oauth/anthropic.ts +1 -1
- package/src/oauth/index.ts +53 -6
- package/src/oauth/kiro-credentials.ts +256 -0
- package/src/oauth/kiro.ts +164 -0
- package/src/oauth/local-token-detect.ts +2 -1
- package/src/oauth/store.ts +36 -3
- package/src/oauth/types.ts +3 -0
- package/src/oauth/xai.ts +1 -1
- package/src/providers/kiro-models.ts +55 -0
- package/src/providers/registry.ts +15 -0
- package/src/redact.ts +71 -0
- package/src/server.ts +40 -22
- package/src/sidecar-tracker.ts +49 -0
- package/src/types.ts +3 -0
- package/src/usage-debug.ts +7 -4
- package/src/usage-log.ts +41 -3
- package/src/vision/describe.ts +11 -2
- package/src/web-search/executor.ts +10 -2
- package/src/web-search/loop.ts +27 -7
- 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 {
|
package/src/usage-debug.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/vision/describe.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
package/src/web-search/loop.ts
CHANGED
|
@@ -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 =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|