@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.
- package/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- 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,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
|
+
}
|
package/dist/context/usage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getModelContextWindow } from "../model-catalog.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
11
|
-
const
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/lsp/service.d.ts
CHANGED
|
@@ -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;
|