@bubblebrain-ai/bubble 0.0.19 → 0.0.20
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/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +305 -17
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +28 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +32 -0
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/slash-commands/commands.js +84 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.js +4 -4
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +260 -155
- package/dist/tui/trace-groups.js +40 -4
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.js +2 -1
- package/dist/tui-ink/trace-groups.js +40 -4
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const CACHE_TTL_MS = 30_000;
|
|
3
|
+
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
|
|
4
|
+
let cache;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the OS-level proxy for a request URL. Used as a fallback when no
|
|
7
|
+
* proxy environment variables are set, so Bubble follows the same proxy that
|
|
8
|
+
* browsers and other GUI apps use (e.g. Clash/Surge "system proxy" mode).
|
|
9
|
+
* Reads `scutil --proxy` on macOS and the Internet Settings registry key on
|
|
10
|
+
* Windows. Returns undefined on other platforms, when disabled via
|
|
11
|
+
* BUBBLE_SYSTEM_PROXY=0, or when the URL matches the OS proxy bypass list.
|
|
12
|
+
*/
|
|
13
|
+
export function getSystemProxyForUrl(url, env = process.env) {
|
|
14
|
+
if (!url || isSystemProxyDisabled(env))
|
|
15
|
+
return undefined;
|
|
16
|
+
const settings = readSystemProxySettings();
|
|
17
|
+
if (!settings)
|
|
18
|
+
return undefined;
|
|
19
|
+
return systemProxyForUrl(url, settings);
|
|
20
|
+
}
|
|
21
|
+
export function systemProxyForUrl(url, settings) {
|
|
22
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
23
|
+
return undefined;
|
|
24
|
+
const hostname = url.hostname.toLowerCase();
|
|
25
|
+
if (isLoopbackHostname(hostname))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (settings.exceptions.some((entry) => systemExceptionMatches(entry, hostname)))
|
|
28
|
+
return undefined;
|
|
29
|
+
if (url.protocol === "https:")
|
|
30
|
+
return settings.httpsProxy ?? settings.httpProxy;
|
|
31
|
+
return settings.httpProxy ?? settings.httpsProxy;
|
|
32
|
+
}
|
|
33
|
+
export function parseScutilProxyOutput(output) {
|
|
34
|
+
const settings = {
|
|
35
|
+
httpProxy: proxyUrlFromKeys(output, "HTTP"),
|
|
36
|
+
httpsProxy: proxyUrlFromKeys(output, "HTTPS"),
|
|
37
|
+
exceptions: parseExceptionsList(output),
|
|
38
|
+
};
|
|
39
|
+
if (!settings.httpProxy && !settings.httpsProxy)
|
|
40
|
+
return undefined;
|
|
41
|
+
return settings;
|
|
42
|
+
}
|
|
43
|
+
export function parseWindowsProxyOutput(output) {
|
|
44
|
+
const enable = readRegistryValue(output, "ProxyEnable", "REG_DWORD");
|
|
45
|
+
if (!enable || Number.parseInt(enable, 16) !== 1)
|
|
46
|
+
return undefined;
|
|
47
|
+
const server = readRegistryValue(output, "ProxyServer", "REG_SZ");
|
|
48
|
+
if (!server)
|
|
49
|
+
return undefined;
|
|
50
|
+
const { httpProxy, httpsProxy } = parseWindowsProxyServer(server);
|
|
51
|
+
if (!httpProxy && !httpsProxy)
|
|
52
|
+
return undefined;
|
|
53
|
+
const override = readRegistryValue(output, "ProxyOverride", "REG_SZ") ?? "";
|
|
54
|
+
const exceptions = override
|
|
55
|
+
.split(";")
|
|
56
|
+
.map((item) => item.trim().toLowerCase())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
return { httpProxy, httpsProxy, exceptions };
|
|
59
|
+
}
|
|
60
|
+
// ProxyServer is either "host:port" (all protocols) or per-protocol
|
|
61
|
+
// "http=host:port;https=host:port;ftp=...;socks=..." — ftp/socks are ignored.
|
|
62
|
+
function parseWindowsProxyServer(value) {
|
|
63
|
+
if (!value.includes("=")) {
|
|
64
|
+
const url = normalizeProxyHostPort(value);
|
|
65
|
+
return { httpProxy: url, httpsProxy: url };
|
|
66
|
+
}
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const part of value.split(";")) {
|
|
69
|
+
const [protocol, hostPort] = part.split("=").map((item) => item.trim());
|
|
70
|
+
if (!protocol || !hostPort)
|
|
71
|
+
continue;
|
|
72
|
+
const url = normalizeProxyHostPort(hostPort);
|
|
73
|
+
if (!url)
|
|
74
|
+
continue;
|
|
75
|
+
if (protocol.toLowerCase() === "http")
|
|
76
|
+
result.httpProxy = url;
|
|
77
|
+
if (protocol.toLowerCase() === "https")
|
|
78
|
+
result.httpsProxy = url;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function normalizeProxyHostPort(value) {
|
|
83
|
+
const trimmed = value.trim().replace(/^https?:\/\//i, "");
|
|
84
|
+
const match = trimmed.match(/^([^\s:]+):(\d+)$/);
|
|
85
|
+
if (!match)
|
|
86
|
+
return undefined;
|
|
87
|
+
const port = Number(match[2]);
|
|
88
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
89
|
+
return undefined;
|
|
90
|
+
return `http://${match[1]}:${port}`;
|
|
91
|
+
}
|
|
92
|
+
function readRegistryValue(output, key, type) {
|
|
93
|
+
const match = output.match(new RegExp(`^\\s*${key}\\s+${type}\\s+(.+?)\\s*$`, "m"));
|
|
94
|
+
return match?.[1];
|
|
95
|
+
}
|
|
96
|
+
export function resetSystemProxyCacheForTests() {
|
|
97
|
+
cache = undefined;
|
|
98
|
+
}
|
|
99
|
+
function isSystemProxyDisabled(env) {
|
|
100
|
+
const value = env.BUBBLE_SYSTEM_PROXY?.trim().toLowerCase();
|
|
101
|
+
return !!value && FALSE_VALUES.has(value);
|
|
102
|
+
}
|
|
103
|
+
function readSystemProxySettings() {
|
|
104
|
+
if (process.platform !== "darwin" && process.platform !== "win32")
|
|
105
|
+
return undefined;
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (cache && now - cache.at < CACHE_TTL_MS)
|
|
108
|
+
return cache.settings;
|
|
109
|
+
let settings;
|
|
110
|
+
try {
|
|
111
|
+
settings = process.platform === "darwin" ? readMacProxySettings() : readWindowsProxySettings();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
settings = undefined;
|
|
115
|
+
}
|
|
116
|
+
cache = { at: now, settings };
|
|
117
|
+
return settings;
|
|
118
|
+
}
|
|
119
|
+
function readMacProxySettings() {
|
|
120
|
+
const output = execFileSync("scutil", ["--proxy"], { encoding: "utf-8", timeout: 2000 });
|
|
121
|
+
return parseScutilProxyOutput(output);
|
|
122
|
+
}
|
|
123
|
+
function readWindowsProxySettings() {
|
|
124
|
+
const output = execFileSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"], { encoding: "utf-8", timeout: 2000, windowsHide: true });
|
|
125
|
+
return parseWindowsProxyOutput(output);
|
|
126
|
+
}
|
|
127
|
+
function proxyUrlFromKeys(output, prefix) {
|
|
128
|
+
if (readScalar(output, `${prefix}Enable`) !== "1")
|
|
129
|
+
return undefined;
|
|
130
|
+
const host = readScalar(output, `${prefix}Proxy`);
|
|
131
|
+
const port = Number(readScalar(output, `${prefix}Port`));
|
|
132
|
+
if (!host || !Number.isInteger(port) || port <= 0 || port > 65535)
|
|
133
|
+
return undefined;
|
|
134
|
+
return `http://${host}:${port}`;
|
|
135
|
+
}
|
|
136
|
+
function readScalar(output, key) {
|
|
137
|
+
const match = output.match(new RegExp(`^\\s*${key}\\s*:\\s*(\\S+)`, "m"));
|
|
138
|
+
return match?.[1];
|
|
139
|
+
}
|
|
140
|
+
function parseExceptionsList(output) {
|
|
141
|
+
const match = output.match(/ExceptionsList\s*:\s*<array>\s*\{([\s\S]*?)\}/);
|
|
142
|
+
if (!match)
|
|
143
|
+
return [];
|
|
144
|
+
return [...match[1].matchAll(/^\s*\d+\s*:\s*(\S+)/gm)].map((item) => item[1].toLowerCase());
|
|
145
|
+
}
|
|
146
|
+
function systemExceptionMatches(entry, hostname) {
|
|
147
|
+
if (entry === "<local>")
|
|
148
|
+
return !hostname.includes(".");
|
|
149
|
+
// CIDR entries (e.g. 192.168.0.0/16) would need the resolved IP to match;
|
|
150
|
+
// skip them so we never wrongly bypass the proxy for public hostnames.
|
|
151
|
+
if (entry.includes("/"))
|
|
152
|
+
return false;
|
|
153
|
+
if (entry.startsWith("*."))
|
|
154
|
+
return hostname === entry.slice(2) || hostname.endsWith(entry.slice(1));
|
|
155
|
+
// Windows ProxyOverride allows wildcards anywhere, e.g. 127.* or 192.168.*
|
|
156
|
+
if (entry.includes("*"))
|
|
157
|
+
return wildcardMatches(entry, hostname);
|
|
158
|
+
if (entry.startsWith("."))
|
|
159
|
+
return hostname.endsWith(entry);
|
|
160
|
+
return entry === hostname;
|
|
161
|
+
}
|
|
162
|
+
function wildcardMatches(pattern, hostname) {
|
|
163
|
+
const regex = new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
|
|
164
|
+
return regex.test(hostname);
|
|
165
|
+
}
|
|
166
|
+
function escapeRegExp(value) {
|
|
167
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
168
|
+
}
|
|
169
|
+
function isLoopbackHostname(hostname) {
|
|
170
|
+
return hostname === "localhost"
|
|
171
|
+
|| hostname === "::1"
|
|
172
|
+
|| hostname === "[::1]"
|
|
173
|
+
|| hostname.startsWith("127.")
|
|
174
|
+
|| hostname.endsWith(".localhost");
|
|
175
|
+
}
|
|
@@ -79,6 +79,7 @@ export declare function buildAnthropicRequest(options: AnthropicProviderOptions,
|
|
|
79
79
|
thinkingLevel?: ThinkingLevel;
|
|
80
80
|
stream?: boolean;
|
|
81
81
|
}): AnthropicRequest;
|
|
82
|
+
export declare function resolveAnthropicMaxTokens(options: AnthropicProviderOptions, model: string): number;
|
|
82
83
|
export declare function supportsAnthropicPromptCache(options: AnthropicProviderOptions, model: string): boolean;
|
|
83
84
|
export declare function toAnthropicMessages(messages: ProviderMessage[], echoThinking?: boolean): {
|
|
84
85
|
system: string;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
|
+
import { isProviderTransportError, normalizeProviderNetworkError, providerFetch } from "./network/provider-transport.js";
|
|
3
|
+
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, retryAfterMsFromResponse, sleepBeforeRetry, } from "./network/retry.js";
|
|
2
4
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
3
5
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
6
|
+
const ANTHROPIC_OPUS_LONG_OUTPUT_MAX_TOKENS = 128000;
|
|
7
|
+
const ANTHROPIC_LONG_OUTPUT_MAX_TOKENS = 64000;
|
|
4
8
|
const ANTHROPIC_PROMPT_CACHE_CONTROL = { type: "ephemeral" };
|
|
5
9
|
const MINIMAX_PROMPT_CACHE_MODELS = new Set([
|
|
6
10
|
"minimax-m2.7",
|
|
@@ -22,14 +26,14 @@ export function createAnthropicMessagesProvider(options) {
|
|
|
22
26
|
thinkingLevel: chatOptions.thinkingLevel,
|
|
23
27
|
stream: true,
|
|
24
28
|
});
|
|
25
|
-
const
|
|
29
|
+
const events = streamAnthropicEventsWithRetry(options, {
|
|
26
30
|
url: resolveAnthropicMessagesUrl(options.baseURL),
|
|
27
31
|
stream: true,
|
|
28
32
|
method: "POST",
|
|
29
33
|
body: JSON.stringify(body),
|
|
30
34
|
signal: chatOptions.abortSignal,
|
|
31
35
|
});
|
|
32
|
-
yield* translateAnthropicStream(
|
|
36
|
+
yield* translateAnthropicStream(events);
|
|
33
37
|
yield { type: "done" };
|
|
34
38
|
}
|
|
35
39
|
async function complete(messages, chatOptions) {
|
|
@@ -65,24 +69,37 @@ export function buildAnthropicRequest(options, messages, chatOptions) {
|
|
|
65
69
|
cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL,
|
|
66
70
|
};
|
|
67
71
|
}
|
|
72
|
+
const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
|
|
68
73
|
const body = {
|
|
69
74
|
model: chatOptions.model,
|
|
70
|
-
max_tokens:
|
|
75
|
+
max_tokens: resolveAnthropicMaxTokens(options, chatOptions.model),
|
|
71
76
|
system: buildAnthropicSystem(system, enablePromptCache),
|
|
72
77
|
messages: anthropicMessages,
|
|
73
78
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
74
79
|
tool_choice: tools && tools.length > 0 ? { type: chatOptions.toolChoice ?? "auto" } : undefined,
|
|
75
80
|
stream: chatOptions.stream || undefined,
|
|
76
81
|
};
|
|
77
|
-
if (typeof chatOptions.temperature === "number"
|
|
82
|
+
if (typeof chatOptions.temperature === "number"
|
|
83
|
+
&& shouldSendTemperature(options, chatOptions.model, effectiveThinkingLevel)) {
|
|
78
84
|
body.temperature = chatOptions.temperature;
|
|
79
85
|
}
|
|
80
|
-
const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
|
|
81
86
|
if (effectiveThinkingLevel !== "off") {
|
|
82
87
|
body.thinking = { type: "adaptive" };
|
|
83
88
|
}
|
|
84
89
|
return body;
|
|
85
90
|
}
|
|
91
|
+
export function resolveAnthropicMaxTokens(options, model) {
|
|
92
|
+
if (!isOfficialAnthropicBaseUrl(options.baseURL)) {
|
|
93
|
+
return DEFAULT_MAX_TOKENS;
|
|
94
|
+
}
|
|
95
|
+
if (isFableModelWith128kOutput(model) || isOpusModelWith128kOutput(model)) {
|
|
96
|
+
return ANTHROPIC_OPUS_LONG_OUTPUT_MAX_TOKENS;
|
|
97
|
+
}
|
|
98
|
+
if (isSonnetOrHaikuModelWith64kOutput(model)) {
|
|
99
|
+
return ANTHROPIC_LONG_OUTPUT_MAX_TOKENS;
|
|
100
|
+
}
|
|
101
|
+
return DEFAULT_MAX_TOKENS;
|
|
102
|
+
}
|
|
86
103
|
function buildAnthropicSystem(system, enablePromptCache) {
|
|
87
104
|
if (!system)
|
|
88
105
|
return undefined;
|
|
@@ -352,27 +369,71 @@ export async function* readSseEvents(response) {
|
|
|
352
369
|
reader.releaseLock();
|
|
353
370
|
}
|
|
354
371
|
}
|
|
372
|
+
async function* streamAnthropicEventsWithRetry(options, request) {
|
|
373
|
+
const maxRetries = getProviderMaxRetries();
|
|
374
|
+
for (let attempt = 0;; attempt++) {
|
|
375
|
+
// Connection-level failures and retryable HTTP statuses are retried
|
|
376
|
+
// inside fetchAnthropicResponseWithRetry; an error thrown from it has
|
|
377
|
+
// already exhausted its budget, so it propagates without another loop.
|
|
378
|
+
const response = await fetchAnthropicResponseWithRetry(options, request);
|
|
379
|
+
let sawSseEvent = false;
|
|
380
|
+
try {
|
|
381
|
+
for await (const event of readSseEvents(response)) {
|
|
382
|
+
sawSseEvent = true;
|
|
383
|
+
yield event;
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
const normalized = normalizeAnthropicTransportError(error, request.url);
|
|
389
|
+
if (sawSseEvent) {
|
|
390
|
+
// Partial content already surfaced — only the agent loop can discard
|
|
391
|
+
// the half-built assistant message and safely re-issue the request.
|
|
392
|
+
if (!request.signal?.aborted && isProviderTransportError(error)) {
|
|
393
|
+
throw new ProviderStreamInterruptedError(normalized.message, { cause: normalized });
|
|
394
|
+
}
|
|
395
|
+
throw normalized;
|
|
396
|
+
}
|
|
397
|
+
if (request.signal?.aborted || attempt >= maxRetries || !isProviderTransportError(error)) {
|
|
398
|
+
throw normalized;
|
|
399
|
+
}
|
|
400
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), request.signal);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
355
404
|
async function fetchAnthropicResponseWithRetry(options, request) {
|
|
356
|
-
const
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
405
|
+
const maxRetries = getProviderMaxRetries();
|
|
406
|
+
for (let attempt = 0;; attempt++) {
|
|
407
|
+
let response;
|
|
408
|
+
try {
|
|
409
|
+
response = await providerFetch(request.url, {
|
|
410
|
+
method: request.method,
|
|
411
|
+
headers: buildAnthropicHeaders(options, request.stream),
|
|
412
|
+
body: request.body,
|
|
413
|
+
signal: request.signal,
|
|
414
|
+
keepalive: false,
|
|
415
|
+
}, {
|
|
416
|
+
providerName: "Anthropic",
|
|
417
|
+
verboseEnvVar: "BUBBLE_ANTHROPIC_FETCH_VERBOSE",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// No response received, so the request is safe to re-issue.
|
|
422
|
+
if (request.signal?.aborted || attempt >= maxRetries || !isProviderTransportError(error)) {
|
|
423
|
+
throw normalizeAnthropicTransportError(error, request.url);
|
|
424
|
+
}
|
|
425
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), request.signal);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
365
428
|
if (response.ok)
|
|
366
429
|
return response;
|
|
367
430
|
const detail = await readAnthropicErrorDetail(response);
|
|
368
431
|
const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
|
|
369
|
-
|
|
370
|
-
if (attempt >= maxAttempts || !isRetryableMiniMaxAnthropicError(response.status, detail)) {
|
|
432
|
+
if (request.signal?.aborted || attempt >= maxRetries || !isRetryableAnthropicHttpError(response.status, detail)) {
|
|
371
433
|
throw error;
|
|
372
434
|
}
|
|
373
|
-
await sleepBeforeRetry(
|
|
435
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1, { retryAfterMs: retryAfterMsFromResponse(response) }), request.signal);
|
|
374
436
|
}
|
|
375
|
-
throw lastError ?? new Error("Anthropic Messages API request failed");
|
|
376
437
|
}
|
|
377
438
|
function resolveAnthropicMessagesUrl(baseURL) {
|
|
378
439
|
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
@@ -403,42 +464,18 @@ async function readAnthropicErrorDetail(response) {
|
|
|
403
464
|
return response.statusText;
|
|
404
465
|
}
|
|
405
466
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|| status === 502
|
|
414
|
-
|| status === 503
|
|
415
|
-
|| status === 504
|
|
416
|
-
|| detail.includes("714 (1000)");
|
|
417
|
-
}
|
|
418
|
-
function getAnthropicRetryDelayMs() {
|
|
419
|
-
if (process.env.NODE_ENV === "test")
|
|
420
|
-
return 0;
|
|
421
|
-
return 800 + Math.floor(Math.random() * 700);
|
|
422
|
-
}
|
|
423
|
-
function sleepBeforeRetry(ms, signal) {
|
|
424
|
-
if (signal?.aborted) {
|
|
425
|
-
return Promise.reject(toAbortError(signal));
|
|
426
|
-
}
|
|
427
|
-
return new Promise((resolve, reject) => {
|
|
428
|
-
const timeout = setTimeout(resolve, ms);
|
|
429
|
-
signal?.addEventListener("abort", () => {
|
|
430
|
-
clearTimeout(timeout);
|
|
431
|
-
reject(toAbortError(signal));
|
|
432
|
-
}, { once: true });
|
|
467
|
+
function normalizeAnthropicTransportError(error, url) {
|
|
468
|
+
if (!isProviderTransportError(error)) {
|
|
469
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
470
|
+
}
|
|
471
|
+
return normalizeProviderNetworkError(error, {
|
|
472
|
+
providerName: "Anthropic",
|
|
473
|
+
input: url,
|
|
433
474
|
});
|
|
434
475
|
}
|
|
435
|
-
function
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return reason;
|
|
439
|
-
const error = new Error("Anthropic request retry aborted.");
|
|
440
|
-
error.name = "AbortError";
|
|
441
|
-
return error;
|
|
476
|
+
function isRetryableAnthropicHttpError(status, detail) {
|
|
477
|
+
// "714 (1000)" is a transient MiniMax backend error surfaced as a 500.
|
|
478
|
+
return isRetryableHttpStatus(status) || detail.includes("714 (1000)");
|
|
442
479
|
}
|
|
443
480
|
function parseSseEvent(raw) {
|
|
444
481
|
const dataLines = [];
|
|
@@ -573,6 +610,44 @@ function shouldEchoThinking(providerId) {
|
|
|
573
610
|
function shouldSendBearerAuth(options) {
|
|
574
611
|
return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
|
|
575
612
|
}
|
|
613
|
+
function shouldSendTemperature(options, model, thinkingLevel) {
|
|
614
|
+
if (!isOfficialAnthropicBaseUrl(options.baseURL))
|
|
615
|
+
return true;
|
|
616
|
+
if (thinkingLevel !== "off")
|
|
617
|
+
return false;
|
|
618
|
+
return !isOpusModelWithoutSamplingControls(model);
|
|
619
|
+
}
|
|
620
|
+
function isOpusModelWith128kOutput(model) {
|
|
621
|
+
return isClaudeFamilyVersionAtLeast(model, "opus", 4, 6);
|
|
622
|
+
}
|
|
623
|
+
function isFableModelWith128kOutput(model) {
|
|
624
|
+
return model.toLowerCase().startsWith("claude-fable-5");
|
|
625
|
+
}
|
|
626
|
+
function isSonnetOrHaikuModelWith64kOutput(model) {
|
|
627
|
+
const normalized = model.toLowerCase();
|
|
628
|
+
return normalized.startsWith("claude-sonnet-4-6")
|
|
629
|
+
|| normalized.startsWith("claude-haiku-4-5");
|
|
630
|
+
}
|
|
631
|
+
function isOpusModelWithoutSamplingControls(model) {
|
|
632
|
+
return isClaudeFamilyVersionAtLeast(model, "opus", 4, 7);
|
|
633
|
+
}
|
|
634
|
+
function isClaudeFamilyVersionAtLeast(model, family, minMajor, minMinor) {
|
|
635
|
+
const normalized = model.toLowerCase();
|
|
636
|
+
if (!normalized.startsWith(`claude-${family}-`))
|
|
637
|
+
return false;
|
|
638
|
+
const [, , majorSegment, minorSegment] = normalized.split("-");
|
|
639
|
+
const major = Number(majorSegment);
|
|
640
|
+
if (!Number.isFinite(major))
|
|
641
|
+
return false;
|
|
642
|
+
if (major > minMajor)
|
|
643
|
+
return true;
|
|
644
|
+
if (major < minMajor)
|
|
645
|
+
return false;
|
|
646
|
+
if (!minorSegment || minorSegment.length > 2)
|
|
647
|
+
return false;
|
|
648
|
+
const minor = Number(minorSegment);
|
|
649
|
+
return Number.isFinite(minor) && minor >= minMinor;
|
|
650
|
+
}
|
|
576
651
|
function isMiniMaxAnthropicEndpoint(options) {
|
|
577
652
|
const providerId = (options.providerId ?? "").toLowerCase();
|
|
578
653
|
if (providerId !== "minimax" && providerId !== "minimax-anthropic")
|
|
@@ -2,11 +2,10 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { listBuiltinModels } from "./model-catalog.js";
|
|
3
3
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
4
4
|
import { chatGptFetch } from "./network/chatgpt-transport.js";
|
|
5
|
+
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, sleepBeforeRetry, } from "./network/retry.js";
|
|
5
6
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
6
7
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
7
8
|
const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
|
|
8
|
-
const CODEX_TRANSPORT_MAX_RETRIES = 2;
|
|
9
|
-
const CODEX_TRANSPORT_RETRY_BASE_DELAY_MS = 250;
|
|
10
9
|
// OpenAI gates new codex models server-side by client_version (each model carries a
|
|
11
10
|
// `minimal_client_version`). Track a recent real Codex CLI release; override via env
|
|
12
11
|
// when OpenAI lifts the gate again before we cut a new release.
|
|
@@ -221,6 +220,13 @@ export function createOpenAICodexProvider(options) {
|
|
|
221
220
|
return;
|
|
222
221
|
}
|
|
223
222
|
catch (error) {
|
|
223
|
+
if (sawParsedSseEvent
|
|
224
|
+
&& !chatOptions.abortSignal?.aborted
|
|
225
|
+
&& isTransientCodexTransportError(error)) {
|
|
226
|
+
// Partial content already surfaced — the agent loop discards the
|
|
227
|
+
// half-built assistant message and re-issues the whole request.
|
|
228
|
+
throw new ProviderStreamInterruptedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
229
|
+
}
|
|
224
230
|
if (!shouldRetryCodexTransportError({
|
|
225
231
|
error,
|
|
226
232
|
attempt,
|
|
@@ -229,7 +235,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
229
235
|
})) {
|
|
230
236
|
throw error;
|
|
231
237
|
}
|
|
232
|
-
await
|
|
238
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), chatOptions.abortSignal);
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
}
|
|
@@ -439,9 +445,17 @@ function shouldRetryCodexTransportError(input) {
|
|
|
439
445
|
return false;
|
|
440
446
|
if (input.sawParsedSseEvent)
|
|
441
447
|
return false;
|
|
442
|
-
if (input.attempt >=
|
|
448
|
+
if (input.attempt >= getProviderMaxRetries())
|
|
449
|
+
return false;
|
|
450
|
+
return isTransientCodexTransportError(input.error) || isRetryableCodexHttpError(input.error);
|
|
451
|
+
}
|
|
452
|
+
function isRetryableCodexHttpError(error) {
|
|
453
|
+
if (!(error instanceof Error))
|
|
454
|
+
return false;
|
|
455
|
+
const match = error.message.match(/^(\d{3}) status code/);
|
|
456
|
+
if (!match)
|
|
443
457
|
return false;
|
|
444
|
-
return
|
|
458
|
+
return isRetryableHttpStatus(Number(match[1]));
|
|
445
459
|
}
|
|
446
460
|
function isTransientCodexTransportError(error) {
|
|
447
461
|
const text = errorMessageChain(error).join("\n");
|
|
@@ -487,30 +501,6 @@ function errorMessageChain(error) {
|
|
|
487
501
|
}
|
|
488
502
|
return messages;
|
|
489
503
|
}
|
|
490
|
-
function codexRetryDelayMs(attempt) {
|
|
491
|
-
return CODEX_TRANSPORT_RETRY_BASE_DELAY_MS * Math.pow(3, attempt);
|
|
492
|
-
}
|
|
493
|
-
function sleepBeforeCodexRetry(ms, signal) {
|
|
494
|
-
if (signal?.aborted)
|
|
495
|
-
return Promise.reject(toAbortError(signal));
|
|
496
|
-
return new Promise((resolve, reject) => {
|
|
497
|
-
const onAbort = () => {
|
|
498
|
-
clearTimeout(timeout);
|
|
499
|
-
signal?.removeEventListener("abort", onAbort);
|
|
500
|
-
reject(toAbortError(signal));
|
|
501
|
-
};
|
|
502
|
-
const timeout = setTimeout(() => {
|
|
503
|
-
signal?.removeEventListener("abort", onAbort);
|
|
504
|
-
resolve();
|
|
505
|
-
}, ms);
|
|
506
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
function toAbortError(signal) {
|
|
510
|
-
if (signal?.reason instanceof Error)
|
|
511
|
-
return signal.reason;
|
|
512
|
-
return new DOMException(typeof signal?.reason === "string" ? signal.reason : "Aborted", "AbortError");
|
|
513
|
-
}
|
|
514
504
|
function buildBaseHeaders(accessToken, accountId, sessionId, extraHeaders) {
|
|
515
505
|
const headers = new Headers(extraHeaders);
|
|
516
506
|
headers.set("Authorization", `Bearer ${accessToken}`);
|
package/dist/session-log.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
|
|
1
|
+
import { sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
|
|
2
2
|
export class SessionLog {
|
|
3
3
|
entries = [];
|
|
4
4
|
load(lines) {
|
|
@@ -130,7 +130,7 @@ export class SessionLog {
|
|
|
130
130
|
role: "assistant",
|
|
131
131
|
content: sanitizeInternalReminderBlocks(entry.message.content),
|
|
132
132
|
reasoning: entry.message.reasoning !== undefined
|
|
133
|
-
?
|
|
133
|
+
? sanitizeInternalReasoningText(entry.message.reasoning)
|
|
134
134
|
: undefined,
|
|
135
135
|
providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(entry.message.providerMetadata)),
|
|
136
136
|
});
|
|
@@ -197,7 +197,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
|
|
|
197
197
|
role: "assistant",
|
|
198
198
|
content: sanitizeInternalReminderBlocks(message.content),
|
|
199
199
|
reasoning: message.reasoning !== undefined
|
|
200
|
-
?
|
|
200
|
+
? sanitizeInternalReasoningText(message.reasoning)
|
|
201
201
|
: undefined,
|
|
202
202
|
model: message.model,
|
|
203
203
|
providerId: message.providerId,
|
|
@@ -6,6 +6,7 @@ import { parseRule } from "../permissions/rule.js";
|
|
|
6
6
|
import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
|
|
7
7
|
import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
|
|
8
8
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
9
|
+
import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
|
|
9
10
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
10
11
|
import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
|
|
11
12
|
import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
|
|
@@ -49,6 +50,45 @@ function handlePermissionsMutation(sub, tokens, ctx) {
|
|
|
49
50
|
return `Rule not found in ${scope} ${list}: ${rule}`;
|
|
50
51
|
return `Removed from ${scope} ${list}: ${rule}`;
|
|
51
52
|
}
|
|
53
|
+
async function handleHooksCommand(args, ctx) {
|
|
54
|
+
const hooks = ctx.hookController;
|
|
55
|
+
if (!hooks)
|
|
56
|
+
return "Hooks controller is not attached to this session.";
|
|
57
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
58
|
+
const sub = tokens[0] ?? "status";
|
|
59
|
+
if (sub === "status" || sub === "list" || sub === "") {
|
|
60
|
+
return hooks.status();
|
|
61
|
+
}
|
|
62
|
+
if (sub === "reload") {
|
|
63
|
+
hooks.reload();
|
|
64
|
+
return `Reloaded hooks.\n\n${hooks.status()}`;
|
|
65
|
+
}
|
|
66
|
+
if (sub === "trust" && tokens[1] === "project") {
|
|
67
|
+
return hooks.trustProject();
|
|
68
|
+
}
|
|
69
|
+
if (sub === "untrust" && tokens[1] === "project") {
|
|
70
|
+
return hooks.untrustProject();
|
|
71
|
+
}
|
|
72
|
+
if (sub === "test") {
|
|
73
|
+
const event = tokens[1];
|
|
74
|
+
if (!isHookEventName(event)) {
|
|
75
|
+
return `Usage: /hooks test <event> [target]\nEvents: ${HOOK_EVENT_NAMES.join(", ")}`;
|
|
76
|
+
}
|
|
77
|
+
return hooks.test(event, tokens.slice(2).join(" ") || undefined);
|
|
78
|
+
}
|
|
79
|
+
if (sub === "explain") {
|
|
80
|
+
const event = tokens[1];
|
|
81
|
+
if (!isHookEventName(event)) {
|
|
82
|
+
return `Usage: /hooks explain <event>\nEvents: ${HOOK_EVENT_NAMES.join(", ")}`;
|
|
83
|
+
}
|
|
84
|
+
return hooks.explain(event);
|
|
85
|
+
}
|
|
86
|
+
if (sub === "logs") {
|
|
87
|
+
const limit = Number(tokens[1] ?? 20);
|
|
88
|
+
return hooks.logs(Number.isFinite(limit) ? limit : 20);
|
|
89
|
+
}
|
|
90
|
+
return "Usage: /hooks [status|reload|trust project|untrust project|test <event> [target]|explain <event>|logs [limit]]";
|
|
91
|
+
}
|
|
52
92
|
function persistSelectedModel(model, ctx) {
|
|
53
93
|
const userConfig = new UserConfig();
|
|
54
94
|
userConfig.setDefaultModel(model);
|
|
@@ -667,6 +707,13 @@ const builtinSlashCommandEntries = [
|
|
|
667
707
|
return lines.join("\n");
|
|
668
708
|
},
|
|
669
709
|
},
|
|
710
|
+
{
|
|
711
|
+
name: "hooks",
|
|
712
|
+
description: "Inspect and manage lifecycle hooks. Usage: /hooks [status|trust project|test <event>]",
|
|
713
|
+
async handler(args, ctx) {
|
|
714
|
+
return handleHooksCommand(args, ctx);
|
|
715
|
+
},
|
|
716
|
+
},
|
|
670
717
|
{
|
|
671
718
|
name: "lsp",
|
|
672
719
|
description: "Inspect or restart language servers. Usage: /lsp [status|diagnostics|restart]",
|
|
@@ -788,8 +835,33 @@ const builtinSlashCommandEntries = [
|
|
|
788
835
|
if (!ctx.sessionManager) {
|
|
789
836
|
return "Compaction requires session persistence. Start an interactive session first.";
|
|
790
837
|
}
|
|
838
|
+
const preHook = await ctx.hookController?.runEvent({
|
|
839
|
+
eventName: "PreCompact",
|
|
840
|
+
cwd: ctx.cwd,
|
|
841
|
+
sessionId: ctx.sessionManager.getSessionFile(),
|
|
842
|
+
agentRole: "driver",
|
|
843
|
+
target: "manual",
|
|
844
|
+
payload: {
|
|
845
|
+
kind: "manual",
|
|
846
|
+
messageCount: ctx.agent.messages.length,
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
if (preHook?.decision === "deny") {
|
|
850
|
+
return preHook.reason ?? `Compaction blocked by hook ${preHook.sourceHookId ?? "<unknown>"}.`;
|
|
851
|
+
}
|
|
791
852
|
const result = ctx.sessionManager.compact();
|
|
792
853
|
if (!result.compacted) {
|
|
854
|
+
await ctx.hookController?.runEvent({
|
|
855
|
+
eventName: "PostCompact",
|
|
856
|
+
cwd: ctx.cwd,
|
|
857
|
+
sessionId: ctx.sessionManager.getSessionFile(),
|
|
858
|
+
agentRole: "driver",
|
|
859
|
+
target: "manual",
|
|
860
|
+
payload: {
|
|
861
|
+
kind: "manual",
|
|
862
|
+
compacted: false,
|
|
863
|
+
},
|
|
864
|
+
});
|
|
793
865
|
return "Session is already compact enough.";
|
|
794
866
|
}
|
|
795
867
|
const systemMessage = ctx.agent.messages.find((message) => message.role === "system");
|
|
@@ -799,6 +871,18 @@ const builtinSlashCommandEntries = [
|
|
|
799
871
|
];
|
|
800
872
|
ctx.agent.resetContextUsageAnchor();
|
|
801
873
|
const dropped = result.droppedEntries ?? 0;
|
|
874
|
+
await ctx.hookController?.runEvent({
|
|
875
|
+
eventName: "PostCompact",
|
|
876
|
+
cwd: ctx.cwd,
|
|
877
|
+
sessionId: ctx.sessionManager.getSessionFile(),
|
|
878
|
+
agentRole: "driver",
|
|
879
|
+
target: "manual",
|
|
880
|
+
payload: {
|
|
881
|
+
kind: "manual",
|
|
882
|
+
compacted: true,
|
|
883
|
+
droppedEntries: dropped,
|
|
884
|
+
},
|
|
885
|
+
});
|
|
802
886
|
return `✓ Compaction complete · ${dropped} log entr${dropped === 1 ? "y" : "ies"} summarized`;
|
|
803
887
|
},
|
|
804
888
|
},
|