@bubblebrain-ai/bubble 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +46 -0
  3. package/dist/agent.d.ts +10 -0
  4. package/dist/agent.js +310 -18
  5. package/dist/approval/controller.d.ts +6 -0
  6. package/dist/approval/controller.js +104 -11
  7. package/dist/checkpoints.d.ts +57 -0
  8. package/dist/checkpoints.js +0 -0
  9. package/dist/debug-trace.js +4 -0
  10. package/dist/feishu/agent-host/run-driver.js +29 -0
  11. package/dist/hooks/config.d.ts +9 -0
  12. package/dist/hooks/config.js +278 -0
  13. package/dist/hooks/controller.d.ts +24 -0
  14. package/dist/hooks/controller.js +254 -0
  15. package/dist/hooks/index.d.ts +6 -0
  16. package/dist/hooks/index.js +4 -0
  17. package/dist/hooks/log.d.ts +14 -0
  18. package/dist/hooks/log.js +54 -0
  19. package/dist/hooks/runner.d.ts +5 -0
  20. package/dist/hooks/runner.js +225 -0
  21. package/dist/hooks/trust.d.ts +37 -0
  22. package/dist/hooks/trust.js +143 -0
  23. package/dist/hooks/types.d.ts +173 -0
  24. package/dist/hooks/types.js +46 -0
  25. package/dist/main.js +86 -13
  26. package/dist/memory/prompts.js +3 -1
  27. package/dist/model-catalog.js +2 -0
  28. package/dist/model-pricing.js +8 -0
  29. package/dist/network/chatgpt-transport.d.ts +0 -1
  30. package/dist/network/chatgpt-transport.js +40 -121
  31. package/dist/network/provider-transport.d.ts +32 -0
  32. package/dist/network/provider-transport.js +265 -0
  33. package/dist/network/retry.d.ts +29 -0
  34. package/dist/network/retry.js +88 -0
  35. package/dist/network/system-proxy.d.ts +18 -0
  36. package/dist/network/system-proxy.js +175 -0
  37. package/dist/provider-anthropic.d.ts +1 -0
  38. package/dist/provider-anthropic.js +127 -52
  39. package/dist/provider-openai-codex.js +19 -29
  40. package/dist/session-log.js +3 -3
  41. package/dist/session.d.ts +31 -0
  42. package/dist/session.js +69 -0
  43. package/dist/slash-commands/commands.js +164 -0
  44. package/dist/slash-commands/types.d.ts +6 -0
  45. package/dist/tools/bash.js +4 -0
  46. package/dist/tools/edit-apply.js +63 -3
  47. package/dist/tools/edit.d.ts +2 -1
  48. package/dist/tools/edit.js +6 -5
  49. package/dist/tools/index.d.ts +7 -0
  50. package/dist/tools/index.js +2 -2
  51. package/dist/tools/write.d.ts +2 -1
  52. package/dist/tools/write.js +2 -1
  53. package/dist/tui/display-history.d.ts +4 -3
  54. package/dist/tui/display-history.js +34 -57
  55. package/dist/tui/display-sanitizer.d.ts +3 -0
  56. package/dist/tui/display-sanitizer.js +38 -0
  57. package/dist/tui/image-paste.d.ts +18 -0
  58. package/dist/tui/image-paste.js +60 -0
  59. package/dist/tui/paste-placeholder.d.ts +1 -0
  60. package/dist/tui/paste-placeholder.js +7 -0
  61. package/dist/tui/run.d.ts +2 -0
  62. package/dist/tui/run.js +568 -223
  63. package/dist/tui/trace-groups.d.ts +16 -0
  64. package/dist/tui/trace-groups.js +82 -5
  65. package/dist/tui/transcript-scroll.d.ts +25 -0
  66. package/dist/tui/transcript-scroll.js +20 -0
  67. package/dist/tui/wordmark.d.ts +1 -0
  68. package/dist/tui/wordmark.js +56 -54
  69. package/dist/tui-ink/app.d.ts +4 -1
  70. package/dist/tui-ink/app.js +303 -248
  71. package/dist/tui-ink/display-history.d.ts +16 -1
  72. package/dist/tui-ink/display-history.js +50 -21
  73. package/dist/tui-ink/footer.d.ts +6 -12
  74. package/dist/tui-ink/footer.js +10 -29
  75. package/dist/tui-ink/image-paste.d.ts +59 -0
  76. package/dist/tui-ink/image-paste.js +277 -0
  77. package/dist/tui-ink/input-box.d.ts +26 -1
  78. package/dist/tui-ink/input-box.js +171 -41
  79. package/dist/tui-ink/message-list.d.ts +1 -1
  80. package/dist/tui-ink/message-list.js +46 -29
  81. package/dist/tui-ink/run.d.ts +7 -2
  82. package/dist/tui-ink/run.js +73 -23
  83. package/dist/tui-ink/terminal-mouse.d.ts +1 -0
  84. package/dist/tui-ink/terminal-mouse.js +4 -0
  85. package/dist/tui-ink/trace-groups.d.ts +16 -0
  86. package/dist/tui-ink/trace-groups.js +90 -6
  87. package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
  88. package/dist/tui-ink/transcript-viewport-math.js +17 -0
  89. package/dist/tui-ink/transcript-viewport.d.ts +24 -0
  90. package/dist/tui-ink/transcript-viewport.js +83 -0
  91. package/dist/tui-ink/welcome.d.ts +9 -7
  92. package/dist/tui-ink/welcome.js +7 -33
  93. package/dist/tui-opentui/app.js +2 -1
  94. package/dist/tui-opentui/trace-groups.js +40 -4
  95. package/dist/types.d.ts +27 -0
  96. 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 response = await fetchAnthropicResponseWithRetry(options, {
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(readSseEvents(response));
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: DEFAULT_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 maxAttempts = shouldRetryMiniMaxAnthropic(options) ? 2 : 1;
357
- let lastError;
358
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
359
- const response = await fetch(request.url, {
360
- method: request.method,
361
- headers: buildAnthropicHeaders(options, request.stream),
362
- body: request.body,
363
- signal: request.signal,
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
- lastError = error;
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(getAnthropicRetryDelayMs(), request.signal);
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 shouldRetryMiniMaxAnthropic(options) {
407
- const providerId = (options.providerId || "").toLowerCase();
408
- const baseURL = options.baseURL.toLowerCase();
409
- return providerId.startsWith("minimax") || baseURL.includes("api.minimaxi.com") || baseURL.includes("api.minimax.io");
410
- }
411
- function isRetryableMiniMaxAnthropicError(status, detail) {
412
- return status === 500
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 toAbortError(signal) {
436
- const reason = signal?.reason;
437
- if (reason instanceof Error)
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 sleepBeforeCodexRetry(codexRetryDelayMs(attempt), chatOptions.abortSignal);
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 >= CODEX_TRANSPORT_MAX_RETRIES)
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 isTransientCodexTransportError(input.error);
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}`);
@@ -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
- ? sanitizeInternalReminderBlocks(entry.message.reasoning)
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
- ? sanitizeInternalReminderBlocks(message.reasoning)
200
+ ? sanitizeInternalReasoningText(message.reasoning)
201
201
  : undefined,
202
202
  model: message.model,
203
203
  providerId: message.providerId,
package/dist/session.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Session Manager - Append-only JSONL persistence over a structured session log.
3
3
  */
4
+ import { CheckpointStore } from "./checkpoints.js";
4
5
  import { type CompactOptions, type CompactResult } from "./context/compact.js";
5
6
  import type { Message, Todo } from "./types.js";
6
7
  import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
@@ -16,9 +17,25 @@ export interface SessionSummary {
16
17
  mtime: number;
17
18
  }
18
19
  export type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
20
+ export interface UserTurn {
21
+ /** Session log entry id of the user message that starts the turn. */
22
+ id: string;
23
+ /** Single-line preview of the user message. */
24
+ preview: string;
25
+ /** Full text of the user message. */
26
+ text: string;
27
+ timestamp: number;
28
+ }
29
+ export interface RewindResult {
30
+ /** Number of log entries removed. */
31
+ removedEntries: number;
32
+ /** Full text of the user message the session was rewound to (for re-editing). */
33
+ targetText: string;
34
+ }
19
35
  export declare class SessionManager {
20
36
  private sessionFile;
21
37
  private log;
38
+ private checkpoints?;
22
39
  constructor(sessionFile: string);
23
40
  static create(cwd: string, sessionName?: string): SessionManager;
24
41
  static resume(cwd: string, sessionName?: string): SessionManager | undefined;
@@ -41,6 +58,20 @@ export declare class SessionManager {
41
58
  getTodos(): Todo[];
42
59
  compact(options?: CompactOptions): CompactResult;
43
60
  getMessages(): Message[];
61
+ /**
62
+ * Pre-edit file snapshot store for this session, used by /rewind.
63
+ * Lives next to the session JSONL as `<session>.checkpoints/`.
64
+ */
65
+ getCheckpoints(): CheckpointStore;
66
+ /** Entry id of the most recent user message, or "0" before the first one. */
67
+ lastUserEntryId(): string;
68
+ /** User messages after the latest /clear, oldest first — the valid rewind anchors. */
69
+ listUserTurns(): UserTurn[];
70
+ /**
71
+ * Truncate the session to just before the user message with the given
72
+ * entry id. Returns undefined when the id does not name a user message.
73
+ */
74
+ rewindToEntry(entryId: string): RewindResult | undefined;
44
75
  getEntries(): SessionLogEntry[];
45
76
  getSessionFile(): string;
46
77
  private maybeAutoCompact;