talon-agent 3.15.0 → 3.15.2
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/package.json +1 -1
- package/src/app.ts +24 -10
- package/src/backend/kilo/factory.ts +26 -0
- package/src/backend/kilo/handler/message.ts +12 -6
- package/src/backend/kilo/handler/turn.ts +43 -40
- package/src/backend/kilo/index.ts +7 -1
- package/src/backend/kilo/server.ts +28 -32
- package/src/backend/kilo/sessions.ts +17 -0
- package/src/backend/opencode/factory.ts +26 -0
- package/src/backend/opencode/handler/message.ts +13 -5
- package/src/backend/opencode/handler/turn.ts +41 -32
- package/src/backend/opencode/index.ts +7 -1
- package/src/backend/opencode/server.ts +27 -16
- package/src/backend/opencode/sessions.ts +17 -0
- package/src/backend/remote-server/client.ts +1 -1
- package/src/backend/remote-server/events.ts +19 -6
- package/src/backend/remote-server/index.ts +16 -1
- package/src/backend/remote-server/lifecycle.ts +5 -1
- package/src/backend/remote-server/mcp.ts +196 -110
- package/src/backend/remote-server/one-shot.ts +53 -6
- package/src/backend/remote-server/providers.ts +12 -0
- package/src/backend/remote-server/session-helpers.ts +69 -0
- package/src/backend/remote-server/sessions.ts +67 -7
- package/src/backend/remote-server/sse-stream.ts +17 -10
- package/src/backend/remote-server/state.ts +6 -0
- package/src/backend/remote-server/turn-timeout.ts +71 -0
- package/src/core/mcp-hub/index.ts +22 -0
- package/src/frontend/telegram/admin.ts +3 -1
- package/src/frontend/telegram/callbacks/model.ts +48 -0
- package/src/frontend/telegram/commands/info.ts +9 -3
- package/src/frontend/telegram/commands/settings.ts +8 -2
- package/src/frontend/telegram/handlers/queue.ts +10 -4
- package/src/frontend/telegram/helpers/menu.ts +5 -2
- package/src/util/respawn.ts +66 -38
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `runOpenCodeTurn` subscribes to SSE before issuing the prompt (so no early
|
|
5
5
|
* events are lost), fires `promptAsync`, awaits the SSE close, then reads the
|
|
6
6
|
* authoritative parts list. `subscribeToTurnEvents` translates relevant SSE
|
|
7
|
-
* events into shared stream-state mutations and
|
|
7
|
+
* events into shared stream-state mutations and observes natural turn idle.
|
|
8
8
|
*
|
|
9
9
|
* The streaming + event-processing logic is shared with the Kilo backend via
|
|
10
10
|
* `backend/remote-server/events.ts`.
|
|
@@ -14,6 +14,7 @@ import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
|
14
14
|
import { logWarn } from "../../../util/log.js";
|
|
15
15
|
import { errMsg } from "../server.js";
|
|
16
16
|
import {
|
|
17
|
+
approvePendingPermissions,
|
|
17
18
|
extractPartsSummary,
|
|
18
19
|
extractAssistantUsage,
|
|
19
20
|
rejectPendingQuestions,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
finalizePartsIntoState,
|
|
26
27
|
} from "../../remote-server/events.js";
|
|
27
28
|
import { subscribeSseStream } from "../../remote-server/sse-stream.js";
|
|
29
|
+
import { awaitRemoteTurn } from "../../remote-server/turn-timeout.js";
|
|
28
30
|
import { findLastAssistantMessage as findLastAssistantMessageShared } from "../../remote-server/messages.js";
|
|
29
31
|
|
|
30
32
|
export interface RunOpenCodeTurnInputs {
|
|
@@ -37,7 +39,9 @@ export interface RunOpenCodeTurnInputs {
|
|
|
37
39
|
state: ReturnType<typeof createStreamState>;
|
|
38
40
|
chatId: string;
|
|
39
41
|
seenQuestionIds: Set<string>;
|
|
42
|
+
seenPermissionIds: Set<string>;
|
|
40
43
|
seenToolCallIds: Set<string>;
|
|
44
|
+
toolOverrides?: Record<string, boolean>;
|
|
41
45
|
onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
|
|
42
46
|
onTextBlock?: (text: string) => Promise<void>;
|
|
43
47
|
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
|
|
@@ -62,7 +66,9 @@ export async function runOpenCodeTurn(
|
|
|
62
66
|
state,
|
|
63
67
|
chatId,
|
|
64
68
|
seenQuestionIds,
|
|
69
|
+
seenPermissionIds,
|
|
65
70
|
seenToolCallIds,
|
|
71
|
+
toolOverrides,
|
|
66
72
|
onStreamDelta,
|
|
67
73
|
onTextBlock,
|
|
68
74
|
onToolUse,
|
|
@@ -81,26 +87,18 @@ export async function runOpenCodeTurn(
|
|
|
81
87
|
onStreamDelta,
|
|
82
88
|
onTextBlock,
|
|
83
89
|
onToolUse,
|
|
84
|
-
onTerminator: async () => {
|
|
85
|
-
// End_turn fired — abort the in-flight session so OpenCode doesn't
|
|
86
|
-
// burn another round-trip "wrapping up" after the model declared
|
|
87
|
-
// done. session.idle then fires for our session and the SSE
|
|
88
|
-
// iterator exits cleanly.
|
|
89
|
-
try {
|
|
90
|
-
await oc.session.abort({ sessionID: sessionId });
|
|
91
|
-
} catch (err) {
|
|
92
|
-
logWarn("agent", `[${chatId}] session.abort failed: ${errMsg(err)}`);
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
90
|
abortSignal: sseAbort.signal,
|
|
96
91
|
});
|
|
97
92
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
93
|
+
// Headless-interaction watchdog: resolve upstream questions and any
|
|
94
|
+
// permission requests that escaped the session ruleset.
|
|
100
95
|
const questionWatchdog = (async () => {
|
|
101
96
|
while (!sseAbort.signal.aborted) {
|
|
102
97
|
try {
|
|
103
|
-
await
|
|
98
|
+
await Promise.all([
|
|
99
|
+
rejectPendingQuestions(oc, sessionId, chatId, seenQuestionIds),
|
|
100
|
+
approvePendingPermissions(oc, sessionId, chatId, seenPermissionIds),
|
|
101
|
+
]);
|
|
104
102
|
} catch (err) {
|
|
105
103
|
logWarn(
|
|
106
104
|
"agent",
|
|
@@ -114,15 +112,21 @@ export async function runOpenCodeTurn(
|
|
|
114
112
|
try {
|
|
115
113
|
// Fire and forget — promptAsync returns immediately. The await below
|
|
116
114
|
// is on the SSE close event.
|
|
117
|
-
await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
await awaitRemoteTurn(
|
|
116
|
+
(async () => {
|
|
117
|
+
await oc.session.promptAsync({
|
|
118
|
+
sessionID: sessionId,
|
|
119
|
+
parts: [{ type: "text", text: prompt }],
|
|
120
|
+
model: { providerID, modelID },
|
|
121
|
+
system: systemPrompt,
|
|
122
|
+
...(toolOverrides ? { tools: toolOverrides } : {}),
|
|
123
|
+
});
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
// Await turn completion via SSE.
|
|
126
|
+
await sseDone;
|
|
127
|
+
})(),
|
|
128
|
+
{ client: oc, sessionId, chatId, label: "OpenCode" },
|
|
129
|
+
);
|
|
126
130
|
|
|
127
131
|
// Read authoritative final state from the messages endpoint.
|
|
128
132
|
const messagesResp = await oc.session.messages({ sessionID: sessionId });
|
|
@@ -153,17 +157,23 @@ export async function runOpenCodeTurn(
|
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
} catch (err) {
|
|
156
|
-
//
|
|
160
|
+
// Explicit user interrupts abort intentionally — swallow that close path.
|
|
157
161
|
if (state.turnTerminated && /abort/i.test(errMsg(err))) {
|
|
158
162
|
return;
|
|
159
163
|
}
|
|
160
164
|
throw err;
|
|
161
165
|
} finally {
|
|
162
166
|
sseAbort.abort();
|
|
163
|
-
|
|
167
|
+
// A dead SSE socket may ignore the local abort flag until another event
|
|
168
|
+
// arrives. Bound cleanup so a timed-out turn cannot wedge its caller in
|
|
169
|
+
// the finally block it was meant to escape.
|
|
170
|
+
await Promise.race([sseDone.catch(() => {}), sleep(1_000)]);
|
|
164
171
|
await questionWatchdog.catch(() => {});
|
|
165
172
|
try {
|
|
166
|
-
await
|
|
173
|
+
await Promise.all([
|
|
174
|
+
rejectPendingQuestions(oc, sessionId, chatId, seenQuestionIds),
|
|
175
|
+
approvePendingPermissions(oc, sessionId, chatId, seenPermissionIds),
|
|
176
|
+
]);
|
|
167
177
|
} catch {
|
|
168
178
|
/* noop */
|
|
169
179
|
}
|
|
@@ -191,7 +201,6 @@ interface SubscribeInputs {
|
|
|
191
201
|
onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
|
|
192
202
|
onTextBlock?: (text: string) => Promise<void>;
|
|
193
203
|
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
|
|
194
|
-
onTerminator: () => Promise<void>;
|
|
195
204
|
abortSignal: AbortSignal;
|
|
196
205
|
}
|
|
197
206
|
|
|
@@ -211,14 +220,13 @@ async function subscribeToTurnEvents(inputs: SubscribeInputs): Promise<void> {
|
|
|
211
220
|
onStreamDelta,
|
|
212
221
|
onTextBlock,
|
|
213
222
|
onToolUse,
|
|
214
|
-
onTerminator,
|
|
215
223
|
abortSignal,
|
|
216
224
|
} = inputs;
|
|
217
225
|
|
|
218
226
|
// `subscribeSseStream` handles the narrowing with a runtime guard + the
|
|
219
227
|
// subscribe-failed warning.
|
|
220
228
|
const stream = await subscribeSseStream(oc, chatId);
|
|
221
|
-
if (!stream)
|
|
229
|
+
if (!stream) throw new Error("OpenCode SSE connection unavailable");
|
|
222
230
|
|
|
223
231
|
try {
|
|
224
232
|
for await (const evt of stream) {
|
|
@@ -254,8 +262,7 @@ async function subscribeToTurnEvents(inputs: SubscribeInputs): Promise<void> {
|
|
|
254
262
|
data?: Record<string, unknown>;
|
|
255
263
|
}
|
|
256
264
|
| undefined;
|
|
257
|
-
// MessageAbortedError is
|
|
258
|
-
// tool fired; expected close path, not an upstream failure.
|
|
265
|
+
// MessageAbortedError is expected for an explicit user interrupt.
|
|
259
266
|
const isOurAbort =
|
|
260
267
|
state.turnTerminated &&
|
|
261
268
|
(errProp?.name === "MessageAbortedError" ||
|
|
@@ -288,7 +295,9 @@ async function subscribeToTurnEvents(inputs: SubscribeInputs): Promise<void> {
|
|
|
288
295
|
});
|
|
289
296
|
|
|
290
297
|
if (outcome.kind === "terminator_fired") {
|
|
291
|
-
|
|
298
|
+
// Aborting here can race with a prompt accepted immediately after
|
|
299
|
+
// this turn settles and cancel that next turn on the reused session.
|
|
300
|
+
// Delivery is already complete, so wait for natural idle instead.
|
|
292
301
|
continue;
|
|
293
302
|
}
|
|
294
303
|
|
|
@@ -23,7 +23,13 @@ export {
|
|
|
23
23
|
getOpenCodeSessionSnapshot,
|
|
24
24
|
} from "./sessions.js";
|
|
25
25
|
|
|
26
|
-
export {
|
|
26
|
+
export {
|
|
27
|
+
initOpenCodeAgent,
|
|
28
|
+
stopOpenCodeServer,
|
|
29
|
+
refreshPluginMcpServers,
|
|
30
|
+
updateSystemPrompt,
|
|
31
|
+
warmSession,
|
|
32
|
+
} from "./server.js";
|
|
27
33
|
|
|
28
34
|
export { handleMessage } from "./handler/index.js";
|
|
29
35
|
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
} from "@opencode-ai/sdk/v2";
|
|
24
24
|
import type { TalonConfig } from "../../util/config.js";
|
|
25
25
|
import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
|
|
26
|
-
import { logWarn } from "../../util/log.js";
|
|
27
26
|
import { buildDeliveryContract } from "../shared/delivery-contract.js";
|
|
28
27
|
import {
|
|
29
28
|
guessProviderID,
|
|
@@ -39,7 +38,9 @@ import {
|
|
|
39
38
|
ensurePluginMcpServers as ensurePluginMcpServersShared,
|
|
40
39
|
buildToolOverrides as buildToolOverridesShared,
|
|
41
40
|
disconnectChatMcpServer as disconnectChatMcpServerShared,
|
|
41
|
+
refreshPluginMcpServers as refreshPluginMcpServersShared,
|
|
42
42
|
ensureRemoteSession,
|
|
43
|
+
warmRemoteSession,
|
|
43
44
|
resolveProviderID as resolveProviderIDShared,
|
|
44
45
|
getRegisteredMcpServerNames as getRegisteredMcpServerNamesShared,
|
|
45
46
|
errMsg as sharedErrMsg,
|
|
@@ -91,20 +92,6 @@ export function initOpenCodeAgent(
|
|
|
91
92
|
state.config = cfg;
|
|
92
93
|
if (getGatewayPort) state.gatewayPortFn = getGatewayPort;
|
|
93
94
|
if (frontend) state.frontendName = frontend;
|
|
94
|
-
|
|
95
|
-
// Pre-warm plugin MCP servers in the background. Same rationale as
|
|
96
|
-
// the Kilo backend's init pre-warm.
|
|
97
|
-
prewarmPluginMcpServers().catch((err) => {
|
|
98
|
-
logWarn(
|
|
99
|
-
"agent",
|
|
100
|
-
`Plugin MCP pre-warm failed (non-fatal): ${sharedErrMsg(err)}`,
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function prewarmPluginMcpServers(): Promise<void> {
|
|
106
|
-
const client = await ensureServer();
|
|
107
|
-
await ensurePluginMcpServers(client, "prewarm");
|
|
108
95
|
}
|
|
109
96
|
|
|
110
97
|
/**
|
|
@@ -155,8 +142,9 @@ export function ensurePluginMcpServers(
|
|
|
155
142
|
export function buildToolOverrides(
|
|
156
143
|
oc: OpencodeClient,
|
|
157
144
|
chatServerName: string,
|
|
145
|
+
pluginServerNames: readonly string[] = [],
|
|
158
146
|
): Promise<Record<string, boolean> | undefined> {
|
|
159
|
-
return buildToolOverridesShared(oc, state, chatServerName);
|
|
147
|
+
return buildToolOverridesShared(oc, state, chatServerName, pluginServerNames);
|
|
160
148
|
}
|
|
161
149
|
|
|
162
150
|
export function disconnectChatMcpServer(
|
|
@@ -166,6 +154,15 @@ export function disconnectChatMcpServer(
|
|
|
166
154
|
return disconnectChatMcpServerShared(oc, state, serverName);
|
|
167
155
|
}
|
|
168
156
|
|
|
157
|
+
export async function refreshPluginMcpServers(chatId: string) {
|
|
158
|
+
const oc = await ensureServer();
|
|
159
|
+
return refreshPluginMcpServersShared(oc, state, chatId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function updateSystemPrompt(prompt: string): void {
|
|
163
|
+
if (state.config) state.config.systemPrompt = prompt;
|
|
164
|
+
}
|
|
165
|
+
|
|
169
166
|
// ── Session management ─────────────────────────────────────────────────────
|
|
170
167
|
|
|
171
168
|
export function ensureSession(
|
|
@@ -175,6 +172,20 @@ export function ensureSession(
|
|
|
175
172
|
return ensureRemoteSession(oc, state, chatId);
|
|
176
173
|
}
|
|
177
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Front-load a chat's cold start after `/reset`. Mirrors the Claude
|
|
177
|
+
* backend's `warmSession` so the `sessions` capability slot behaves the
|
|
178
|
+
* same across backends. Never throws — see `warmRemoteSession`.
|
|
179
|
+
*/
|
|
180
|
+
export function warmSession(chatId: string): Promise<void> {
|
|
181
|
+
return warmRemoteSession(state, chatId, {
|
|
182
|
+
ensureServer,
|
|
183
|
+
ensureSession,
|
|
184
|
+
ensureChatMcpServer,
|
|
185
|
+
ensurePluginMcpServers,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
// ── Provider resolution ────────────────────────────────────────────────────
|
|
179
190
|
|
|
180
191
|
export function resolveProviderID(
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getTurnSummary,
|
|
19
19
|
getSessionSnapshot,
|
|
20
20
|
rejectPendingQuestions as rejectPendingQuestionsShared,
|
|
21
|
+
approvePendingPermissions as approvePendingPermissionsShared,
|
|
21
22
|
type RemoteAssistantInfo,
|
|
22
23
|
type RemoteSessionSnapshot,
|
|
23
24
|
type RemoteSessionClient,
|
|
@@ -97,3 +98,19 @@ export function rejectPendingQuestions(
|
|
|
97
98
|
"OpenCode",
|
|
98
99
|
);
|
|
99
100
|
}
|
|
101
|
+
|
|
102
|
+
/** Auto-approve pending OpenCode permissions for this headless session. */
|
|
103
|
+
export function approvePendingPermissions(
|
|
104
|
+
oc: OpencodeClient,
|
|
105
|
+
sessionId: string,
|
|
106
|
+
chatId: string,
|
|
107
|
+
seenPermissionIds: Set<string>,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
return approvePendingPermissionsShared(
|
|
110
|
+
oc as unknown as RemoteSessionClient,
|
|
111
|
+
sessionId,
|
|
112
|
+
chatId,
|
|
113
|
+
seenPermissionIds,
|
|
114
|
+
"OpenCode",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -35,7 +35,7 @@ export type RemoteMcpServerConfig =
|
|
|
35
35
|
|
|
36
36
|
/** Permission-rule shape both SDKs share. */
|
|
37
37
|
export interface RemotePermissionRule {
|
|
38
|
-
permission: "tool" | "edit" | "bash";
|
|
38
|
+
permission: "tool" | "edit" | "bash" | "external_directory";
|
|
39
39
|
pattern: string;
|
|
40
40
|
action: "allow" | "deny" | "ask";
|
|
41
41
|
}
|
|
@@ -108,9 +108,21 @@ export async function processStreamEvent(
|
|
|
108
108
|
): Promise<ProcessEventOutcome> {
|
|
109
109
|
if (!event || typeof event !== "object") return { kind: "continue" };
|
|
110
110
|
const props = event.properties ?? {};
|
|
111
|
-
// Scope to our session only
|
|
111
|
+
// Scope to our session only. OpenCode/Kilo use three wire shapes:
|
|
112
|
+
// session events put sessionID on properties, part events put it inside
|
|
113
|
+
// properties.part, and message events put it inside properties.info.
|
|
114
|
+
// Missing the nested part shape lets a concurrent heartbeat/dream tool
|
|
115
|
+
// event terminate the active chat turn.
|
|
116
|
+
const part = props.part as Record<string, unknown> | undefined;
|
|
117
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
112
118
|
const evtSessionID =
|
|
113
|
-
typeof props.sessionID === "string"
|
|
119
|
+
typeof props.sessionID === "string"
|
|
120
|
+
? props.sessionID
|
|
121
|
+
: typeof part?.sessionID === "string"
|
|
122
|
+
? part.sessionID
|
|
123
|
+
: typeof info?.sessionID === "string"
|
|
124
|
+
? info.sessionID
|
|
125
|
+
: undefined;
|
|
114
126
|
if (evtSessionID && evtSessionID !== ctx.sessionId) {
|
|
115
127
|
return { kind: "stop", reason: "out_of_scope" };
|
|
116
128
|
}
|
|
@@ -246,11 +258,12 @@ async function processPartUpdate(
|
|
|
246
258
|
const stateObj = part.state as
|
|
247
259
|
{ status?: string; input?: Record<string, unknown> } | undefined;
|
|
248
260
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
261
|
+
// Record a tool only after the upstream reports successful completion.
|
|
262
|
+
// Treating `running` as delivery made failed MCP calls flip end_turn and
|
|
263
|
+
// suppress fallback text even though nothing reached the user.
|
|
251
264
|
if (
|
|
252
265
|
!stateObj ||
|
|
253
|
-
|
|
266
|
+
stateObj.status !== "completed" ||
|
|
254
267
|
!callID ||
|
|
255
268
|
ctx.seenToolCallIds.has(callID)
|
|
256
269
|
) {
|
|
@@ -436,7 +449,7 @@ export function finalizePartsIntoState(inputs: FinalizePartsInputs): {
|
|
|
436
449
|
const callID = typeof part.callID === "string" ? part.callID : "";
|
|
437
450
|
const toolName = typeof part.tool === "string" ? part.tool : "tool";
|
|
438
451
|
if (callID && seenToolCallIds.has(callID)) continue;
|
|
439
|
-
if (stateObj?.input) {
|
|
452
|
+
if (stateObj?.status === "completed" && stateObj.input) {
|
|
440
453
|
if (callID) seenToolCallIds.add(callID);
|
|
441
454
|
recordToolUse(state, toolName, stateObj.input);
|
|
442
455
|
toolsProcessed++;
|
|
@@ -51,15 +51,30 @@ export {
|
|
|
51
51
|
|
|
52
52
|
export {
|
|
53
53
|
TALON_MCP_SERVER_NAME,
|
|
54
|
+
TALON_PLUGIN_MCP_SERVER_NAME,
|
|
54
55
|
getChatMcpServerName,
|
|
56
|
+
getPluginMcpServerName,
|
|
57
|
+
safeMcpNamePart,
|
|
55
58
|
isTalonToolID,
|
|
56
59
|
ensureChatMcpServer,
|
|
57
60
|
ensurePluginMcpServers,
|
|
58
61
|
buildToolOverrides,
|
|
59
62
|
disconnectChatMcpServer,
|
|
63
|
+
refreshPluginMcpServers,
|
|
60
64
|
getRegisteredMcpServerNames,
|
|
61
65
|
} from "./mcp.js";
|
|
62
66
|
|
|
63
|
-
export {
|
|
67
|
+
export {
|
|
68
|
+
buildPermissionRuleset,
|
|
69
|
+
ensureRemoteSession,
|
|
70
|
+
warmRemoteSession,
|
|
71
|
+
type RemoteWarmDeps,
|
|
72
|
+
} from "./sessions.js";
|
|
64
73
|
|
|
65
74
|
export { type ProviderResolverHooks, resolveProviderID } from "./providers.js";
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
RemoteTurnTimeoutError,
|
|
78
|
+
remoteTurnTimeoutMs,
|
|
79
|
+
awaitRemoteTurn,
|
|
80
|
+
} from "./turn-timeout.js";
|
|
@@ -182,6 +182,8 @@ export function stopRemoteServer<TClient extends RemoteAgentClient>(
|
|
|
182
182
|
state.clientPromise = null;
|
|
183
183
|
state.modelProviderCache.clear();
|
|
184
184
|
state.registeredMcpServers.clear();
|
|
185
|
+
state.registeredMcpTools.clear();
|
|
186
|
+
state.pluginMcpServersByChat.clear();
|
|
185
187
|
try {
|
|
186
188
|
extraCleanup?.();
|
|
187
189
|
} catch (err) {
|
|
@@ -193,7 +195,9 @@ export function stopRemoteServer<TClient extends RemoteAgentClient>(
|
|
|
193
195
|
if (state.serverHandle) {
|
|
194
196
|
state.serverHandle.close();
|
|
195
197
|
state.serverHandle = null;
|
|
196
|
-
state.client = null;
|
|
197
198
|
log("agent", `${state.label} server stopped`);
|
|
198
199
|
}
|
|
200
|
+
// Always discard this process's client, including when the server was
|
|
201
|
+
// reused and is intentionally left running for its external owner.
|
|
202
|
+
state.client = null;
|
|
199
203
|
}
|