@cjhyy/code-shell-core 0.7.0 → 0.7.1
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/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- package/dist/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +10 -1
- package/dist/credentials/access.js +76 -2
- package/dist/credentials/index.d.ts +3 -2
- package/dist/credentials/index.js +2 -1
- package/dist/credentials/oauth.d.ts +6 -1
- package/dist/credentials/oauth.js +66 -1
- package/dist/credentials/store.d.ts +1 -1
- package/dist/credentials/store.js +16 -7
- package/dist/credentials/types.d.ts +38 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +61 -32
- package/dist/engine/engine.js +289 -55
- package/dist/engine/goal.d.ts +5 -3
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/turn-loop.d.ts +18 -0
- package/dist/engine/turn-loop.js +202 -27
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +14 -4
- package/dist/hooks/goal-stop-hook.js +626 -71
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +290 -72
- package/dist/protocol/types.d.ts +34 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +362 -26
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
- package/dist/tool-system/builtin/background-jobs.js +15 -11
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +45 -2
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +17 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +6 -0
- package/dist/tool-system/mcp-manager.js +55 -5
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
package/dist/protocol/server.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
|
|
17
17
|
import { diskDefaultsFrom } from "../engine/engine.js";
|
|
18
18
|
import { isProtectedSettingKey } from "../settings/manager.js";
|
|
19
|
-
import {
|
|
19
|
+
import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
20
20
|
import { getArenaStatus } from "../tool-system/builtin/arena.js";
|
|
21
21
|
import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
|
|
22
22
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
@@ -24,9 +24,32 @@ import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js
|
|
|
24
24
|
import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
|
|
25
25
|
import { logger } from "../logging/logger.js";
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
|
-
import { SessionManager } from "../session/session-manager.js";
|
|
27
|
+
import { assertSafeSessionId, SessionManager } from "../session/session-manager.js";
|
|
28
28
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
29
29
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
30
|
+
function isValidRunAttachment(value) {
|
|
31
|
+
if (!value || typeof value !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
const attachment = value;
|
|
34
|
+
const hasPath = [attachment.path, attachment.relPath, attachment.absPath].some((path) => typeof path === "string" && path.trim().length > 0);
|
|
35
|
+
return (typeof attachment.id === "string" &&
|
|
36
|
+
attachment.id.length > 0 &&
|
|
37
|
+
typeof attachment.sessionId === "string" &&
|
|
38
|
+
attachment.sessionId.length > 0 &&
|
|
39
|
+
(attachment.kind === "image" ||
|
|
40
|
+
attachment.kind === "file" ||
|
|
41
|
+
attachment.kind === "directory") &&
|
|
42
|
+
hasPath);
|
|
43
|
+
}
|
|
44
|
+
function runInputError(params) {
|
|
45
|
+
if (typeof params.task !== "string")
|
|
46
|
+
return "task must be a string";
|
|
47
|
+
const hasAttachment = Array.isArray(params.attachments) && params.attachments.some(isValidRunAttachment);
|
|
48
|
+
if (params.task.trim().length === 0 && !hasAttachment) {
|
|
49
|
+
return "task or a valid attachment is required";
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
30
53
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
31
54
|
"micro",
|
|
32
55
|
"summary",
|
|
@@ -50,10 +73,17 @@ export class AgentServer {
|
|
|
50
73
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
51
74
|
readActiveGoalFromDisk;
|
|
52
75
|
workspaceBridgeEnabled;
|
|
76
|
+
connectionId;
|
|
77
|
+
strictApprovalRouting;
|
|
78
|
+
approvalRouter;
|
|
79
|
+
approvalConnectionUnregister = null;
|
|
80
|
+
disconnected = false;
|
|
81
|
+
pendingApprovalTargets = new Map();
|
|
53
82
|
/**
|
|
54
83
|
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
55
84
|
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
56
|
-
* with the same cwd/
|
|
85
|
+
* with the same cwd/trust inputs before draining the queue. Per-turn
|
|
86
|
+
* permission/plan overrides deliberately do not survive into wakeup turns.
|
|
57
87
|
*/
|
|
58
88
|
lastSliceBySession = new Map();
|
|
59
89
|
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
@@ -98,12 +128,20 @@ export class AgentServer {
|
|
|
98
128
|
* itself outlives the server (it's a process-local singleton).
|
|
99
129
|
*/
|
|
100
130
|
bgAgentBusUnsubscribe = null;
|
|
131
|
+
wakeupsInFlight = new Set();
|
|
101
132
|
constructor(options) {
|
|
102
133
|
this.chatManager = options.chatManager ?? null;
|
|
103
134
|
this.legacyEngine = options.engine ?? null;
|
|
104
135
|
this.settingsReader = options.settingsReader ?? null;
|
|
105
136
|
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
106
137
|
this.workspaceBridgeEnabled = options.workspaceBridge === true;
|
|
138
|
+
this.connectionId = options.connectionId ?? "legacy-agent-server";
|
|
139
|
+
this.strictApprovalRouting = options.connectionId !== undefined;
|
|
140
|
+
this.approvalRouter = options.approvalRouter ?? getApprovalRouter();
|
|
141
|
+
if (!this.strictApprovalRouting) {
|
|
142
|
+
this.approvalRouter.deregister(this.connectionId, "legacy approval host replaced");
|
|
143
|
+
}
|
|
144
|
+
getInteractiveApprovalBackend().clearPromptFn();
|
|
107
145
|
if (!this.chatManager && !this.legacyEngine) {
|
|
108
146
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
109
147
|
}
|
|
@@ -116,9 +154,10 @@ export class AgentServer {
|
|
|
116
154
|
});
|
|
117
155
|
}
|
|
118
156
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
157
|
+
this.approvalConnectionUnregister = this.approvalRouter.addConnection(this.connectionId, {
|
|
158
|
+
requestApproval: (request, target) => this.requestApprovalFromClient(request, target),
|
|
159
|
+
ownershipLost: (targets, reason) => this.failClosedApprovalTargets(targets, reason),
|
|
160
|
+
}, { claimUnownedSessions: !this.strictApprovalRouting });
|
|
122
161
|
if (this.legacyEngine) {
|
|
123
162
|
// Only wire an interactive askUser when a human is present. For
|
|
124
163
|
// unattended (headless) runs we leave askUser undefined so
|
|
@@ -174,48 +213,60 @@ export class AgentServer {
|
|
|
174
213
|
* the first drains all currently-pending items; subsequent bus events for
|
|
175
214
|
* the same session find it busy (or find an empty queue) and no-op.
|
|
176
215
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* before its first await. So when bus events #2..N arrive — still on the same
|
|
181
|
-
* synchronous fan-out as #1 — they already see the session as busy and no-op.
|
|
182
|
-
* If either path is ever made async (e.g. an await is added inside
|
|
183
|
-
* enqueueTurn before `active` is set, or the bus starts deferring delivery),
|
|
184
|
-
* this degrades into N concurrent wakeups for N completions. Keep both
|
|
185
|
-
* synchronous, or replace this comment's assumption with an explicit
|
|
186
|
-
* "wakeup in flight" guard flag.
|
|
216
|
+
* `wakeupsInFlight` serializes the now-async rehydrate path, so a burst of
|
|
217
|
+
* completion events cannot create duplicate sessions or enqueue duplicate
|
|
218
|
+
* wakeup turns while getOrCreate waits for a closing generation to settle.
|
|
187
219
|
*/
|
|
188
220
|
maybeWakeIdleSession(sessionId) {
|
|
189
|
-
if (
|
|
221
|
+
if (this.wakeupsInFlight.has(sessionId))
|
|
190
222
|
return;
|
|
191
|
-
|
|
223
|
+
this.wakeupsInFlight.add(sessionId);
|
|
224
|
+
void this.wakeIdleSession(sessionId)
|
|
225
|
+
.then((ranTurn) => {
|
|
226
|
+
this.wakeupsInFlight.delete(sessionId);
|
|
227
|
+
if (ranTurn && notificationQueue.getSnapshot(sessionId).length > 0) {
|
|
228
|
+
this.maybeWakeIdleSession(sessionId);
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
.catch(() => {
|
|
232
|
+
this.wakeupsInFlight.delete(sessionId);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async wakeIdleSession(sessionId) {
|
|
236
|
+
if (!this.chatManager)
|
|
237
|
+
return false;
|
|
238
|
+
if (this.chatManager.isUnavailable(sessionId))
|
|
239
|
+
return false;
|
|
240
|
+
const session = this.chatManager.get(sessionId) ?? (await this.rehydrateSessionForWake(sessionId));
|
|
192
241
|
if (!session || session.isBusy())
|
|
193
|
-
return;
|
|
242
|
+
return false;
|
|
194
243
|
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
195
244
|
// is gone, so there's no consumer for a woken continuation turn. Headless
|
|
196
245
|
// already drained its background sub-agents inside engine.run before
|
|
197
246
|
// returning; any remaining queued notification (video/shell) must NOT spin
|
|
198
247
|
// an orphan turn. Only the interactive path auto-continues.
|
|
199
248
|
if (session.engine.isHeadless())
|
|
200
|
-
return;
|
|
249
|
+
return false;
|
|
201
250
|
// Don't resurrect a session the user just Stopped: cancel() leaves it idle
|
|
202
251
|
// (active=null) so isBusy() reads false, but auto-running a fresh turn here
|
|
203
252
|
// would defeat the Stop. The flag clears the moment the user sends again.
|
|
204
253
|
if (session.wasCancelledSinceLastTurn())
|
|
205
|
-
return;
|
|
254
|
+
return false;
|
|
206
255
|
const pending = notificationQueue.drainAll(sessionId);
|
|
207
256
|
if (pending.length === 0)
|
|
208
|
-
return;
|
|
257
|
+
return false;
|
|
209
258
|
const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
|
|
210
|
-
|
|
211
|
-
.enqueueTurn(task, {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
259
|
+
try {
|
|
260
|
+
await session.enqueueTurn(task, {
|
|
261
|
+
// Synthetic notification, not the user's own input: persisted with an
|
|
262
|
+
// `injected` flag so a disk rebuild doesn't render it as a phantom user
|
|
263
|
+
// bubble (the live UI shows only the woken assistant's reply).
|
|
264
|
+
injected: true,
|
|
265
|
+
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
266
|
+
approvalRouter: this.approvalRouter,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
219
270
|
// A wakeup turn failing must not crash the bus fan-out. The drained
|
|
220
271
|
// notifications are already in the transcript via the run's messages;
|
|
221
272
|
// log and move on.
|
|
@@ -236,21 +287,14 @@ export class AgentServer {
|
|
|
236
287
|
sessionId,
|
|
237
288
|
event: { type: "error", error: err?.message ?? "background wakeup failed" },
|
|
238
289
|
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Run-boundary re-check (trigger B): the woken summarize turn may have
|
|
242
|
-
// spawned NEW background work (e.g. goal "generate 2 videos" → after #1
|
|
243
|
-
// completes, this turn submits #2). When #2 finishes its notification
|
|
244
|
-
// arrives while we're idle again — but if it arrived DURING this turn
|
|
245
|
-
// (busy), trigger A skipped it. Re-checking at the run boundary drains
|
|
246
|
-
// anything that landed while busy, chaining wakeups until the queue is
|
|
247
|
-
// truly empty. Replaces the old engine for(;;) outer loop.
|
|
248
|
-
this.maybeWakeIdleSession(sessionId);
|
|
249
|
-
});
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
250
292
|
}
|
|
251
|
-
rehydrateSessionForWake(sessionId) {
|
|
293
|
+
async rehydrateSessionForWake(sessionId) {
|
|
252
294
|
if (!this.chatManager)
|
|
253
295
|
return null;
|
|
296
|
+
if (this.chatManager.isUnavailable(sessionId))
|
|
297
|
+
return null;
|
|
254
298
|
try {
|
|
255
299
|
const pending = notificationQueue.getSnapshot(sessionId);
|
|
256
300
|
if (pending.length === 0) {
|
|
@@ -273,7 +317,7 @@ export class AgentServer {
|
|
|
273
317
|
});
|
|
274
318
|
return null;
|
|
275
319
|
}
|
|
276
|
-
const session = this.chatManager.getOrCreate(sessionId, slice);
|
|
320
|
+
const session = await this.chatManager.getOrCreate(sessionId, slice);
|
|
277
321
|
this.wireInteractiveSession(session, sessionId);
|
|
278
322
|
logger.debug("bg_wakeup.rehydrated_session", {
|
|
279
323
|
sessionId,
|
|
@@ -300,7 +344,6 @@ export class AgentServer {
|
|
|
300
344
|
if (!cwd)
|
|
301
345
|
return null;
|
|
302
346
|
return {
|
|
303
|
-
permissionMode: "default",
|
|
304
347
|
projectTrusted: false,
|
|
305
348
|
cwd,
|
|
306
349
|
};
|
|
@@ -324,6 +367,9 @@ export class AgentServer {
|
|
|
324
367
|
case Methods.Run:
|
|
325
368
|
await this.handleRun(req);
|
|
326
369
|
break;
|
|
370
|
+
case Methods.ForkSession:
|
|
371
|
+
this.handleForkSession(req);
|
|
372
|
+
break;
|
|
327
373
|
case Methods.Approve:
|
|
328
374
|
this.handleApprove(req);
|
|
329
375
|
break;
|
|
@@ -346,7 +392,7 @@ export class AgentServer {
|
|
|
346
392
|
this.handleUnsteer(req);
|
|
347
393
|
break;
|
|
348
394
|
case Methods.CloseSession:
|
|
349
|
-
this.handleCloseSession(req);
|
|
395
|
+
await this.handleCloseSession(req);
|
|
350
396
|
break;
|
|
351
397
|
case Methods.ReleaseWorkspace:
|
|
352
398
|
this.handleReleaseWorkspace(req);
|
|
@@ -371,6 +417,73 @@ export class AgentServer {
|
|
|
371
417
|
}
|
|
372
418
|
}
|
|
373
419
|
// ─── Run ────────────────────────────────────────────────────────
|
|
420
|
+
handleForkSession(req) {
|
|
421
|
+
const params = req.params ?? {};
|
|
422
|
+
const sourceSessionId = typeof params.sourceSessionId === "string" ? params.sourceSessionId : "";
|
|
423
|
+
const targetSessionId = typeof params.targetSessionId === "string" ? params.targetSessionId : undefined;
|
|
424
|
+
const throughEventId = typeof params.throughEventId === "string" ? params.throughEventId : undefined;
|
|
425
|
+
if (!sourceSessionId || params.mode !== "full") {
|
|
426
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "forkSession requires sourceSessionId and mode=full"));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (params.targetSessionId !== undefined && typeof params.targetSessionId !== "string") {
|
|
430
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "targetSessionId must be a string"));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (params.throughEventId !== undefined && typeof params.throughEventId !== "string") {
|
|
434
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "throughEventId must be a string"));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
assertSafeSessionId(sourceSessionId);
|
|
439
|
+
if (targetSessionId)
|
|
440
|
+
assertSafeSessionId(targetSessionId);
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err instanceof Error ? err.message : String(err)));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
let source;
|
|
447
|
+
try {
|
|
448
|
+
source = this.chatManager?.get(sourceSessionId);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, err instanceof Error ? err.message : String(err)));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const engine = source?.engine ?? this.legacyEngine ?? this.anyEngine();
|
|
455
|
+
if (!engine || !engine.sessionExistsOnDisk(sourceSessionId)) {
|
|
456
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${sourceSessionId}`));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (targetSessionId && engine.sessionExistsOnDisk(targetSessionId)) {
|
|
460
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `Session already exists: ${targetSessionId}`));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const result = engine.forkSession(sourceSessionId, { targetSessionId, throughEventId });
|
|
465
|
+
const workspace = result.bundle.state.workspace ?? {
|
|
466
|
+
root: result.bundle.state.cwd,
|
|
467
|
+
kind: "main",
|
|
468
|
+
};
|
|
469
|
+
this.transport.send(createResponse(req.id, {
|
|
470
|
+
sessionId: result.bundle.state.sessionId,
|
|
471
|
+
mode: "full",
|
|
472
|
+
forkedFrom: result.lineage,
|
|
473
|
+
workspace,
|
|
474
|
+
copiedEventCount: result.copiedEventCount,
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
479
|
+
const code = /not found/i.test(message)
|
|
480
|
+
? ErrorCodes.SessionNotFound
|
|
481
|
+
: /already exists|invalid|cursor|metadata|unfinished|orphaned|unsupported|malformed/i.test(message)
|
|
482
|
+
? ErrorCodes.InvalidParams
|
|
483
|
+
: ErrorCodes.InternalError;
|
|
484
|
+
this.transport.send(createErrorResponse(req.id, code, message));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
374
487
|
async handleRun(req) {
|
|
375
488
|
// ── ChatSessionManager path (multi-session) ──────────────────
|
|
376
489
|
if (this.chatManager) {
|
|
@@ -386,12 +499,12 @@ export class AgentServer {
|
|
|
386
499
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
387
500
|
return;
|
|
388
501
|
}
|
|
389
|
-
|
|
390
|
-
|
|
502
|
+
const inputError = runInputError(params);
|
|
503
|
+
if (inputError) {
|
|
504
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
391
505
|
return;
|
|
392
506
|
}
|
|
393
507
|
const sessionConfig = {
|
|
394
|
-
permissionMode: params.permissionMode,
|
|
395
508
|
cwd: params.cwd,
|
|
396
509
|
projectTrusted: params.projectTrusted,
|
|
397
510
|
goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
|
|
@@ -415,13 +528,19 @@ export class AgentServer {
|
|
|
415
528
|
}
|
|
416
529
|
let session;
|
|
417
530
|
try {
|
|
418
|
-
session = cm.getOrCreate(params.sessionId, sessionConfig);
|
|
531
|
+
session = await cm.getOrCreate(params.sessionId, sessionConfig);
|
|
419
532
|
}
|
|
420
533
|
catch (err) {
|
|
421
534
|
const code = err.code ?? ErrorCodes.InternalError;
|
|
422
535
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
423
536
|
return;
|
|
424
537
|
}
|
|
538
|
+
const sid = params.sessionId;
|
|
539
|
+
const approvalRegistration = this.approvalRouter.register(sid, this.connectionId);
|
|
540
|
+
if (!approvalRegistration.ok) {
|
|
541
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${sid} is owned by another connection`));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
425
544
|
this.rememberSessionSlice(params.sessionId, sessionConfig);
|
|
426
545
|
if (params.model !== undefined) {
|
|
427
546
|
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
@@ -436,16 +555,12 @@ export class AgentServer {
|
|
|
436
555
|
return;
|
|
437
556
|
}
|
|
438
557
|
}
|
|
439
|
-
if (typeof params.planMode === "boolean") {
|
|
440
|
-
session.engine.setPlanMode(params.planMode);
|
|
441
|
-
}
|
|
442
|
-
const sid = params.sessionId;
|
|
443
558
|
// Wire AskUserQuestion and host bridges for this interactive session. The
|
|
444
559
|
// chatManager path builds a fresh per-session Engine via engineFactory, so
|
|
445
560
|
// these host callbacks must be installed after every create/recreate.
|
|
446
561
|
this.wireInteractiveSession(session, sid);
|
|
447
562
|
try {
|
|
448
|
-
const
|
|
563
|
+
const run = session.enqueueTurn(params.task, {
|
|
449
564
|
cwd: params.cwd,
|
|
450
565
|
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
451
566
|
goal: typeof params.goal === "string" ||
|
|
@@ -454,7 +569,12 @@ export class AgentServer {
|
|
|
454
569
|
: undefined,
|
|
455
570
|
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
|
|
456
571
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
572
|
+
permissionMode: params.permissionMode,
|
|
573
|
+
planMode: params.planMode,
|
|
574
|
+
approvalRouter: this.approvalRouter,
|
|
457
575
|
});
|
|
576
|
+
this.notify(Methods.RunAccepted, { requestId: req.id, sessionId: sid });
|
|
577
|
+
const result = await run;
|
|
458
578
|
const runResult = {
|
|
459
579
|
text: result.text,
|
|
460
580
|
reason: result.reason,
|
|
@@ -483,10 +603,18 @@ export class AgentServer {
|
|
|
483
603
|
return;
|
|
484
604
|
}
|
|
485
605
|
const params = (req.params ?? {});
|
|
486
|
-
|
|
487
|
-
|
|
606
|
+
const inputError = runInputError(params);
|
|
607
|
+
if (inputError) {
|
|
608
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
488
609
|
return;
|
|
489
610
|
}
|
|
611
|
+
if (typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
612
|
+
const approvalRegistration = this.approvalRouter.register(params.sessionId, this.connectionId);
|
|
613
|
+
if (!approvalRegistration.ok) {
|
|
614
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${params.sessionId} is owned by another connection`));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
490
618
|
if (params.cwd !== undefined && (typeof params.cwd !== "string" || params.cwd.length === 0)) {
|
|
491
619
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "cwd must be a non-empty string"));
|
|
492
620
|
return;
|
|
@@ -528,12 +656,17 @@ export class AgentServer {
|
|
|
528
656
|
// `signal.aborted` below it's already gone.
|
|
529
657
|
const runController = this.abortController;
|
|
530
658
|
try {
|
|
659
|
+
this.notify(Methods.RunAccepted, {
|
|
660
|
+
requestId: req.id,
|
|
661
|
+
sessionId: params.sessionId ?? "",
|
|
662
|
+
});
|
|
531
663
|
const result = await this.legacyEngine.run(params.task, {
|
|
532
664
|
cwd: params.cwd,
|
|
533
665
|
sessionId: params.sessionId,
|
|
534
666
|
signal: runController.signal,
|
|
535
667
|
onStream: streamToClient,
|
|
536
668
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
669
|
+
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
537
670
|
goal: typeof params.goal === "string" ||
|
|
538
671
|
(params.goal != null && typeof params.goal === "object")
|
|
539
672
|
? params.goal
|
|
@@ -579,6 +712,19 @@ export class AgentServer {
|
|
|
579
712
|
// ─── Approve ────────────────────────────────────────────────────
|
|
580
713
|
handleApprove(req) {
|
|
581
714
|
const params = (req.params ?? {});
|
|
715
|
+
if (this.strictApprovalRouting) {
|
|
716
|
+
const pending = this.pendingApprovalTargets.get(params.requestId);
|
|
717
|
+
const matchesPending = pending !== undefined &&
|
|
718
|
+
params.connectionId === this.connectionId &&
|
|
719
|
+
params.sessionId === pending.sessionId &&
|
|
720
|
+
params.connectionId === pending.connectionId &&
|
|
721
|
+
params.generation === pending.generation &&
|
|
722
|
+
this.approvalRouter.matches(pending);
|
|
723
|
+
if (!matchesPending) {
|
|
724
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "Approval response does not match connectionId, sessionId, generation, and requestId"));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
582
728
|
// ChatSessionManager path: approvals are scoped by (sessionId, requestId).
|
|
583
729
|
// Never fall back from a session-tagged response to the legacy global map:
|
|
584
730
|
// a stale/misrouted UI response must fail closed instead of resolving a
|
|
@@ -595,6 +741,7 @@ export class AgentServer {
|
|
|
595
741
|
return;
|
|
596
742
|
}
|
|
597
743
|
s.pendingApprovals.delete(params.requestId);
|
|
744
|
+
this.pendingApprovalTargets.delete(params.requestId);
|
|
598
745
|
// Cancel the pending timeout for requests that arm one (browser actions,
|
|
599
746
|
// credential injection, and tool approvals). AskUserQuestion does not use
|
|
600
747
|
// a timeout; this is harmless for those request ids.
|
|
@@ -610,6 +757,7 @@ export class AgentServer {
|
|
|
610
757
|
return;
|
|
611
758
|
}
|
|
612
759
|
this.pendingApprovals.delete(params.requestId);
|
|
760
|
+
this.pendingApprovalTargets.delete(params.requestId);
|
|
613
761
|
this.clearApprovalTimer(params.requestId);
|
|
614
762
|
resolve(params.decision);
|
|
615
763
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
@@ -817,7 +965,7 @@ export class AgentServer {
|
|
|
817
965
|
this.transport.send(createResponse(req.id, { items }));
|
|
818
966
|
}
|
|
819
967
|
// ─── CloseSession ───────────────────────────────────────────────
|
|
820
|
-
handleCloseSession(req) {
|
|
968
|
+
async handleCloseSession(req) {
|
|
821
969
|
const params = (req.params ?? {});
|
|
822
970
|
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
823
971
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
@@ -828,13 +976,15 @@ export class AgentServer {
|
|
|
828
976
|
if (session) {
|
|
829
977
|
this.cancelSessionApprovals(session, "session closed");
|
|
830
978
|
}
|
|
831
|
-
this.
|
|
979
|
+
this.approvalRouter.release(params.sessionId, this.connectionId, "session closed");
|
|
980
|
+
await this.chatManager.close(params.sessionId);
|
|
832
981
|
}
|
|
833
982
|
// Explicit session teardown — reap that session's background shells
|
|
834
983
|
// (design §6 "session 被显式删除 → killSession"). This is the RPC path
|
|
835
984
|
// (agent/closeSession from the host on delete), distinct from the idle
|
|
836
|
-
// sweeper's
|
|
837
|
-
|
|
985
|
+
// sweeper's idle eviction which must NOT kill (§6). Await teardown so the
|
|
986
|
+
// close RPC is a real deletion barrier for Desktop.
|
|
987
|
+
await backgroundShellManager.killSession(params.sessionId);
|
|
838
988
|
// Drop this session's retained background jobs too (#2/#5): finished jobs
|
|
839
989
|
// are kept for the panel, so explicit teardown is where they're released.
|
|
840
990
|
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
@@ -1127,6 +1277,10 @@ export class AgentServer {
|
|
|
1127
1277
|
let compactEngine = engine;
|
|
1128
1278
|
if (this.chatManager) {
|
|
1129
1279
|
if (compactSessionId) {
|
|
1280
|
+
if (this.chatManager.isUnavailable(compactSessionId)) {
|
|
1281
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${compactSessionId}`));
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1130
1284
|
let session = this.chatManager.get(compactSessionId);
|
|
1131
1285
|
if (!session) {
|
|
1132
1286
|
const probeEngine = this.anyEngine();
|
|
@@ -1135,7 +1289,7 @@ export class AgentServer {
|
|
|
1135
1289
|
return;
|
|
1136
1290
|
}
|
|
1137
1291
|
try {
|
|
1138
|
-
session = this.chatManager.getOrCreate(compactSessionId, {
|
|
1292
|
+
session = await this.chatManager.getOrCreate(compactSessionId, {
|
|
1139
1293
|
cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
|
|
1140
1294
|
});
|
|
1141
1295
|
}
|
|
@@ -1452,6 +1606,10 @@ export class AgentServer {
|
|
|
1452
1606
|
}
|
|
1453
1607
|
// In the chatManager path, inject into the session's engine
|
|
1454
1608
|
if (this.chatManager) {
|
|
1609
|
+
if (this.chatManager.isUnavailable(params.sessionId)) {
|
|
1610
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${params.sessionId}`));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1455
1613
|
const s = this.chatManager.get(params.sessionId);
|
|
1456
1614
|
if (!s) {
|
|
1457
1615
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
@@ -1485,6 +1643,10 @@ export class AgentServer {
|
|
|
1485
1643
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "text and sessionId required"));
|
|
1486
1644
|
return;
|
|
1487
1645
|
}
|
|
1646
|
+
if (this.chatManager?.isUnavailable(params.sessionId)) {
|
|
1647
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${params.sessionId}`));
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1488
1650
|
const engine = this.chatManager
|
|
1489
1651
|
? this.chatManager.get(params.sessionId)?.engine
|
|
1490
1652
|
: this.legacyEngine;
|
|
@@ -1506,6 +1668,10 @@ export class AgentServer {
|
|
|
1506
1668
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "id and sessionId required"));
|
|
1507
1669
|
return;
|
|
1508
1670
|
}
|
|
1671
|
+
if (this.chatManager?.isUnavailable(params.sessionId)) {
|
|
1672
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${params.sessionId}`));
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1509
1675
|
const engine = this.chatManager
|
|
1510
1676
|
? this.chatManager.get(params.sessionId)?.engine
|
|
1511
1677
|
: this.legacyEngine;
|
|
@@ -1527,10 +1693,15 @@ export class AgentServer {
|
|
|
1527
1693
|
/**
|
|
1528
1694
|
* Ask the client to approve a tool operation (legacy single-engine path).
|
|
1529
1695
|
*/
|
|
1530
|
-
requestApprovalFromClient(request) {
|
|
1696
|
+
requestApprovalFromClient(request, route) {
|
|
1531
1697
|
return new Promise((resolve) => {
|
|
1532
1698
|
const requestId = nanoid(12);
|
|
1533
|
-
const
|
|
1699
|
+
const effectiveRoute = route ?? {
|
|
1700
|
+
connectionId: this.connectionId,
|
|
1701
|
+
sessionId: request.sessionId ?? "",
|
|
1702
|
+
generation: 0,
|
|
1703
|
+
};
|
|
1704
|
+
const sessionId = effectiveRoute.sessionId;
|
|
1534
1705
|
const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
|
|
1535
1706
|
if (this.chatManager && sessionId && !session) {
|
|
1536
1707
|
resolve({ approved: false, reason: `session closed: ${sessionId}` });
|
|
@@ -1544,22 +1715,34 @@ export class AgentServer {
|
|
|
1544
1715
|
else {
|
|
1545
1716
|
this.pendingApprovals.set(requestId, resolve);
|
|
1546
1717
|
}
|
|
1718
|
+
this.pendingApprovalTargets.set(requestId, { ...effectiveRoute, requestId });
|
|
1547
1719
|
const timer = setTimeout(() => {
|
|
1548
1720
|
const pending = session?.pendingApprovals ?? this.pendingApprovals;
|
|
1549
1721
|
if (pending.has(requestId)) {
|
|
1550
1722
|
pending.delete(requestId);
|
|
1723
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1551
1724
|
this.approvalTimers.delete(requestId);
|
|
1552
1725
|
resolve({ approved: false, reason: "approval timed out" });
|
|
1553
1726
|
}
|
|
1554
1727
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1555
1728
|
this.approvalTimers.set(requestId, timer);
|
|
1556
1729
|
this.notify(Methods.ApprovalRequest, {
|
|
1730
|
+
connectionId: effectiveRoute.connectionId,
|
|
1557
1731
|
...(sessionId ? { sessionId } : {}),
|
|
1732
|
+
generation: effectiveRoute.generation,
|
|
1558
1733
|
requestId,
|
|
1559
1734
|
request,
|
|
1560
1735
|
});
|
|
1561
1736
|
});
|
|
1562
1737
|
}
|
|
1738
|
+
approvalRouteEnvelope(sessionId, requestId) {
|
|
1739
|
+
const route = this.approvalRouter.current(sessionId);
|
|
1740
|
+
if (!route || route.connectionId !== this.connectionId)
|
|
1741
|
+
return { sessionId, requestId };
|
|
1742
|
+
const target = { ...route, requestId };
|
|
1743
|
+
this.pendingApprovalTargets.set(requestId, target);
|
|
1744
|
+
return target;
|
|
1745
|
+
}
|
|
1563
1746
|
/**
|
|
1564
1747
|
* Per-session AskUserQuestion for the chatManager path. Resolves via the
|
|
1565
1748
|
* SESSION's pendingApprovals (the chatManager approve handler looks there,
|
|
@@ -1593,8 +1776,7 @@ export class AgentServer {
|
|
|
1593
1776
|
if (opts?.optionsOnly !== undefined)
|
|
1594
1777
|
args.optionsOnly = opts.optionsOnly;
|
|
1595
1778
|
this.notify(Methods.ApprovalRequest, {
|
|
1596
|
-
sessionId,
|
|
1597
|
-
requestId,
|
|
1779
|
+
...this.approvalRouteEnvelope(sessionId, requestId),
|
|
1598
1780
|
request: {
|
|
1599
1781
|
toolName: "__ask_user__",
|
|
1600
1782
|
args,
|
|
@@ -1622,6 +1804,7 @@ export class AgentServer {
|
|
|
1622
1804
|
const resolveFn = session.pendingApprovals.get(requestId);
|
|
1623
1805
|
if (resolveFn) {
|
|
1624
1806
|
session.pendingApprovals.delete(requestId);
|
|
1807
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1625
1808
|
this.approvalTimers.delete(requestId);
|
|
1626
1809
|
resolveFn("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
|
|
1627
1810
|
// Tell every client the ask is resolved (nobody answered) so the
|
|
@@ -1691,14 +1874,14 @@ export class AgentServer {
|
|
|
1691
1874
|
const timer = setTimeout(() => {
|
|
1692
1875
|
if (session.pendingApprovals.has(requestId)) {
|
|
1693
1876
|
session.pendingApprovals.delete(requestId);
|
|
1877
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1694
1878
|
this.approvalTimers.delete(requestId);
|
|
1695
1879
|
resolve({ ok: false, detail: "browser action timed out" });
|
|
1696
1880
|
}
|
|
1697
1881
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1698
1882
|
this.approvalTimers.set(requestId, timer);
|
|
1699
1883
|
this.notify(Methods.ApprovalRequest, {
|
|
1700
|
-
sessionId,
|
|
1701
|
-
requestId,
|
|
1884
|
+
...this.approvalRouteEnvelope(sessionId, requestId),
|
|
1702
1885
|
request: {
|
|
1703
1886
|
toolName: "__browser_action__",
|
|
1704
1887
|
args: { action, ...payload },
|
|
@@ -1741,14 +1924,14 @@ export class AgentServer {
|
|
|
1741
1924
|
const timer = setTimeout(() => {
|
|
1742
1925
|
if (session.pendingApprovals.has(requestId)) {
|
|
1743
1926
|
session.pendingApprovals.delete(requestId);
|
|
1927
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1744
1928
|
this.approvalTimers.delete(requestId);
|
|
1745
1929
|
resolve({ ok: false, error: "credential inject timed out" });
|
|
1746
1930
|
}
|
|
1747
1931
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1748
1932
|
this.approvalTimers.set(requestId, timer);
|
|
1749
1933
|
this.notify(Methods.ApprovalRequest, {
|
|
1750
|
-
sessionId,
|
|
1751
|
-
requestId,
|
|
1934
|
+
...this.approvalRouteEnvelope(sessionId, requestId),
|
|
1752
1935
|
request: {
|
|
1753
1936
|
toolName: "__credential_action__",
|
|
1754
1937
|
args: { action: "injectCookie", credentialId, credentialScope },
|
|
@@ -1795,14 +1978,14 @@ export class AgentServer {
|
|
|
1795
1978
|
const timer = setTimeout(() => {
|
|
1796
1979
|
if (session.pendingApprovals.has(requestId)) {
|
|
1797
1980
|
session.pendingApprovals.delete(requestId);
|
|
1981
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1798
1982
|
this.approvalTimers.delete(requestId);
|
|
1799
1983
|
reject(new Error("workspace switch timed out"));
|
|
1800
1984
|
}
|
|
1801
1985
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1802
1986
|
this.approvalTimers.set(requestId, timer);
|
|
1803
1987
|
this.notify(Methods.ApprovalRequest, {
|
|
1804
|
-
sessionId,
|
|
1805
|
-
requestId,
|
|
1988
|
+
...this.approvalRouteEnvelope(sessionId, requestId),
|
|
1806
1989
|
request: {
|
|
1807
1990
|
toolName: "__workspace_action__",
|
|
1808
1991
|
args: { action: "switch", target },
|
|
@@ -1876,6 +2059,7 @@ export class AgentServer {
|
|
|
1876
2059
|
}
|
|
1877
2060
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
1878
2061
|
close() {
|
|
2062
|
+
this.disconnect("server closing");
|
|
1879
2063
|
// Detach the bg-agent bus subscription first so a final flurry of
|
|
1880
2064
|
// completions during shutdown can't race the `shutdown` status
|
|
1881
2065
|
// notify below. Safe to call repeatedly — the unsubscribe is a
|
|
@@ -1912,6 +2096,18 @@ export class AgentServer {
|
|
|
1912
2096
|
this.notify(Methods.Status, { status: "shutdown" });
|
|
1913
2097
|
this.transport.close();
|
|
1914
2098
|
}
|
|
2099
|
+
/** Release only this transport's approval ownership. Safe on socket close
|
|
2100
|
+
* even when the ChatSessionManager is shared by other TCP connections. */
|
|
2101
|
+
disconnect(reason = "approval connection disconnected") {
|
|
2102
|
+
if (this.disconnected)
|
|
2103
|
+
return;
|
|
2104
|
+
this.disconnected = true;
|
|
2105
|
+
const unregister = this.approvalConnectionUnregister;
|
|
2106
|
+
this.approvalConnectionUnregister = null;
|
|
2107
|
+
if (unregister) {
|
|
2108
|
+
this.approvalRouter.deregister(this.connectionId, reason);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
1915
2111
|
clearApprovalTimer(requestId) {
|
|
1916
2112
|
const timer = this.approvalTimers.get(requestId);
|
|
1917
2113
|
if (timer) {
|
|
@@ -1929,6 +2125,7 @@ export class AgentServer {
|
|
|
1929
2125
|
cancelSessionApprovals(session, reason = "cancelled") {
|
|
1930
2126
|
for (const [requestId, resolve] of session.pendingApprovals) {
|
|
1931
2127
|
this.clearApprovalTimer(requestId);
|
|
2128
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1932
2129
|
try {
|
|
1933
2130
|
resolve({ approved: false, reason });
|
|
1934
2131
|
}
|
|
@@ -1938,6 +2135,27 @@ export class AgentServer {
|
|
|
1938
2135
|
}
|
|
1939
2136
|
session.pendingApprovals.clear();
|
|
1940
2137
|
}
|
|
2138
|
+
failClosedApprovalTargets(targets, reason) {
|
|
2139
|
+
for (const target of targets) {
|
|
2140
|
+
const session = this.chatManager?.get(target.sessionId);
|
|
2141
|
+
if (session) {
|
|
2142
|
+
this.cancelSessionApprovals(session, reason);
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
for (const [requestId, pending] of this.pendingApprovalTargets) {
|
|
2146
|
+
if (pending.connectionId !== target.connectionId ||
|
|
2147
|
+
pending.sessionId !== target.sessionId ||
|
|
2148
|
+
pending.generation !== target.generation) {
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
const resolve = this.pendingApprovals.get(requestId);
|
|
2152
|
+
this.pendingApprovals.delete(requestId);
|
|
2153
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
2154
|
+
this.clearApprovalTimer(requestId);
|
|
2155
|
+
resolve?.({ approved: false, reason });
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
1941
2159
|
clearAllApprovalTimers() {
|
|
1942
2160
|
for (const timer of this.approvalTimers.values()) {
|
|
1943
2161
|
clearTimeout(timer);
|