@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.
Files changed (69) hide show
  1. package/dist/context/compaction.d.ts +30 -0
  2. package/dist/context/compaction.js +93 -0
  3. package/dist/context/manager.d.ts +18 -0
  4. package/dist/context/manager.js +156 -44
  5. package/dist/context/token-counter.js +13 -0
  6. package/dist/engine/engine.d.ts +22 -12
  7. package/dist/engine/engine.js +263 -81
  8. package/dist/engine/model-connections-pool.js +1 -0
  9. package/dist/engine/model-facade.js +2 -12
  10. package/dist/engine/query.js +2 -0
  11. package/dist/engine/runtime.d.ts +2 -0
  12. package/dist/engine/runtime.js +25 -0
  13. package/dist/engine/session-usage.d.ts +12 -0
  14. package/dist/engine/session-usage.js +56 -0
  15. package/dist/engine/steer-queue.d.ts +2 -1
  16. package/dist/engine/steer-queue.js +2 -2
  17. package/dist/engine/turn-loop.d.ts +28 -2
  18. package/dist/engine/turn-loop.js +153 -26
  19. package/dist/git/utils.d.ts +12 -0
  20. package/dist/git/utils.js +33 -6
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +4 -3
  23. package/dist/llm/capabilities/rules.js +1 -1
  24. package/dist/llm/model-pool.d.ts +7 -0
  25. package/dist/llm/model-pool.js +8 -1
  26. package/dist/model-catalog/builtin.js +6 -1
  27. package/dist/preset/index.d.ts +5 -1
  28. package/dist/preset/index.js +21 -2
  29. package/dist/prompt/composer.d.ts +5 -0
  30. package/dist/prompt/composer.js +10 -2
  31. package/dist/prompt/sections/base.md +1 -0
  32. package/dist/protocol/chat-session-manager.d.ts +1 -0
  33. package/dist/protocol/chat-session-manager.js +2 -0
  34. package/dist/protocol/chat-session.d.ts +4 -1
  35. package/dist/protocol/chat-session.js +9 -3
  36. package/dist/protocol/client.d.ts +5 -1
  37. package/dist/protocol/client.js +8 -2
  38. package/dist/protocol/server.d.ts +13 -12
  39. package/dist/protocol/server.js +199 -67
  40. package/dist/protocol/types.d.ts +14 -0
  41. package/dist/runtime/background-shell.js +14 -0
  42. package/dist/runtime/safe-spawn.js +89 -11
  43. package/dist/runtime/spawn-common.d.ts +15 -4
  44. package/dist/runtime/spawn-common.js +113 -12
  45. package/dist/session/session-manager.js +7 -1
  46. package/dist/session/transcript.d.ts +4 -0
  47. package/dist/session/transcript.js +21 -0
  48. package/dist/tool-system/builtin/bash.js +3 -2
  49. package/dist/tool-system/builtin/cron.js +10 -2
  50. package/dist/tool-system/builtin/edit-model-catalog.js +15 -5
  51. package/dist/tool-system/builtin/generate-video.js +3 -0
  52. package/dist/tool-system/builtin/grep.d.ts +9 -0
  53. package/dist/tool-system/builtin/grep.js +100 -3
  54. package/dist/tool-system/builtin/index.d.ts +3 -1
  55. package/dist/tool-system/builtin/index.js +5 -5
  56. package/dist/tool-system/builtin/powershell.js +4 -1
  57. package/dist/tool-system/builtin/sleep.js +5 -0
  58. package/dist/tool-system/context.d.ts +10 -0
  59. package/dist/tool-system/executor.js +25 -2
  60. package/dist/tool-system/mcp-manager.js +17 -0
  61. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  62. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  63. package/dist/tool-system/permission.d.ts +3 -1
  64. package/dist/tool-system/permission.js +2 -1
  65. package/dist/tool-system/sandbox/off.js +7 -1
  66. package/dist/types.d.ts +35 -1
  67. package/dist/utils/exec.d.ts +8 -0
  68. package/dist/utils/exec.js +10 -0
  69. package/package.json +1 -1
@@ -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
- // reusing its proven resolve/timeout/cleanup. The main-side handler
328
- // replies with the action's JSON result.
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: look up per-session pendingApprovals
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 THIS request (the ask path armed one).
469
- // The legacy single-engine branch below already does this; without it
470
- // here a decided request leaves a live timer that fires at
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 they only settle on a client reply
506
- // or the 5-minute APPROVAL_TIMEOUT_MS. Left alone, the awaiting tool call
507
- // hangs until that timeout (delaying run wind-down) and the server-side
508
- // approvalTimers entry leaks. Resolve them as cancelled now and clear the
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
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${sid}`));
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.forEachSession((s) => reload(s.engine));
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, { ok: true, model: entry.model, key: entry.key }));
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
- if (!engine) {
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 = engine.forceCompact();
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
- this.pendingApprovals.set(requestId, resolve);
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
- if (this.pendingApprovals.has(requestId)) {
1297
- this.pendingApprovals.delete(requestId);
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, { requestId, request });
1443
+ this.notify(Methods.ApprovalRequest, {
1444
+ ...(sessionId ? { sessionId } : {}),
1445
+ requestId,
1446
+ request,
1447
+ });
1304
1448
  });
1305
1449
  }
1306
1450
  /**
1307
- * Ask the client to answer a question from the agent (legacy single-engine path).
1308
- */
1309
- /**
1310
- * Per-session AskUserQuestion for the chatManager path. Mirrors
1311
- * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
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
- * the matching server-side approval timers. Used by handleCancel's
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 until APPROVAL_TIMEOUT_MS (and leak the timer). Each
1594
- * requestId in the session map has a same-keyed entry in this.approvalTimers
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: "cancelled" });
1733
+ resolve({ approved: false, reason });
1602
1734
  }
1603
1735
  catch {
1604
1736
  /* a resolver must never break cancel cleanup */
@@ -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
+ }