@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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 +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { getCurrentVersion } from "./update/index.js";
|
|
2
2
|
const MAX_FIELD_CHARS = 160;
|
|
3
3
|
const RUNTIME_STARTED_AT = Date.now();
|
|
4
|
+
const TRANSPORT_ERROR_CODES = new Set([
|
|
5
|
+
"UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT",
|
|
6
|
+
"ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN",
|
|
7
|
+
]);
|
|
4
8
|
const SAFE_ERROR_CODES = new Map([
|
|
5
9
|
["authentication_error", "authentication_error"],
|
|
6
10
|
["bad_request", "bad_request"],
|
|
@@ -8,6 +12,7 @@ const SAFE_ERROR_CODES = new Map([
|
|
|
8
12
|
["failed_precondition", "failed_precondition"],
|
|
9
13
|
["internal_error", "internal_error"],
|
|
10
14
|
["invalid_argument", "invalid_argument"],
|
|
15
|
+
["invalid_request_error", "invalid_request_error"],
|
|
11
16
|
["invalidparameter", "invalid_parameter"],
|
|
12
17
|
["invalid_parameter", "invalid_parameter"],
|
|
13
18
|
["location_not_supported", "location_not_supported"],
|
|
@@ -20,6 +25,7 @@ const SAFE_ERROR_CODES = new Map([
|
|
|
20
25
|
["unauthenticated", "unauthenticated"],
|
|
21
26
|
["unsupported_parameter", "unsupported_parameter"],
|
|
22
27
|
["unsupported_value", "unsupported_value"],
|
|
28
|
+
...Array.from(TRANSPORT_ERROR_CODES, (code) => [code.toLowerCase(), code]),
|
|
23
29
|
]);
|
|
24
30
|
const SAFE_PARAMETER_NAMES = new Set([
|
|
25
31
|
"contents",
|
|
@@ -59,7 +65,8 @@ export function createSanitizedProviderError(error, context) {
|
|
|
59
65
|
const rawMessage = firstString(sources, ["message"])
|
|
60
66
|
|| (typeof error === "string" ? error : "Provider request failed.");
|
|
61
67
|
const httpStatus = firstHttpStatus(sources);
|
|
62
|
-
|
|
68
|
+
// A wrapper's unknown code must not hide an allowlisted nested transport code.
|
|
69
|
+
const code = sources.flatMap((source) => ["code", "errorCode", "type"].map((key) => typeof source[key] === "string" ? sanitizeErrorCode(source[key]) : undefined)).find((value) => value !== undefined);
|
|
63
70
|
const parameter = sanitizeParameterName(firstScalarString(sources, ["param", "parameter"]));
|
|
64
71
|
return {
|
|
65
72
|
providerId: sanitizeControlledIdentifier(context.providerId),
|
|
@@ -67,17 +74,37 @@ export function createSanitizedProviderError(error, context) {
|
|
|
67
74
|
...(context.model ? { model: sanitizeControlledIdentifier(context.model) } : {}),
|
|
68
75
|
thinkingLevel: context.thinkingLevel,
|
|
69
76
|
name: "ProviderError",
|
|
70
|
-
message:
|
|
77
|
+
message: code && TRANSPORT_ERROR_CODES.has(code)
|
|
78
|
+
? code.includes("TIMEOUT") || code === "ETIMEDOUT" ? "Provider request timed out." : "Provider connection failed."
|
|
79
|
+
: sanitizeProviderErrorText(rawMessage),
|
|
71
80
|
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
72
81
|
...(code ? { code } : {}),
|
|
73
82
|
...(parameter ? { parameter } : {}),
|
|
74
83
|
messageCount: Math.max(0, Math.trunc(context.messageCount)),
|
|
75
84
|
toolCount: Math.max(0, Math.trunc(context.toolCount)),
|
|
85
|
+
...sanitizedRequestShape(sources),
|
|
76
86
|
bubbleVersion: getCurrentVersion(),
|
|
77
87
|
pid: process.pid,
|
|
78
88
|
runtimeStartedAt: RUNTIME_STARTED_AT,
|
|
89
|
+
...(context.retry ? { retry: {
|
|
90
|
+
attempt: context.retry.attempt,
|
|
91
|
+
maxAttempts: context.retry.maxAttempts,
|
|
92
|
+
} } : {}),
|
|
79
93
|
};
|
|
80
94
|
}
|
|
95
|
+
function sanitizedRequestShape(sources) {
|
|
96
|
+
const raw = sources.find(source => asRecord(source.requestShape))?.requestShape;
|
|
97
|
+
const shape = asRecord(raw);
|
|
98
|
+
if (!shape)
|
|
99
|
+
return {};
|
|
100
|
+
const safe = {};
|
|
101
|
+
for (const key of ["messages", "tools", "assistant", "emptyAssistant", "reasoning", "toolCalls", "toolResults", "contentChars"]) {
|
|
102
|
+
const value = shape[key];
|
|
103
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
104
|
+
safe[key] = value;
|
|
105
|
+
}
|
|
106
|
+
return { requestShape: safe };
|
|
107
|
+
}
|
|
81
108
|
export function sanitizeProviderErrorText(value) {
|
|
82
109
|
const text = value.replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
83
110
|
// Fail closed: provider/SDK messages can embed request headers, bodies,
|
|
@@ -99,6 +126,9 @@ export function sanitizeProviderErrorText(value) {
|
|
|
99
126
|
if (/forbidden|permission denied|access denied|\b403\b/i.test(text)) {
|
|
100
127
|
return "Provider denied the request.";
|
|
101
128
|
}
|
|
129
|
+
if (/unprocessable entity|\b422\b/i.test(text)) {
|
|
130
|
+
return "Provider rejected the request (unprocessable entity).";
|
|
131
|
+
}
|
|
102
132
|
if (/invalid[^.]{0,60}(?:parameter|argument)|unsupported[^.]{0,60}(?:parameter|argument)|bad request|\b400\b/i.test(text)) {
|
|
103
133
|
return "Provider rejected a request parameter.";
|
|
104
134
|
}
|
|
@@ -115,18 +145,16 @@ export function sanitizeProviderErrorText(value) {
|
|
|
115
145
|
}
|
|
116
146
|
function errorSources(error) {
|
|
117
147
|
const sources = [];
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (causeError)
|
|
129
|
-
sources.push(causeError);
|
|
148
|
+
const pending = [error];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
while (pending.length && sources.length < 12) {
|
|
151
|
+
const source = asRecord(pending.shift());
|
|
152
|
+
if (!source || seen.has(source))
|
|
153
|
+
continue;
|
|
154
|
+
seen.add(source);
|
|
155
|
+
sources.push(source);
|
|
156
|
+
pending.push(source.error, source.cause);
|
|
157
|
+
}
|
|
130
158
|
return sources;
|
|
131
159
|
}
|
|
132
160
|
function asRecord(value) {
|
package/dist/provider-grok.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* identity headers the proxy gates on, and a fetch wrapper that keeps the
|
|
8
8
|
* OAuth bearer fresh across long sessions.
|
|
9
9
|
*/
|
|
10
|
+
import { waitForOAuth } from "./oauth/refresh-control.js";
|
|
10
11
|
import { getChatGptFetch } from "./network/chatgpt-transport.js";
|
|
11
12
|
import { THINKING_LEVELS } from "./types.js";
|
|
12
13
|
export const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
@@ -49,7 +50,10 @@ export function createGrokSubscriptionFetch(auth, baseFetch = getChatGptFetch())
|
|
|
49
50
|
return credentials.accessToken || undefined;
|
|
50
51
|
}
|
|
51
52
|
return async (input, init) => {
|
|
52
|
-
const
|
|
53
|
+
const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
54
|
+
signal?.throwIfAborted();
|
|
55
|
+
const token = await waitForOAuth(freshAccessToken(), signal);
|
|
56
|
+
signal?.throwIfAborted();
|
|
53
57
|
if (!token) {
|
|
54
58
|
return baseFetch(input, init);
|
|
55
59
|
}
|
|
@@ -14,6 +14,8 @@ export interface CodexModelDescriptor {
|
|
|
14
14
|
/** Server-declared per-tool-output token cap (truncation_policy.limit when mode=tokens). */
|
|
15
15
|
toolOutputTokenLimit?: number;
|
|
16
16
|
}
|
|
17
|
+
/** The Codex client version Bubble claims; the account catalog the server returns depends on it. */
|
|
18
|
+
export declare function getCodexClientVersion(): string;
|
|
17
19
|
export declare function isOpenAICodexBaseUrl(baseURL: string): boolean;
|
|
18
20
|
export declare function getOpenAICodexFallbackModels(): string[];
|
|
19
21
|
export interface OpenAICodexAuthAdapter {
|
|
@@ -45,6 +47,8 @@ export declare function fetchOpenAICodexModels(options: {
|
|
|
45
47
|
export interface OpenAICodexModelCatalogResult {
|
|
46
48
|
descriptors: CodexModelDescriptor[];
|
|
47
49
|
status: "success" | "unavailable";
|
|
50
|
+
/** Why each discovery path failed, when status is "unavailable". */
|
|
51
|
+
reason?: string;
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
54
|
* Fetches the account-scoped Codex catalog while preserving the distinction
|
|
@@ -54,5 +58,9 @@ export declare function fetchOpenAICodexModelCatalog(options: {
|
|
|
54
58
|
baseURL: string;
|
|
55
59
|
accessToken: string;
|
|
56
60
|
fetch?: ChatGptFetch;
|
|
61
|
+
/** Bounds both catalog GETs; a stalled warm-up must not pin the in-flight slot. */
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
/** Per-request timeout, so a slow first path still leaves the fallback a full window. */
|
|
64
|
+
timeoutMs?: number;
|
|
57
65
|
}): Promise<OpenAICodexModelCatalogResult>;
|
|
58
66
|
export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { THINKING_LEVELS } from "./types.js";
|
|
3
|
+
import { waitForOAuth } from "./oauth/refresh-control.js";
|
|
3
4
|
import { getBuiltinModel, listBuiltinModels } from "./model-catalog.js";
|
|
4
5
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
5
6
|
import { chatGptFetch } from "./network/chatgpt-transport.js";
|
|
7
|
+
import { logProviderTransportFailure } from "./network/provider-transport-log.js";
|
|
6
8
|
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, sleepBeforeRetry, } from "./network/retry.js";
|
|
7
9
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
8
10
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
@@ -10,7 +12,13 @@ const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
|
|
|
10
12
|
// OpenAI gates new codex models server-side by client_version (each model carries a
|
|
11
13
|
// `minimal_client_version`). Track a recent real Codex CLI release; override via env
|
|
12
14
|
// when OpenAI lifts the gate again before we cut a new release.
|
|
13
|
-
|
|
15
|
+
// 2026-09-17: gpt-6-astra requires 0.153.0.
|
|
16
|
+
// 2026-10-06: gpt-6.1-sol requires 0.159.0, the version the Codex app sends.
|
|
17
|
+
const CODEX_CLIENT_VERSION = process.env.BUBBLE_CODEX_CLIENT_VERSION?.trim() || "0.159.0";
|
|
18
|
+
/** The Codex client version Bubble claims; the account catalog the server returns depends on it. */
|
|
19
|
+
export function getCodexClientVersion() {
|
|
20
|
+
return CODEX_CLIENT_VERSION;
|
|
21
|
+
}
|
|
14
22
|
const MODEL_DISCOVERY_PATHS = [
|
|
15
23
|
`/codex/models?client_version=${CODEX_CLIENT_VERSION}`,
|
|
16
24
|
"/models",
|
|
@@ -45,13 +53,20 @@ export function createOpenAICodexProvider(options) {
|
|
|
45
53
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`;
|
|
46
54
|
const fetchImpl = options.fetch ?? chatGptFetch;
|
|
47
55
|
let refreshPromise;
|
|
48
|
-
|
|
56
|
+
// `rejectedAccessToken`: the token a 401 was returned for. If storage already
|
|
57
|
+
// holds a different one, another process rotated it while the request was in
|
|
58
|
+
// flight — use that instead of spending its refresh token on a second rotation.
|
|
59
|
+
async function resolveRequestAuth(forceRefresh = false, rejectedAccessToken) {
|
|
49
60
|
let credentials = await options.auth?.getCredentials();
|
|
50
61
|
if (credentials && options.auth) {
|
|
51
62
|
const expired = options.auth.isExpired
|
|
52
63
|
? options.auth.isExpired(credentials, TOKEN_REFRESH_GRACE_MS)
|
|
53
64
|
: Date.now() >= credentials.expiresAt - TOKEN_REFRESH_GRACE_MS;
|
|
54
|
-
|
|
65
|
+
const alreadyRotated = rejectedAccessToken !== undefined
|
|
66
|
+
&& !!credentials.accessToken
|
|
67
|
+
&& credentials.accessToken !== rejectedAccessToken;
|
|
68
|
+
const mustRefresh = (forceRefresh && !alreadyRotated) || !credentials.accessToken || expired;
|
|
69
|
+
if (mustRefresh && credentials.refreshToken) {
|
|
55
70
|
if (!refreshPromise) {
|
|
56
71
|
refreshPromise = options.auth.refreshCredentials(credentials).finally(() => {
|
|
57
72
|
refreshPromise = undefined;
|
|
@@ -80,8 +95,12 @@ export function createOpenAICodexProvider(options) {
|
|
|
80
95
|
promptCacheKey: options.promptCacheKey,
|
|
81
96
|
useResponsesLite,
|
|
82
97
|
}));
|
|
98
|
+
let sentAccessToken;
|
|
83
99
|
const sendRequest = async (forceRefresh = false) => {
|
|
84
|
-
|
|
100
|
+
chatOptions.abortSignal?.throwIfAborted();
|
|
101
|
+
const { accessToken, accountId } = await waitForOAuth(resolveRequestAuth(forceRefresh, forceRefresh ? sentAccessToken : undefined), chatOptions.abortSignal);
|
|
102
|
+
chatOptions.abortSignal?.throwIfAborted();
|
|
103
|
+
sentAccessToken = accessToken;
|
|
85
104
|
return fetchImpl(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
|
|
86
105
|
accessToken,
|
|
87
106
|
accountId,
|
|
@@ -92,14 +111,25 @@ export function createOpenAICodexProvider(options) {
|
|
|
92
111
|
}));
|
|
93
112
|
};
|
|
94
113
|
for (let attempt = 0;; attempt++) {
|
|
114
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
let responseStatus;
|
|
117
|
+
let receivedEvents = 0;
|
|
118
|
+
let lastEventAt;
|
|
95
119
|
let sawParsedSseEvent = false;
|
|
96
120
|
let currentToolCall;
|
|
121
|
+
// OpenAI streams one reasoning summary per part; each part is a
|
|
122
|
+
// self-contained markdown block. Separate parts so Thinking does not
|
|
123
|
+
// read as one run-on line.
|
|
124
|
+
let summaryPartsSeen = 0;
|
|
97
125
|
try {
|
|
98
126
|
let response = await sendRequest();
|
|
127
|
+
responseStatus = response.status;
|
|
99
128
|
if (!response.ok) {
|
|
100
129
|
const errorText = await response.text().catch(() => "");
|
|
101
130
|
if (response.status === 401 && options.auth && isTokenExpiredError(errorText)) {
|
|
102
131
|
response = await sendRequest(true);
|
|
132
|
+
responseStatus = response.status;
|
|
103
133
|
}
|
|
104
134
|
else {
|
|
105
135
|
throw new Error(`${response.status} status code${errorText ? `: ${errorText}` : " (no body)"}`);
|
|
@@ -111,6 +141,8 @@ export function createOpenAICodexProvider(options) {
|
|
|
111
141
|
}
|
|
112
142
|
for await (const event of parseSse(response)) {
|
|
113
143
|
sawParsedSseEvent = true;
|
|
144
|
+
receivedEvents += 1;
|
|
145
|
+
lastEventAt = Date.now();
|
|
114
146
|
const type = typeof event.type === "string" ? event.type : undefined;
|
|
115
147
|
if (!type)
|
|
116
148
|
continue;
|
|
@@ -151,6 +183,13 @@ export function createOpenAICodexProvider(options) {
|
|
|
151
183
|
}
|
|
152
184
|
continue;
|
|
153
185
|
}
|
|
186
|
+
if (type === "response.reasoning_summary_part.added") {
|
|
187
|
+
summaryPartsSeen += 1;
|
|
188
|
+
if (summaryPartsSeen > 1) {
|
|
189
|
+
yield { type: "reasoning_delta", content: "\n\n" };
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
154
193
|
if (type === "response.reasoning_summary_text.delta") {
|
|
155
194
|
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
156
195
|
if (delta) {
|
|
@@ -224,19 +263,30 @@ export function createOpenAICodexProvider(options) {
|
|
|
224
263
|
return;
|
|
225
264
|
}
|
|
226
265
|
catch (error) {
|
|
227
|
-
|
|
266
|
+
const delegateRetry = sawParsedSseEvent
|
|
228
267
|
&& !chatOptions.abortSignal?.aborted
|
|
229
|
-
&& isTransientCodexTransportError(error)
|
|
268
|
+
&& isTransientCodexTransportError(error);
|
|
269
|
+
const retry = shouldRetryCodexTransportError({
|
|
270
|
+
error, attempt, sawParsedSseEvent, signal: chatOptions.abortSignal,
|
|
271
|
+
});
|
|
272
|
+
logProviderTransportFailure(error, {
|
|
273
|
+
providerId: options.providerId ?? "openai-codex",
|
|
274
|
+
modelId: chatOptions.model,
|
|
275
|
+
thinkingLevel: chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off",
|
|
276
|
+
messageCount: messages.length,
|
|
277
|
+
toolCount: chatOptions.tools?.length ?? 0,
|
|
278
|
+
sessionId, requestId, attempt: attempt + 1, responseStatus, receivedEvents,
|
|
279
|
+
elapsedMs: Date.now() - startedAt,
|
|
280
|
+
lastEventAgeMs: lastEventAt === undefined ? undefined : Date.now() - lastEventAt,
|
|
281
|
+
decision: chatOptions.abortSignal?.aborted ? "cancelled"
|
|
282
|
+
: delegateRetry ? "delegate_retry" : retry ? "retry" : "fail",
|
|
283
|
+
});
|
|
284
|
+
if (delegateRetry) {
|
|
230
285
|
// Partial content already surfaced — the agent loop discards the
|
|
231
286
|
// half-built assistant message and re-issues the whole request.
|
|
232
|
-
throw new ProviderStreamInterruptedError(
|
|
287
|
+
throw new ProviderStreamInterruptedError("ChatGPT connection interrupted while receiving a response.", { cause: error });
|
|
233
288
|
}
|
|
234
|
-
if (!
|
|
235
|
-
error,
|
|
236
|
-
attempt,
|
|
237
|
-
sawParsedSseEvent,
|
|
238
|
-
signal: chatOptions.abortSignal,
|
|
239
|
-
})) {
|
|
289
|
+
if (!retry) {
|
|
240
290
|
throw error;
|
|
241
291
|
}
|
|
242
292
|
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), chatOptions.abortSignal);
|
|
@@ -297,27 +347,50 @@ export async function fetchOpenAICodexModels(options) {
|
|
|
297
347
|
export async function fetchOpenAICodexModelCatalog(options) {
|
|
298
348
|
const accountId = extractChatGptAccountId(options.accessToken);
|
|
299
349
|
if (!accountId) {
|
|
300
|
-
return { descriptors: [], status: "unavailable" };
|
|
350
|
+
return { descriptors: [], status: "unavailable", reason: "access token has no ChatGPT account id" };
|
|
301
351
|
}
|
|
302
352
|
const fetchImpl = options.fetch ?? chatGptFetch;
|
|
353
|
+
const failures = [];
|
|
303
354
|
for (const path of MODEL_DISCOVERY_PATHS) {
|
|
355
|
+
const signal = options.timeoutMs === undefined
|
|
356
|
+
? options.signal
|
|
357
|
+
: options.signal
|
|
358
|
+
? AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs)])
|
|
359
|
+
: AbortSignal.timeout(options.timeoutMs);
|
|
360
|
+
const startedAt = Date.now();
|
|
304
361
|
const response = await fetchImpl(resolveRelativeUrl(options.baseURL, path), {
|
|
305
362
|
method: "GET",
|
|
306
363
|
headers: buildBaseHeaders(options.accessToken, accountId, globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`, { accept: "application/json" }),
|
|
307
|
-
|
|
308
|
-
|
|
364
|
+
signal,
|
|
365
|
+
}).catch((error) => {
|
|
366
|
+
failures.push(`${path}: ${error instanceof Error ? error.message : String(error)} after ${Date.now() - startedAt}ms`);
|
|
367
|
+
return undefined;
|
|
368
|
+
});
|
|
369
|
+
if (!response)
|
|
370
|
+
continue;
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
failures.push(`${path}: HTTP ${response.status} after ${Date.now() - startedAt}ms`);
|
|
309
373
|
continue;
|
|
374
|
+
}
|
|
310
375
|
const parsed = await response.json()
|
|
311
376
|
.then((payload) => ({ ok: true, payload }))
|
|
312
377
|
.catch(() => ({ ok: false }));
|
|
313
|
-
if (!parsed.ok)
|
|
378
|
+
if (!parsed.ok) {
|
|
379
|
+
failures.push(`${path}: invalid JSON`);
|
|
314
380
|
continue;
|
|
381
|
+
}
|
|
382
|
+
const payload = parsed.payload;
|
|
383
|
+
if (!Array.isArray(payload) && !(payload && typeof payload === "object"
|
|
384
|
+
&& (Array.isArray(payload.models) || Array.isArray(payload.data)))) {
|
|
385
|
+
failures.push(`${path}: unexpected payload`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
315
388
|
return {
|
|
316
|
-
descriptors: sortCodexModelDescriptors(extractCodexModelDescriptors(
|
|
389
|
+
descriptors: sortCodexModelDescriptors(extractCodexModelDescriptors(payload)),
|
|
317
390
|
status: "success",
|
|
318
391
|
};
|
|
319
392
|
}
|
|
320
|
-
return { descriptors: [], status: "unavailable" };
|
|
393
|
+
return { descriptors: [], status: "unavailable", reason: failures.join("; ") };
|
|
321
394
|
}
|
|
322
395
|
function buildRequestBody(messages, options) {
|
|
323
396
|
const instructions = messages
|
|
@@ -379,6 +452,11 @@ function buildRequestBody(messages, options) {
|
|
|
379
452
|
const wireEffort = options.reasoningEffort === "ultra" ? "max" : options.reasoningEffort;
|
|
380
453
|
body.reasoning = {
|
|
381
454
|
effort: wireEffort,
|
|
455
|
+
// The Codex backend's default_reasoning_summary is "none" for every
|
|
456
|
+
// current model, so without an explicit summary request no
|
|
457
|
+
// response.reasoning_summary_text.delta events arrive and the TUI never
|
|
458
|
+
// shows Thinking. Ask for the server-chosen summary like Codex CLI does.
|
|
459
|
+
summary: "auto",
|
|
382
460
|
...(options.useResponsesLite ? { context: "all_turns" } : {}),
|
|
383
461
|
};
|
|
384
462
|
}
|
|
@@ -515,6 +593,7 @@ function isTransientCodexTransportError(error) {
|
|
|
515
593
|
/\bConnectionClosed\b/i,
|
|
516
594
|
/\bECONNRESET\b/i,
|
|
517
595
|
/\bUND_ERR_SOCKET\b/i,
|
|
596
|
+
/\bUND_ERR_(?:CONNECT|HEADERS|BODY)_TIMEOUT\b/i,
|
|
518
597
|
/\bEPIPE\b/i,
|
|
519
598
|
/socket hang up/i,
|
|
520
599
|
/fetch failed/i,
|
|
@@ -533,6 +612,9 @@ function errorMessageChain(error) {
|
|
|
533
612
|
for (let depth = 0; current && depth < 6; depth++) {
|
|
534
613
|
if (current instanceof Error) {
|
|
535
614
|
messages.push(current.name, current.message);
|
|
615
|
+
const code = current.code;
|
|
616
|
+
if (typeof code === "string")
|
|
617
|
+
messages.push(code);
|
|
536
618
|
current = current.cause;
|
|
537
619
|
continue;
|
|
538
620
|
}
|
|
@@ -669,14 +751,15 @@ function extractCodexModelDescriptors(payload) {
|
|
|
669
751
|
visit(payload);
|
|
670
752
|
return out;
|
|
671
753
|
}
|
|
672
|
-
// Extracts the family version from a codex slug (e.g. "gpt-5.5-codex" → 5005
|
|
673
|
-
// Used so models from a newer family float to the top
|
|
674
|
-
// catalog knows about them.
|
|
754
|
+
// Extracts the family version from a codex slug (e.g. "gpt-5.5-codex" → 5005,
|
|
755
|
+
// "gpt-6-astra" → 6000). Used so models from a newer family float to the top
|
|
756
|
+
// even before the static catalog knows about them. Integer-major slugs must
|
|
757
|
+
// rank above every older major.minor family, not fall to zero.
|
|
675
758
|
function parseCodexFamilyRank(id) {
|
|
676
|
-
const match = id.match(/(\d+)
|
|
759
|
+
const match = id.match(/(\d+)(?:\.(\d+))?/);
|
|
677
760
|
if (!match)
|
|
678
761
|
return 0;
|
|
679
|
-
return parseInt(match[1], 10) * 1000 + parseInt(match[2], 10);
|
|
762
|
+
return parseInt(match[1], 10) * 1000 + (match[2] ? parseInt(match[2], 10) : 0);
|
|
680
763
|
}
|
|
681
764
|
export function sortCodexModelDescriptors(descriptors) {
|
|
682
765
|
const preferred = new Map(getOpenAICodexFallbackModels().map((id, index) => [id, index]));
|
|
@@ -64,7 +64,8 @@ export interface ModelDiscoveryResult {
|
|
|
64
64
|
*/
|
|
65
65
|
export interface CachedDiscoverySnapshot {
|
|
66
66
|
models: ModelInfo[];
|
|
67
|
-
|
|
67
|
+
/** "cache" marks a confirmed catalog retained past the live result's freshness. */
|
|
68
|
+
source: ModelDiscoverySource;
|
|
68
69
|
complete: boolean;
|
|
69
70
|
expiresAt: number;
|
|
70
71
|
identityKey: string;
|
|
@@ -107,12 +108,50 @@ export declare class ProviderRegistry {
|
|
|
107
108
|
private handleAuthMutation;
|
|
108
109
|
getModelConfig(): ModelConfig;
|
|
109
110
|
getAuthStorage(): AuthStorage;
|
|
111
|
+
/**
|
|
112
|
+
* Run discovery for an account-scoped (OAuth) provider so the routing
|
|
113
|
+
* snapshot knows the account's real catalog before the routing prompt is
|
|
114
|
+
* composed and before the first subagent spawn. Discovery was previously
|
|
115
|
+
* triggered only by the model picker, which left tier routing on the static
|
|
116
|
+
* builtin list for the whole session unless the user opened /model.
|
|
117
|
+
* Resolves when discovery settles (it never throws: failures resolve to the
|
|
118
|
+
* local fallback); resolves immediately when a fresh cache exists or the
|
|
119
|
+
* provider is not account-scoped. Hosts bound the wait, see waitForModelDiscovery.
|
|
120
|
+
*/
|
|
121
|
+
warmModelDiscovery(providerId: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Warm discovery but never hold startup hostage to the network: after
|
|
124
|
+
* `maxWaitMs` the host proceeds with whatever the snapshot has, and the
|
|
125
|
+
* still-running discovery updates the live accessor when it lands.
|
|
126
|
+
*/
|
|
127
|
+
waitForModelDiscovery(providerId: string, maxWaitMs: number): Promise<void>;
|
|
110
128
|
supportsOAuth(providerId: string): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Auth-storage keys currently backing a provider's OAuth login. The visible
|
|
131
|
+
* `openai` provider may still be backed by the legacy `openai-codex` key,
|
|
132
|
+
* so UI state and logout must not check `has(providerId)` directly.
|
|
133
|
+
*/
|
|
134
|
+
getOAuthLoginKeys(providerId: string): string[];
|
|
111
135
|
private resolveOAuthAuthKey;
|
|
112
136
|
createOpenAICodexAuthAdapter(providerId: string): OpenAICodexAuthAdapter | undefined;
|
|
113
137
|
createGrokAuthAdapter(providerId: string): GrokAuthAdapter | undefined;
|
|
114
138
|
getDefaultModel(providerId: string, authType?: ProviderProfile["authType"]): string | undefined;
|
|
115
139
|
prepareProvider(providerId: string): Promise<void>;
|
|
140
|
+
private refreshOpenAICredentials;
|
|
141
|
+
private refreshGrokCredentials;
|
|
142
|
+
/**
|
|
143
|
+
* The one place a refresh token is spent. auth.json is shared by every
|
|
144
|
+
* Bubble process and refresh tokens are single-use, so the token is only
|
|
145
|
+
* presented under the cross-process lock and only after re-reading the file:
|
|
146
|
+
* if another process already rotated it, its result is adopted and no
|
|
147
|
+
* request is made. A rejected token gets the same second look before the
|
|
148
|
+
* error surfaces, because the rotation may have landed mid-request.
|
|
149
|
+
*
|
|
150
|
+
* `used` is the credential set the caller was holding when it asked (the
|
|
151
|
+
* provider passes it, also for a forced refresh after a 401): "rotated" means
|
|
152
|
+
* the file no longer holds that set, not merely that it is unexpired.
|
|
153
|
+
*/
|
|
154
|
+
private refreshSharedCredentials;
|
|
116
155
|
getConfigured(): ProviderProfile[];
|
|
117
156
|
getEnabled(): ProviderProfile[];
|
|
118
157
|
getDefault(): ProviderProfile | undefined;
|
|
@@ -136,8 +175,12 @@ export declare class ProviderRegistry {
|
|
|
136
175
|
* derived strictly: only a successful full remote catalog qualifies —
|
|
137
176
|
* never static or fallback results, whose internal `authoritative` flag
|
|
138
177
|
* this design does not trust.
|
|
178
|
+
* Desktop catalogs can explicitly retain the last confirmation beyond its
|
|
179
|
+
* routing horizon; ordinary routing callers keep the default expiry policy.
|
|
139
180
|
*/
|
|
140
|
-
getCachedDiscoverySnapshot(providerId: string
|
|
181
|
+
getCachedDiscoverySnapshot(providerId: string, options?: {
|
|
182
|
+
allowExpiredConfirmation?: boolean;
|
|
183
|
+
}): CachedDiscoverySnapshot | undefined;
|
|
141
184
|
private isCurrentModelDiscovery;
|
|
142
185
|
private applyDynamicDiscoveryMetadata;
|
|
143
186
|
}
|