@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
@@ -0,0 +1,162 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { writeFileSync, linkSync, readFileSync, unlinkSync } from "node:fs";
4
+ function ownerPid(token) {
5
+ const pid = Number(token.split(":")[0]);
6
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
7
+ }
8
+ function deadOwner(token) {
9
+ const pid = ownerPid(token);
10
+ if (pid === undefined)
11
+ return false;
12
+ try {
13
+ process.kill(pid, 0);
14
+ return false;
15
+ }
16
+ catch (error) {
17
+ return error.code === "ESRCH";
18
+ }
19
+ }
20
+ /** Identity of one incarnation of a pid: the OS start time of the process that
21
+ * currently has it. Pids are recycled, so a pid that answers kill(0) may be an
22
+ * unrelated process; only a matching start time proves it is the same owner.
23
+ * Undefined when the platform cannot say, which callers treat as "unknown",
24
+ * never as "gone". Exported for tests. */
25
+ export function processStartId(pid) {
26
+ try {
27
+ if (process.platform === "linux") {
28
+ return linuxStartId(readFileSync(`/proc/${pid}/stat`, "utf8"), readFileSync("/proc/sys/kernel/random/boot_id", "utf8"));
29
+ }
30
+ if (process.platform === "win32")
31
+ return undefined;
32
+ // lstart is rendered in the caller's locale and time zone. Owner and
33
+ // contender can be different hosts (terminal TUI vs GUI desktop) with
34
+ // different environments, so pin both or a live owner looks recycled.
35
+ return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
36
+ encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"],
37
+ env: { PATH: process.env.PATH ?? "/bin:/usr/bin", LC_ALL: "C", TZ: "UTC" },
38
+ }).trim() || undefined;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ /** Field 22 (starttime) counts clock ticks since boot, and lock files outlive
45
+ * reboots, so the boot id is part of the identity: the same pid and tick after
46
+ * another boot is a different process. The comm field may itself contain
47
+ * spaces and parentheses, hence the split after the last ")". Exported for tests. */
48
+ export function linuxStartId(stat, bootId) {
49
+ const startTicks = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
50
+ return startTicks && bootId.trim() ? `${bootId.trim()}/${startTicks}` : undefined;
51
+ }
52
+ let ownStartId = null;
53
+ function ownerToken() {
54
+ if (ownStartId === null)
55
+ ownStartId = processStartId(process.pid);
56
+ // Start ids contain ":" (ps lstart) — keep pid and uuid as the leading fields.
57
+ return `${process.pid}:${randomUUID()}${ownStartId ? `:${ownStartId}` : ""}`;
58
+ }
59
+ /** The pid is alive but belongs to a different process than the one that took
60
+ * the lock. Elapsed age is deliberately NOT evidence: a live writer can be
61
+ * suspended or stalled in fsync for arbitrarily long. */
62
+ function recycledOwner(token) {
63
+ const pid = ownerPid(token);
64
+ const recorded = token.split(":").slice(2).join(":");
65
+ if (pid === undefined || !recorded)
66
+ return false; // Legacy token: unknown, never expire.
67
+ const current = processStartId(pid);
68
+ return current !== undefined && current !== recorded;
69
+ }
70
+ function release(path, token) {
71
+ try {
72
+ if (readFileSync(path, "utf8") === token)
73
+ unlinkSync(path);
74
+ }
75
+ catch (error) {
76
+ if (error.code !== "ENOENT")
77
+ throw error;
78
+ }
79
+ }
80
+ /** Publish a fully written owner atomically. No empty lock can be left by a crash. */
81
+ function publish(path, token) {
82
+ // Only pid and uuid: the start id may hold spaces and colons.
83
+ const owner = `${path}.${token.split(":").slice(0, 2).join("-")}.owner`;
84
+ writeFileSync(owner, token, { mode: 0o600, flag: "wx" });
85
+ try {
86
+ linkSync(owner, path);
87
+ }
88
+ finally {
89
+ unlinkSync(owner);
90
+ }
91
+ }
92
+ /** A live owner held the lock past the bounded wait. Nothing was written. */
93
+ export class SessionWriteLockBusyError extends Error {
94
+ constructor(path) {
95
+ super(`Session write lock is held by another live writer: ${path}`);
96
+ this.name = "SessionWriteLockBusyError";
97
+ }
98
+ }
99
+ const LIVE_OWNER_WAIT_MS = 2_000;
100
+ const sleepCell = new Int32Array(new SharedArrayBuffer(4));
101
+ function sleepSync(ms) { Atomics.wait(sleepCell, 0, 0, ms); }
102
+ /** Synchronous short transaction lock. An owner is abandoned only when its
103
+ * process is provably gone: the pid no longer exists, or it now belongs to a
104
+ * different process incarnation. Live owners are never expired by age;
105
+ * contenders wait a bounded time for them, since transactions are short. */
106
+ export function withSessionWriteLock(path, action, waitMs = LIVE_OWNER_WAIT_MS) {
107
+ const token = ownerToken();
108
+ const deadline = Date.now() + waitMs;
109
+ const verifiedLive = new Set(); // One incarnation probe per observed owner.
110
+ for (let delay = 2;; delay = Math.min(delay * 2, 50)) {
111
+ try {
112
+ publish(path, token);
113
+ break;
114
+ }
115
+ catch (error) {
116
+ if (error.code !== "EEXIST")
117
+ throw error;
118
+ const timedOut = Date.now() >= deadline;
119
+ let observed;
120
+ try {
121
+ observed = readFileSync(path, "utf8");
122
+ }
123
+ catch (readError) {
124
+ if (readError.code !== "ENOENT")
125
+ throw readError;
126
+ if (timedOut)
127
+ throw new SessionWriteLockBusyError(path);
128
+ continue;
129
+ }
130
+ // kill(0) is cheap and re-checked every round. The incarnation probe may
131
+ // spawn `ps`, so an owner is probed when first seen and once more before
132
+ // giving up: it can exit and have its pid recycled while we wait.
133
+ const abandoned = () => deadOwner(observed)
134
+ || ((timedOut || !verifiedLive.has(observed)) && recycledOwner(observed));
135
+ if (!abandoned()) {
136
+ if (timedOut)
137
+ throw new SessionWriteLockBusyError(path);
138
+ verifiedLive.add(observed);
139
+ sleepSync(delay);
140
+ continue;
141
+ }
142
+ // Serialize recovery. Recovery locks themselves use the same owner-safe
143
+ // protocol, so killing a recovering process cannot permanently block it.
144
+ withSessionWriteLock(`${path}.recovery`, () => {
145
+ try {
146
+ if (readFileSync(path, "utf8") === observed && abandoned())
147
+ unlinkSync(path);
148
+ }
149
+ catch (recoveryError) {
150
+ if (recoveryError.code !== "ENOENT")
151
+ throw recoveryError;
152
+ }
153
+ }, Math.max(0, deadline - Date.now()));
154
+ }
155
+ }
156
+ try {
157
+ return action();
158
+ }
159
+ finally {
160
+ release(path, token);
161
+ }
162
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolResult, UserMessage } from "../types.js";
2
+ /** All tool responses must precede the observation (valid for every provider). */
3
+ export declare function toolImageObservation(results: ToolResult[]): UserMessage | null;
@@ -0,0 +1,35 @@
1
+ /** All tool responses must precede the observation (valid for every provider). */
2
+ export function toolImageObservation(results) {
3
+ const content = [];
4
+ let bytes = 0;
5
+ for (const result of results) {
6
+ if (result.isError)
7
+ continue;
8
+ for (const image of result.images ?? []) {
9
+ if (content.length >= 8 ||
10
+ !["image/png", "image/jpeg"].includes(image.mimeType))
11
+ continue;
12
+ if (typeof image.data !== "string" ||
13
+ image.data.length > 4_000_000 ||
14
+ !/^[a-zA-Z0-9+/]+={0,2}$/.test(image.data))
15
+ continue;
16
+ const raw = Buffer.from(image.data, "base64");
17
+ if (bytes + raw.length > 6_000_000)
18
+ continue;
19
+ if (image.mimeType === "image/png"
20
+ ? raw.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a"
21
+ : raw.subarray(0, 3).toString("hex") !== "ffd8ff")
22
+ continue;
23
+ bytes += raw.length;
24
+ content.push({
25
+ type: "text",
26
+ text: `Screenshot observation from the preceding tool result (not a new user request):\n${result.content.slice(0, 2000)}`,
27
+ });
28
+ content.push({
29
+ type: "image_url",
30
+ image_url: { url: `data:${image.mimeType};base64,${image.data}` },
31
+ });
32
+ }
33
+ }
34
+ return content.length ? { role: "user", content, toolObservation: true } : null;
35
+ }
@@ -1,5 +1,5 @@
1
1
  import { getModelContextWindow } from "../model-catalog.js";
