@cjhyy/code-shell-core 0.6.0-rc.1 → 0.6.0-rc.10
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/context/compaction.d.ts +30 -0
- package/dist/context/compaction.js +93 -0
- package/dist/context/manager.d.ts +18 -0
- package/dist/context/manager.js +156 -44
- package/dist/context/token-counter.js +13 -0
- package/dist/engine/engine.d.ts +22 -12
- package/dist/engine/engine.js +263 -81
- package/dist/engine/model-connections-pool.js +1 -0
- package/dist/engine/model-facade.js +2 -12
- package/dist/engine/query.js +2 -0
- package/dist/engine/runtime.d.ts +2 -0
- package/dist/engine/runtime.js +25 -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/turn-loop.d.ts +28 -2
- package/dist/engine/turn-loop.js +153 -26
- package/dist/git/utils.d.ts +12 -0
- package/dist/git/utils.js +33 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/llm/capabilities/rules.js +1 -1
- package/dist/llm/model-pool.d.ts +7 -0
- package/dist/llm/model-pool.js +8 -1
- package/dist/model-catalog/builtin.js +6 -1
- package/dist/preset/index.d.ts +5 -1
- package/dist/preset/index.js +21 -2
- package/dist/prompt/composer.d.ts +5 -0
- package/dist/prompt/composer.js +10 -2
- package/dist/prompt/sections/base.md +1 -0
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +2 -0
- package/dist/protocol/chat-session.d.ts +4 -1
- package/dist/protocol/chat-session.js +9 -3
- package/dist/protocol/client.d.ts +5 -1
- package/dist/protocol/client.js +8 -2
- package/dist/protocol/server.d.ts +13 -12
- package/dist/protocol/server.js +199 -67
- package/dist/protocol/types.d.ts +14 -0
- package/dist/runtime/background-shell.js +14 -0
- package/dist/runtime/safe-spawn.js +89 -11
- package/dist/runtime/spawn-common.d.ts +15 -4
- package/dist/runtime/spawn-common.js +113 -12
- package/dist/session/session-manager.js +7 -1
- package/dist/session/transcript.d.ts +4 -0
- package/dist/session/transcript.js +21 -0
- package/dist/tool-system/builtin/bash.js +3 -2
- package/dist/tool-system/builtin/cron.js +10 -2
- package/dist/tool-system/builtin/edit-model-catalog.js +15 -5
- package/dist/tool-system/builtin/generate-video.js +3 -0
- package/dist/tool-system/builtin/grep.d.ts +9 -0
- package/dist/tool-system/builtin/grep.js +100 -3
- package/dist/tool-system/builtin/index.d.ts +3 -1
- package/dist/tool-system/builtin/index.js +5 -5
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/sleep.js +5 -0
- package/dist/tool-system/context.d.ts +10 -0
- package/dist/tool-system/executor.js +25 -2
- package/dist/tool-system/mcp-manager.js +17 -0
- 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/permission.d.ts +3 -1
- package/dist/tool-system/permission.js +2 -1
- package/dist/tool-system/sandbox/off.js +7 -1
- package/dist/types.d.ts +35 -1
- package/dist/utils/exec.d.ts +8 -0
- package/dist/utils/exec.js +10 -0
- package/package.json +1 -1
package/dist/protocol/server.js
CHANGED
|
@@ -26,6 +26,19 @@ import { logger } from "../logging/logger.js";
|
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
27
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
28
28
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
29
|
+
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
30
|
+
"micro",
|
|
31
|
+
"summary",
|
|
32
|
+
"window",
|
|
33
|
+
"snip",
|
|
34
|
+
"emergency",
|
|
35
|
+
"compacted",
|
|
36
|
+
]);
|
|
37
|
+
function toCompactStreamStrategy(strategy) {
|
|
38
|
+
return COMPACT_STREAM_STRATEGIES.has(strategy)
|
|
39
|
+
? strategy
|
|
40
|
+
: "compacted";
|
|
41
|
+
}
|
|
29
42
|
export class AgentServer {
|
|
30
43
|
chatManager;
|
|
31
44
|
legacyEngine;
|
|
@@ -307,6 +320,19 @@ export class AgentServer {
|
|
|
307
320
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
308
321
|
return;
|
|
309
322
|
}
|
|
323
|
+
if (params.model !== undefined) {
|
|
324
|
+
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
325
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
session.requestModelSwitch(params.model);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
310
336
|
if (typeof params.planMode === "boolean") {
|
|
311
337
|
session.engine.setPlanMode(params.planMode);
|
|
312
338
|
}
|
|
@@ -323,9 +349,9 @@ export class AgentServer {
|
|
|
323
349
|
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
324
350
|
// Browser automation bridge: each method routes a browser action to the
|
|
325
351
|
// client (Electron main drives the webview via CDP) over the SAME
|
|
326
|
-
// request/response channel as askUser (pendingApprovals + requestId)
|
|
327
|
-
//
|
|
328
|
-
//
|
|
352
|
+
// request/response channel as askUser (pendingApprovals + requestId).
|
|
353
|
+
// Browser actions still keep their own bounded timeout; AskUserQuestion
|
|
354
|
+
// waits for a real answer or Stop/cancel.
|
|
329
355
|
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
330
356
|
// Cookie→browser injection (InjectCredential tool): same cross-process
|
|
331
357
|
// channel; main restores the cookie jar into the built-in browser.
|
|
@@ -339,6 +365,7 @@ export class AgentServer {
|
|
|
339
365
|
? params.goal
|
|
340
366
|
: undefined,
|
|
341
367
|
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
|
|
368
|
+
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
342
369
|
});
|
|
343
370
|
const runResult = {
|
|
344
371
|
text: result.text,
|
|
@@ -387,6 +414,19 @@ export class AgentServer {
|
|
|
387
414
|
// Engine.setPermissionMode now keeps this.permissionMode + this.planMode
|
|
388
415
|
// in sync and tools read them via ToolContext.permissionMode/planMode.
|
|
389
416
|
}
|
|
417
|
+
if (params.model !== undefined) {
|
|
418
|
+
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
419
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
this.legacyEngine.switchModel(params.model);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
390
430
|
this.running = true;
|
|
391
431
|
this.abortController = new AbortController();
|
|
392
432
|
this.notify(Methods.Status, { status: "running" });
|
|
@@ -405,6 +445,7 @@ export class AgentServer {
|
|
|
405
445
|
sessionId: params.sessionId,
|
|
406
446
|
signal: runController.signal,
|
|
407
447
|
onStream: streamToClient,
|
|
448
|
+
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
408
449
|
goal: typeof params.goal === "string" ||
|
|
409
450
|
(params.goal != null && typeof params.goal === "object")
|
|
410
451
|
? params.goal
|
|
@@ -452,7 +493,10 @@ export class AgentServer {
|
|
|
452
493
|
// ─── Approve ────────────────────────────────────────────────────
|
|
453
494
|
handleApprove(req) {
|
|
454
495
|
const params = (req.params ?? {});
|
|
455
|
-
// ChatSessionManager path:
|
|
496
|
+
// ChatSessionManager path: approvals are scoped by (sessionId, requestId).
|
|
497
|
+
// Never fall back from a session-tagged response to the legacy global map:
|
|
498
|
+
// a stale/misrouted UI response must fail closed instead of resolving a
|
|
499
|
+
// pending prompt from another session.
|
|
456
500
|
if (this.chatManager && typeof params.sessionId === "string") {
|
|
457
501
|
const s = this.chatManager.get(params.sessionId);
|
|
458
502
|
if (!s) {
|
|
@@ -461,14 +505,13 @@ export class AgentServer {
|
|
|
461
505
|
}
|
|
462
506
|
const resolve = s.pendingApprovals.get(params.requestId);
|
|
463
507
|
if (!resolve) {
|
|
464
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval: ${params.requestId}`));
|
|
508
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
|
|
465
509
|
return;
|
|
466
510
|
}
|
|
467
511
|
s.pendingApprovals.delete(params.requestId);
|
|
468
|
-
// Cancel the pending timeout for
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
// APPROVAL_TIMEOUT_MS and churns a dead map entry.
|
|
512
|
+
// Cancel the pending timeout for requests that arm one (browser actions,
|
|
513
|
+
// credential injection, and tool approvals). AskUserQuestion does not use
|
|
514
|
+
// a timeout; this is harmless for those request ids.
|
|
472
515
|
this.clearApprovalTimer(params.requestId);
|
|
473
516
|
resolve(params.decision);
|
|
474
517
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
@@ -502,11 +545,10 @@ export class AgentServer {
|
|
|
502
545
|
s.cancel();
|
|
503
546
|
// s.cancel() only aborts the engine controller + drains queued turns. The
|
|
504
547
|
// session's pendingApprovals (askUser / browser_action / tool approvals)
|
|
505
|
-
// are NOT driven by the abort signal
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
// matching timers, mirroring the legacy path below.
|
|
548
|
+
// are NOT driven directly by the abort signal. Left alone, an awaiting
|
|
549
|
+
// AskUserQuestion would now wait forever, while bounded request types
|
|
550
|
+
// would wait until APPROVAL_TIMEOUT_MS. Resolve them as cancelled now and
|
|
551
|
+
// clear any matching timers, mirroring the legacy path below.
|
|
510
552
|
this.cancelSessionApprovals(s);
|
|
511
553
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
512
554
|
return;
|
|
@@ -689,6 +731,10 @@ export class AgentServer {
|
|
|
689
731
|
return;
|
|
690
732
|
}
|
|
691
733
|
if (this.chatManager) {
|
|
734
|
+
const session = this.chatManager.get(params.sessionId);
|
|
735
|
+
if (session) {
|
|
736
|
+
this.cancelSessionApprovals(session, "session closed");
|
|
737
|
+
}
|
|
692
738
|
this.chatManager.close(params.sessionId);
|
|
693
739
|
}
|
|
694
740
|
// Explicit session teardown — reap that session's background shells
|
|
@@ -709,7 +755,11 @@ export class AgentServer {
|
|
|
709
755
|
const sid = params.sessionId;
|
|
710
756
|
const s = this.chatManager.get(sid);
|
|
711
757
|
if (!s) {
|
|
712
|
-
|
|
758
|
+
// Session not found (already cleaned by idle sweeper, or never created).
|
|
759
|
+
// Don't create one just for configure — let the subsequent run() do it
|
|
760
|
+
// with proper per-session config. Return OK since there's nothing to
|
|
761
|
+
// configure on a non-existent session.
|
|
762
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
713
763
|
return;
|
|
714
764
|
}
|
|
715
765
|
if (typeof params.planMode === "boolean")
|
|
@@ -717,13 +767,32 @@ export class AgentServer {
|
|
|
717
767
|
if (typeof params.permissionMode === "string") {
|
|
718
768
|
s.engine.setPermissionMode(params.permissionMode);
|
|
719
769
|
}
|
|
770
|
+
if (params.clearModels) {
|
|
771
|
+
this.chatManager.runtime.clearModels();
|
|
772
|
+
}
|
|
773
|
+
if (params.reloadModels) {
|
|
774
|
+
try {
|
|
775
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
720
782
|
// Per-session model switch — the missing piece that made model changes
|
|
721
783
|
// worker-global (session-isolation research §3). requestModelSwitch
|
|
722
784
|
// applies immediately when idle, defers to the run boundary when busy
|
|
723
785
|
// so it never swaps the model under a running LLM client.
|
|
724
786
|
if (typeof params.model === "string") {
|
|
725
787
|
try {
|
|
726
|
-
s.requestModelSwitch(params.model);
|
|
788
|
+
const entry = s.requestModelSwitch(params.model);
|
|
789
|
+
this.transport.send(createResponse(req.id, {
|
|
790
|
+
ok: true,
|
|
791
|
+
model: entry.model,
|
|
792
|
+
key: entry.key,
|
|
793
|
+
maxContextTokens: entry.maxContextTokens,
|
|
794
|
+
}));
|
|
795
|
+
return;
|
|
727
796
|
}
|
|
728
797
|
catch (err) {
|
|
729
798
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
@@ -746,22 +815,33 @@ export class AgentServer {
|
|
|
746
815
|
// Global configure — delegate to legacyEngine if available,
|
|
747
816
|
// or to any session's engine from chatManager for settings ops
|
|
748
817
|
const engine = this.legacyEngine ?? this.anyEngine();
|
|
818
|
+
if (params.clearModels) {
|
|
819
|
+
if (this.chatManager) {
|
|
820
|
+
this.chatManager.runtime.clearModels();
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
this.legacyEngine?.getModelPool().clear();
|
|
824
|
+
this.globalQueryEngine?.getModelPool().clear();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
749
827
|
if (params.reloadModels) {
|
|
750
828
|
try {
|
|
751
|
-
const seen = new Set();
|
|
752
|
-
const reload = (target) => {
|
|
753
|
-
if (!target || seen.has(target))
|
|
754
|
-
return;
|
|
755
|
-
seen.add(target);
|
|
756
|
-
target.reloadModelPool();
|
|
757
|
-
};
|
|
758
|
-
reload(this.legacyEngine);
|
|
759
|
-
reload(this.globalQueryEngine);
|
|
760
829
|
if (this.chatManager) {
|
|
761
|
-
this.chatManager.
|
|
830
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
const seen = new Set();
|
|
834
|
+
const reload = (target) => {
|
|
835
|
+
if (!target || seen.has(target))
|
|
836
|
+
return;
|
|
837
|
+
seen.add(target);
|
|
838
|
+
target.reloadModelPool();
|
|
839
|
+
};
|
|
840
|
+
reload(this.legacyEngine);
|
|
841
|
+
reload(this.globalQueryEngine);
|
|
842
|
+
if (seen.size === 0)
|
|
843
|
+
reload(engine);
|
|
762
844
|
}
|
|
763
|
-
if (seen.size === 0)
|
|
764
|
-
reload(engine);
|
|
765
845
|
}
|
|
766
846
|
catch (err) {
|
|
767
847
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -771,7 +851,12 @@ export class AgentServer {
|
|
|
771
851
|
if (params.model !== undefined && engine) {
|
|
772
852
|
try {
|
|
773
853
|
const entry = engine.switchModel(params.model);
|
|
774
|
-
this.transport.send(createResponse(req.id, {
|
|
854
|
+
this.transport.send(createResponse(req.id, {
|
|
855
|
+
ok: true,
|
|
856
|
+
model: entry.model,
|
|
857
|
+
key: entry.key,
|
|
858
|
+
maxContextTokens: entry.maxContextTokens,
|
|
859
|
+
}));
|
|
775
860
|
return;
|
|
776
861
|
}
|
|
777
862
|
catch (err) {
|
|
@@ -921,12 +1006,53 @@ export class AgentServer {
|
|
|
921
1006
|
break;
|
|
922
1007
|
}
|
|
923
1008
|
case "compact": {
|
|
924
|
-
|
|
1009
|
+
const compactSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
1010
|
+
? params.sessionId
|
|
1011
|
+
: undefined;
|
|
1012
|
+
let compactEngine = engine;
|
|
1013
|
+
if (this.chatManager) {
|
|
1014
|
+
if (compactSessionId) {
|
|
1015
|
+
let session = this.chatManager.get(compactSessionId);
|
|
1016
|
+
if (!session) {
|
|
1017
|
+
const probeEngine = this.anyEngine();
|
|
1018
|
+
if (!probeEngine?.sessionExistsOnDisk(compactSessionId)) {
|
|
1019
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${compactSessionId}`));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
session = this.chatManager.getOrCreate(compactSessionId, {
|
|
1024
|
+
cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch (err) {
|
|
1028
|
+
this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
compactEngine = session.engine;
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
compactEngine = this.anyEngine();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (!compactEngine) {
|
|
925
1039
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for compact query"));
|
|
926
1040
|
return;
|
|
927
1041
|
}
|
|
928
1042
|
try {
|
|
929
|
-
const result =
|
|
1043
|
+
const result = await compactEngine.forceCompact(compactSessionId);
|
|
1044
|
+
if (result.before > result.after) {
|
|
1045
|
+
const event = {
|
|
1046
|
+
type: "context_compact",
|
|
1047
|
+
strategy: toCompactStreamStrategy(result.strategy),
|
|
1048
|
+
before: result.before,
|
|
1049
|
+
after: result.after,
|
|
1050
|
+
};
|
|
1051
|
+
this.notify(Methods.StreamEvent, {
|
|
1052
|
+
sessionId: compactSessionId ?? "",
|
|
1053
|
+
event,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
930
1056
|
this.transport.send(createResponse(req.id, {
|
|
931
1057
|
type: "compact",
|
|
932
1058
|
data: result,
|
|
@@ -1254,8 +1380,8 @@ export class AgentServer {
|
|
|
1254
1380
|
return;
|
|
1255
1381
|
}
|
|
1256
1382
|
try {
|
|
1257
|
-
engine.enqueueSteer(params.sessionId, params.text, params.id);
|
|
1258
|
-
this.transport.send(createResponse(req.id, { ok: true }));
|
|
1383
|
+
const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId);
|
|
1384
|
+
this.transport.send(createResponse(req.id, { ok: true, ...result }));
|
|
1259
1385
|
}
|
|
1260
1386
|
catch (err) {
|
|
1261
1387
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -1291,27 +1417,42 @@ export class AgentServer {
|
|
|
1291
1417
|
requestApprovalFromClient(request) {
|
|
1292
1418
|
return new Promise((resolve) => {
|
|
1293
1419
|
const requestId = nanoid(12);
|
|
1294
|
-
|
|
1420
|
+
const sessionId = typeof request.sessionId === "string" ? request.sessionId : undefined;
|
|
1421
|
+
const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
|
|
1422
|
+
if (this.chatManager && sessionId && !session) {
|
|
1423
|
+
resolve({ approved: false, reason: `session closed: ${sessionId}` });
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (session) {
|
|
1427
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1428
|
+
resolve(decision);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
this.pendingApprovals.set(requestId, resolve);
|
|
1433
|
+
}
|
|
1295
1434
|
const timer = setTimeout(() => {
|
|
1296
|
-
|
|
1297
|
-
|
|
1435
|
+
const pending = session?.pendingApprovals ?? this.pendingApprovals;
|
|
1436
|
+
if (pending.has(requestId)) {
|
|
1437
|
+
pending.delete(requestId);
|
|
1298
1438
|
this.approvalTimers.delete(requestId);
|
|
1299
1439
|
resolve({ approved: false, reason: "approval timed out" });
|
|
1300
1440
|
}
|
|
1301
1441
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1302
1442
|
this.approvalTimers.set(requestId, timer);
|
|
1303
|
-
this.notify(Methods.ApprovalRequest, {
|
|
1443
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1444
|
+
...(sessionId ? { sessionId } : {}),
|
|
1445
|
+
requestId,
|
|
1446
|
+
request,
|
|
1447
|
+
});
|
|
1304
1448
|
});
|
|
1305
1449
|
}
|
|
1306
1450
|
/**
|
|
1307
|
-
*
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
* (the chatManager approve handler looks there, keyed by sessionId+requestId)
|
|
1313
|
-
* and tags the notify with sessionId so the renderer routes the question to
|
|
1314
|
-
* the right chat tab.
|
|
1451
|
+
* Per-session AskUserQuestion for the chatManager path. Resolves via the
|
|
1452
|
+
* SESSION's pendingApprovals (the chatManager approve handler looks there,
|
|
1453
|
+
* keyed by sessionId+requestId) and tags the notify with sessionId so the
|
|
1454
|
+
* renderer routes the question to the right chat tab. This intentionally has
|
|
1455
|
+
* no wall-clock timeout; Stop/cancel drains the pending ask.
|
|
1315
1456
|
*/
|
|
1316
1457
|
requestAskUserForSession(session, sessionId, question, opts) {
|
|
1317
1458
|
return new Promise((resolve) => {
|
|
@@ -1327,14 +1468,6 @@ export class AgentServer {
|
|
|
1327
1468
|
resolve(typeof decision === "string" ? decision : "");
|
|
1328
1469
|
}
|
|
1329
1470
|
});
|
|
1330
|
-
const timer = setTimeout(() => {
|
|
1331
|
-
if (session.pendingApprovals.has(requestId)) {
|
|
1332
|
-
session.pendingApprovals.delete(requestId);
|
|
1333
|
-
this.approvalTimers.delete(requestId);
|
|
1334
|
-
resolve("(approval timed out)");
|
|
1335
|
-
}
|
|
1336
|
-
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1337
|
-
this.approvalTimers.set(requestId, timer);
|
|
1338
1471
|
const args = { question };
|
|
1339
1472
|
if (opts?.header !== undefined)
|
|
1340
1473
|
args.header = opts.header;
|
|
@@ -1479,6 +1612,11 @@ export class AgentServer {
|
|
|
1479
1612
|
});
|
|
1480
1613
|
});
|
|
1481
1614
|
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Ask the client to answer a question from the agent (legacy single-engine
|
|
1617
|
+
* path). This intentionally has no wall-clock timeout; Stop/cancel drains
|
|
1618
|
+
* the pending ask.
|
|
1619
|
+
*/
|
|
1482
1620
|
requestAskUserFromClient(question, opts) {
|
|
1483
1621
|
return new Promise((resolve) => {
|
|
1484
1622
|
const requestId = nanoid(12);
|
|
@@ -1491,14 +1629,6 @@ export class AgentServer {
|
|
|
1491
1629
|
resolve(result.reason ?? "(user declined to answer)");
|
|
1492
1630
|
}
|
|
1493
1631
|
});
|
|
1494
|
-
const timer = setTimeout(() => {
|
|
1495
|
-
if (this.pendingApprovals.has(requestId)) {
|
|
1496
|
-
this.pendingApprovals.delete(requestId);
|
|
1497
|
-
this.approvalTimers.delete(requestId);
|
|
1498
|
-
resolve("(approval timed out)");
|
|
1499
|
-
}
|
|
1500
|
-
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1501
|
-
this.approvalTimers.set(requestId, timer);
|
|
1502
1632
|
const args = { question };
|
|
1503
1633
|
if (opts?.header !== undefined)
|
|
1504
1634
|
args.header = opts.header;
|
|
@@ -1560,6 +1690,9 @@ export class AgentServer {
|
|
|
1560
1690
|
this.bgAgentBusUnsubscribe = null;
|
|
1561
1691
|
}
|
|
1562
1692
|
if (this.chatManager) {
|
|
1693
|
+
this.chatManager.forEachSession((session) => {
|
|
1694
|
+
this.cancelSessionApprovals(session, "server closing");
|
|
1695
|
+
});
|
|
1563
1696
|
this.chatManager.closeAll();
|
|
1564
1697
|
}
|
|
1565
1698
|
// Legacy path cleanup
|
|
@@ -1588,17 +1721,16 @@ export class AgentServer {
|
|
|
1588
1721
|
}
|
|
1589
1722
|
/**
|
|
1590
1723
|
* Resolve all of a chat session's pending approvals as cancelled and clear
|
|
1591
|
-
*
|
|
1724
|
+
* any matching server-side approval timers. Used by handleCancel's
|
|
1592
1725
|
* per-session path so a Stop while a tool is awaiting approval doesn't leave
|
|
1593
|
-
* the tool hanging
|
|
1594
|
-
*
|
|
1595
|
-
* (see requestAskUserForSession / makeBrowserBridge).
|
|
1726
|
+
* the tool hanging. Bounded request types have same-keyed timer entries;
|
|
1727
|
+
* AskUserQuestion does not.
|
|
1596
1728
|
*/
|
|
1597
|
-
cancelSessionApprovals(session) {
|
|
1729
|
+
cancelSessionApprovals(session, reason = "cancelled") {
|
|
1598
1730
|
for (const [requestId, resolve] of session.pendingApprovals) {
|
|
1599
1731
|
this.clearApprovalTimer(requestId);
|
|
1600
1732
|
try {
|
|
1601
|
-
resolve({ approved: false, reason
|
|
1733
|
+
resolve({ approved: false, reason });
|
|
1602
1734
|
}
|
|
1603
1735
|
catch {
|
|
1604
1736
|
/* a resolver must never break cancel cleanup */
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export declare const ErrorCodes: {
|
|
|
51
51
|
export interface RunParams {
|
|
52
52
|
sessionId: string;
|
|
53
53
|
task: string;
|
|
54
|
+
/** Stable id for the user's submit intent; duplicate ids are idempotent. */
|
|
55
|
+
clientMessageId?: string;
|
|
54
56
|
/**
|
|
55
57
|
* Working directory for this run. When omitted, the Engine uses its
|
|
56
58
|
* configured cwd.
|
|
@@ -61,6 +63,12 @@ export interface RunParams {
|
|
|
61
63
|
* When omitted, the engine keeps its configured default.
|
|
62
64
|
*/
|
|
63
65
|
permissionMode?: PermissionMode;
|
|
66
|
+
/**
|
|
67
|
+
* Per-run model pool key. Applied after the session exists and before the
|
|
68
|
+
* turn starts, so cold desktop runs don't need a separate pre-run configure
|
|
69
|
+
* request to a worker that has not been spawned yet.
|
|
70
|
+
*/
|
|
71
|
+
model?: string;
|
|
64
72
|
/**
|
|
65
73
|
* Workspace trust for this run's project (`cwd`), asserted by the host
|
|
66
74
|
* (desktop main from its trust-store) — never by the renderer. When false,
|
|
@@ -127,6 +135,8 @@ export interface SteerParams {
|
|
|
127
135
|
/** Stable host-side id for this queued draft. Rides through to the
|
|
128
136
|
* steer_injected event and is the handle Unsteer uses to revoke it. */
|
|
129
137
|
id?: string;
|
|
138
|
+
/** Stable submit-intent id, distinct from the queued steer id. */
|
|
139
|
+
clientMessageId?: string;
|
|
130
140
|
}
|
|
131
141
|
/** Revoke a still-pending steer entry by id (before the loop consumes it). */
|
|
132
142
|
export interface UnsteerParams {
|
|
@@ -149,6 +159,8 @@ export interface ConfigureParams {
|
|
|
149
159
|
* running engine picks them up without a process restart.
|
|
150
160
|
*/
|
|
151
161
|
reloadModels?: boolean;
|
|
162
|
+
/** Clear the live model pool, used after logout removes saved credentials. */
|
|
163
|
+
clearModels?: boolean;
|
|
152
164
|
/**
|
|
153
165
|
* Re-read disk settings and hot-push the disk-default config fields (preset /
|
|
154
166
|
* customSystemPrompt / appendSystemPrompt / personalization / mcpServers) +
|
|
@@ -243,6 +255,8 @@ export interface AgentStreamEventNotification {
|
|
|
243
255
|
}
|
|
244
256
|
/** Server requests approval from the client (UI). */
|
|
245
257
|
export interface ApprovalRequestNotification {
|
|
258
|
+
/** Originating engine session when known. */
|
|
259
|
+
sessionId?: string;
|
|
246
260
|
requestId: string;
|
|
247
261
|
request: ApprovalRequest;
|
|
248
262
|
}
|
|
@@ -89,6 +89,7 @@ export class BackgroundShellManager {
|
|
|
89
89
|
error: `Too many background shells for this session (max ${MAX_SHELLS_PER_SESSION}). KillShell some first.`,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
const profileStartedAt = backgroundSpawnProfileEnabled() ? performance.now() : 0;
|
|
92
93
|
const shell = opts.shell ?? defaultShellBinary();
|
|
93
94
|
const { file, args } = resolveSpawnTarget(opts.command, {
|
|
94
95
|
cwd: opts.cwd,
|
|
@@ -113,8 +114,10 @@ export class BackgroundShellManager {
|
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
catch (err) {
|
|
117
|
+
logBackgroundSpawnProfile(file, args, elapsedProfileMs(profileStartedAt), "spawn_failed");
|
|
116
118
|
return { ok: false, error: `Failed to spawn background shell: ${err.message}` };
|
|
117
119
|
}
|
|
120
|
+
logBackgroundSpawnProfile(file, args, elapsedProfileMs(profileStartedAt), "started");
|
|
118
121
|
if (child.pid === undefined) {
|
|
119
122
|
return { ok: false, error: "Failed to spawn background shell: no pid" };
|
|
120
123
|
}
|
|
@@ -493,3 +496,14 @@ export class BackgroundShellManager {
|
|
|
493
496
|
}
|
|
494
497
|
/** Process-local singleton, mirroring asyncAgentRegistry's lifetime contract. */
|
|
495
498
|
export const backgroundShellManager = new BackgroundShellManager();
|
|
499
|
+
function backgroundSpawnProfileEnabled() {
|
|
500
|
+
return process.env.CODESHELL_SPAWN_PROFILE === "1";
|
|
501
|
+
}
|
|
502
|
+
function elapsedProfileMs(startedAt) {
|
|
503
|
+
return startedAt > 0 ? Math.round(performance.now() - startedAt) : undefined;
|
|
504
|
+
}
|
|
505
|
+
function logBackgroundSpawnProfile(file, args, elapsedMs, status) {
|
|
506
|
+
if (!backgroundSpawnProfileEnabled())
|
|
507
|
+
return;
|
|
508
|
+
console.error(`[spawn-profile] background shell=${JSON.stringify(file)} flag=${JSON.stringify(args[0] ?? "")} elapsedMs=${elapsedMs ?? "n/a"} status=${status}`);
|
|
509
|
+
}
|