@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.
Files changed (177) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +38 -1
  20. package/dist/context/compaction.js +138 -0
  21. package/dist/context/manager.d.ts +34 -2
  22. package/dist/context/manager.js +254 -58
  23. package/dist/context/token-counter.js +13 -0
  24. package/dist/credentials/access.d.ts +56 -0
  25. package/dist/credentials/access.js +183 -0
  26. package/dist/credentials/index.d.ts +1 -0
  27. package/dist/credentials/index.js +1 -0
  28. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  29. package/dist/credentials/inject-credential-tool.js +30 -11
  30. package/dist/credentials/types.d.ts +2 -2
  31. package/dist/credentials/use-credential-tool.d.ts +9 -1
  32. package/dist/credentials/use-credential-tool.js +58 -45
  33. package/dist/engine/engine.d.ts +38 -12
  34. package/dist/engine/engine.js +477 -163
  35. package/dist/engine/image-policy.d.ts +6 -0
  36. package/dist/engine/image-policy.js +17 -6
  37. package/dist/engine/input-attachments.d.ts +13 -0
  38. package/dist/engine/input-attachments.js +255 -0
  39. package/dist/engine/model-facade.d.ts +5 -2
  40. package/dist/engine/model-facade.js +6 -16
  41. package/dist/engine/parse-task.d.ts +10 -0
  42. package/dist/engine/parse-task.js +5 -0
  43. package/dist/engine/query.js +2 -0
  44. package/dist/engine/session-usage.d.ts +12 -0
  45. package/dist/engine/session-usage.js +56 -0
  46. package/dist/engine/steer-queue.d.ts +2 -1
  47. package/dist/engine/steer-queue.js +2 -2
  48. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  49. package/dist/engine/streaming-tool-queue.js +11 -7
  50. package/dist/engine/turn-loop.d.ts +34 -2
  51. package/dist/engine/turn-loop.js +269 -52
  52. package/dist/engine/types.d.ts +8 -0
  53. package/dist/git/worktree/crud.d.ts +69 -0
  54. package/dist/git/worktree/crud.js +206 -0
  55. package/dist/git/worktree/diff.d.ts +14 -0
  56. package/dist/git/worktree/diff.js +82 -0
  57. package/dist/git/worktree/git-exec.d.ts +7 -0
  58. package/dist/git/worktree/git-exec.js +51 -0
  59. package/dist/git/worktree/index.d.ts +5 -0
  60. package/dist/git/worktree/index.js +5 -0
  61. package/dist/git/worktree/query.d.ts +42 -0
  62. package/dist/git/worktree/query.js +121 -0
  63. package/dist/git/worktree/slug.d.ts +11 -0
  64. package/dist/git/worktree/slug.js +58 -0
  65. package/dist/git/worktree.d.ts +1 -84
  66. package/dist/git/worktree.js +5 -230
  67. package/dist/index.d.ts +31 -28
  68. package/dist/index.js +27 -25
  69. package/dist/logging/logger.js +6 -6
  70. package/dist/logging/sanitize-messages.d.ts +10 -2
  71. package/dist/logging/sanitize-messages.js +21 -6
  72. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  73. package/dist/plugins/installer/checkUpdate.js +4 -2
  74. package/dist/plugins/installer/install.js +2 -0
  75. package/dist/plugins/installer/installFromSource.js +9 -1
  76. package/dist/plugins/installer/parseSource.d.ts +4 -1
  77. package/dist/plugins/installer/parseSource.js +28 -10
  78. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  79. package/dist/plugins/installer/sourcePath.js +50 -0
  80. package/dist/plugins/installer/update.d.ts +4 -1
  81. package/dist/plugins/installer/update.js +5 -3
  82. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  83. package/dist/plugins/parseMarketplaceInput.js +4 -3
  84. package/dist/plugins/pluginInstaller.js +24 -22
  85. package/dist/preset/index.d.ts +1 -0
  86. package/dist/preset/index.js +16 -9
  87. package/dist/prompt/sections/base.md +1 -1
  88. package/dist/protocol/chat-session-manager.d.ts +2 -0
  89. package/dist/protocol/chat-session-manager.js +38 -2
  90. package/dist/protocol/chat-session.d.ts +5 -0
  91. package/dist/protocol/chat-session.js +2 -0
  92. package/dist/protocol/client.d.ts +13 -5
  93. package/dist/protocol/client.js +26 -3
  94. package/dist/protocol/server.d.ts +43 -12
  95. package/dist/protocol/server.js +326 -104
  96. package/dist/protocol/types.d.ts +51 -0
  97. package/dist/protocol/types.js +6 -0
  98. package/dist/run/FileRunStore.js +10 -1
  99. package/dist/run/Heartbeat.js +12 -0
  100. package/dist/run/RunApprovalBackend.d.ts +3 -0
  101. package/dist/run/RunApprovalBackend.js +41 -6
  102. package/dist/run/RunLock.js +2 -0
  103. package/dist/run/RunManager.d.ts +2 -0
  104. package/dist/run/RunManager.js +64 -24
  105. package/dist/run/ids.d.ts +2 -0
  106. package/dist/run/ids.js +23 -0
  107. package/dist/runtime/background-shell.d.ts +1 -0
  108. package/dist/runtime/background-shell.js +23 -13
  109. package/dist/runtime/safe-spawn.js +74 -11
  110. package/dist/runtime/spawn-common.js +10 -0
  111. package/dist/services/auto-dream.d.ts +15 -4
  112. package/dist/services/auto-dream.js +20 -20
  113. package/dist/services/dream-consolidation.d.ts +2 -3
  114. package/dist/services/dream-consolidation.js +65 -15
  115. package/dist/services/extract-memories.d.ts +14 -5
  116. package/dist/services/extract-memories.js +20 -3
  117. package/dist/services/global-dream-promotion.d.ts +23 -0
  118. package/dist/services/global-dream-promotion.js +112 -0
  119. package/dist/services/memory-orchestrator.js +347 -34
  120. package/dist/session/memory.d.ts +61 -18
  121. package/dist/session/memory.js +342 -79
  122. package/dist/session/session-manager.d.ts +35 -1
  123. package/dist/session/session-manager.js +197 -4
  124. package/dist/session/transcript.d.ts +5 -1
  125. package/dist/session/transcript.js +38 -5
  126. package/dist/settings/manager.d.ts +1 -0
  127. package/dist/settings/manager.js +87 -37
  128. package/dist/settings/schema-export.d.ts +2 -3
  129. package/dist/settings/schema-export.js +2 -3
  130. package/dist/settings/schema.d.ts +21 -0
  131. package/dist/settings/schema.js +12 -0
  132. package/dist/skills/scanner.d.ts +3 -2
  133. package/dist/skills/scanner.js +12 -9
  134. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  135. package/dist/tool-system/builtin/background-jobs.js +12 -1
  136. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  137. package/dist/tool-system/builtin/background-work.js +51 -5
  138. package/dist/tool-system/builtin/config.d.ts +2 -1
  139. package/dist/tool-system/builtin/config.js +16 -11
  140. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  141. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  142. package/dist/tool-system/builtin/edit.js +5 -2
  143. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  144. package/dist/tool-system/builtin/generate-video.js +13 -4
  145. package/dist/tool-system/builtin/index.d.ts +8 -3
  146. package/dist/tool-system/builtin/index.js +37 -21
  147. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  148. package/dist/tool-system/builtin/lsp.js +6 -3
  149. package/dist/tool-system/builtin/memory.js +40 -8
  150. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  151. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  152. package/dist/tool-system/builtin/powershell.js +11 -7
  153. package/dist/tool-system/builtin/read.js +118 -6
  154. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  155. package/dist/tool-system/builtin/view-image.js +109 -19
  156. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  157. package/dist/tool-system/builtin/worktree.js +297 -74
  158. package/dist/tool-system/builtin/write.js +5 -3
  159. package/dist/tool-system/context.d.ts +23 -1
  160. package/dist/tool-system/executor.d.ts +1 -5
  161. package/dist/tool-system/executor.js +94 -115
  162. package/dist/tool-system/mcp-manager.d.ts +18 -5
  163. package/dist/tool-system/mcp-manager.js +157 -74
  164. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  165. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  166. package/dist/tool-system/path-policy.d.ts +2 -0
  167. package/dist/tool-system/path-policy.js +41 -12
  168. package/dist/tool-system/permission.d.ts +28 -7
  169. package/dist/tool-system/permission.js +130 -49
  170. package/dist/tool-system/registry.js +11 -4
  171. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  172. package/dist/tool-system/tool-result-redaction.js +48 -0
  173. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  174. package/dist/tool-system/workspace-bridge.js +1 -0
  175. package/dist/types.d.ts +86 -5
  176. package/dist/utils/toolDisplay.js +1 -1
  177. package/package.json +4 -3