2
- import { formatSkillsPrompt } from "../skills/format.js";
2
+ import { SKILL_CATALOG_HEADING, SKILL_CATALOG_INTRO } from "../skills/format.js";
3
3
  import { buildDeferredToolsReminder } from "../prompt/reminders.js";
4
4
  import { AUTOCOMPACT_BUFFER_TOKENS, estimateMessageTokens, estimateTextTokens, MIN_WINDOW_FOR_RESERVE, OUTPUT_RESERVE_TOKENS, } from "./budget.js";
5
5
  export function buildContextUsageSnapshot(input) {
@@ -7,8 +7,12 @@ export function buildContextUsageSnapshot(input) {
7
7
  const otherMessages = input.messages.filter((message) => message.role !== "system");
8
8
  const deferredToolEntries = input.deferredToolEntries ?? [];
9
9
  const systemContent = systemMessages.map((message) => message.content).join("\n\n");
10
- const skillsPrompt = formatSkillsPrompt(input.skills);
11
- const skillsInSystemPrompt = !!skillsPrompt && systemContent.includes(skillsPrompt);
10
+ // The catalog is whatever Skills section the prompt actually carries, whatever its budget.
11
+ const skillsPrompt = extractSkillCatalog(systemContent);
12
+ const skillsInSystemPrompt = !!skillsPrompt;
13
+ const advertisedSkills = skillsPrompt
14
+ ? skillsPrompt.split("\n").filter((line) => line.startsWith("- ") && !line.startsWith("- … ")).length
15
+ : 0;
12
16
  const skillsTokens = skillsInSystemPrompt ? estimateTextTokens(skillsPrompt, input.providerId) : 0;
13
17
  const systemPromptTokens = Math.max(0, estimateTextTokens(systemContent, input.providerId) - skillsTokens);
14
18
  const toolsTokens = estimateToolEntriesTokens(input.toolEntries, input.providerId);
@@ -37,7 +41,7 @@ export function buildContextUsageSnapshot(input) {
37
41
  skills: {
38
42
  label: "Skills",
39
43
  tokens: skillsTokens,
40
- detail: skillsInSystemPrompt && input.skills.length > 0 ? `${input.skills.length} advertised skill${input.skills.length === 1 ? "" : "s"}` : "none in current prompt",
44
+ detail: advertisedSkills > 0 ? `${advertisedSkills} advertised skill${advertisedSkills === 1 ? "" : "s"}` : "none in current prompt",
41
45
  },
42
46
  deferredTools: {
43
47
  label: "Deferred/MCP",
@@ -54,7 +58,7 @@ export function buildContextUsageSnapshot(input) {
54
58
  },
55
59
  toolCount: input.toolEntries.length,
56
60
  deferredToolCount: deferredToolEntries.length,
57
- skillCount: skillsInSystemPrompt ? input.skills.length : 0,
61
+ skillCount: advertisedSkills,
58
62
  messageCount: input.messages.length,
59
63
  };
60
64
  }
@@ -211,3 +215,16 @@ function colorForLabel(label) {
211
215
  return ANSI_DARK_GRAY;
212
216
  return ANSI_GRAY;
213
217
  }
218
+ /**
219
+ * The Skills catalog the prompt carries: heading plus its fixed intro line, so a
220
+ * "## Skills" heading in AGENTS.md or memory is never mistaken for it. The last
221
+ * match wins (the catalog is appended after memory); it ends at the next blank line.
222
+ */
223
+ function extractSkillCatalog(systemContent) {
224
+ const marker = `${SKILL_CATALOG_HEADING}\n${SKILL_CATALOG_INTRO}\n`;
225
+ const at = systemContent.lastIndexOf(marker);
226
+ if (at < 0 || (at > 0 && !systemContent.slice(0, at).endsWith("\n\n")))
227
+ return "";
228
+ const end = systemContent.indexOf("\n\n", at);
229
+ return systemContent.slice(at, end < 0 ? undefined : end);
230
+ }
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import chalk from "chalk";
15
15
  import { Agent } from "../../agent.js";
16
+ import { splitLeadingContext } from "../../context/compact.js";
16
17
  import { BudgetLedger } from "../../agent/budget-ledger.js";
17
18
  import { PermissionAwareApprovalController } from "../../approval/controller.js";
18
19
  import { BashAllowlist } from "../../approval/session-cache.js";
@@ -23,6 +24,7 @@ import { createRoutableModelIndex, createRoutingSnapshotAccessor } from "../../a
23
24
  import { buildModelRoutingPrompt } from "../../prompt/routing.js";
24
25
  import { FileStateTracker } from "../../tools/file-state.js";
25
26
  import { buildToolPromptOptions, createAllTools } from "../../tools/index.js";
27
+ import { gateMcpTools } from "../../mcp/manager.js";
26
28
  import { displayModel, encodeModel, decodeModel } from "../../provider-registry.js";
27
29
  import { buildMemoryPrompt, recordMemoryCitations } from "../../memory/index.js";
28
30
  import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../../provider-transform.js";
@@ -80,7 +82,7 @@ export class RunDriver {
80
82
  // questionController intentionally omitted — Feishu v1 doesn't surface
81
83
  // the question tool to the agent.
82
84
  });
83
- tools.push(...this.opts.deps.mcpManager.getToolEntries());
85
+ tools.push(...gateMcpTools(this.opts.deps.mcpManager.getToolEntries(), approvalController));
84
86
  const promptCacheKey = session.manager.getOrCreatePromptCacheKey();
85
87
  const { provider, providerId, model } = await this.resolveProvider(session, promptCacheKey);
86
88
  const skills = this.opts.deps.skillRegistry.summaries();
@@ -97,6 +99,8 @@ export class RunDriver {
97
99
  const routingSnapshotAccessor = createRoutingSnapshotAccessor(this.opts.deps.providerRegistry, () => this.opts.deps.userConfig.getAgentCategories(), () => agentRouting);
98
100
  const systemPrompt = buildSystemPrompt({
99
101
  agentName: "Bubble",
102
+ skills,
103
+ skillCatalogChars: this.opts.deps.userConfig.getSkillCatalogChars(),
100
104
  configuredProvider: providerId || "none",
101
105
  configuredModel: model ? displayModel(model) : "none",
102
106
  configuredModelId: model || "none",
@@ -111,6 +115,29 @@ export class RunDriver {
111
115
  });
112
116
  const budgetLedger = new BudgetLedger();
113
117
  let sessionTitleUpdater;
118
+ let contextRevision = session.manager.getRevision();
119
+ // A fence rejection means the durable log diverged from this run's resident
120
+ // snapshot (a foreign append or clear landed mid-flight). Refuse the write,
121
+ // then roll the resident transcript back to the file's truth so the rejected
122
+ // message does not linger and later writes are not wedged behind the stale
123
+ // revision.
124
+ const persistFenced = (write) => {
125
+ try {
126
+ write();
127
+ }
128
+ catch (error) {
129
+ // Any refused write (fence rejection, busy lock, I/O) leaves the agent's
130
+ // already-pushed message unpersisted. getMessages() refreshes, so read
131
+ // the history first and adopt the revision of that same snapshot.
132
+ try {
133
+ const reloaded = session.manager.getMessages();
134
+ contextRevision = session.manager.getRevision();
135
+ agent.messages = [...splitLeadingContext(agent.messages).leading, ...reloaded];
136
+ }
137
+ catch { /* surface the original write failure */ }
138
+ throw error;
139
+ }
140
+ };
114
141
  const agent = new Agent({
115
142
  provider,
116
143
  providerId,
@@ -124,31 +151,46 @@ export class RunDriver {
124
151
  onMessageAppend: (message) => {
125
152
  if (message.role === "system" || message.role === "meta")
126
153
  return;
127
- session.manager.appendMessage(message);
154
+ persistFenced(() => {
155
+ session.manager.appendMessage(message, contextRevision);
156
+ contextRevision = session.manager.getRevision();
157
+ });
128
158
  sessionTitleUpdater?.handlePersistedMessage(message);
129
159
  if (message.role === "assistant") {
130
160
  recordMemoryCitations(session.cwd, message.content);
131
161
  }
132
162
  },
133
163
  onProviderError: (error) => {
134
- session.manager.appendProviderError(error);
164
+ persistFenced(() => session.manager.appendProviderError(error, contextRevision));
135
165
  },
136
- onCompactionApplied: (summary) => {
137
- session.manager.applyLLMCompaction(summary);
166
+ getContextRevision: () => contextRevision,
167
+ onContextCheckpoint: (checkpoint) => {
168
+ persistFenced(() => {
169
+ session.manager.commitContextCheckpoint(checkpoint, contextRevision);
170
+ contextRevision = session.manager.getRevision();
171
+ });
138
172
  },
139
173
  onToolResult: (toolName, result) => {
140
174
  if (toolName !== "skill" || result.isError)
141
175
  return;
142
176
  const match = result.content.match(/^Skill:\s+([^\n]+)$/m);
143
- if (match?.[1])
144
- session.manager.appendMarker("skill_activated", match[1].trim());
177
+ if (match?.[1]) {
178
+ persistFenced(() => {
179
+ session.manager.appendMarker("skill_activated", match[1].trim(), contextRevision);
180
+ contextRevision = session.manager.getRevision();
181
+ });
182
+ }
145
183
  },
146
184
  onModeUpdate: (mode) => {
147
- session.manager.appendMarker("mode_switch", mode);
185
+ persistFenced(() => {
186
+ session.manager.appendMarker("mode_switch", mode, contextRevision);
187
+ contextRevision = session.manager.getRevision();
188
+ });
148
189
  this.opts.binder.setMode(req.scopeKey, mode);
149
190
  },
150
191
  budgetLedger,
151
192
  skills,
193
+ skillCatalogChars: this.opts.deps.userConfig.getSkillCatalogChars(),
152
194
  memoryPrompt,
153
195
  fileStateTracker,
154
196
  agentCategories: this.opts.deps.userConfig.getAgentCategories(),
@@ -186,6 +228,7 @@ export class RunDriver {
186
228
  // Restore prior history into the running Agent instance.
187
229
  if (!session.fresh) {
188
230
  const history = session.manager.getMessages();
231
+ contextRevision = session.manager.getRevision();
189
232
  if (history.length > 0) {
190
233
  agent.messages = [{ role: "system", content: systemPrompt }, ...history];
191
234
  if (agent.mode === "plan")
@@ -317,8 +360,12 @@ export class RunDriver {
317
360
  const activeProviderId = effectiveProviderId || fallbackProviderId;
318
361
  if (effectiveModelId)
319
362
  assertProviderModelAllowed(activeProviderId, effectiveModelId);
320
- if (registry.supportsOAuth(activeProviderId) && registry.getAuthStorage().has(activeProviderId)) {
363
+ // getConfigured() resolves the legacy openai-codex auth alias; the raw
364
+ // auth key would skip token refresh and discovery for such logins.
365
+ if (registry.getConfigured().find((item) => item.id === activeProviderId)?.authType === "oauth") {
321
366
  await registry.prepareProvider(activeProviderId);
367
+ // Same bounded wait as main.ts: the routing prompt is composed right after.
368
+ await registry.waitForModelDiscovery(activeProviderId, 3_000);
322
369
  }
323
370
  const target = registry.getConfigured().find((p) => p.id === activeProviderId) || defaultProvider;
324
371
  if (!target?.apiKey) {
@@ -32,11 +32,19 @@ export interface LspService {
32
32
  incomingCalls(input: LspLocationInput): Promise<unknown[]>;
33
33
  outgoingCalls(input: LspLocationInput): Promise<unknown[]>;
34
34
  }
35
+ export interface LspTimeouts {
36
+ initializeMs?: number;
37
+ requestMs?: number;
38
+ shutdownMs?: number;
39
+ }
35
40
  export declare class ProjectLspService implements LspService {
36
41
  private readonly cwd;
42
+ private readonly timeouts;
37
43
  private readonly emitter;
38
44
  private readonly clients;
39
45
  private readonly spawning;
46
+ private readonly initializing;
47
+ private generation;
40
48
  private readonly starting;
41
49
  private readonly broken;
42
50
  private readonly unavailable;
@@ -44,7 +52,7 @@ export declare class ProjectLspService implements LspService {
44
52
  private readonly waiters;
45
53
  private disposed;
46
54
  private config;
47
- constructor(cwd: string, config?: LspConfig);
55
+ constructor(cwd: string, config?: LspConfig, timeouts?: LspTimeouts);
48
56
  updateConfig(config?: LspConfig): void;
49
57
  restart(): Promise<void>;
50
58
  onStatusChange(listener: () => void): () => void;
@@ -69,6 +77,10 @@ export declare class ProjectLspService implements LspService {
69
77
  private getClients;
70
78
  private getClient;
71
79
  private spawnClient;
80
+ private request;
81
+ private notify;
82
+ private withDeadline;
83
+ private stopClient;
72
84
  private openOrChange;
73
85
  private waitForDiagnostics;
74
86
  private resolveDiagnosticWaiters;