linkshell-cli 0.5.30 → 0.5.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +10 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +212 -6
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +16 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +161 -30
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +44 -19
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +321 -0
- package/dist/shared-protocol/src/index.js +22 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/agent-workspace.ts +216 -5
- package/src/runtime/acp/claude-sdk-client.ts +161 -29
- package/src/runtime/acp/claude-stream-json-client.ts +42 -19
- package/web/assets/index-BVK7mz6S.css +33 -0
- package/web/assets/{index-DABStn7S.js → index-DMiyoLqp.js} +44 -44
- package/web/index.html +2 -2
- package/web/assets/index-CRyJIhW8.css +0 -33
|
@@ -37,12 +37,20 @@ export declare class AgentWorkspaceProxy {
|
|
|
37
37
|
handleEnvelope(envelope: Envelope): Promise<void>;
|
|
38
38
|
stop(): void;
|
|
39
39
|
private fsWatchers;
|
|
40
|
+
private watchedRoots;
|
|
40
41
|
private fsSyncTimer;
|
|
41
42
|
private fsSyncRunning;
|
|
43
|
+
private lastPushedConversationSignature;
|
|
42
44
|
private static readonly FS_SYNC_THROTTLE_MS;
|
|
43
45
|
private startSessionWatchers;
|
|
44
46
|
private scheduleSessionSync;
|
|
45
47
|
private syncAndPushSessions;
|
|
48
|
+
/** A compact fingerprint of the fields the session tree actually renders, so
|
|
49
|
+
* syncAndPushSessions can skip a broadcast when nothing visible changed.
|
|
50
|
+
* Deliberately excludes usage token counts / lastActivityAt timestamps that
|
|
51
|
+
* tick on every append without altering the tree — status, title, model, and
|
|
52
|
+
* membership are what matter. */
|
|
53
|
+
private conversationListSignature;
|
|
46
54
|
private clientForProvider;
|
|
47
55
|
private shouldRecycleProviderClient;
|
|
48
56
|
private recycleProviderClient;
|
|
@@ -62,6 +70,8 @@ export declare class AgentWorkspaceProxy {
|
|
|
62
70
|
private executeCommand;
|
|
63
71
|
private executeNativeCommand;
|
|
64
72
|
private handleRequest;
|
|
73
|
+
private mergeMcpServers;
|
|
74
|
+
private handleMcpStartupStatus;
|
|
65
75
|
private handleNotification;
|
|
66
76
|
private handleAgentMessageDelta;
|
|
67
77
|
private handlePlanUpdated;
|
|
@@ -7,6 +7,63 @@ import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
|
7
7
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
8
8
|
import { aggregateUsage } from "../usage-report.js";
|
|
9
9
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
10
|
+
// Normalize the many status spellings Claude/Codex emit into our enum. Unknown
|
|
11
|
+
// values fall back to "pending" (known-but-not-yet-up) rather than dropping the
|
|
12
|
+
// server. Kept liberal because the real event vocabulary isn't fully pinned.
|
|
13
|
+
function normalizeMcpStatus(raw) {
|
|
14
|
+
const s = typeof raw === "string" ? raw.trim().toLowerCase().replace(/[\s-]+/g, "_") : "";
|
|
15
|
+
if (s === "connected" || s === "ok" || s === "ready" || s === "running" || s === "success")
|
|
16
|
+
return "connected";
|
|
17
|
+
if (s === "failed" || s === "error" || s === "errored" || s === "disconnected")
|
|
18
|
+
return "failed";
|
|
19
|
+
if (s === "connecting" || s === "starting" || s === "pending_connection")
|
|
20
|
+
return "connecting";
|
|
21
|
+
if (s === "needs_auth" || s === "unauthorized" || s === "needs_authentication" || s === "auth_required" || s === "pending_auth")
|
|
22
|
+
return "needs_auth";
|
|
23
|
+
if (s === "disabled" || s === "off")
|
|
24
|
+
return "disabled";
|
|
25
|
+
return "pending";
|
|
26
|
+
}
|
|
27
|
+
// Parse an MCP server list from either an array of {name,status,...} objects or
|
|
28
|
+
// a {name: status} record — Claude's init `mcp_servers` shape isn't fixed.
|
|
29
|
+
function parseMcpServerList(value) {
|
|
30
|
+
const out = [];
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
const rec = asRecord(entry);
|
|
34
|
+
if (rec) {
|
|
35
|
+
const name = firstString(rec, ["name", "server", "serverName", "id"]);
|
|
36
|
+
if (!name)
|
|
37
|
+
continue;
|
|
38
|
+
out.push({
|
|
39
|
+
name,
|
|
40
|
+
status: normalizeMcpStatus(rec.status ?? rec.state),
|
|
41
|
+
error: firstString(rec, ["error", "message", "reason"]),
|
|
42
|
+
toolCount: firstNumber(rec, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(rec.tools) ? rec.tools.length : undefined),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (typeof entry === "string" && entry) {
|
|
46
|
+
out.push({ name: entry, status: "pending" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
const rec = asRecord(value);
|
|
52
|
+
if (rec) {
|
|
53
|
+
for (const [name, raw] of Object.entries(rec)) {
|
|
54
|
+
if (!name)
|
|
55
|
+
continue;
|
|
56
|
+
const inner = asRecord(raw);
|
|
57
|
+
out.push({
|
|
58
|
+
name,
|
|
59
|
+
status: normalizeMcpStatus(inner ? (inner.status ?? inner.state) : raw),
|
|
60
|
+
error: inner ? firstString(inner, ["error", "message", "reason"]) : undefined,
|
|
61
|
+
toolCount: inner ? (firstNumber(inner, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(inner.tools) ? inner.tools.length : undefined)) : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
10
67
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
11
68
|
const MAX_TIMELINE_ITEMS = 200;
|
|
12
69
|
function id(prefix) {
|
|
@@ -682,6 +739,7 @@ const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
|
682
739
|
"fork",
|
|
683
740
|
"hooks",
|
|
684
741
|
"ide",
|
|
742
|
+
"help",
|
|
685
743
|
"init",
|
|
686
744
|
"install-github-app",
|
|
687
745
|
"install-slack-app",
|
|
@@ -711,6 +769,7 @@ const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
|
711
769
|
"setup-bedrock",
|
|
712
770
|
"setup-vertex",
|
|
713
771
|
"statusline",
|
|
772
|
+
"status",
|
|
714
773
|
"stickers",
|
|
715
774
|
"tasks",
|
|
716
775
|
"teleport",
|
|
@@ -1093,13 +1152,16 @@ function parseModelListCapabilities(provider, value) {
|
|
|
1093
1152
|
? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
|
|
1094
1153
|
: undefined;
|
|
1095
1154
|
const commands = raw && Array.isArray(raw.commands) ? runtimeCommands(provider, raw.commands) : undefined;
|
|
1096
|
-
|
|
1155
|
+
const mcpList = raw ? parseMcpServerList(raw.mcpServers ?? raw.mcp_servers) : [];
|
|
1156
|
+
const mcpServers = mcpList.length > 0 ? new Map(mcpList.map((s) => [s.name, s])) : undefined;
|
|
1157
|
+
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length && !commands?.length && !mcpServers)
|
|
1097
1158
|
return undefined;
|
|
1098
1159
|
return {
|
|
1099
1160
|
...(models.length > 0 ? { models } : {}),
|
|
1100
1161
|
...(defaultModel ? { defaultModel } : {}),
|
|
1101
1162
|
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
1102
1163
|
...(commands?.length ? { commands } : {}),
|
|
1164
|
+
...(mcpServers ? { mcpServers } : {}),
|
|
1103
1165
|
};
|
|
1104
1166
|
}
|
|
1105
1167
|
function parseTimestamp(value) {
|
|
@@ -1759,6 +1821,7 @@ export class AgentWorkspaceProxy {
|
|
|
1759
1821
|
catch { /* already closed */ }
|
|
1760
1822
|
}
|
|
1761
1823
|
this.fsWatchers = [];
|
|
1824
|
+
this.watchedRoots.clear();
|
|
1762
1825
|
if (this.fsSyncTimer) {
|
|
1763
1826
|
clearTimeout(this.fsSyncTimer);
|
|
1764
1827
|
this.fsSyncTimer = undefined;
|
|
@@ -1777,17 +1840,24 @@ export class AgentWorkspaceProxy {
|
|
|
1777
1840
|
// zero cost while idle, throttled to at most one re-scan per window during a
|
|
1778
1841
|
// burst of writes (an active turn appends constantly).
|
|
1779
1842
|
fsWatchers = [];
|
|
1843
|
+
watchedRoots = new Set();
|
|
1780
1844
|
fsSyncTimer;
|
|
1781
1845
|
fsSyncRunning = false;
|
|
1846
|
+
lastPushedConversationSignature;
|
|
1782
1847
|
static FS_SYNC_THROTTLE_MS = 1500;
|
|
1783
1848
|
startSessionWatchers() {
|
|
1784
|
-
if (this.fsWatchers.length > 0)
|
|
1785
|
-
return; // already watching
|
|
1786
1849
|
const roots = [
|
|
1787
1850
|
join(homedir(), ".claude", "projects"),
|
|
1788
1851
|
join(homedir(), ".codex", "sessions"),
|
|
1789
1852
|
];
|
|
1790
1853
|
for (const root of roots) {
|
|
1854
|
+
// Retry per-root each time we're called, so a store directory created
|
|
1855
|
+
// AFTER startup (e.g. the user runs `codex` for the first time this
|
|
1856
|
+
// session) gets a watcher attached lazily — instead of being gated out
|
|
1857
|
+
// forever by a single "already watching" flag. Roots that don't exist yet
|
|
1858
|
+
// stay un-added so a later call retries them.
|
|
1859
|
+
if (this.watchedRoots.has(root))
|
|
1860
|
+
continue;
|
|
1791
1861
|
if (!existsSync(root))
|
|
1792
1862
|
continue;
|
|
1793
1863
|
try {
|
|
@@ -1796,6 +1866,7 @@ export class AgentWorkspaceProxy {
|
|
|
1796
1866
|
const watcher = watch(root, { recursive: true, persistent: false }, () => this.scheduleSessionSync());
|
|
1797
1867
|
watcher.on("error", () => { });
|
|
1798
1868
|
this.fsWatchers.push(watcher);
|
|
1869
|
+
this.watchedRoots.add(root);
|
|
1799
1870
|
}
|
|
1800
1871
|
catch {
|
|
1801
1872
|
// recursive watch unavailable on this platform — skip silently.
|
|
@@ -1816,13 +1887,27 @@ export class AgentWorkspaceProxy {
|
|
|
1816
1887
|
this.fsSyncRunning = true;
|
|
1817
1888
|
try {
|
|
1818
1889
|
await this.syncProviderSessions();
|
|
1890
|
+
// A store directory may have appeared since the last call — attach a
|
|
1891
|
+
// watcher to it now (no-op for roots already watched).
|
|
1892
|
+
this.startSessionWatchers();
|
|
1893
|
+
const conversations = [...this.conversations.values()].filter((c) => !c.archived);
|
|
1894
|
+
// Only broadcast when the list actually changed since the last push. An
|
|
1895
|
+
// fs-watch fires on every transcript append (once per streamed token for
|
|
1896
|
+
// an active session), but most of those don't change any field the tree
|
|
1897
|
+
// renders — re-sending the whole list each time is wasted relay traffic
|
|
1898
|
+
// and client re-renders. A cheap signature over the render-relevant fields
|
|
1899
|
+
// gates the push.
|
|
1900
|
+
const signature = this.conversationListSignature(conversations);
|
|
1901
|
+
if (signature === this.lastPushedConversationSignature)
|
|
1902
|
+
return;
|
|
1903
|
+
this.lastPushedConversationSignature = signature;
|
|
1819
1904
|
// Push the conversation list (statuses) — lighter than a full snapshot and
|
|
1820
1905
|
// exactly what refreshes the tree. The web handles an unsolicited
|
|
1821
1906
|
// conversation.list.result the same as a requested one.
|
|
1822
1907
|
this.input.send(createEnvelope({
|
|
1823
1908
|
type: "agent.v2.conversation.list.result",
|
|
1824
1909
|
sessionId: this.input.sessionId,
|
|
1825
|
-
payload: { conversations
|
|
1910
|
+
payload: { conversations },
|
|
1826
1911
|
}));
|
|
1827
1912
|
}
|
|
1828
1913
|
catch (error) {
|
|
@@ -1834,6 +1919,17 @@ export class AgentWorkspaceProxy {
|
|
|
1834
1919
|
this.fsSyncRunning = false;
|
|
1835
1920
|
}
|
|
1836
1921
|
}
|
|
1922
|
+
/** A compact fingerprint of the fields the session tree actually renders, so
|
|
1923
|
+
* syncAndPushSessions can skip a broadcast when nothing visible changed.
|
|
1924
|
+
* Deliberately excludes usage token counts / lastActivityAt timestamps that
|
|
1925
|
+
* tick on every append without altering the tree — status, title, model, and
|
|
1926
|
+
* membership are what matter. */
|
|
1927
|
+
conversationListSignature(conversations) {
|
|
1928
|
+
return conversations
|
|
1929
|
+
.map((c) => `${c.id}${c.status}${c.title ?? ""}${c.model ?? ""}${c.provider ?? ""}`)
|
|
1930
|
+
.sort()
|
|
1931
|
+
.join("");
|
|
1932
|
+
}
|
|
1837
1933
|
clientForProvider(provider) {
|
|
1838
1934
|
return this.clients.get(provider);
|
|
1839
1935
|
}
|
|
@@ -2008,12 +2104,24 @@ export class AgentWorkspaceProxy {
|
|
|
2008
2104
|
commands: runtimeCapabilities?.commands,
|
|
2009
2105
|
modes: runtimeCapabilities?.modes,
|
|
2010
2106
|
currentMode: runtimeCapabilities?.currentMode,
|
|
2107
|
+
// Preserve any MCP status already accumulated from startupStatus events.
|
|
2108
|
+
mcpServers: runtimeCapabilities?.mcpServers ?? this.providerCapabilities.get(provider)?.mcpServers,
|
|
2011
2109
|
};
|
|
2012
2110
|
this.providerCapabilities.set(provider, merged);
|
|
2013
2111
|
return;
|
|
2014
2112
|
}
|
|
2015
|
-
if (runtimeCapabilities)
|
|
2113
|
+
if (runtimeCapabilities) {
|
|
2114
|
+
// Don't clobber MCP status that arrived via startupStatus before discovery;
|
|
2115
|
+
// fold both sources into one map.
|
|
2116
|
+
const priorMcp = this.providerCapabilities.get(provider)?.mcpServers;
|
|
2117
|
+
if (priorMcp && priorMcp.size > 0) {
|
|
2118
|
+
const map = new Map(priorMcp);
|
|
2119
|
+
for (const [name, desc] of runtimeCapabilities.mcpServers ?? [])
|
|
2120
|
+
map.set(name, desc);
|
|
2121
|
+
runtimeCapabilities = { ...runtimeCapabilities, mcpServers: map };
|
|
2122
|
+
}
|
|
2016
2123
|
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
2124
|
+
}
|
|
2017
2125
|
}
|
|
2018
2126
|
async syncProviderSessions() {
|
|
2019
2127
|
await this.initialize();
|
|
@@ -2288,6 +2396,9 @@ export class AgentWorkspaceProxy {
|
|
|
2288
2396
|
reasoningEffort: supportsReasoningEffort,
|
|
2289
2397
|
streamJsonFallback: isClaudeFallback,
|
|
2290
2398
|
},
|
|
2399
|
+
mcpServers: runtimeCapabilities?.mcpServers && runtimeCapabilities.mcpServers.size > 0
|
|
2400
|
+
? [...runtimeCapabilities.mcpServers.values()]
|
|
2401
|
+
: undefined,
|
|
2291
2402
|
};
|
|
2292
2403
|
});
|
|
2293
2404
|
const anyEnabled = providers.some((p) => p.enabled);
|
|
@@ -2349,6 +2460,44 @@ export class AgentWorkspaceProxy {
|
|
|
2349
2460
|
}
|
|
2350
2461
|
return this.openFailure(payload, message, cwd);
|
|
2351
2462
|
}
|
|
2463
|
+
// Fork: seed a NEW conversation from an existing one's transcript, truncated
|
|
2464
|
+
// at forkFromTurnId. Claude only — the SDK client exposes forkSession(); the
|
|
2465
|
+
// stream-json fallback and Codex don't, so we notify and fall through to a
|
|
2466
|
+
// plain new conversation rather than pretend. On success we set agentSessionId
|
|
2467
|
+
// to the freshly-written transcript id and let the normal load path hydrate it.
|
|
2468
|
+
if (payload.forkFromConversationId) {
|
|
2469
|
+
const source = this.conversations.get(payload.forkFromConversationId);
|
|
2470
|
+
const sourceSessionId = source?.agentSessionId;
|
|
2471
|
+
const forkable = client.forkSession;
|
|
2472
|
+
if (provider === "claude" && typeof forkable === "function" && sourceSessionId) {
|
|
2473
|
+
if (source?.cwd && !payload.cwd)
|
|
2474
|
+
cwd = source.cwd;
|
|
2475
|
+
const forked = forkable.call(client, { sourceSessionId, uptoTurnId: payload.forkFromTurnId });
|
|
2476
|
+
if (forked?.sessionId) {
|
|
2477
|
+
agentSessionId = forked.sessionId;
|
|
2478
|
+
// Inherit the source's config so the fork opens like its parent.
|
|
2479
|
+
payload = {
|
|
2480
|
+
...payload,
|
|
2481
|
+
model: payload.model ?? source?.model,
|
|
2482
|
+
reasoningEffort: payload.reasoningEffort ?? source?.reasoningEffort,
|
|
2483
|
+
permissionMode: payload.permissionMode ?? source?.permissionMode,
|
|
2484
|
+
collaborationMode: payload.collaborationMode ?? source?.collaborationMode,
|
|
2485
|
+
title: payload.title ?? (source?.title ? `${source.title} (分叉)` : undefined),
|
|
2486
|
+
};
|
|
2487
|
+
existingConversation = undefined; // force the new-conversation path
|
|
2488
|
+
}
|
|
2489
|
+
else {
|
|
2490
|
+
this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
else {
|
|
2494
|
+
this.emitNotice({
|
|
2495
|
+
kind: "native_unsupported",
|
|
2496
|
+
title: "暂不支持分叉",
|
|
2497
|
+
detail: provider === "claude" ? "该会话无法分叉。" : `${providerLabel(provider)} 暂不支持从历史分叉,已新建空会话。`,
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2352
2501
|
if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
|
|
2353
2502
|
const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
|
|
2354
2503
|
if (payload.conversationId &&
|
|
@@ -2897,6 +3046,41 @@ export class AgentWorkspaceProxy {
|
|
|
2897
3046
|
}
|
|
2898
3047
|
return {};
|
|
2899
3048
|
}
|
|
3049
|
+
// Merge MCP server descriptors into a provider's runtime capabilities,
|
|
3050
|
+
// last-writer-wins by server name. Creates the map lazily.
|
|
3051
|
+
mergeMcpServers(provider, servers) {
|
|
3052
|
+
if (servers.length === 0)
|
|
3053
|
+
return;
|
|
3054
|
+
const existing = this.providerCapabilities.get(provider) ?? {};
|
|
3055
|
+
const map = existing.mcpServers ?? new Map();
|
|
3056
|
+
for (const server of servers)
|
|
3057
|
+
map.set(server.name, server);
|
|
3058
|
+
this.providerCapabilities.set(provider, { ...existing, mcpServers: map });
|
|
3059
|
+
}
|
|
3060
|
+
// A live `mcpServer/startupStatus/<name>` notification. The server name is
|
|
3061
|
+
// presumed to be the method suffix (falls back to a name field in params);
|
|
3062
|
+
// the status/error/toolCount shape is parsed defensively since the real event
|
|
3063
|
+
// vocabulary isn't fully pinned. Re-emits capabilities so the console updates.
|
|
3064
|
+
handleMcpStartupStatus(method, params) {
|
|
3065
|
+
// MCP is Claude-only today; target the enabled provider that supports it.
|
|
3066
|
+
const provider = this.input.availableProviders.find((p) => p === "claude" && this.clients.has(p))
|
|
3067
|
+
?? this.input.availableProviders.find((p) => this.clients.has(p));
|
|
3068
|
+
if (!provider)
|
|
3069
|
+
return;
|
|
3070
|
+
const raw = asRecord(params) ?? {};
|
|
3071
|
+
const suffix = method.slice(method.lastIndexOf("/") + 1);
|
|
3072
|
+
const name = (suffix && suffix !== "startupStatus" ? suffix : undefined)
|
|
3073
|
+
?? firstString(raw, ["name", "server", "serverName"]);
|
|
3074
|
+
if (!name)
|
|
3075
|
+
return;
|
|
3076
|
+
this.mergeMcpServers(provider, [{
|
|
3077
|
+
name,
|
|
3078
|
+
status: normalizeMcpStatus(raw.status ?? raw.state),
|
|
3079
|
+
error: firstString(raw, ["error", "message", "reason"]),
|
|
3080
|
+
toolCount: firstNumber(raw, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(raw.tools) ? raw.tools.length : undefined),
|
|
3081
|
+
}]);
|
|
3082
|
+
this.sendCapabilities();
|
|
3083
|
+
}
|
|
2900
3084
|
handleNotification(method, params) {
|
|
2901
3085
|
if (this.input.verbose) {
|
|
2902
3086
|
process.stderr.write(`[agent:v2] ${method} ${stringify(params).slice(0, 500)}\n`);
|
|
@@ -2924,10 +3108,24 @@ export class AgentWorkspaceProxy {
|
|
|
2924
3108
|
this.emitConversation(conversation);
|
|
2925
3109
|
}
|
|
2926
3110
|
}
|
|
3111
|
+
// Seed the MCP server list from the init payload (stream-json carries
|
|
3112
|
+
// `mcp_servers`; the SDK path may carry `mcpServers`). Late per-server
|
|
3113
|
+
// startupStatus events refine this afterward.
|
|
3114
|
+
if (provider) {
|
|
3115
|
+
const initMcp = parseMcpServerList((asRecord(params) ?? {}).mcpServers ??
|
|
3116
|
+
(asRecord(params) ?? {}).mcp_servers);
|
|
3117
|
+
if (initMcp.length > 0) {
|
|
3118
|
+
this.mergeMcpServers(provider, initMcp);
|
|
3119
|
+
this.sendCapabilities();
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (method.startsWith("mcpServer/startupStatus/")) {
|
|
3125
|
+
this.handleMcpStartupStatus(method, params);
|
|
2927
3126
|
return;
|
|
2928
3127
|
}
|
|
2929
3128
|
if (method.startsWith("account/") ||
|
|
2930
|
-
method.startsWith("mcpServer/startupStatus/") ||
|
|
2931
3129
|
method === "serverRequest/resolved" ||
|
|
2932
3130
|
method === "mcpServer/oauthLogin/completed") {
|
|
2933
3131
|
return;
|
|
@@ -3202,6 +3400,14 @@ export class AgentWorkspaceProxy {
|
|
|
3202
3400
|
if (bufferedOutput && !toolCall.output)
|
|
3203
3401
|
toolCall.output = bufferedOutput;
|
|
3204
3402
|
this.upsertTool(conversationId, toolCall);
|
|
3403
|
+
// The buffer only exists to stitch streaming deltas onto the final item.
|
|
3404
|
+
// Once the tool call reaches a terminal state its output is folded into the
|
|
3405
|
+
// toolCall above and will never be appended to again, so drop the buffer —
|
|
3406
|
+
// otherwise the map grows unbounded across a long-lived workspace (one entry
|
|
3407
|
+
// per tool call / exec ever seen).
|
|
3408
|
+
if (toolCall.status === "completed" || toolCall.status === "failed") {
|
|
3409
|
+
this.toolOutputBuffers.delete(toolCall.id);
|
|
3410
|
+
}
|
|
3205
3411
|
}
|
|
3206
3412
|
handleToolDelta(method, params) {
|
|
3207
3413
|
const raw = asRecord(params);
|