@@ -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
- // `requireExisting`: reject if the target session isn't on disk rather than
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 chatManager path
328
- // builds a fresh per-session Engine via engineFactory, which (unlike the
329
- // legacy single-engine path) never had askUser wired so AskUserQuestion
330
- // always fell into its "not available in headless mode" branch in normal
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: look up per-session pendingApprovals
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
- // Tool approvals still resolve through the interactive backend's
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 THIS request (the ask path armed one).
506
- // The legacy single-engine branch below already does this; without it
507
- // here a decided request leaves a live timer that fires at
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 they only settle on a client reply
543
- // or the 5-minute APPROVAL_TIMEOUT_MS. Left alone, the awaiting tool call
544
- // hangs until that timeout (delaying run wind-down) and the server-side
545
- // approvalTimers entry leaks. Resolve them as cancelled now and clear the
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 items = listBackgroundWorkForUI(sessionId);
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.pendingApprovals.set(requestId, resolve);
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
- if (this.pendingApprovals.has(requestId)) {
1403
- this.pendingApprovals.delete(requestId);
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
- * Ask the client to answer a question from the agent (legacy single-engine path).
1418
- */
1419
- /**
1420
- * Per-session AskUserQuestion for the chatManager path. Mirrors
1421
- * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
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 ? (result.answer ?? "") : (result.reason ?? "(user declined to answer)"));
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
- * the matching server-side approval timers. Used by handleCancel's
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 until APPROVAL_TIMEOUT_MS (and leak the timer). Each
1704
- * requestId in the session map has a same-keyed entry in this.approvalTimers
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: "cancelled" });
1933
+ resolve({ approved: false, reason });
1712
1934
  }
1713
1935
  catch {
1714
1936
  /* a resolver must never break cancel cleanup */