@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
package/dist/protocol/server.js
CHANGED
|
@@ -24,8 +24,22 @@ 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
28
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
28
29
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
30
|
+
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
31
|
+
"micro",
|
|
32
|
+
"summary",
|
|
33
|
+
"window",
|
|
34
|
+
"snip",
|
|
35
|
+
"emergency",
|
|
36
|
+
"compacted",
|
|
37
|
+
]);
|
|
38
|
+
function toCompactStreamStrategy(strategy) {
|
|
39
|
+
return COMPACT_STREAM_STRATEGIES.has(strategy)
|
|
40
|
+
? strategy
|
|
41
|
+
: "compacted";
|
|
42
|
+
}
|
|
29
43
|
export class AgentServer {
|
|
30
44
|
chatManager;
|
|
31
45
|
legacyEngine;
|
|
@@ -35,6 +49,15 @@ export class AgentServer {
|
|
|
35
49
|
settingsReader;
|
|
36
50
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
37
51
|
readActiveGoalFromDisk;
|
|
52
|
+
workspaceBridgeEnabled;
|
|
53
|
+
/**
|
|
54
|
+
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
55
|
+
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
56
|
+
* with the same cwd/permission/trust inputs before draining the queue.
|
|
57
|
+
*/
|
|
58
|
+
lastSliceBySession = new Map();
|
|
59
|
+
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
60
|
+
diskSessionReader = null;
|
|
38
61
|
/**
|
|
39
62
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
40
63
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -60,6 +83,14 @@ export class AgentServer {
|
|
|
60
83
|
approvalTimers = new Map();
|
|
61
84
|
/** Default approval timeout: 5 minutes */
|
|
62
85
|
static APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
86
|
+
/**
|
|
87
|
+
* Wall-clock timeout for AskUserQuestion during a GOAL run (10 minutes).
|
|
88
|
+
* Plain interactive asks stay untimed; only a self-driving goal run arms this
|
|
89
|
+
* so a mid-goal question can't suspend the loop forever with nobody watching.
|
|
90
|
+
* Longer than the tool-approval timeout — a user who IS present deserves more
|
|
91
|
+
* time to compose a real answer before we assume-and-continue.
|
|
92
|
+
*/
|
|
93
|
+
static GOAL_ASKUSER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
63
94
|
/**
|
|
64
95
|
* Unsubscribe handle for the process-local `agentNotificationBus`
|
|
65
96
|
* subscription set up in the constructor. Called from `close()` so a
|
|
@@ -72,6 +103,7 @@ export class AgentServer {
|
|
|
72
103
|
this.legacyEngine = options.engine ?? null;
|
|
73
104
|
this.settingsReader = options.settingsReader ?? null;
|
|
74
105
|
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
106
|
+
this.workspaceBridgeEnabled = options.workspaceBridge === true;
|
|
75
107
|
if (!this.chatManager && !this.legacyEngine) {
|
|
76
108
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
77
109
|
}
|
|
@@ -156,7 +188,7 @@ export class AgentServer {
|
|
|
156
188
|
maybeWakeIdleSession(sessionId) {
|
|
157
189
|
if (!this.chatManager)
|
|
158
190
|
return;
|
|
159
|
-
const session = this.chatManager.get(sessionId);
|
|
191
|
+
const session = this.chatManager.get(sessionId) ?? this.rehydrateSessionForWake(sessionId);
|
|
160
192
|
if (!session || session.isBusy())
|
|
161
193
|
return;
|
|
162
194
|
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
@@ -216,6 +248,76 @@ export class AgentServer {
|
|
|
216
248
|
this.maybeWakeIdleSession(sessionId);
|
|
217
249
|
});
|
|
218
250
|
}
|
|
251
|
+
rehydrateSessionForWake(sessionId) {
|
|
252
|
+
if (!this.chatManager)
|
|
253
|
+
return null;
|
|
254
|
+
try {
|
|
255
|
+
const pending = notificationQueue.getSnapshot(sessionId);
|
|
256
|
+
if (pending.length === 0) {
|
|
257
|
+
logger.debug("bg_wakeup.rehydrate_skipped_no_pending", { sessionId });
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const slice = this.sliceForWakeRehydrate(sessionId);
|
|
261
|
+
if (!slice) {
|
|
262
|
+
logger.warn("bg_wakeup.rehydrate_skipped_missing_disk_session", {
|
|
263
|
+
sessionId,
|
|
264
|
+
pendingCount: pending.length,
|
|
265
|
+
reason: "state_json_cwd_missing",
|
|
266
|
+
});
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (!this.chatManager.sessionExistsOnDisk(sessionId, slice)) {
|
|
270
|
+
logger.warn("bg_wakeup.rehydrate_skipped_missing_disk_session", {
|
|
271
|
+
sessionId,
|
|
272
|
+
pendingCount: pending.length,
|
|
273
|
+
});
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const session = this.chatManager.getOrCreate(sessionId, slice);
|
|
277
|
+
this.wireInteractiveSession(session, sessionId);
|
|
278
|
+
logger.debug("bg_wakeup.rehydrated_session", {
|
|
279
|
+
sessionId,
|
|
280
|
+
pendingCount: pending.length,
|
|
281
|
+
source: this.lastSliceBySession.has(sessionId) ? "last_slice" : "state_json",
|
|
282
|
+
});
|
|
283
|
+
return session;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
logger.warn("bg_wakeup.rehydrate_failed", {
|
|
287
|
+
sessionId,
|
|
288
|
+
error: err instanceof Error ? err.message : String(err),
|
|
289
|
+
});
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
sliceForWakeRehydrate(sessionId) {
|
|
294
|
+
const cached = this.lastSliceBySession.get(sessionId);
|
|
295
|
+
if (cached)
|
|
296
|
+
return { ...cached };
|
|
297
|
+
if (!this.diskSessionReader)
|
|
298
|
+
this.diskSessionReader = new SessionManager();
|
|
299
|
+
const cwd = this.diskSessionReader.readCwd(sessionId);
|
|
300
|
+
if (!cwd)
|
|
301
|
+
return null;
|
|
302
|
+
return {
|
|
303
|
+
permissionMode: "default",
|
|
304
|
+
projectTrusted: false,
|
|
305
|
+
cwd,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
rememberSessionSlice(sessionId, slice) {
|
|
309
|
+
this.lastSliceBySession.set(sessionId, { ...slice });
|
|
310
|
+
}
|
|
311
|
+
wireInteractiveSession(session, sid) {
|
|
312
|
+
if (session.engine.isHeadless())
|
|
313
|
+
return;
|
|
314
|
+
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
315
|
+
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
316
|
+
session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
|
|
317
|
+
if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
|
|
318
|
+
session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
219
321
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
220
322
|
async handleRequest(req) {
|
|
221
323
|
switch (req.method) {
|
|
@@ -246,6 +348,9 @@ export class AgentServer {
|
|
|
246
348
|
case Methods.CloseSession:
|
|
247
349
|
this.handleCloseSession(req);
|
|
248
350
|
break;
|
|
351
|
+
case Methods.ReleaseWorkspace:
|
|
352
|
+
this.handleReleaseWorkspace(req);
|
|
353
|
+
break;
|
|
249
354
|
case Methods.GoalExtend:
|
|
250
355
|
this.handleGoalExtend(req);
|
|
251
356
|
break;
|
|
@@ -285,28 +390,39 @@ export class AgentServer {
|
|
|
285
390
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "task is required"));
|
|
286
391
|
return;
|
|
287
392
|
}
|
|
393
|
+
const sessionConfig = {
|
|
394
|
+
permissionMode: params.permissionMode,
|
|
395
|
+
cwd: params.cwd,
|
|
396
|
+
projectTrusted: params.projectTrusted,
|
|
397
|
+
goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
|
|
398
|
+
? params.goal
|
|
399
|
+
: undefined,
|
|
400
|
+
};
|
|
401
|
+
if (params.requireExisting === true && !cm.get(params.sessionId)) {
|
|
402
|
+
let existsOnDisk = false;
|
|
403
|
+
try {
|
|
404
|
+
existsOnDisk = cm.sessionExistsOnDisk(params.sessionId, sessionConfig);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const code = err.code ?? ErrorCodes.InternalError;
|
|
408
|
+
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (!existsOnDisk) {
|
|
412
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
288
416
|
let session;
|
|
289
417
|
try {
|
|
290
|
-
session = cm.getOrCreate(params.sessionId,
|
|
291
|
-
permissionMode: params.permissionMode,
|
|
292
|
-
cwd: params.cwd,
|
|
293
|
-
projectTrusted: params.projectTrusted,
|
|
294
|
-
});
|
|
418
|
+
session = cm.getOrCreate(params.sessionId, sessionConfig);
|
|
295
419
|
}
|
|
296
420
|
catch (err) {
|
|
297
421
|
const code = err.code ?? ErrorCodes.InternalError;
|
|
298
422
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
299
423
|
return;
|
|
300
424
|
}
|
|
301
|
-
|
|
302
|
-
// running the prompt against a freshly-created blank session. A cron
|
|
303
|
-
// "continue this conversation" job whose session the user deleted must fail
|
|
304
|
-
// loudly here (SessionNotFound) so the scheduler can auto-disable it,
|
|
305
|
-
// instead of silently executing with no transcript/goal/context.
|
|
306
|
-
if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
|
|
307
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
425
|
+
this.rememberSessionSlice(params.sessionId, sessionConfig);
|
|
310
426
|
if (params.model !== undefined) {
|
|
311
427
|
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
312
428
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
@@ -324,34 +440,20 @@ export class AgentServer {
|
|
|
324
440
|
session.engine.setPlanMode(params.planMode);
|
|
325
441
|
}
|
|
326
442
|
const sid = params.sessionId;
|
|
327
|
-
// Wire AskUserQuestion for this interactive session. The
|
|
328
|
-
// builds a fresh per-session Engine via engineFactory,
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
// chat (and in a resumed automation session). Route it to the client with
|
|
332
|
-
// this session's id so the renderer attributes the question to the right
|
|
333
|
-
// tab, resolving via the session's own pendingApprovals. Skip genuinely
|
|
334
|
-
// headless engines (no human to answer).
|
|
335
|
-
if (!session.engine.isHeadless()) {
|
|
336
|
-
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
337
|
-
// Browser automation bridge: each method routes a browser action to the
|
|
338
|
-
// client (Electron main drives the webview via CDP) over the SAME
|
|
339
|
-
// request/response channel as askUser (pendingApprovals + requestId),
|
|
340
|
-
// reusing its proven resolve/timeout/cleanup. The main-side handler
|
|
341
|
-
// replies with the action's JSON result.
|
|
342
|
-
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
343
|
-
// Cookie→browser injection (InjectCredential tool): same cross-process
|
|
344
|
-
// channel; main restores the cookie jar into the built-in browser.
|
|
345
|
-
session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
|
|
346
|
-
}
|
|
443
|
+
// Wire AskUserQuestion and host bridges for this interactive session. The
|
|
444
|
+
// chatManager path builds a fresh per-session Engine via engineFactory, so
|
|
445
|
+
// these host callbacks must be installed after every create/recreate.
|
|
446
|
+
this.wireInteractiveSession(session, sid);
|
|
347
447
|
try {
|
|
348
448
|
const result = await session.enqueueTurn(params.task, {
|
|
349
449
|
cwd: params.cwd,
|
|
450
|
+
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
350
451
|
goal: typeof params.goal === "string" ||
|
|
351
452
|
(params.goal != null && typeof params.goal === "object")
|
|
352
453
|
? params.goal
|
|
353
454
|
: undefined,
|
|
354
455
|
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
|
|
456
|
+
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
355
457
|
});
|
|
356
458
|
const runResult = {
|
|
357
459
|
text: result.text,
|
|
@@ -431,6 +533,7 @@ export class AgentServer {
|
|
|
431
533
|
sessionId: params.sessionId,
|
|
432
534
|
signal: runController.signal,
|
|
433
535
|
onStream: streamToClient,
|
|
536
|
+
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
434
537
|
goal: typeof params.goal === "string" ||
|
|
435
538
|
(params.goal != null && typeof params.goal === "object")
|
|
436
539
|
? params.goal
|
|
@@ -452,9 +555,7 @@ export class AgentServer {
|
|
|
452
555
|
// the user already knows they pressed stop; clients should
|
|
453
556
|
// just clear busy.
|
|
454
557
|
const errName = err.name;
|
|
455
|
-
const aborted = runController.signal.aborted ||
|
|
456
|
-
errName === "AbortError" ||
|
|
457
|
-
errName === "APIUserAbortError";
|
|
558
|
+
const aborted = runController.signal.aborted || errName === "AbortError" || errName === "APIUserAbortError";
|
|
458
559
|
if (aborted) {
|
|
459
560
|
const cancelledResult = {
|
|
460
561
|
text: "",
|
|
@@ -478,7 +579,10 @@ export class AgentServer {
|
|
|
478
579
|
// ─── Approve ────────────────────────────────────────────────────
|
|
479
580
|
handleApprove(req) {
|
|
480
581
|
const params = (req.params ?? {});
|
|
481
|
-
// ChatSessionManager path:
|
|
582
|
+
// ChatSessionManager path: approvals are scoped by (sessionId, requestId).
|
|
583
|
+
// Never fall back from a session-tagged response to the legacy global map:
|
|
584
|
+
// a stale/misrouted UI response must fail closed instead of resolving a
|
|
585
|
+
// pending prompt from another session.
|
|
482
586
|
if (this.chatManager && typeof params.sessionId === "string") {
|
|
483
587
|
const s = this.chatManager.get(params.sessionId);
|
|
484
588
|
if (!s) {
|
|
@@ -487,25 +591,13 @@ export class AgentServer {
|
|
|
487
591
|
}
|
|
488
592
|
const resolve = s.pendingApprovals.get(params.requestId);
|
|
489
593
|
if (!resolve) {
|
|
490
|
-
|
|
491
|
-
// legacy pending map; the sessionId on their envelope is UI routing
|
|
492
|
-
// metadata. Accept a session-tagged response for those requests too.
|
|
493
|
-
const legacyResolve = this.pendingApprovals.get(params.requestId);
|
|
494
|
-
if (!legacyResolve) {
|
|
495
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval: ${params.requestId}`));
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
this.pendingApprovals.delete(params.requestId);
|
|
499
|
-
this.clearApprovalTimer(params.requestId);
|
|
500
|
-
legacyResolve(params.decision);
|
|
501
|
-
this.transport.send(createResponse(req.id, { ok: true }));
|
|
594
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
|
|
502
595
|
return;
|
|
503
596
|
}
|
|
504
597
|
s.pendingApprovals.delete(params.requestId);
|
|
505
|
-
// Cancel the pending timeout for
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
// APPROVAL_TIMEOUT_MS and churns a dead map entry.
|
|
598
|
+
// Cancel the pending timeout for requests that arm one (browser actions,
|
|
599
|
+
// credential injection, and tool approvals). AskUserQuestion does not use
|
|
600
|
+
// a timeout; this is harmless for those request ids.
|
|
509
601
|
this.clearApprovalTimer(params.requestId);
|
|
510
602
|
resolve(params.decision);
|
|
511
603
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
@@ -539,11 +631,10 @@ export class AgentServer {
|
|
|
539
631
|
s.cancel();
|
|
540
632
|
// s.cancel() only aborts the engine controller + drains queued turns. The
|
|
541
633
|
// session's pendingApprovals (askUser / browser_action / tool approvals)
|
|
542
|
-
// are NOT driven by the abort signal
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// matching timers, mirroring the legacy path below.
|
|
634
|
+
// are NOT driven directly by the abort signal. Left alone, an awaiting
|
|
635
|
+
// AskUserQuestion would now wait forever, while bounded request types
|
|
636
|
+
// would wait until APPROVAL_TIMEOUT_MS. Resolve them as cancelled now and
|
|
637
|
+
// clear any matching timers, mirroring the legacy path below.
|
|
547
638
|
this.cancelSessionApprovals(s);
|
|
548
639
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
549
640
|
return;
|
|
@@ -618,6 +709,12 @@ export class AgentServer {
|
|
|
618
709
|
: params.sessionId
|
|
619
710
|
? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
|
|
620
711
|
: false;
|
|
712
|
+
if (cleared && typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
713
|
+
this.notify(Methods.StreamEvent, {
|
|
714
|
+
sessionId: params.sessionId,
|
|
715
|
+
event: { type: "goal_cleared" },
|
|
716
|
+
});
|
|
717
|
+
}
|
|
621
718
|
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
622
719
|
}
|
|
623
720
|
/**
|
|
@@ -715,7 +812,8 @@ export class AgentServer {
|
|
|
715
812
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
716
813
|
return;
|
|
717
814
|
}
|
|
718
|
-
const
|
|
815
|
+
const scope = params.scope === "all" ? "all" : "session";
|
|
816
|
+
const items = listBackgroundWorkForUI(sessionId, { scope });
|
|
719
817
|
this.transport.send(createResponse(req.id, { items }));
|
|
720
818
|
}
|
|
721
819
|
// ─── CloseSession ───────────────────────────────────────────────
|
|
@@ -726,6 +824,10 @@ export class AgentServer {
|
|
|
726
824
|
return;
|
|
727
825
|
}
|
|
728
826
|
if (this.chatManager) {
|
|
827
|
+
const session = this.chatManager.get(params.sessionId);
|
|
828
|
+
if (session) {
|
|
829
|
+
this.cancelSessionApprovals(session, "session closed");
|
|
830
|
+
}
|
|
729
831
|
this.chatManager.close(params.sessionId);
|
|
730
832
|
}
|
|
731
833
|
// Explicit session teardown — reap that session's background shells
|
|
@@ -738,6 +840,28 @@ export class AgentServer {
|
|
|
738
840
|
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
739
841
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
740
842
|
}
|
|
843
|
+
// ─── ReleaseWorkspace ──────────────────────────────────────────
|
|
844
|
+
handleReleaseWorkspace(req) {
|
|
845
|
+
const params = (req.params ?? {});
|
|
846
|
+
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
847
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (this.chatManager) {
|
|
851
|
+
const session = this.chatManager.get(params.sessionId);
|
|
852
|
+
if (!session) {
|
|
853
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const engine = session.engine;
|
|
857
|
+
const workspace = engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
858
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const engine = this.legacyEngine;
|
|
862
|
+
const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
863
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
864
|
+
}
|
|
741
865
|
// ─── Configure ──────────────────────────────────────────────────
|
|
742
866
|
handleConfigure(req) {
|
|
743
867
|
const params = (req.params ?? {});
|
|
@@ -1031,7 +1155,19 @@ export class AgentServer {
|
|
|
1031
1155
|
return;
|
|
1032
1156
|
}
|
|
1033
1157
|
try {
|
|
1034
|
-
const result = compactEngine.forceCompact(compactSessionId);
|
|
1158
|
+
const result = await compactEngine.forceCompact(compactSessionId);
|
|
1159
|
+
if (result.before > result.after) {
|
|
1160
|
+
const event = {
|
|
1161
|
+
type: "context_compact",
|
|
1162
|
+
strategy: toCompactStreamStrategy(result.strategy),
|
|
1163
|
+
before: result.before,
|
|
1164
|
+
after: result.after,
|
|
1165
|
+
};
|
|
1166
|
+
this.notify(Methods.StreamEvent, {
|
|
1167
|
+
sessionId: compactSessionId ?? "",
|
|
1168
|
+
event,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1035
1171
|
this.transport.send(createResponse(req.id, {
|
|
1036
1172
|
type: "compact",
|
|
1037
1173
|
data: result,
|
|
@@ -1218,8 +1354,7 @@ export class AgentServer {
|
|
|
1218
1354
|
const key = params.key;
|
|
1219
1355
|
if (!key)
|
|
1220
1356
|
throw new Error("provider_refresh: key required");
|
|
1221
|
-
const providers = engine.readSetting("providers") ??
|
|
1222
|
-
[];
|
|
1357
|
+
const providers = engine.readSetting("providers") ?? [];
|
|
1223
1358
|
const p = providers.find((x) => x.key === key);
|
|
1224
1359
|
if (!p)
|
|
1225
1360
|
throw new Error(`provider not found: ${key}`);
|
|
@@ -1255,8 +1390,7 @@ export class AgentServer {
|
|
|
1255
1390
|
if (refs.length > 0) {
|
|
1256
1391
|
throw new Error(`provider ${key} referenced by models: ${refs.join(", ")}`);
|
|
1257
1392
|
}
|
|
1258
|
-
const providers = engine.readSetting("providers") ??
|
|
1259
|
-
[];
|
|
1393
|
+
const providers = engine.readSetting("providers") ?? [];
|
|
1260
1394
|
const next = providers.filter((p) => p.key !== key);
|
|
1261
1395
|
engine.updateConfig("providers", next);
|
|
1262
1396
|
this.transport.send(createResponse(req.id, { type: "provider_delete", data: { ok: true } }));
|
|
@@ -1359,8 +1493,8 @@ export class AgentServer {
|
|
|
1359
1493
|
return;
|
|
1360
1494
|
}
|
|
1361
1495
|
try {
|
|
1362
|
-
engine.enqueueSteer(params.sessionId, params.text, params.id);
|
|
1363
|
-
this.transport.send(createResponse(req.id, { ok: true }));
|
|
1496
|
+
const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId);
|
|
1497
|
+
this.transport.send(createResponse(req.id, { ok: true, ...result }));
|
|
1364
1498
|
}
|
|
1365
1499
|
catch (err) {
|
|
1366
1500
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -1397,10 +1531,23 @@ export class AgentServer {
|
|
|
1397
1531
|
return new Promise((resolve) => {
|
|
1398
1532
|
const requestId = nanoid(12);
|
|
1399
1533
|
const sessionId = typeof request.sessionId === "string" ? request.sessionId : undefined;
|
|
1400
|
-
this.
|
|
1534
|
+
const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
|
|
1535
|
+
if (this.chatManager && sessionId && !session) {
|
|
1536
|
+
resolve({ approved: false, reason: `session closed: ${sessionId}` });
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (session) {
|
|
1540
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1541
|
+
resolve(decision);
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
this.pendingApprovals.set(requestId, resolve);
|
|
1546
|
+
}
|
|
1401
1547
|
const timer = setTimeout(() => {
|
|
1402
|
-
|
|
1403
|
-
|
|
1548
|
+
const pending = session?.pendingApprovals ?? this.pendingApprovals;
|
|
1549
|
+
if (pending.has(requestId)) {
|
|
1550
|
+
pending.delete(requestId);
|
|
1404
1551
|
this.approvalTimers.delete(requestId);
|
|
1405
1552
|
resolve({ approved: false, reason: "approval timed out" });
|
|
1406
1553
|
}
|
|
@@ -1414,14 +1561,11 @@ export class AgentServer {
|
|
|
1414
1561
|
});
|
|
1415
1562
|
}
|
|
1416
1563
|
/**
|
|
1417
|
-
*
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
* (the chatManager approve handler looks there, keyed by sessionId+requestId)
|
|
1423
|
-
* and tags the notify with sessionId so the renderer routes the question to
|
|
1424
|
-
* the right chat tab.
|
|
1564
|
+
* Per-session AskUserQuestion for the chatManager path. Resolves via the
|
|
1565
|
+
* SESSION's pendingApprovals (the chatManager approve handler looks there,
|
|
1566
|
+
* keyed by sessionId+requestId) and tags the notify with sessionId so the
|
|
1567
|
+
* renderer routes the question to the right chat tab. This intentionally has
|
|
1568
|
+
* no wall-clock timeout; Stop/cancel drains the pending ask.
|
|
1425
1569
|
*/
|
|
1426
1570
|
requestAskUserForSession(session, sessionId, question, opts) {
|
|
1427
1571
|
return new Promise((resolve) => {
|
|
@@ -1430,21 +1574,15 @@ export class AgentServer {
|
|
|
1430
1574
|
this.clearApprovalTimer(requestId);
|
|
1431
1575
|
const result = decision;
|
|
1432
1576
|
if (result && typeof result === "object" && "approved" in result) {
|
|
1433
|
-
resolve(result.approved
|
|
1577
|
+
resolve(result.approved
|
|
1578
|
+
? (result.answer ?? "")
|
|
1579
|
+
: (result.reason ?? "(user declined to answer)"));
|
|
1434
1580
|
}
|
|
1435
1581
|
else {
|
|
1436
1582
|
// chatManager approve handler resolves with the raw decision value.
|
|
1437
1583
|
resolve(typeof decision === "string" ? decision : "");
|
|
1438
1584
|
}
|
|
1439
1585
|
});
|
|
1440
|
-
const timer = setTimeout(() => {
|
|
1441
|
-
if (session.pendingApprovals.has(requestId)) {
|
|
1442
|
-
session.pendingApprovals.delete(requestId);
|
|
1443
|
-
this.approvalTimers.delete(requestId);
|
|
1444
|
-
resolve("(approval timed out)");
|
|
1445
|
-
}
|
|
1446
|
-
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1447
|
-
this.approvalTimers.set(requestId, timer);
|
|
1448
1586
|
const args = { question };
|
|
1449
1587
|
if (opts?.header !== undefined)
|
|
1450
1588
|
args.header = opts.header;
|
|
@@ -1464,6 +1602,37 @@ export class AgentServer {
|
|
|
1464
1602
|
riskLevel: "low",
|
|
1465
1603
|
},
|
|
1466
1604
|
});
|
|
1605
|
+
// Goal mode is an unattended, self-driving run: nobody may be watching to
|
|
1606
|
+
// answer, and this ask channel otherwise has NO wall-clock timeout — so a
|
|
1607
|
+
// model that calls AskUserQuestion mid-goal would suspend the turn loop
|
|
1608
|
+
// forever (only a manual Stop could unblock it). When a goal is active,
|
|
1609
|
+
// arm the shared 10-minute approval timeout: on expiry, drain the pending
|
|
1610
|
+
// ask and resolve with a nudge so the loop keeps making progress instead
|
|
1611
|
+
// of hanging. Plain interactive (no-goal) asks keep their intentional
|
|
1612
|
+
// no-timeout behavior — a human can take as long as they like.
|
|
1613
|
+
let goalActive;
|
|
1614
|
+
try {
|
|
1615
|
+
goalActive = !!session.getGoal();
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
goalActive = false;
|
|
1619
|
+
}
|
|
1620
|
+
if (goalActive) {
|
|
1621
|
+
const timer = setTimeout(() => {
|
|
1622
|
+
const resolveFn = session.pendingApprovals.get(requestId);
|
|
1623
|
+
if (resolveFn) {
|
|
1624
|
+
session.pendingApprovals.delete(requestId);
|
|
1625
|
+
this.approvalTimers.delete(requestId);
|
|
1626
|
+
resolveFn("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
|
|
1627
|
+
// Tell every client the ask is resolved (nobody answered) so the
|
|
1628
|
+
// stale AskUserQuestion card is dismissed instead of lingering as
|
|
1629
|
+
// if still waiting. Reuses the same envelope shape the desktop main
|
|
1630
|
+
// already broadcasts / the renderer's onApprovalResolved consumes.
|
|
1631
|
+
this.notify(Methods.ApprovalResolved, { sessionId, requestId });
|
|
1632
|
+
}
|
|
1633
|
+
}, AgentServer.GOAL_ASKUSER_TIMEOUT_MS);
|
|
1634
|
+
this.approvalTimers.set(requestId, timer);
|
|
1635
|
+
}
|
|
1467
1636
|
});
|
|
1468
1637
|
}
|
|
1469
1638
|
/**
|
|
@@ -1545,7 +1714,7 @@ export class AgentServer {
|
|
|
1545
1714
|
* calls restoreCookiesToBrowser and replies with a JSON result string we
|
|
1546
1715
|
* parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
|
|
1547
1716
|
*/
|
|
1548
|
-
requestCredentialInjectForSession(session, sessionId, credentialId) {
|
|
1717
|
+
requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
|
|
1549
1718
|
return new Promise((resolve) => {
|
|
1550
1719
|
const requestId = nanoid(12);
|
|
1551
1720
|
session.pendingApprovals.set(requestId, (decision) => {
|
|
@@ -1582,13 +1751,72 @@ export class AgentServer {
|
|
|
1582
1751
|
requestId,
|
|
1583
1752
|
request: {
|
|
1584
1753
|
toolName: "__credential_action__",
|
|
1585
|
-
args: { action: "injectCookie", credentialId },
|
|
1754
|
+
args: { action: "injectCookie", credentialId, credentialScope },
|
|
1586
1755
|
description: `credential:inject:${credentialId}`,
|
|
1587
1756
|
riskLevel: "low",
|
|
1588
1757
|
},
|
|
1589
1758
|
});
|
|
1590
1759
|
});
|
|
1591
1760
|
}
|
|
1761
|
+
makeWorkspaceBridge(session, sessionId) {
|
|
1762
|
+
return {
|
|
1763
|
+
switch: (target) => this.requestWorkspaceSwitchForSession(session, sessionId, target),
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
requestWorkspaceSwitchForSession(session, sessionId, target) {
|
|
1767
|
+
return new Promise((resolve, reject) => {
|
|
1768
|
+
const requestId = nanoid(12);
|
|
1769
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1770
|
+
this.clearApprovalTimer(requestId);
|
|
1771
|
+
let raw;
|
|
1772
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
1773
|
+
const r = decision;
|
|
1774
|
+
raw = r.approved ? r.answer : undefined;
|
|
1775
|
+
}
|
|
1776
|
+
else if (typeof decision === "string") {
|
|
1777
|
+
raw = decision;
|
|
1778
|
+
}
|
|
1779
|
+
if (raw === undefined) {
|
|
1780
|
+
reject(new Error("workspace switch declined or unavailable"));
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
const parsed = JSON.parse(raw);
|
|
1785
|
+
if ("ok" in parsed && parsed.ok === false) {
|
|
1786
|
+
reject(new Error(parsed.error ?? "workspace switch failed"));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
resolve(parsed);
|
|
1790
|
+
}
|
|
1791
|
+
catch {
|
|
1792
|
+
reject(new Error("malformed workspace switch result"));
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
const timer = setTimeout(() => {
|
|
1796
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
1797
|
+
session.pendingApprovals.delete(requestId);
|
|
1798
|
+
this.approvalTimers.delete(requestId);
|
|
1799
|
+
reject(new Error("workspace switch timed out"));
|
|
1800
|
+
}
|
|
1801
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1802
|
+
this.approvalTimers.set(requestId, timer);
|
|
1803
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1804
|
+
sessionId,
|
|
1805
|
+
requestId,
|
|
1806
|
+
request: {
|
|
1807
|
+
toolName: "__workspace_action__",
|
|
1808
|
+
args: { action: "switch", target },
|
|
1809
|
+
description: `workspace:switch:${target}`,
|
|
1810
|
+
riskLevel: "low",
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Ask the client to answer a question from the agent (legacy single-engine
|
|
1817
|
+
* path). This intentionally has no wall-clock timeout; Stop/cancel drains
|
|
1818
|
+
* the pending ask.
|
|
1819
|
+
*/
|
|
1592
1820
|
requestAskUserFromClient(question, opts) {
|
|
1593
1821
|
return new Promise((resolve) => {
|
|
1594
1822
|
const requestId = nanoid(12);
|
|
@@ -1601,14 +1829,6 @@ export class AgentServer {
|
|
|
1601
1829
|
resolve(result.reason ?? "(user declined to answer)");
|
|
1602
1830
|
}
|
|
1603
1831
|
});
|
|
1604
|
-
const timer = setTimeout(() => {
|
|
1605
|
-
if (this.pendingApprovals.has(requestId)) {
|
|
1606
|
-
this.pendingApprovals.delete(requestId);
|
|
1607
|
-
this.approvalTimers.delete(requestId);
|
|
1608
|
-
resolve("(approval timed out)");
|
|
1609
|
-
}
|
|
1610
|
-
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1611
|
-
this.approvalTimers.set(requestId, timer);
|
|
1612
1832
|
const args = { question };
|
|
1613
1833
|
if (opts?.header !== undefined)
|
|
1614
1834
|
args.header = opts.header;
|
|
@@ -1670,6 +1890,9 @@ export class AgentServer {
|
|
|
1670
1890
|
this.bgAgentBusUnsubscribe = null;
|
|
1671
1891
|
}
|
|
1672
1892
|
if (this.chatManager) {
|
|
1893
|
+
this.chatManager.forEachSession((session) => {
|
|
1894
|
+
this.cancelSessionApprovals(session, "server closing");
|
|
1895
|
+
});
|
|
1673
1896
|
this.chatManager.closeAll();
|
|
1674
1897
|
}
|
|
1675
1898
|
// Legacy path cleanup
|
|
@@ -1698,17 +1921,16 @@ export class AgentServer {
|
|
|
1698
1921
|
}
|
|
1699
1922
|
/**
|
|
1700
1923
|
* Resolve all of a chat session's pending approvals as cancelled and clear
|
|
1701
|
-
*
|
|
1924
|
+
* any matching server-side approval timers. Used by handleCancel's
|
|
1702
1925
|
* per-session path so a Stop while a tool is awaiting approval doesn't leave
|
|
1703
|
-
* the tool hanging
|
|
1704
|
-
*
|
|
1705
|
-
* (see requestAskUserForSession / makeBrowserBridge).
|
|
1926
|
+
* the tool hanging. Bounded request types have same-keyed timer entries;
|
|
1927
|
+
* AskUserQuestion does not.
|
|
1706
1928
|
*/
|
|
1707
|
-
cancelSessionApprovals(session) {
|
|
1929
|
+
cancelSessionApprovals(session, reason = "cancelled") {
|
|
1708
1930
|
for (const [requestId, resolve] of session.pendingApprovals) {
|
|
1709
1931
|
this.clearApprovalTimer(requestId);
|
|
1710
1932
|
try {
|
|
1711
|
-
resolve({ approved: false, reason
|
|
1933
|
+
resolve({ approved: false, reason });
|
|
1712
1934
|
}
|
|
1713
1935
|
catch {
|
|
1714
1936
|
/* a resolver must never break cancel cleanup */
|