@bubblebrain-ai/bubble 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +56 -6
  2. package/dist/agent/child-runner.js +5 -1
  3. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
  4. package/dist/agent/internal-reminder-sanitizer.js +7 -1
  5. package/dist/agent/profiles.js +4 -4
  6. package/dist/agent/routing-catalog.d.ts +19 -1
  7. package/dist/agent/routing-catalog.js +32 -13
  8. package/dist/agent/subagent/runtime.d.ts +28 -0
  9. package/dist/agent/subagent/runtime.js +34 -30
  10. package/dist/agent/subagent-control.d.ts +7 -0
  11. package/dist/agent/subagent-control.js +2 -0
  12. package/dist/agent/subagent-lifecycle-reminder.js +2 -1
  13. package/dist/agent/subagent-wait-guidance.d.ts +2 -0
  14. package/dist/agent/subagent-wait-guidance.js +8 -0
  15. package/dist/agent/subtask-policy.js +4 -4
  16. package/dist/agent.d.ts +20 -11
  17. package/dist/agent.js +200 -117
  18. package/dist/anthropic-thinking.d.ts +4 -0
  19. package/dist/anthropic-thinking.js +11 -0
  20. package/dist/approval/controller.d.ts +20 -6
  21. package/dist/approval/controller.js +67 -8
  22. package/dist/approval/protected-paths.d.ts +1 -0
  23. package/dist/approval/protected-paths.js +35 -0
  24. package/dist/approval/session-cache.d.ts +12 -26
  25. package/dist/approval/session-cache.js +20 -50
  26. package/dist/approval/types.d.ts +27 -1
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.js +25 -2
  29. package/dist/config.d.ts +3 -0
  30. package/dist/config.js +7 -0
  31. package/dist/context/checkpoint.d.ts +18 -0
  32. package/dist/context/checkpoint.js +117 -0
  33. package/dist/context/compact-llm.js +7 -0
  34. package/dist/context/compact.d.ts +8 -0
  35. package/dist/context/compact.js +40 -7
  36. package/dist/context/llm-compactor.d.ts +36 -2
  37. package/dist/context/llm-compactor.js +227 -44
  38. package/dist/context/session-write-lock.d.ts +20 -0
  39. package/dist/context/session-write-lock.js +162 -0
  40. package/dist/context/tool-images.d.ts +3 -0
  41. package/dist/context/tool-images.js +35 -0
  42. package/dist/context/usage.js +22 -5
  43. package/dist/feishu/agent-host/run-driver.js +56 -9
  44. package/dist/lsp/service.d.ts +13 -1
  45. package/dist/lsp/service.js +163 -43
  46. package/dist/main.js +82 -16
  47. package/dist/mcp/config.d.ts +4 -0
  48. package/dist/mcp/config.js +17 -0
  49. package/dist/mcp/manager.d.ts +13 -1
  50. package/dist/mcp/manager.js +32 -1
  51. package/dist/mcp/transports.js +11 -0
  52. package/dist/memory/db.d.ts +1 -0
  53. package/dist/memory/db.js +20 -4
  54. package/dist/memory/phase1.js +122 -16
  55. package/dist/memory/prompts.js +3 -0
  56. package/dist/model-catalog.js +19 -3
  57. package/dist/model-config.d.ts +1 -0
  58. package/dist/model-config.js +5 -5
  59. package/dist/model-pricing.js +26 -9
  60. package/dist/network/provider-transport-log.d.ts +12 -0
  61. package/dist/network/provider-transport-log.js +32 -0
  62. package/dist/network/provider-transport.js +6 -1
  63. package/dist/network/retry.d.ts +1 -1
  64. package/dist/network/retry.js +1 -1
  65. package/dist/oauth/grok.d.ts +1 -0
  66. package/dist/oauth/grok.js +20 -16
  67. package/dist/oauth/openai-codex.d.ts +1 -0
  68. package/dist/oauth/openai-codex.js +29 -25
  69. package/dist/oauth/refresh-control.d.ts +12 -0
  70. package/dist/oauth/refresh-control.js +42 -0
  71. package/dist/oauth/storage.d.ts +55 -1
  72. package/dist/oauth/storage.js +281 -16
  73. package/dist/permissions/rule.d.ts +6 -3
  74. package/dist/permissions/rule.js +71 -12
  75. package/dist/permissions/settings.d.ts +28 -1
  76. package/dist/permissions/settings.js +93 -3
  77. package/dist/permissions/shell-command.d.ts +36 -0
  78. package/dist/permissions/shell-command.js +226 -0
  79. package/dist/permissions/trust-prompt.d.ts +10 -0
  80. package/dist/permissions/trust-prompt.js +34 -0
  81. package/dist/permissions/trust.d.ts +26 -0
  82. package/dist/permissions/trust.js +113 -0
  83. package/dist/prompt/compose.d.ts +3 -0
  84. package/dist/prompt/compose.js +24 -7
  85. package/dist/prompt/environment.js +2 -2
  86. package/dist/prompt/reminders.js +1 -1
  87. package/dist/prompt/runtime.js +1 -1
  88. package/dist/provider-anthropic.d.ts +6 -2
  89. package/dist/provider-anthropic.js +98 -18
  90. package/dist/provider-error-record.d.ts +9 -0
  91. package/dist/provider-error-record.js +42 -14
  92. package/dist/provider-grok.js +5 -1
  93. package/dist/provider-openai-codex.d.ts +8 -0
  94. package/dist/provider-openai-codex.js +107 -24
  95. package/dist/provider-registry.d.ts +45 -2
  96. package/dist/provider-registry.js +300 -88
  97. package/dist/provider-transform.js +8 -0
  98. package/dist/provider-zen-errors.d.ts +9 -0
  99. package/dist/provider-zen-errors.js +36 -0
  100. package/dist/provider.js +36 -5
  101. package/dist/sdk/index.d.ts +69 -8
  102. package/dist/sdk/index.js +252 -39
  103. package/dist/sdk/replay-event-log.d.ts +7 -0
  104. package/dist/sdk/replay-event-log.js +66 -16
  105. package/dist/session-context-fence.d.ts +16 -0
  106. package/dist/session-context-fence.js +26 -0
  107. package/dist/session-log.d.ts +11 -2
  108. package/dist/session-log.js +187 -39
  109. package/dist/session-types.d.ts +5 -1
  110. package/dist/session.d.ts +39 -7
  111. package/dist/session.js +341 -93
  112. package/dist/skills/format.d.ts +31 -1
  113. package/dist/skills/format.js +45 -24
  114. package/dist/slash-commands/commands.d.ts +3 -0
  115. package/dist/slash-commands/commands.js +29 -14
  116. package/dist/system-prompt.d.ts +3 -1
  117. package/dist/tasks/manager.d.ts +7 -0
  118. package/dist/tasks/manager.js +97 -18
  119. package/dist/tools/agent-lifecycle.js +11 -7
  120. package/dist/tools/child-tools.d.ts +1 -1
  121. package/dist/tools/child-tools.js +4 -4
  122. package/dist/tools/edit-apply.d.ts +0 -3
  123. package/dist/tools/edit-apply.js +45 -249
  124. package/dist/tools/edit.js +3 -3
  125. package/dist/tools/exit-plan-mode.d.ts +5 -0
  126. package/dist/tools/exit-plan-mode.js +3 -2
  127. package/dist/tools/file-state.d.ts +4 -0
  128. package/dist/tools/file-state.js +12 -3
  129. package/dist/tools/grep.js +20 -1
  130. package/dist/tools/index.d.ts +1 -1
  131. package/dist/tools/index.js +3 -3
  132. package/dist/tools/ls.d.ts +3 -0
  133. package/dist/tools/ls.js +85 -0
  134. package/dist/tools/path-utils.d.ts +6 -0
  135. package/dist/tools/path-utils.js +38 -1
  136. package/dist/tools/sensitive-paths.d.ts +8 -0
  137. package/dist/tools/sensitive-paths.js +85 -3
  138. package/dist/tools/skill.d.ts +2 -0
  139. package/dist/tools/skill.js +34 -2
  140. package/dist/tools/task-tools.js +11 -7
  141. package/dist/tui/app.js +22 -12
  142. package/dist/tui/components/approval-dialog.d.ts +5 -5
  143. package/dist/tui/components/approval-dialog.js +16 -51
  144. package/dist/tui/components/skills-panel.d.ts +4 -0
  145. package/dist/tui/components/skills-panel.js +15 -1
  146. package/dist/tui/components/streaming-message.js +7 -2
  147. package/dist/tui/components/tasks-pane.d.ts +38 -0
  148. package/dist/tui/components/tasks-pane.js +182 -32
  149. package/dist/tui/components/transcript.d.ts +11 -4
  150. package/dist/tui/components/transcript.js +158 -71
  151. package/dist/tui/composer-autocomplete.d.ts +17 -2
  152. package/dist/tui/composer-autocomplete.js +79 -2
  153. package/dist/tui/controller/controller.d.ts +9 -4
  154. package/dist/tui/controller/controller.js +87 -95
  155. package/dist/tui/controller/goal-runtime-controller.js +1 -1
  156. package/dist/tui/controller/session-transition.js +1 -1
  157. package/dist/tui/model/subagent-view.d.ts +11 -19
  158. package/dist/tui/model/subagent-view.js +56 -85
  159. package/dist/tui/model/task-lifecycle.d.ts +57 -0
  160. package/dist/tui/model/task-lifecycle.js +124 -0
  161. package/dist/tui/model/trace-groups.js +10 -9
  162. package/dist/tui/trace-groups.js +10 -9
  163. package/dist/types.d.ts +20 -0
  164. package/dist/variant/variant-resolver.js +2 -0
  165. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
  166. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
  167. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
  168. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
  169. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
  170. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
  171. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
  172. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
  173. package/package.json +11 -3
  174. package/dist/prompt/skills.d.ts +0 -2
  175. package/dist/prompt/skills.js +0 -4
  176. package/dist/tools/glob.d.ts +0 -5
  177. package/dist/tools/glob.js +0 -199
