@bubblebrain-ai/bubble 0.0.15 → 0.0.16

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.
@@ -1,6 +1,7 @@
1
1
  import { getContextBudget } from "./budget.js";
2
2
  import { compactCurrentTurnToolGroups, compactMessages } from "./compact.js";
3
3
  import { pruneMessages } from "./prune.js";
4
+ import { formatInternalContextBlock, formatInternalReminderBlock } from "../agent/internal-reminder-sanitizer.js";
4
5
  // Prefix-cache invariant: only the leading static system prompt is promoted to
5
6
  // the first provider message. Runtime meta reminders stay in the conversational
6
7
  // body at their original relative position, so a new per-turn reminder does not
@@ -166,14 +167,14 @@ function isEmptyAssistantMessage(message) {
166
167
  function formatMetaMessage(message) {
167
168
  switch (message.kind) {
168
169
  case "system-reminder":
169
- return `Runtime reminder:\n${message.content}`;
170
+ return formatInternalReminderBlock(message.kind, message.content);
170
171
  case "runtime-context":
171
172
  default:
172
- return `Runtime context:\n${message.content}`;
173
+ return formatInternalContextBlock(message.kind, message.content);
173
174
  }
174
175
  }
175
176
  function formatRuntimeSystemMessage(message) {
176
- return `Runtime context:\n${message.content}`;
177
+ return formatInternalContextBlock("runtime-system", message.content);
177
178
  }
178
179
  function cloneMessage(message) {
179
180
  if (message.role === "assistant") {
@@ -9,6 +9,7 @@ export const BUILTIN_PROVIDERS = [
9
9
  { id: "zai", name: "Z.AI", baseURL: "https://api.z.ai/api/paas/v4" },
10
10
  { id: "zai-coding-plan", name: "Z.AI Coding Plan", baseURL: "https://api.z.ai/api/coding/paas/v4" },
11
11
  { id: "alibaba", name: "Alibaba DashScope", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
12
+ { id: "stepfun", name: "StepFun Step Plan", baseURL: "https://api.stepfun.com/step_plan/v1" },
12
13
  { id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
13
14
  { id: "moonshot-intl", name: "Moonshot (海外 platform.moonshot.ai)", baseURL: "https://api.moonshot.ai/v1" },
14
15
  { id: "kimi-for-coding", name: "Kimi for Coding", baseURL: "https://api.kimi.com/coding/v1" },
@@ -24,6 +25,7 @@ const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
24
25
  const OPENAI_CHAT_LEVELS = ["off"];
25
26
  const TOGGLE_THINKING_LEVELS = ["off", "medium"];
26
27
  const DEEPSEEK_V4_LEVELS = ["high", "max"];
28
+ const STEPFUN_REASONING_LEVELS = ["off", "low", "medium", "high"];
27
29
  export const BUILTIN_MODELS = [
28
30
  { id: "gpt-5.5", name: "gpt-5.5", providerId: "openai-codex", reasoningLevels: ALL_OPENAI_LEVELS, contextWindow: 272000, toolOutputTokenLimit: 10000 },
29
31
  { id: "gpt-5.4", name: "gpt-5.4", providerId: "openai-codex", reasoningLevels: ALL_OPENAI_LEVELS, contextWindow: 272000 },
@@ -59,6 +61,10 @@ export const BUILTIN_MODELS = [
59
61
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
60
62
  { id: "qwen3.6-plus", name: "Qwen3.6 Plus", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
61
63
  { id: "qwen3.7-max", name: "Qwen3.7 Max", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
64
+ { id: "step-3.7-flash", name: "Step 3.7 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS, contextWindow: 256000 },
65
+ { id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
66
+ { id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
67
+ { id: "step-router-v1", name: "Step Router V1", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
62
68
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
63
69
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
64
70
  { id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
@@ -1,8 +1,9 @@
1
1
  import type { TokenUsage } from "./types.js";
2
+ export type PricingCurrency = "USD" | "CNY";
2
3
  export interface ModelPricing {
3
4
  providerId: string;
4
5
  modelId: string;
5
- currency: "USD";
6
+ currency: PricingCurrency;
6
7
  inputCacheHitPerMillion: number;
7
8
  inputCacheMissPerMillion: number;
8
9
  outputPerMillion: number;
@@ -14,7 +15,7 @@ export interface ModelPricing {
14
15
  };
15
16
  }
16
17
  export interface UsageCost {
17
- currency: "USD";
18
+ currency: PricingCurrency;
18
19
  cost: number;
19
20
  estimated: boolean;
20
21
  }
@@ -21,6 +21,14 @@ export const MODEL_PRICING = [
21
21
  outputPerMillion: 3.48,
22
22
  },
23
23
  },
24
+ {
25
+ providerId: "stepfun",
26
+ modelId: "step-3.7-flash",
27
+ currency: "CNY",
28
+ inputCacheHitPerMillion: 0.27,
29
+ inputCacheMissPerMillion: 1.35,
30
+ outputPerMillion: 8.1,
31
+ },
24
32
  ];
25
33
  export function getModelPricing(providerId, modelId) {
26
34
  return MODEL_PRICING.find((item) => item.providerId === providerId && item.modelId === modelId);
@@ -0,0 +1,16 @@
1
+ import { type Dispatcher } from "undici";
2
+ export type ChatGptFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ export interface ChatGptFetchOptions {
4
+ fetch?: ChatGptFetch;
5
+ env?: NodeJS.ProcessEnv;
6
+ }
7
+ type RequestInitWithDispatcher = RequestInit & {
8
+ dispatcher?: Dispatcher;
9
+ };
10
+ export declare function chatGptFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
11
+ export declare function getChatGptFetch(env?: NodeJS.ProcessEnv): ChatGptFetch;
12
+ export declare function createChatGptFetch(options?: ChatGptFetchOptions): ChatGptFetch;
13
+ export declare function createChatGptDispatcher(env?: NodeJS.ProcessEnv, input?: RequestInfo | URL): Dispatcher | undefined;
14
+ export declare function withChatGptNetworkOptions(input: RequestInfo | URL, init: RequestInit | undefined, env?: NodeJS.ProcessEnv, dispatcher?: Dispatcher | undefined): RequestInitWithDispatcher;
15
+ export declare function normalizeChatGptNetworkError(error: unknown, env?: NodeJS.ProcessEnv): Error;
16
+ export {};
@@ -0,0 +1,240 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { delimiter } from "node:path";
3
+ import { rootCertificates } from "node:tls";
4
+ import { Agent, ProxyAgent } from "undici";
5
+ let cachedDefaultFetch;
6
+ export function chatGptFetch(input, init) {
7
+ return getChatGptFetch()(input, init);
8
+ }
9
+ export function getChatGptFetch(env = process.env) {
10
+ const signature = networkEnvSignature(env);
11
+ if (!cachedDefaultFetch || cachedDefaultFetch.signature !== signature) {
12
+ cachedDefaultFetch = {
13
+ signature,
14
+ fetch: createChatGptFetch({ env }),
15
+ };
16
+ }
17
+ return cachedDefaultFetch.fetch;
18
+ }
19
+ export function createChatGptFetch(options = {}) {
20
+ const env = options.env ?? process.env;
21
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
22
+ const dispatcher = createChatGptDispatcher(env);
23
+ return async (input, init) => {
24
+ const requestInit = withChatGptNetworkOptions(input, init, env, dispatcher);
25
+ try {
26
+ return await fetchImpl(input, requestInit);
27
+ }
28
+ catch (error) {
29
+ throw normalizeChatGptNetworkError(error, env);
30
+ }
31
+ };
32
+ }
33
+ export function createChatGptDispatcher(env = process.env, input) {
34
+ if (isBunRuntime())
35
+ return undefined;
36
+ const ca = loadExtraCaCertificates(env);
37
+ if (!hasProxyEnv(env) && ca.length === 0)
38
+ return undefined;
39
+ const proxy = input ? nodeProxyForUrl(input, env) : defaultNodeProxy(env);
40
+ const caOptions = ca.length > 0 ? { ca: [...rootCertificates, ...ca] } : undefined;
41
+ if (proxy) {
42
+ return new ProxyAgent({
43
+ uri: proxy,
44
+ ...(caOptions ? { requestTls: caOptions, proxyTls: caOptions } : {}),
45
+ });
46
+ }
47
+ return caOptions ? new Agent({ connect: caOptions }) : undefined;
48
+ }
49
+ export function withChatGptNetworkOptions(input, init, env = process.env, dispatcher = createChatGptDispatcher(env, input)) {
50
+ const next = { ...(init ?? {}) };
51
+ if (isBunRuntime()) {
52
+ const proxy = bunProxyForUrl(input, env);
53
+ if (proxy)
54
+ next.proxy = proxy;
55
+ const ca = bunExtraCaFiles(env);
56
+ if (ca.length > 0)
57
+ next.tls = { ...(next.tls ?? {}), ca };
58
+ return next;
59
+ }
60
+ if (dispatcher)
61
+ next.dispatcher = dispatcher;
62
+ return next;
63
+ }
64
+ export function normalizeChatGptNetworkError(error, env = process.env) {
65
+ const text = errorMessageChain(error).join("\n");
66
+ if (!isChatGptNetworkErrorText(text)) {
67
+ return error instanceof Error ? error : new Error(String(error));
68
+ }
69
+ const message = [
70
+ "ChatGPT connection failed before Bubble received a response.",
71
+ isCertificateErrorText(text)
72
+ ? "TLS certificate verification failed. If you are on a corporate proxy, VPN, or HTTPS inspection network, start Bubble with NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem or BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem."
73
+ : "This looks like a proxy or network transport failure.",
74
+ hasProxyEnv(env)
75
+ ? "Bubble is using proxy environment variables for ChatGPT requests. Make sure NO_PROXY includes localhost,127.0.0.1."
76
+ : "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
77
+ "Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
78
+ `Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
79
+ ].join(" ");
80
+ return new Error(message, { cause: error });
81
+ }
82
+ function hasProxyEnv(env) {
83
+ return Boolean(env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy);
84
+ }
85
+ function isBunRuntime() {
86
+ return typeof globalThis.Bun !== "undefined";
87
+ }
88
+ function bunProxyForUrl(input, env) {
89
+ const url = urlFromInput(input);
90
+ if (!url || shouldBypassProxy(url, env))
91
+ return undefined;
92
+ const allProxy = env.ALL_PROXY ?? env.all_proxy;
93
+ if (url.protocol === "https:")
94
+ return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy;
95
+ if (url.protocol === "http:")
96
+ return env.HTTP_PROXY ?? env.http_proxy ?? allProxy;
97
+ return undefined;
98
+ }
99
+ function nodeProxyForUrl(input, env) {
100
+ const url = urlFromInput(input);
101
+ if (!url || shouldBypassProxy(url, env))
102
+ return undefined;
103
+ if (url.protocol === "https:")
104
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy;
105
+ if (url.protocol === "http:")
106
+ return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
107
+ return defaultNodeProxy(env);
108
+ }
109
+ function defaultNodeProxy(env) {
110
+ return env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
111
+ }
112
+ function bunExtraCaFiles(env) {
113
+ const bun = globalThis.Bun;
114
+ if (!bun?.file)
115
+ return [];
116
+ return extraCaCertificatePaths(env).map((path) => bun.file(path));
117
+ }
118
+ function urlFromInput(input) {
119
+ if (input instanceof URL)
120
+ return input;
121
+ if (typeof input === "string")
122
+ return URL.canParse(input) ? new URL(input) : undefined;
123
+ const url = input.url;
124
+ return URL.canParse(url) ? new URL(url) : undefined;
125
+ }
126
+ function shouldBypassProxy(url, env) {
127
+ const noProxy = (env.NO_PROXY ?? env.no_proxy ?? "").trim();
128
+ if (!noProxy)
129
+ return false;
130
+ if (noProxy === "*")
131
+ return true;
132
+ const hostname = url.hostname.toLowerCase();
133
+ const port = url.port;
134
+ return noProxy
135
+ .split(/[,\s]+/)
136
+ .filter(Boolean)
137
+ .some((entry) => noProxyEntryMatches(entry.toLowerCase(), hostname, port));
138
+ }
139
+ function noProxyEntryMatches(entry, hostname, port) {
140
+ const [entryHost, entryPort] = entry.includes(":") ? entry.split(":") : [entry, ""];
141
+ if (entryPort && entryPort !== port)
142
+ return false;
143
+ if (entryHost === hostname)
144
+ return true;
145
+ if (entryHost.startsWith("*."))
146
+ return hostname.endsWith(entryHost.slice(1));
147
+ if (entryHost.startsWith("."))
148
+ return hostname.endsWith(entryHost);
149
+ return false;
150
+ }
151
+ function loadExtraCaCertificates(env) {
152
+ const paths = extraCaCertificatePaths(env);
153
+ return paths.map((path) => {
154
+ try {
155
+ return readFileSync(path, "utf-8");
156
+ }
157
+ catch (error) {
158
+ throw new Error(`Failed to read ChatGPT custom CA certificate at ${path}. Check NODE_EXTRA_CA_CERTS or BUBBLE_EXTRA_CA_CERTS.`, {
159
+ cause: error,
160
+ });
161
+ }
162
+ });
163
+ }
164
+ function extraCaCertificatePaths(env) {
165
+ const bubbleValue = env.BUBBLE_EXTRA_CA_CERTS?.trim();
166
+ if (bubbleValue) {
167
+ return bubbleValue.split(delimiter).map((item) => item.trim()).filter(Boolean);
168
+ }
169
+ const nodeValue = env.NODE_EXTRA_CA_CERTS?.trim();
170
+ return nodeValue ? [nodeValue] : [];
171
+ }
172
+ function networkEnvSignature(env) {
173
+ return [
174
+ env.HTTP_PROXY,
175
+ env.http_proxy,
176
+ env.HTTPS_PROXY,
177
+ env.https_proxy,
178
+ env.ALL_PROXY,
179
+ env.all_proxy,
180
+ env.NO_PROXY,
181
+ env.no_proxy,
182
+ env.NODE_EXTRA_CA_CERTS,
183
+ env.BUBBLE_EXTRA_CA_CERTS,
184
+ ].join("\0");
185
+ }
186
+ function isChatGptNetworkErrorText(text) {
187
+ return [
188
+ /fetch failed/i,
189
+ /network.*failed/i,
190
+ /socket connection was closed unexpectedly/i,
191
+ /\bConnectionClosed\b/i,
192
+ /\bECONNRESET\b/i,
193
+ /\bECONNREFUSED\b/i,
194
+ /\bETIMEDOUT\b/i,
195
+ /\bEPIPE\b/i,
196
+ /\bUND_ERR_/i,
197
+ /socket hang up/i,
198
+ /certificate/i,
199
+ /unable to verify/i,
200
+ /self[- ]signed/i,
201
+ ].some((pattern) => pattern.test(text));
202
+ }
203
+ function isCertificateErrorText(text) {
204
+ return [
205
+ /unknown certificate verification error/i,
206
+ /certificate (?:verify|verification) (?:failed|error)/i,
207
+ /unable to verify (?:the )?(?:first )?certificate/i,
208
+ /UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
209
+ /SELF_SIGNED_CERT_IN_CHAIN/i,
210
+ /DEPTH_ZERO_SELF_SIGNED_CERT/i,
211
+ /CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
212
+ /self[- ]signed certificate/i,
213
+ ].some((pattern) => pattern.test(text));
214
+ }
215
+ function firstMeaningfulErrorMessage(error) {
216
+ return errorMessageChain(error).find((item) => item && item !== "Error");
217
+ }
218
+ function errorMessageChain(error) {
219
+ const messages = [];
220
+ let current = error;
221
+ for (let depth = 0; current && depth < 8; depth++) {
222
+ if (current instanceof Error) {
223
+ messages.push(current.name, current.message);
224
+ current = current.cause;
225
+ continue;
226
+ }
227
+ if (typeof current === "object") {
228
+ const record = current;
229
+ for (const key of ["name", "code", "message"]) {
230
+ if (typeof record[key] === "string")
231
+ messages.push(record[key]);
232
+ }
233
+ current = record.cause;
234
+ continue;
235
+ }
236
+ messages.push(String(current));
237
+ break;
238
+ }
239
+ return messages;
240
+ }
@@ -2,8 +2,13 @@
2
2
  * OpenAI Codex OAuth login (PKCE + local callback).
3
3
  */
4
4
  import type { OAuthTokens } from "./types.js";
5
+ import { type ChatGptFetch } from "../network/chatgpt-transport.js";
5
6
  export interface OpenAICodexLoginCallbacks {
6
7
  onStatus: (message: string) => void;
7
8
  }
8
- export declare function loginOpenAICodex(callbacks?: OpenAICodexLoginCallbacks): Promise<OAuthTokens>;
9
- export declare function refreshOpenAICodex(refreshToken: string): Promise<OAuthTokens>;
9
+ export declare function loginOpenAICodex(callbacks?: OpenAICodexLoginCallbacks, options?: {
10
+ fetch?: ChatGptFetch;
11
+ }): Promise<OAuthTokens>;
12
+ export declare function refreshOpenAICodex(refreshToken: string, options?: {
13
+ fetch?: ChatGptFetch;
14
+ }): Promise<OAuthTokens>;
@@ -4,6 +4,7 @@
4
4
  import { createServer } from "node:http";
5
5
  import { exec } from "node:child_process";
6
6
  import { randomBytes, createHash } from "node:crypto";
7
+ import { chatGptFetch } from "../network/chatgpt-transport.js";
7
8
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8
9
  const AUTH_URL = "https://auth.openai.com/oauth/authorize";
9
10
  const TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -92,7 +93,8 @@ function extractAccountId(idToken) {
92
93
  const auth = claims?.["https://api.openai.com/auth"];
93
94
  return auth?.chatgpt_account_id || auth?.account_id || claims?.sub;
94
95
  }
95
- export async function loginOpenAICodex(callbacks) {
96
+ export async function loginOpenAICodex(callbacks, options = {}) {
97
+ const fetchImpl = options.fetch ?? chatGptFetch;
96
98
  callbacks?.onStatus("Starting OpenAI Codex OAuth login...");
97
99
  const pkce = generatePKCE();
98
100
  const state = generateState();
@@ -119,7 +121,7 @@ export async function loginOpenAICodex(callbacks) {
119
121
  throw new Error("OAuth state mismatch. Possible CSRF attack.");
120
122
  }
121
123
  callbacks?.onStatus("Exchanging authorization code for tokens...");
122
- const response = await fetch(TOKEN_URL, {
124
+ const response = await fetchImpl(TOKEN_URL, {
123
125
  method: "POST",
124
126
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
125
127
  body: new URLSearchParams({
@@ -146,8 +148,9 @@ export async function loginOpenAICodex(callbacks) {
146
148
  accountId,
147
149
  };
148
150
  }
149
- export async function refreshOpenAICodex(refreshToken) {
150
- const response = await fetch(TOKEN_URL, {
151
+ export async function refreshOpenAICodex(refreshToken, options = {}) {
152
+ const fetchImpl = options.fetch ?? chatGptFetch;
153
+ const response = await fetchImpl(TOKEN_URL, {
151
154
  method: "POST",
152
155
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
153
156
  body: new URLSearchParams({
@@ -2,6 +2,7 @@ import { classifyTask } from "../agent/task-classifier.js";
2
2
  import { classifyTaskSize } from "../agent/task-size.js";
3
3
  import { EvidenceTracker } from "../agent/evidence-tracker.js";
4
4
  import { ExecutionGovernor } from "../agent/execution-governor.js";
5
+ import { DiscoveryBarrier } from "../agent/discovery-barrier.js";
5
6
  import { arbitrateToolCall } from "../agent/tool-arbiter.js";
6
7
  import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildTaskSummaryReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
7
8
  import { reminderForTaskType } from "../prompt/task-reminders.js";
@@ -14,6 +15,11 @@ export function createDefaultHooks() {
14
15
  const taskType = classifyTask(ctx.input);
15
16
  ctx.state.taskType = taskType;
16
17
  ctx.state.governor = new ExecutionGovernor(taskType);
18
+ ctx.state.discoveryBarrier = new DiscoveryBarrier({
19
+ cwd: ctx.cwd,
20
+ input: ctx.input,
21
+ enabled: taskType === "repo_orientation",
22
+ });
17
23
  const taskReminder = reminderForTaskType(taskType);
18
24
  if (taskReminder) {
19
25
  ctx.queueReminder(taskReminder);
@@ -63,8 +69,12 @@ export function createDefaultHooks() {
63
69
  {
64
70
  beforeToolCall(ctx) {
65
71
  const arbitration = arbitrateToolCall(ctx.toolCall);
66
- ctx.replaceToolCall({ ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) });
67
- ctx.state.governor?.beforeToolCall(ctx.toolCall);
72
+ const toolCall = { ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) };
73
+ ctx.replaceToolCall(toolCall);
74
+ ctx.state.governor?.beforeToolCall(toolCall);
75
+ const blockedResult = ctx.state.discoveryBarrier?.beforeToolCall(toolCall);
76
+ if (blockedResult)
77
+ ctx.blockToolCall(blockedResult);
68
78
  },
69
79
  afterToolCall(ctx) {
70
80
  if (ctx.toolCall.arbiterNote) {
@@ -78,6 +88,7 @@ export function createDefaultHooks() {
78
88
  }
79
89
  ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
80
90
  ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
91
+ ctx.state.discoveryBarrier?.afterToolCall(ctx.toolCall, ctx.result);
81
92
  // Edit/write retry-escalation: models can spiral on "identical content"
82
93
  // or "not found" errors. Nudge them to re-ground or switch strategy.
83
94
  if (isMutationTool(ctx.toolCall.name) && ctx.result.isError) {
@@ -3,10 +3,12 @@ import type { ContentPart, ParsedToolCall, ToolRegistryEntry, ToolResult } from
3
3
  import type { TaskType } from "../agent/task-classifier.js";
4
4
  import type { ExecutionGovernor } from "../agent/execution-governor.js";
5
5
  import type { EvidenceTracker } from "../agent/evidence-tracker.js";
6
+ import type { DiscoveryBarrier } from "../agent/discovery-barrier.js";
6
7
  import type { WorkflowPhase } from "./workflow.js";
7
8
  export interface TurnHookState {
8
9
  taskType?: TaskType;
9
10
  governor?: ExecutionGovernor;
11
+ discoveryBarrier?: DiscoveryBarrier;
10
12
  evidenceTracker?: EvidenceTracker;
11
13
  workflowPhase?: WorkflowPhase;
12
14
  workflowKey?: string;
@@ -1,5 +1,6 @@
1
1
  import type { Provider, ReasoningEffort, ThinkingLevel, TokenUsage } from "./types.js";
2
2
  import type { OAuthCredentials } from "./oauth/types.js";
3
+ import { type ChatGptFetch } from "./network/chatgpt-transport.js";
3
4
  export interface CodexModelDescriptor {
4
5
  id: string;
5
6
  displayName?: string;
@@ -25,6 +26,7 @@ export declare function createOpenAICodexProvider(options: {
25
26
  thinkingLevel?: ThinkingLevel;
26
27
  promptCacheKey?: string;
27
28
  auth?: OpenAICodexAuthAdapter;
29
+ fetch?: ChatGptFetch;
28
30
  }): Provider;
29
31
  export declare function normalizeOpenAICodexUsage(usage: any): TokenUsage;
30
32
  export declare function buildOpenAICodexPromptCacheKey(input: {
@@ -35,5 +37,6 @@ export declare function buildOpenAICodexPromptCacheKey(input: {
35
37
  export declare function fetchOpenAICodexModels(options: {
36
38
  baseURL: string;
37
39
  accessToken: string;
40
+ fetch?: ChatGptFetch;
38
41
  }): Promise<CodexModelDescriptor[]>;
39
42
  export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { listBuiltinModels } from "./model-catalog.js";
3
3
  import { resolveProviderRequestConfig } from "./provider-transform.js";
4
+ import { chatGptFetch } from "./network/chatgpt-transport.js";
4
5
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
5
6
  const OPENAI_BETA_RESPONSES = "responses=experimental";
6
7
  const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
@@ -42,6 +43,7 @@ export function extractChatGptAccountId(accessToken) {
42
43
  }
43
44
  export function createOpenAICodexProvider(options) {
44
45
  const sessionId = globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`;
46
+ const fetchImpl = options.fetch ?? chatGptFetch;
45
47
  let refreshPromise;
46
48
  async function resolveRequestAuth(forceRefresh = false) {
47
49
  let credentials = await options.auth?.getCredentials();
@@ -77,7 +79,7 @@ export function createOpenAICodexProvider(options) {
77
79
  }));
78
80
  const sendRequest = async (forceRefresh = false) => {
79
81
  const { accessToken, accountId } = await resolveRequestAuth(forceRefresh);
80
- return fetch(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
82
+ return fetchImpl(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
81
83
  accessToken,
82
84
  accountId,
83
85
  sessionId,
@@ -278,8 +280,9 @@ export async function fetchOpenAICodexModels(options) {
278
280
  if (!accountId) {
279
281
  return [];
280
282
  }
283
+ const fetchImpl = options.fetch ?? chatGptFetch;
281
284
  for (const path of MODEL_DISCOVERY_PATHS) {
282
- const response = await fetch(resolveRelativeUrl(options.baseURL, path), {
285
+ const response = await fetchImpl(resolveRelativeUrl(options.baseURL, path), {
283
286
  method: "GET",
284
287
  headers: buildBaseHeaders(options.accessToken, accountId, globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`, { accept: "application/json" }),
285
288
  }).catch(() => undefined);
@@ -451,6 +454,12 @@ function isTransientCodexTransportError(error) {
451
454
  /\bEPIPE\b/i,
452
455
  /socket hang up/i,
453
456
  /fetch failed/i,
457
+ /unknown certificate verification error/i,
458
+ /certificate (?:verify|verification) (?:failed|error)/i,
459
+ /unable to verify (?:the )?(?:first )?certificate/i,
460
+ /UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
461
+ /SELF_SIGNED_CERT_IN_CHAIN/i,
462
+ /CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
454
463
  ].some((pattern) => pattern.test(text));
455
464
  }
456
465
  function errorMessageChain(error) {
@@ -36,6 +36,15 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
36
36
  },
37
37
  };
38
38
  }
39
+ if (providerId === "stepfun") {
40
+ return {
41
+ effectiveThinkingLevel,
42
+ reasoningContentEcho: "none",
43
+ extraBody: effectiveThinkingLevel === "off"
44
+ ? undefined
45
+ : { reasoning_effort: effectiveThinkingLevel },
46
+ };
47
+ }
39
48
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
40
49
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
41
50
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
@@ -1,8 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
+ import { sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
4
5
  const DEBUG_PATH = process.env.BUBBLE_DEBUG_REASONING_STREAM?.trim();
5
6
  const INCLUDE_PREVIEW = process.env.BUBBLE_DEBUG_REASONING_PREVIEW !== "0";
7
+ const INCLUDE_RAW_PREVIEW = ["1", "true", "yes", "on"].includes(process.env.BUBBLE_DEBUG_REASONING_RAW?.trim().toLowerCase() ?? "");
6
8
  const PREVIEW_CHARS = 180;
7
9
  let sequence = 0;
8
10
  export function summarizeDebugText(value) {
@@ -13,7 +15,8 @@ export function summarizeDebugText(value) {
13
15
  const hash = createHash("sha256").update(value).digest("hex").slice(0, 16);
14
16
  const summary = { length: value.length, hash };
15
17
  if (INCLUDE_PREVIEW) {
16
- summary.preview = value.replace(/\s+/g, " ").slice(0, PREVIEW_CHARS);
18
+ const previewValue = INCLUDE_RAW_PREVIEW ? value : sanitizeInternalReminderBlocks(value);
19
+ summary.preview = previewValue.replace(/\s+/g, " ").slice(0, PREVIEW_CHARS);
17
20
  }
18
21
  return summary;
19
22
  }
@@ -1,3 +1,4 @@
1
+ import { sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
1
2
  export class SessionLog {
2
3
  entries = [];
3
4
  load(lines) {
@@ -190,7 +191,9 @@ function normalizeMessageToEntries(message, id, timestamp) {
190
191
  message: {
191
192
  role: "assistant",
192
193
  content: message.content,
193
- reasoning: message.reasoning,
194
+ reasoning: message.reasoning !== undefined
195
+ ? sanitizeInternalReminderBlocks(message.reasoning)
196
+ : undefined,
194
197
  model: message.model,
195
198
  providerId: message.providerId,
196
199
  modelId: message.modelId,
@@ -1,3 +1,4 @@
1
+ import type { PricingCurrency } from "../model-pricing.js";
1
2
  export type StatsRange = "7d" | "30d";
2
3
  export interface DailyUsage {
3
4
  date: string;
@@ -22,6 +23,7 @@ export interface ModelUsageStats {
22
23
  reasoningTokens: number;
23
24
  totalTokens: number;
24
25
  cost?: number;
26
+ costCurrency?: PricingCurrency;
25
27
  }
26
28
  export interface UsageStats {
27
29
  range: StatsRange;
@@ -32,7 +34,9 @@ export interface UsageStats {
32
34
  heatmap: HeatmapColumn[];
33
35
  models: ModelUsageStats[];
34
36
  totalTokens: number;
37
+ trackedCosts?: Partial<Record<PricingCurrency, number>>;
35
38
  trackedCost?: number;
39
+ trackedCostCurrency?: PricingCurrency;
36
40
  activeDays: number;
37
41
  sessionsScanned: number;
38
42
  sessionsWithoutTokenData: number;