@@ -7,6 +7,7 @@
7
7
  * agent down.
8
8
  */
9
9
  import type { ToolRegistryEntry } from "../types.js";
10
+ import type { ApprovalController } from "../approval/types.js";
10
11
  import type { UnifiedCommand } from "../slash-commands/unified.js";
11
12
  import type { McpServerState, ScopedMcpServerConfig } from "./types.js";
12
13
  export interface McpManagerOptions {
@@ -32,7 +33,10 @@ export declare class McpManager {
32
33
  /** Shutdown all connections. Best-effort; errors are ignored. */
33
34
  shutdown(): Promise<void>;
34
35
  getStates(): McpServerState[];
35
- /** Produce ToolRegistryEntry[] for every tool from every connected server. */
36
+ /**
37
+ * Produce ToolRegistryEntry[] for every tool from every connected server.
38
+ * Hosts must pass them through gateMcpTools before handing them to an agent.
39
+ */
36
40
  getToolEntries(): ToolRegistryEntry[];
37
41
  /**
38
42
  * Produce UnifiedCommand[] for every prompt from every connected server.
@@ -53,3 +57,11 @@ export declare class McpManager {
53
57
  */
54
58
  getPromptCommands(): UnifiedCommand[];
55
59
  }
60
+ /**
61
+ * Puts MCP tools behind the approval controller. MCP servers can do anything
62
+ * (open PRs, post messages, write databases) and their read-only hints are
63
+ * self-reported, so each call is an `external_tool` request: allowed by rules
64
+ * (`mcp__server__tool`, or `mcp__server` for the whole server), a session
65
+ * grant, or bypassPermissions — otherwise the user is asked.
66
+ */
67
+ export declare function gateMcpTools(tools: ToolRegistryEntry[], approval: ApprovalController | undefined): ToolRegistryEntry[];
@@ -6,6 +6,7 @@
6
6
  * per-server state so /mcp can surface them to the user without taking the
7
7
  * agent down.
8
8
  */
9
+ import { gateToolAction } from "../approval/tool-helper.js";
9
10
  import { MCPClient } from "./client.js";
10
11
  import { HttpTransport, StdioTransport } from "./transports.js";
11
12
  import { buildMcpToolName, normalizeNameForMCP } from "./name.js";
@@ -99,7 +100,10 @@ export class McpManager {
99
100
  getStates() {
100
101
  return [...this.connections.values()].map((c) => c.state);
101
102
  }
102
- /** Produce ToolRegistryEntry[] for every tool from every connected server. */
103
+ /**
104
+ * Produce ToolRegistryEntry[] for every tool from every connected server.
105
+ * Hosts must pass them through gateMcpTools before handing them to an agent.
106
+ */
103
107
  getToolEntries() {
104
108
  const entries = [];
105
109
  for (const conn of this.connections.values()) {
@@ -295,3 +299,30 @@ function formatToolContent(content) {
295
299
  }
296
300
  return parts.join("\n");
297
301
  }
302
+ /**
303
+ * Puts MCP tools behind the approval controller. MCP servers can do anything
304
+ * (open PRs, post messages, write databases) and their read-only hints are
305
+ * self-reported, so each call is an `external_tool` request: allowed by rules
306
+ * (`mcp__server__tool`, or `mcp__server` for the whole server), a session
307
+ * grant, or bypassPermissions — otherwise the user is asked.
308
+ */
309
+ export function gateMcpTools(tools, approval) {
310
+ if (!approval)
311
+ return tools;
312
+ return tools.map((tool) => ({
313
+ ...tool,
314
+ requiresApproval: true,
315
+ async execute(args, ctx) {
316
+ const gate = await gateToolAction(approval, {
317
+ type: "external_tool",
318
+ toolCallId: ctx?.toolCall?.id ?? "",
319
+ title: tool.name,
320
+ kind: "mcp",
321
+ rawInput: args,
322
+ });
323
+ if (!gate.approved)
324
+ return gate.result;
325
+ return tool.execute(args, ctx);
326
+ },
327
+ }));
328
+ }
@@ -49,6 +49,14 @@ export class StdioTransport {
49
49
  child.on("error", (err) => {
50
50
  this.errorHandler?.(err);
51
51
  });
52
+ // A server that exits (or closes stdin) makes our next write fail with
53
+ // EPIPE on the pipe itself; unhandled, that error event kills the whole
54
+ // host. Report it as a transport failure instead.
55
+ child.stdin.on("error", (err) => {
56
+ this.errorHandler?.(new Error(`stdio server stopped reading its input (${err.code ?? err.message})${this.stderrBuffer ? `\nstderr:\n${this.stderrBuffer.trim()}` : ""}`));
57
+ });
58
+ child.stdout.on("error", (err) => this.errorHandler?.(err));
59
+ child.stderr.on("error", (err) => this.errorHandler?.(err));
52
60
  child.on("exit", (code, signal) => {
53
61
  if (this.closed)
54
62
  return;
@@ -80,6 +88,9 @@ export class StdioTransport {
80
88
  async send(message) {
81
89
  if (!this.child || this.closed)
82
90
  throw new Error("Transport not started or already closed");
91
+ if (this.child.stdin.destroyed || this.child.stdin.writableEnded) {
92
+ throw new Error("stdio server is no longer accepting input");
93
+ }
83
94
  this.child.stdin.write(JSON.stringify(message) + "\n");
84
95
  }
85
96
  onMessage(handler) {
@@ -5,6 +5,7 @@ export interface Stage1Output {
5
5
  cwd: string;
6
6
  entryCount: number;
7
7
  sourceUpdatedAt: string;
8
+ extractorVersion?: string;
8
9
  generatedAt: string;
9
10
  rawMemory: string;
10
11
  rolloutSummary: string;
package/dist/memory/db.js CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname } from "node:path";
4
4
  import { getMemoryPaths } from "./paths.js";
5
- const SCHEMA_VERSION = 1;
5
+ const SCHEMA_VERSION = 2;
6
6
  const GLOBAL_CONSOLIDATION_KIND = "memory_consolidate_global";
7
7
  const GLOBAL_CONSOLIDATION_KEY = "global";
8
8
  const PHASE1_KIND = "memory_phase1_extract";
@@ -26,8 +26,8 @@ export class MemoryDatabase {
26
26
  this.db.prepare(`
27
27
  INSERT INTO memory_stage1_outputs (
28
28
  session_file, cwd, entry_count, source_updated_at, generated_at,
29
- raw_memory, rollout_summary, rollout_slug, usage_count, selected_for_phase2
30
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0)
29
+ raw_memory, rollout_summary, rollout_slug, extractor_version, usage_count, selected_for_phase2
30
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0)
31
31
  ON CONFLICT(session_file) DO UPDATE SET
32
32
  cwd = excluded.cwd,
33
33
  entry_count = excluded.entry_count,
@@ -36,12 +36,14 @@ export class MemoryDatabase {
36
36
  raw_memory = excluded.raw_memory,
37
37
  rollout_summary = excluded.rollout_summary,
38
38
  rollout_slug = excluded.rollout_slug,
39
+ extractor_version = excluded.extractor_version,
39
40
  selected_for_phase2 = CASE
40
41
  WHEN memory_stage1_outputs.selected_for_phase2_source_updated_at = excluded.source_updated_at
42
+ AND memory_stage1_outputs.extractor_version IS excluded.extractor_version
41
43
  THEN memory_stage1_outputs.selected_for_phase2
42
44
  ELSE 0
43
45
  END
44
- `).run(output.sessionFile, output.cwd, output.entryCount, output.sourceUpdatedAt, output.generatedAt, output.rawMemory, output.rolloutSummary, output.rolloutSlug ?? null);
46
+ `).run(output.sessionFile, output.cwd, output.entryCount, output.sourceUpdatedAt, output.generatedAt, output.rawMemory, output.rolloutSummary, output.rolloutSlug ?? null, output.extractorVersion ?? null);
45
47
  }
46
48
  getStage1Output(sessionFile) {
47
49
  const row = this.db.prepare("SELECT * FROM memory_stage1_outputs WHERE session_file = ?").get(sessionFile);
@@ -261,6 +263,19 @@ export class MemoryDatabase {
261
263
  updated_at TEXT NOT NULL
262
264
  )
263
265
  `).run();
266
+ const hasExtractorVersion = () => this.db.prepare("PRAGMA table_info(memory_stage1_outputs)").all()
267
+ .some(column => column.name === "extractor_version");
268
+ if (!hasExtractorVersion()) {
269
+ try {
270
+ this.db.prepare("ALTER TABLE memory_stage1_outputs ADD COLUMN extractor_version TEXT").run();
271
+ }
272
+ catch (error) {
273
+ // Another process sharing this database can win the first-start
274
+ // migration between the check and the ALTER; that is success.
275
+ if (!hasExtractorVersion())
276
+ throw error;
277
+ }
278
+ }
264
279
  this.db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(SCHEMA_VERSION));
265
280
  }
266
281
  }
@@ -270,6 +285,7 @@ function mapStage1(row) {
270
285
  cwd: String(row.cwd),
271
286
  entryCount: Number(row.entry_count),
272
287
  sourceUpdatedAt: String(row.source_updated_at),
288
+ extractorVersion: typeof row.extractor_version === "string" ? row.extractor_version : undefined,
273
289
  generatedAt: String(row.generated_at),
274
290
  rawMemory: String(row.raw_memory),
275
291
  rolloutSummary: String(row.rollout_summary),
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
5
+ import { tryCheckpointMessages } from "../context/checkpoint.js";
6
+ import { isCompactionSummaryMessage } from "../context/compact.js";
4
7
  import { MemoryDatabase } from "./db.js";
5
8
  import { getBubbleHome } from "./paths.js";
6
9
  import { buildStageOneMessages, parseJsonObject } from "./prompts.js";
@@ -11,6 +14,8 @@ const DEFAULT_MIN_ENTRIES = 4;
11
14
  const DEFAULT_LIMIT = 24;
12
15
  const MAX_TRANSCRIPT_CHARS = 70_000;
13
16
  const MAX_CONTENT_CHARS = 3_000;
17
+ // Bump when the extraction input/selection contract changes, even if the archive does not.
18
+ const EXTRACTOR_VERSION = "checkpoint-segments-v1";
14
19
  export async function runMemoryPhase1(options) {
15
20
  const result = { scanned: 0, claimed: 0, succeeded: 0, empty: 0, failed: 0, skipped: 0, errors: [] };
16
21
  if (!options.model) {
@@ -48,7 +53,8 @@ export async function runMemoryPhase1(options) {
48
53
  }
49
54
  const sourceUpdatedAt = statSync(sessionFile).mtime.toISOString();
50
55
  const existing = db.getStage1Output(sessionFile);
51
- if (existing && existing.entryCount === entryCount && existing.sourceUpdatedAt === sourceUpdatedAt) {
56
+ if (existing && existing.extractorVersion === EXTRACTOR_VERSION
57
+ && existing.entryCount === entryCount && existing.sourceUpdatedAt === sourceUpdatedAt) {
52
58
  result.skipped++;
53
59
  continue;
54
60
  }
@@ -81,6 +87,7 @@ export async function runMemoryPhase1(options) {
81
87
  cwd: sessionCwd,
82
88
  entryCount,
83
89
  sourceUpdatedAt,
90
+ extractorVersion: EXTRACTOR_VERSION,
84
91
  generatedAt: (options.now ?? new Date()).toISOString(),
85
92
  rawMemory: redactSecrets(rawMemory || rolloutSummary).text,
86
93
  rolloutSummary: redactSecrets(rolloutSummary || rawMemory).text,
@@ -131,35 +138,134 @@ function collectSessionFiles(dir) {
131
138
  return files;
132
139
  }
133
140
  function serializeSessionEntries(entries) {
134
- const parts = [];
135
- let total = 0;
141
+ // A checkpoint supersedes originals only within its /clear segment. Unlike
142
+ // live context, durable memory must still consider completed pre-clear work.
143
+ const segments = [];
144
+ let segment = [];
136
145
  for (const entry of entries) {
137
- if (total >= MAX_TRANSCRIPT_CHARS)
138
- break;
139
- const line = serializeSessionEntry(entry);
140
- if (!line)
146
+ if (entry.type === "marker" && entry.kind === "conversation_clear") {
147
+ if (segment.length)
148
+ segments.push(segment);
149
+ segment = [];
150
+ }
151
+ else if (entry.type === "context_checkpoint") {
152
+ // An unreadable checkpoint supersedes nothing; its originals stay in play.
153
+ const messages = tryCheckpointMessages(entry.checkpoint);
154
+ if (!messages)
155
+ continue;
156
+ segment = messages.flatMap(serializeCheckpointMessage);
157
+ if (entry.checkpoint.summary && !segment.some(line => line.summary)) {
158
+ segment.unshift({ text: `[summary] ${cleanText(entry.checkpoint.summary, MAX_CONTENT_CHARS)}`, summary: true });
159
+ }
160
+ }
161
+ else if (entry.type === "summary") {
162
+ // Legacy summaries have the same superseding semantics as checkpoints.
163
+ segment = [{ text: serializeSessionEntry(entry), summary: true }];
164
+ }
165
+ else {
166
+ const text = serializeSessionEntry(entry);
167
+ if (text)
168
+ segment.push({ text });
169
+ }
170
+ }
171
+ if (segment.length)
172
+ segments.push(segment);
173
+ // Give every clear segment a share so a long new conversation cannot erase
174
+ // all prior durable evidence. Spend unused shares on the newest segments.
175
+ const budgets = segments.map(() => Math.floor(MAX_TRANSCRIPT_CHARS / Math.max(1, segments.length)));
176
+ const sizes = segments.map(lines => lines.reduce((total, line) => total + line.text.length + 1, 0) + 40);
177
+ let spare = MAX_TRANSCRIPT_CHARS;
178
+ for (let i = 0; i < budgets.length; i++) {
179
+ budgets[i] = Math.min(budgets[i], sizes[i]);
180
+ spare -= budgets[i];
181
+ }
182
+ for (let i = budgets.length - 1; i >= 0 && spare > 0; i--) {
183
+ const extra = Math.min(spare, sizes[i] - budgets[i]);
184
+ budgets[i] += extra;
185
+ spare -= extra;
186
+ }
187
+ return segments.map((lines, i) => selectTranscriptLines(lines, budgets[i], i)).filter(Boolean).join("\n");
188
+ }
189
+ function selectTranscriptLines(lines, budget, segment) {
190
+ const header = `[conversation segment ${segment + 1}]\n`;
191
+ if (budget <= header.length + 1)
192
+ return "";
193
+ let remaining = budget - header.length - 1;
194
+ if (lines.reduce((total, line) => total + line.text.length + 1, 0) <= remaining) {
195
+ return header + lines.map(line => line.text).join("\n");
196
+ }
197
+ const selected = new Map();
198
+ // Reserve at most half for summaries, leaving room for retained and suffix
199
+ // evidence (especially corrections/revocations). Emit in chronological order.
200
+ let summaryBudget = Math.floor(remaining / 2);
201
+ for (let i = lines.length - 1; i >= 0 && summaryBudget > 40; i--) {
202
+ if (!lines[i].summary)
141
203
  continue;
142
- parts.push(line);
143
- total += line.length + 1;
204
+ const text = truncate(lines[i].text, Math.min(summaryBudget, remaining) - 1);
205
+ selected.set(i, text);
206
+ remaining -= text.length + 1;
207
+ summaryBudget -= text.length + 1;
208
+ }
209
+ for (let i = lines.length - 1; i >= 0 && remaining > 1; i--) {
210
+ if (selected.has(i))
211
+ continue;
212
+ if (lines[i].text.length >= remaining && remaining <= 40)
213
+ break;
214
+ const text = truncate(lines[i].text, remaining - 1);
215
+ selected.set(i, text);
216
+ remaining -= text.length + 1;
144
217
  }
145
- return truncate(parts.join("\n"), MAX_TRANSCRIPT_CHARS);
218
+ return header + [...selected].sort(([a], [b]) => a - b).map(([, text]) => text).join("\n");
219
+ }
220
+ function serializeCheckpointMessage(message) {
221
+ if (isCompactionSummaryMessage(message)) {
222
+ // Projected summaries use an internal-context envelope, but unlike runtime
223
+ // reminders its payload is durable. Unwrap only this recognized summary.
224
+ const text = contentToText(message.content).trim()
225
+ .replace(/^<bubble_internal_context\b[^>]*>\s*/, "")
226
+ .replace(/\s*<\/bubble_internal_context>$/, "");
227
+ return [{ text: `[summary] ${cleanText(text, MAX_CONTENT_CHARS)}`, summary: true }];
228
+ }
229
+ switch (message.role) {
230
+ case "user":
231
+ return textLine("user", contentToText(message.content), MAX_CONTENT_CHARS);
232
+ case "assistant":
233
+ return [
234
+ ...textLine("assistant", message.content, MAX_CONTENT_CHARS),
235
+ ...(message.toolCalls ?? []).flatMap(call => textLine(`tool_call:${call.name}`, call.arguments, 1_500)),
236
+ ];
237
+ case "tool":
238
+ return textLine(`tool_result${message.isError ? " error=true" : ""}`, message.content, 2_000);
239
+ case "system":
240
+ case "meta":
241
+ return []; // Host prompts and ephemeral runtime reminders are not memory.
242
+ }
243
+ }
244
+ function textLine(label, content, limit) {
245
+ const text = cleanText(content, limit);
246
+ return text.trim() ? [{ text: `[${label}] ${text}` }] : [];
247
+ }
248
+ function cleanText(content, limit) {
249
+ return truncate(sanitizeInternalReminderBlocks(content), limit);
146
250
  }
147
251
  function serializeSessionEntry(entry) {
148
252
  switch (entry.type) {
149
253
  case "metadata":
150
254
  return `[metadata] ${JSON.stringify(entry.metadata)}`;
151
255
  case "summary":
152
- return `[summary] ${truncate(entry.summary, MAX_CONTENT_CHARS)}`;
256
+ return `[summary] ${cleanText(entry.summary, MAX_CONTENT_CHARS)}`;
153
257
  case "marker":
154
- return `[marker:${entry.kind}] ${entry.value}`;
258
+ return `[marker:${entry.kind}] ${cleanText(entry.value, MAX_CONTENT_CHARS)}`;
155
259
  case "user_message":
156
- return `[user] ${truncate(contentToText(entry.message.content), MAX_CONTENT_CHARS)}`;
260
+ return serializeCheckpointMessage(entry.message).map(line => line.text).join("\n");
157
261
  case "assistant_message":
158
- return `[assistant] ${truncate(entry.message.content, MAX_CONTENT_CHARS)}`;
262
+ return textLine("assistant", entry.message.content, MAX_CONTENT_CHARS).map(line => line.text).join("\n");
159
263
  case "tool_call":
160
- return `[tool_call:${entry.toolCall.name}] ${truncate(entry.toolCall.arguments, 1_500)}`;
264
+ return `[tool_call:${entry.toolCall.name}] ${cleanText(entry.toolCall.arguments, 1_500)}`;
161
265
  case "tool_result":
162
- return `[tool_result${entry.message.isError ? " error=true" : ""}] ${truncate(entry.message.content, 2_000)}`;
266
+ return `[tool_result${entry.message.isError ? " error=true" : ""}] ${cleanText(entry.message.content, 2_000)}`;
267
+ case "context_checkpoint":
268
+ return ""; // Handled as a segment projection above.
163
269
  case "provider_error":
164
270
  // Operational diagnostics stay out of the memory-extraction prompt. The
165
271
  // structured session record remains available for local debugging.
@@ -9,6 +9,9 @@ export function buildStageOneMessages(input) {
9
9
  "Do not include secrets, credentials, API keys, tokens, private keys, or full large logs.",
10
10
  "Prefer concrete project facts, user preferences, workflows, decisions, and gotchas.",
11
11
  "Skip transient progress updates and one-off chatter.",
12
+ "The transcript is a bounded chronological projection; checkpoint summaries replace superseded originals.",
13
+ "Conversation segments are separated by context clears, not requests to forget durable memory.",
14
+ "Prefer later corrections and decisions over earlier claims; do not retain revoked facts as current truth.",
12
15
  ].join("\n"),
13
16
  },
14
17
  {
@@ -24,6 +24,7 @@ export const BUILTIN_PROVIDERS = [
24
24
  { id: "doubao", name: "Doubao (Volcengine Ark)", baseURL: "https://ark.cn-beijing.volces.com/api/v3", protocol: "ark-responses" },
25
25
  { id: "minimax", name: "MiniMax Token Plan", baseURL: "https://api.minimaxi.com/anthropic", protocol: "anthropic-messages" },
26
26
  { id: "minimax-anthropic", name: "MiniMax API", baseURL: "https://api.minimaxi.com/anthropic", protocol: "anthropic-messages" },
27
+ { id: "mimo-token-plan", name: "Xiaomi MiMo Token Plan", baseURL: "https://token-plan-cn.xiaomimimo.com/anthropic", protocol: "anthropic-messages" },
27
28
  { id: "stepfun", name: "StepFun Step Plan", baseURL: "https://api.stepfun.com/step_plan/v1" },
28
29
  { id: "stepfun-api", name: "StepFun API", baseURL: "https://api.stepfun.com/v1", protocol: "openai-chat" },
29
30
  { id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
@@ -78,9 +79,8 @@ const DOUBAO_SEED_REASONING_LEVELS = ["minimal", "low", "medium", "high"];
78
79
  const MINIMAX_M3_REASONING_LEVELS = ["off", "medium"];
79
80
  const MINIMAX_REASONING_LEVELS = ["medium"];
80
81
  // Anthropic exposes reasoning depth through output_config.effort (low | medium
81
- // | high | xhigh | max), not a token budget. xhigh is Opus 4.7+ only; max is
82
- // Opus 4.6+/Sonnet 4.6/Fable 5; Haiku 4.5 does not accept the effort param.
83
- // Fable 5 has thinking always on, so it has no "off". "off" disables thinking.
82
+ // | high | xhigh | max), not a token budget. Sonnet 5 also supports xhigh.
83
+ // Fable 5+ and Opus 5.5 have thinking always on, so they have no "off".
84
84
  const ANTHROPIC_OPUS_EFFORT_LEVELS = ["off", "low", "medium", "high", "xhigh", "max"];
85
85
  const ANTHROPIC_SONNET_EFFORT_LEVELS = ["off", "low", "medium", "high", "max"];
86
86
  const ANTHROPIC_FABLE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
@@ -100,6 +100,9 @@ export const BUILTIN_MODELS = [
100
100
  // only Ox Alpha. Discovery refreshes this entry's public metadata without
101
101
  // expanding provider membership.
102
102
  { id: OPENROUTER_MODEL_ID, name: "Ox Alpha", providerId: "openrouter", tier: "strong", reasoningLevels: OPENROUTER_OX_REASONING_LEVELS, defaultReasoningLevel: "max", contextWindow: 1048576 },
103
+ // Verified against /codex/models on 2026-09-17: same request shape as gpt-5.6-terra
104
+ // (responses-lite, six effort levels, 10k tool-output cap); minimal_client_version 0.153.0.
105
+ { id: "gpt-6-astra", name: "GPT-6-Astra", providerId: "openai-codex", tier: "strong", reasoningLevels: GPT56_LEVELS, defaultReasoningLevel: "medium", contextWindow: 372000, useResponsesLite: true, toolOutputTokenLimit: 10000 },
103
106
  { id: "gpt-5.6-sol", name: "GPT-5.6-Sol", providerId: "openai-codex", tier: "balanced", reasoningLevels: GPT56_LEVELS, defaultReasoningLevel: "low", contextWindow: 372000, useResponsesLite: true, toolOutputTokenLimit: 10000 },
104
107
  { id: "gpt-5.6-terra", name: "GPT-5.6-Terra", providerId: "openai-codex", tier: "strong", reasoningLevels: GPT56_LEVELS, defaultReasoningLevel: "medium", contextWindow: 372000, useResponsesLite: true, toolOutputTokenLimit: 10000 },
105
108
  { id: "gpt-5.6-luna", name: "GPT-5.6-Luna", providerId: "openai-codex", tier: "strong", reasoningLevels: GPT56_LUNA_LEVELS, defaultReasoningLevel: "medium", contextWindow: 372000, useResponsesLite: true, toolOutputTokenLimit: 10000 },
@@ -123,6 +126,13 @@ export const BUILTIN_MODELS = [
123
126
  { id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Free", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
124
127
  { id: "muse-spark-1.2", name: "Muse Spark 1.2", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
125
128
  { id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Free", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
129
+ // Zen /chat/completions; models.dev/opencode metadata checked 2026-09-26.
130
+ { id: "space-bunny-free", name: "Space Bunny Free", providerId: "opencode-zen", reasoningLevels: ["low", "medium", "high", "xhigh", "max"], defaultReasoningLevel: "high", contextWindow: 1048576 },
131
+ // Current public lineup, verified against platform.claude.com on 2026-09-23.
132
+ { id: "claude-fable-5-1", name: "Claude Fable 5.1", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_FABLE_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
133
+ { id: "claude-opus-5-5", name: "Claude Opus 5.5", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_FABLE_EFFORT_LEVELS, defaultReasoningLevel: "medium", contextWindow: 1000000 },
134
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", providerId: "anthropic", tier: "balanced", reasoningLevels: ANTHROPIC_OPUS_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
135
+ { id: "claude-opus-5", name: "Claude Opus 5", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_OPUS_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
126
136
  { id: "claude-fable-5", name: "Claude Fable 5", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_FABLE_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
127
137
  { id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_OPUS_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
128
138
  { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providerId: "anthropic", tier: "balanced", reasoningLevels: ANTHROPIC_SONNET_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
@@ -189,6 +199,12 @@ export const BUILTIN_MODELS = [
189
199
  { id: "MiniMax-M3", name: "MiniMax M3", providerId: "minimax-anthropic", tier: "strong", reasoningLevels: MINIMAX_M3_REASONING_LEVELS, contextWindow: 1000000 },
190
200
  { id: "MiniMax-M2.7", name: "MiniMax M2.7", providerId: "minimax-anthropic", tier: "balanced", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
191
201
  { id: "MiniMax-M2.7-highspeed", name: "MiniMax M2.7 Highspeed", providerId: "minimax-anthropic", tier: "fast", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
202
+ // Token Plan's text models; V2.5 remains available until 2026-10-21.
203
+ // https://mimo.mi.com/docs/zh-CN/tokenplan/integration/claudecode
204
+ { id: "mimo-v2.6-pro", name: "MiMo V2.6 Pro", providerId: "mimo-token-plan", tier: "strong", reasoningLevels: TOGGLE_THINKING_LEVELS, defaultReasoningLevel: "medium", contextWindow: 1000000 },
205
+ { id: "mimo-v2.6-flash", name: "MiMo V2.6 Flash", providerId: "mimo-token-plan", tier: "fast", reasoningLevels: TOGGLE_THINKING_LEVELS, defaultReasoningLevel: "medium", contextWindow: 1000000 },
206
+ { id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro", providerId: "mimo-token-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, defaultReasoningLevel: "medium", contextWindow: 1000000 },
207
+ { id: "mimo-v2.5", name: "MiMo V2.5", providerId: "mimo-token-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, defaultReasoningLevel: "medium", contextWindow: 1000000 },
192
208
  { id: "step-3.7-flash", name: "Step 3.7 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS, contextWindow: 256000 },
193
209
  { id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
194
210
  { id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
@@ -23,6 +23,7 @@ export interface ModelsConfig {
23
23
  providers: Record<string, ProviderModelConfig>;
24
24
  }
25
25
  export declare class ModelConfig {
26
+ private readonly modelsPath;
26
27
  private data?;
27
28
  private loadError?;
28
29
  constructor();
@@ -6,9 +6,8 @@
6
6
  */
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { homedir } from "node:os";
9
+ import { getBubbleHome } from "./bubble-home.js";
10
10
  import { filterProviderModels } from "./provider-model-policy.js";
11
- const MODELS_PATH = join(homedir(), ".bubble", "models.json");
12
11
  const MODEL_TIERS = ["fast", "balanced", "strong"];
13
12
  function sanitizeTier(value) {
14
13
  return typeof value === "string" && MODEL_TIERS.includes(value)
@@ -19,16 +18,17 @@ function sanitizeRoutingPriority(value) {
19
18
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
20
19
  }
21
20
  export class ModelConfig {
21
+ modelsPath = join(getBubbleHome(), "models.json");
22
22
  data;
23
23
  loadError;
24
24
  constructor() {
25
25
  this.load();
26
26
  }
27
27
  load() {
28
- if (!existsSync(MODELS_PATH))
28
+ if (!existsSync(this.modelsPath))
29
29
  return;
30
30
  try {
31
- const raw = readFileSync(MODELS_PATH, "utf-8");
31
+ const raw = readFileSync(this.modelsPath, "utf-8");
32
32
  this.data = JSON.parse(raw);
33
33
  }
34
34
  catch (err) {
@@ -39,7 +39,7 @@ export class ModelConfig {
39
39
  return this.loadError;
40
40
  }
41
41
  getPath() {
42
- return MODELS_PATH;
42
+ return this.modelsPath;
43
43
  }
44
44
  getProviderConfig(providerId) {
45
45
  return this.data?.providers?.[providerId];
@@ -30,6 +30,7 @@ export const MODEL_PRICING_OVERRIDES = [
30
30
  ...[
31
31
  "muse-spark-1.3-contributor-free",
32
32
  "muse-spark-1.2-contributor-free",
33
+ "space-bunny-free",
33
34
  ].map((modelId) => ({
34
35
  providerId: "opencode-zen",
35
36
  modelId,
@@ -87,6 +88,9 @@ export const MODEL_PRICING_OVERRIDES = [
87
88
  const prior = kind === "pro"
88
89
  ? { inputCacheHitPerMillion: 0.003625, inputCacheMissPerMillion: 0.435, outputPerMillion: 0.87 }
89
90
  : { inputCacheHitPerMillion: 0.0028, inputCacheMissPerMillion: 0.14, outputPerMillion: 0.28 };
91
+ // The 2026-08-16 schedule was already Monday-Friday only; the earlier
92
+ // every-day model here was an omission, so applying the weekday rule to
93
+ // this tariff corrects August weekend history rather than repricing it.
90
94
  const previous = {
91
95
  ...peak,
92
96
  peakWindowsUtc: DEEPSEEK_PEAK_WINDOWS_UTC,
@@ -113,8 +117,28 @@ export const MODEL_PRICING_OVERRIDES = [
113
117
  }),
114
118
  };
115
119
  }),
116
- // models.dev carries the Sonnet 5 launch-promo rate but not its expiry or
117
- // the post-promo price; keep both so cost estimates survive the promo end.
120
+ // Official current prices, verified 2026-09-23:
121
+ // https://platform.claude.com/docs/en/about-claude/pricing
122
+ // Cache reads are 2.5% (Fable 5.1) / 5% (Opus 5.5), not the older 10%.
123
+ {
124
+ providerId: "anthropic",
125
+ modelId: "claude-fable-5-1",
126
+ currency: "USD",
127
+ inputCacheHitPerMillion: 0.25,
128
+ inputCacheMissPerMillion: 10,
129
+ inputCacheWritePerMillion: 12.5,
130
+ outputPerMillion: 50,
131
+ },
132
+ {
133
+ providerId: "anthropic",
134
+ modelId: "claude-opus-5-5",
135
+ currency: "USD",
136
+ inputCacheHitPerMillion: 0.2,
137
+ inputCacheMissPerMillion: 4,
138
+ inputCacheWritePerMillion: 5,
139
+ outputPerMillion: 20,
140
+ },
141
+ // The current official Sonnet 5 price is $2/$10 without an expiry.
118
142
  {
119
143
  providerId: "anthropic",
120
144
  modelId: "claude-sonnet-5",
@@ -123,13 +147,6 @@ export const MODEL_PRICING_OVERRIDES = [
123
147
  inputCacheMissPerMillion: 2,
124
148
  inputCacheWritePerMillion: 2.5,
125
149
  outputPerMillion: 10,
126
- effectiveUntil: "2026-08-31T23:59:00Z",
127
- original: {
128
- inputCacheHitPerMillion: 0.3,
129
- inputCacheMissPerMillion: 3,
130
- inputCacheWritePerMillion: 3.75,
131
- outputPerMillion: 15,
132
- },
133
150
  },
134
151
  // The StepFun step plan bills in CNY; models.dev normalizes prices to USD.
135
152
  {
@@ -0,0 +1,12 @@
1
+ import { type ProviderErrorContext } from "../provider-error-record.js";
2
+ export interface ProviderTransportFailureContext extends ProviderErrorContext {
3
+ sessionId: string;
4
+ requestId: string;
5
+ attempt: number;
6
+ responseStatus?: number;
7
+ receivedEvents: number;
8
+ elapsedMs: number;
9
+ lastEventAgeMs?: number;
10
+ decision: "retry" | "delegate_retry" | "fail" | "cancelled";
11
+ }
12
+ export declare function logProviderTransportFailure(error: unknown, context: ProviderTransportFailureContext): void;
@@ -0,0 +1,32 @@
1
+ import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getBubbleHome } from "../bubble-home.js";
4
+ import { createSanitizedProviderError } from "../provider-error-record.js";
5
+ // Failure-only diagnostics are enabled without BUBBLE_TRACE. Keep at most two
6
+ // 2 MiB files; never persist request/response bodies, headers, or raw errors.
7
+ export function logProviderTransportFailure(error, context) {
8
+ try {
9
+ const dir = join(getBubbleHome(), "logs");
10
+ mkdirSync(dir, { recursive: true });
11
+ const file = join(dir, "provider-transport.jsonl");
12
+ if (existsSync(file) && statSync(file).size >= 2 * 1024 * 1024) {
13
+ renameSync(file, `${file}.1`);
14
+ }
15
+ appendFileSync(file, JSON.stringify({
16
+ timestamp: new Date().toISOString(),
17
+ phase: "provider_transport_failure",
18
+ sessionId: context.sessionId,
19
+ requestId: context.requestId,
20
+ attempt: context.attempt,
21
+ responseStatus: context.responseStatus,
22
+ receivedEvents: context.receivedEvents,
23
+ elapsedMs: context.elapsedMs,
24
+ lastEventAgeMs: context.lastEventAgeMs,
25
+ decision: context.decision,
26
+ error: createSanitizedProviderError(error, context),
27
+ }) + "\n", { encoding: "utf8", mode: 0o600 });
28
+ }
29
+ catch {
30
+ // Disk errors and concurrent rotation must never affect the request.
31
+ }
32
+ }
@@ -229,7 +229,12 @@ function errorMessageChain(error) {
229
229
  for (let depth = 0; current && depth < 8; depth++) {
230
230
  if (current instanceof Error) {
231
231
  messages.push(current.name, current.message);
232
- current = current.cause;
232
+ // Node/undici stores errno on Error.code, including nested causes such
233
+ // as TypeError("terminated") -> SocketError { code: "UND_ERR_SOCKET" }.
234
+ const codedError = current;
235
+ if (typeof codedError.code === "string")
236
+ messages.push(codedError.code);
237
+ current = codedError.cause;
233
238
  continue;
234
239
  }
235
240
  if (typeof current === "object") {
@@ -7,7 +7,7 @@
7
7
  * signalled with ProviderStreamInterruptedError so the agent loop can
8
8
  * discard the partial assistant message and re-issue the whole request.
9
9
  */
10
- export declare const MAX_STREAM_INTERRUPTION_RETRIES = 2;
10
+ export declare const MAX_STREAM_INTERRUPTION_RETRIES = 10;
11
11
  export declare class ProviderStreamInterruptedError extends Error {
12
12
  readonly isProviderStreamInterruption = true;
13
13
  readonly retryAfterMs?: number;
@@ -12,7 +12,7 @@ const MAX_CONFIGURABLE_RETRIES = 10;
12
12
  const BASE_DELAY_MS = 1000;
13
13
  const MAX_DELAY_MS = 32_000;
14
14
  const MAX_RETRY_AFTER_MS = 60_000;
15
- export const MAX_STREAM_INTERRUPTION_RETRIES = 2;
15
+ export const MAX_STREAM_INTERRUPTION_RETRIES = 10;
16
16
  const RETRYABLE_HTTP_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
17
17
  export class ProviderStreamInterruptedError extends Error {
18
18
  isProviderStreamInterruption = true;
@@ -16,6 +16,7 @@ export declare function loginGrok(callbacks?: GrokLoginCallbacks, options?: {
16
16
  }): Promise<OAuthTokens>;
17
17
  export declare function refreshGrok(refreshToken: string, options?: {
18
18
  fetch?: ChatGptFetch;
19
+ timeoutMs?: number;
19
20
  }): Promise<OAuthTokens>;
20
21
  /**
21
22
  * One-time credential import from the official Grok CLI's auth store (either