@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0

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 (193) 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 +4 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +11 -1
  8. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  9. package/dist/cc-orchestrator/codex-session-history.js +65 -5
  10. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  11. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  13. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  14. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  15. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  16. package/dist/cc-orchestrator/external-agent-driver.js +265 -50
  17. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  18. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  19. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  20. package/dist/cc-orchestrator/session-history.js +98 -15
  21. package/dist/cli/agent-server-stdio.js +9 -2
  22. package/dist/context/compaction.d.ts +8 -1
  23. package/dist/context/compaction.js +49 -4
  24. package/dist/context/manager.d.ts +16 -2
  25. package/dist/context/manager.js +103 -19
  26. package/dist/credentials/access.d.ts +57 -0
  27. package/dist/credentials/access.js +185 -0
  28. package/dist/credentials/index.d.ts +3 -1
  29. package/dist/credentials/index.js +2 -0
  30. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  31. package/dist/credentials/inject-credential-tool.js +30 -11
  32. package/dist/credentials/oauth.d.ts +20 -0
  33. package/dist/credentials/oauth.js +114 -0
  34. package/dist/credentials/store.d.ts +1 -0
  35. package/dist/credentials/store.js +3 -1
  36. package/dist/credentials/types.d.ts +49 -3
  37. package/dist/credentials/use-credential-tool.d.ts +9 -1
  38. package/dist/credentials/use-credential-tool.js +58 -45
  39. package/dist/engine/engine.d.ts +23 -3
  40. package/dist/engine/engine.js +380 -255
  41. package/dist/engine/goal.d.ts +17 -0
  42. package/dist/engine/goal.js +16 -6
  43. package/dist/engine/image-policy.d.ts +6 -0
  44. package/dist/engine/image-policy.js +17 -6
  45. package/dist/engine/input-attachments.d.ts +13 -0
  46. package/dist/engine/input-attachments.js +398 -0
  47. package/dist/engine/model-facade.d.ts +5 -2
  48. package/dist/engine/model-facade.js +4 -4
  49. package/dist/engine/parse-task.d.ts +10 -0
  50. package/dist/engine/parse-task.js +5 -0
  51. package/dist/engine/run-image-input.d.ts +22 -0
  52. package/dist/engine/run-image-input.js +195 -0
  53. package/dist/engine/steer-queue.d.ts +3 -1
  54. package/dist/engine/steer-queue.js +10 -2
  55. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  56. package/dist/engine/streaming-tool-queue.js +11 -7
  57. package/dist/engine/turn-loop.d.ts +37 -2
  58. package/dist/engine/turn-loop.js +228 -43
  59. package/dist/engine/types.d.ts +8 -0
  60. package/dist/git/worktree/crud.d.ts +69 -0
  61. package/dist/git/worktree/crud.js +206 -0
  62. package/dist/git/worktree/diff.d.ts +14 -0
  63. package/dist/git/worktree/diff.js +82 -0
  64. package/dist/git/worktree/git-exec.d.ts +7 -0
  65. package/dist/git/worktree/git-exec.js +51 -0
  66. package/dist/git/worktree/index.d.ts +5 -0
  67. package/dist/git/worktree/index.js +5 -0
  68. package/dist/git/worktree/query.d.ts +42 -0
  69. package/dist/git/worktree/query.js +121 -0
  70. package/dist/git/worktree/slug.d.ts +11 -0
  71. package/dist/git/worktree/slug.js +58 -0
  72. package/dist/git/worktree.d.ts +1 -84
  73. package/dist/git/worktree.js +5 -230
  74. package/dist/hooks/goal-stop-hook.d.ts +33 -1
  75. package/dist/hooks/goal-stop-hook.js +202 -34
  76. package/dist/index.d.ts +31 -28
  77. package/dist/index.js +27 -25
  78. package/dist/logging/logger.js +6 -6
  79. package/dist/logging/sanitize-messages.d.ts +10 -2
  80. package/dist/logging/sanitize-messages.js +21 -6
  81. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  82. package/dist/plugins/installer/checkUpdate.js +4 -2
  83. package/dist/plugins/installer/install.js +2 -0
  84. package/dist/plugins/installer/installFromSource.js +9 -1
  85. package/dist/plugins/installer/parseSource.d.ts +4 -1
  86. package/dist/plugins/installer/parseSource.js +28 -10
  87. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  88. package/dist/plugins/installer/sourcePath.js +50 -0
  89. package/dist/plugins/installer/update.d.ts +4 -1
  90. package/dist/plugins/installer/update.js +5 -3
  91. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  92. package/dist/plugins/parseMarketplaceInput.js +4 -3
  93. package/dist/plugins/pluginInstaller.js +24 -22
  94. package/dist/preset/index.d.ts +1 -0
  95. package/dist/preset/index.js +30 -13
  96. package/dist/prompt/sections/base.md +1 -1
  97. package/dist/protocol/chat-session-manager.d.ts +2 -0
  98. package/dist/protocol/chat-session-manager.js +38 -2
  99. package/dist/protocol/chat-session.d.ts +3 -0
  100. package/dist/protocol/chat-session.js +1 -0
  101. package/dist/protocol/client.d.ts +9 -4
  102. package/dist/protocol/client.js +18 -1
  103. package/dist/protocol/server.d.ts +30 -0
  104. package/dist/protocol/server.js +247 -47
  105. package/dist/protocol/types.d.ts +49 -0
  106. package/dist/protocol/types.js +6 -0
  107. package/dist/run/FileRunStore.js +10 -1
  108. package/dist/run/Heartbeat.js +12 -0
  109. package/dist/run/RunApprovalBackend.d.ts +3 -0
  110. package/dist/run/RunApprovalBackend.js +41 -6
  111. package/dist/run/RunLock.js +2 -0
  112. package/dist/run/RunManager.d.ts +2 -0
  113. package/dist/run/RunManager.js +64 -24
  114. package/dist/run/ids.d.ts +2 -0
  115. package/dist/run/ids.js +23 -0
  116. package/dist/runtime/background-shell.d.ts +1 -0
  117. package/dist/runtime/background-shell.js +23 -13
  118. package/dist/runtime/spawn-common.js +10 -0
  119. package/dist/services/auto-dream.d.ts +15 -4
  120. package/dist/services/auto-dream.js +20 -20
  121. package/dist/services/dream-consolidation.d.ts +2 -3
  122. package/dist/services/dream-consolidation.js +65 -15
  123. package/dist/services/extract-memories.d.ts +14 -5
  124. package/dist/services/extract-memories.js +20 -3
  125. package/dist/services/global-dream-promotion.d.ts +23 -0
  126. package/dist/services/global-dream-promotion.js +112 -0
  127. package/dist/services/memory-orchestrator.js +347 -34
  128. package/dist/session/memory.d.ts +61 -18
  129. package/dist/session/memory.js +342 -79
  130. package/dist/session/session-manager.d.ts +35 -1
  131. package/dist/session/session-manager.js +224 -4
  132. package/dist/session/transcript.d.ts +1 -1
  133. package/dist/session/transcript.js +17 -5
  134. package/dist/settings/manager.d.ts +1 -0
  135. package/dist/settings/manager.js +87 -37
  136. package/dist/settings/schema-export.d.ts +2 -3
  137. package/dist/settings/schema-export.js +2 -3
  138. package/dist/settings/schema.d.ts +21 -0
  139. package/dist/settings/schema.js +12 -0
  140. package/dist/skills/scanner.d.ts +3 -2
  141. package/dist/skills/scanner.js +12 -9
  142. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  143. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  144. package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
  145. package/dist/tool-system/builtin/background-jobs.js +116 -7
  146. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  147. package/dist/tool-system/builtin/background-work.js +51 -5
  148. package/dist/tool-system/builtin/config.d.ts +2 -1
  149. package/dist/tool-system/builtin/config.js +16 -11
  150. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  151. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  152. package/dist/tool-system/builtin/cron.d.ts +1 -2
  153. package/dist/tool-system/builtin/cron.js +9 -7
  154. package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
  155. package/dist/tool-system/builtin/drive-claude-code.js +589 -48
  156. package/dist/tool-system/builtin/edit.js +5 -2
  157. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  158. package/dist/tool-system/builtin/generate-video.js +13 -4
  159. package/dist/tool-system/builtin/index.d.ts +8 -3
  160. package/dist/tool-system/builtin/index.js +50 -22
  161. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  162. package/dist/tool-system/builtin/lsp.js +6 -3
  163. package/dist/tool-system/builtin/memory.js +40 -8
  164. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  165. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  166. package/dist/tool-system/builtin/powershell.js +11 -7
  167. package/dist/tool-system/builtin/read.js +118 -6
  168. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  169. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  170. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  171. package/dist/tool-system/builtin/sleep.js +1 -22
  172. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  173. package/dist/tool-system/builtin/view-image.js +109 -19
  174. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  175. package/dist/tool-system/builtin/worktree.js +297 -74
  176. package/dist/tool-system/builtin/write.js +5 -3
  177. package/dist/tool-system/context.d.ts +41 -1
  178. package/dist/tool-system/executor.d.ts +1 -5
  179. package/dist/tool-system/executor.js +94 -115
  180. package/dist/tool-system/mcp-manager.d.ts +31 -6
  181. package/dist/tool-system/mcp-manager.js +193 -78
  182. package/dist/tool-system/path-policy.d.ts +2 -0
  183. package/dist/tool-system/path-policy.js +41 -12
  184. package/dist/tool-system/permission.d.ts +28 -7
  185. package/dist/tool-system/permission.js +130 -49
  186. package/dist/tool-system/registry.js +11 -4
  187. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  188. package/dist/tool-system/tool-result-redaction.js +48 -0
  189. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  190. package/dist/tool-system/workspace-bridge.js +1 -0
  191. package/dist/types.d.ts +78 -11
  192. package/dist/utils/toolDisplay.js +1 -1
  193. package/package.json +4 -3
@@ -24,6 +24,7 @@ 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";
29
30
  const COMPACT_STREAM_STRATEGIES = new Set([
@@ -48,6 +49,15 @@ export class AgentServer {
48
49
  settingsReader;
49
50
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
50
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;
51
61
  /**
52
62
  * Monotonic config-reload version, bumped per reloadSettings request so each
53
63
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -73,6 +83,14 @@ export class AgentServer {
73
83
  approvalTimers = new Map();
74
84
  /** Default approval timeout: 5 minutes */
75
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;
76
94
  /**
77
95
  * Unsubscribe handle for the process-local `agentNotificationBus`
78
96
  * subscription set up in the constructor. Called from `close()` so a
@@ -85,6 +103,7 @@ export class AgentServer {
85
103
  this.legacyEngine = options.engine ?? null;
86
104
  this.settingsReader = options.settingsReader ?? null;
87
105
  this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
106
+ this.workspaceBridgeEnabled = options.workspaceBridge === true;
88
107
  if (!this.chatManager && !this.legacyEngine) {
89
108
  throw new Error("AgentServer: either chatManager or engine must be supplied");
90
109
  }
@@ -169,7 +188,7 @@ export class AgentServer {
169
188
  maybeWakeIdleSession(sessionId) {
170
189
  if (!this.chatManager)
171
190
  return;
172
- const session = this.chatManager.get(sessionId);
191
+ const session = this.chatManager.get(sessionId) ?? this.rehydrateSessionForWake(sessionId);
173
192
  if (!session || session.isBusy())
174
193
  return;
175
194
  // Headless / automation runs are one-shot: the caller takes result.text and
@@ -229,6 +248,76 @@ export class AgentServer {
229
248
  this.maybeWakeIdleSession(sessionId);
230
249
  });
231
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
+ }
232
321
  // ─── Request Dispatch ───────────────────────────────────────────
233
322
  async handleRequest(req) {
234
323
  switch (req.method) {
@@ -259,6 +348,9 @@ export class AgentServer {
259
348
  case Methods.CloseSession:
260
349
  this.handleCloseSession(req);
261
350
  break;
351
+ case Methods.ReleaseWorkspace:
352
+ this.handleReleaseWorkspace(req);
353
+ break;
262
354
  case Methods.GoalExtend:
263
355
  this.handleGoalExtend(req);
264
356
  break;
@@ -298,28 +390,39 @@ export class AgentServer {
298
390
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "task is required"));
299
391
  return;
300
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
+ }
301
416
  let session;
302
417
  try {
303
- session = cm.getOrCreate(params.sessionId, {
304
- permissionMode: params.permissionMode,
305
- cwd: params.cwd,
306
- projectTrusted: params.projectTrusted,
307
- });
418
+ session = cm.getOrCreate(params.sessionId, sessionConfig);
308
419
  }
309
420
  catch (err) {
310
421
  const code = err.code ?? ErrorCodes.InternalError;
311
422
  this.transport.send(createErrorResponse(req.id, code, err.message));
312
423
  return;
313
424
  }
314
- // `requireExisting`: reject if the target session isn't on disk rather than
315
- // running the prompt against a freshly-created blank session. A cron
316
- // "continue this conversation" job whose session the user deleted must fail
317
- // loudly here (SessionNotFound) so the scheduler can auto-disable it,
318
- // instead of silently executing with no transcript/goal/context.
319
- if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
320
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
321
- return;
322
- }
425
+ this.rememberSessionSlice(params.sessionId, sessionConfig);
323
426
  if (params.model !== undefined) {
324
427
  if (typeof params.model !== "string" || params.model.length === 0) {
325
428
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
@@ -337,29 +440,14 @@ export class AgentServer {
337
440
  session.engine.setPlanMode(params.planMode);
338
441
  }
339
442
  const sid = params.sessionId;
340
- // Wire AskUserQuestion for this interactive session. The chatManager path
341
- // builds a fresh per-session Engine via engineFactory, which (unlike the
342
- // legacy single-engine path) never had askUser wired so AskUserQuestion
343
- // always fell into its "not available in headless mode" branch in normal
344
- // chat (and in a resumed automation session). Route it to the client with
345
- // this session's id so the renderer attributes the question to the right
346
- // tab, resolving via the session's own pendingApprovals. Skip genuinely
347
- // headless engines (no human to answer).
348
- if (!session.engine.isHeadless()) {
349
- session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
350
- // Browser automation bridge: each method routes a browser action to the
351
- // client (Electron main drives the webview via CDP) over the SAME
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.
355
- session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
356
- // Cookie→browser injection (InjectCredential tool): same cross-process
357
- // channel; main restores the cookie jar into the built-in browser.
358
- session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
359
- }
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);
360
447
  try {
361
448
  const result = await session.enqueueTurn(params.task, {
362
449
  cwd: params.cwd,
450
+ attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
363
451
  goal: typeof params.goal === "string" ||
364
452
  (params.goal != null && typeof params.goal === "object")
365
453
  ? params.goal
@@ -467,9 +555,7 @@ export class AgentServer {
467
555
  // the user already knows they pressed stop; clients should
468
556
  // just clear busy.
469
557
  const errName = err.name;
470
- const aborted = runController.signal.aborted ||
471
- errName === "AbortError" ||
472
- errName === "APIUserAbortError";
558
+ const aborted = runController.signal.aborted || errName === "AbortError" || errName === "APIUserAbortError";
473
559
  if (aborted) {
474
560
  const cancelledResult = {
475
561
  text: "",
@@ -623,6 +709,12 @@ export class AgentServer {
623
709
  : params.sessionId
624
710
  ? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
625
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
+ }
626
718
  this.transport.send(createResponse(req.id, { ok: true, cleared }));
627
719
  }
628
720
  /**
@@ -720,7 +812,8 @@ export class AgentServer {
720
812
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
721
813
  return;
722
814
  }
723
- const items = listBackgroundWorkForUI(sessionId);
815
+ const scope = params.scope === "all" ? "all" : "session";
816
+ const items = listBackgroundWorkForUI(sessionId, { scope });
724
817
  this.transport.send(createResponse(req.id, { items }));
725
818
  }
726
819
  // ─── CloseSession ───────────────────────────────────────────────
@@ -747,6 +840,28 @@ export class AgentServer {
747
840
  backgroundJobRegistry.dropForSession(params.sessionId);
748
841
  this.transport.send(createResponse(req.id, { ok: true }));
749
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
+ }
750
865
  // ─── Configure ──────────────────────────────────────────────────
751
866
  handleConfigure(req) {
752
867
  const params = (req.params ?? {});
@@ -1239,8 +1354,7 @@ export class AgentServer {
1239
1354
  const key = params.key;
1240
1355
  if (!key)
1241
1356
  throw new Error("provider_refresh: key required");
1242
- const providers = engine.readSetting("providers") ??
1243
- [];
1357
+ const providers = engine.readSetting("providers") ?? [];
1244
1358
  const p = providers.find((x) => x.key === key);
1245
1359
  if (!p)
1246
1360
  throw new Error(`provider not found: ${key}`);
@@ -1276,8 +1390,7 @@ export class AgentServer {
1276
1390
  if (refs.length > 0) {
1277
1391
  throw new Error(`provider ${key} referenced by models: ${refs.join(", ")}`);
1278
1392
  }
1279
- const providers = engine.readSetting("providers") ??
1280
- [];
1393
+ const providers = engine.readSetting("providers") ?? [];
1281
1394
  const next = providers.filter((p) => p.key !== key);
1282
1395
  engine.updateConfig("providers", next);
1283
1396
  this.transport.send(createResponse(req.id, { type: "provider_delete", data: { ok: true } }));
@@ -1380,7 +1493,7 @@ export class AgentServer {
1380
1493
  return;
1381
1494
  }
1382
1495
  try {
1383
- const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId);
1496
+ const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId, params.attachments);
1384
1497
  this.transport.send(createResponse(req.id, { ok: true, ...result }));
1385
1498
  }
1386
1499
  catch (err) {
@@ -1461,7 +1574,9 @@ export class AgentServer {
1461
1574
  this.clearApprovalTimer(requestId);
1462
1575
  const result = decision;
1463
1576
  if (result && typeof result === "object" && "approved" in result) {
1464
- 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)"));
1465
1580
  }
1466
1581
  else {
1467
1582
  // chatManager approve handler resolves with the raw decision value.
@@ -1487,6 +1602,37 @@ export class AgentServer {
1487
1602
  riskLevel: "low",
1488
1603
  },
1489
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
+ }
1490
1636
  });
1491
1637
  }
1492
1638
  /**
@@ -1568,7 +1714,7 @@ export class AgentServer {
1568
1714
  * calls restoreCookiesToBrowser and replies with a JSON result string we
1569
1715
  * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
1570
1716
  */
1571
- requestCredentialInjectForSession(session, sessionId, credentialId) {
1717
+ requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
1572
1718
  return new Promise((resolve) => {
1573
1719
  const requestId = nanoid(12);
1574
1720
  session.pendingApprovals.set(requestId, (decision) => {
@@ -1605,13 +1751,67 @@ export class AgentServer {
1605
1751
  requestId,
1606
1752
  request: {
1607
1753
  toolName: "__credential_action__",
1608
- args: { action: "injectCookie", credentialId },
1754
+ args: { action: "injectCookie", credentialId, credentialScope },
1609
1755
  description: `credential:inject:${credentialId}`,
1610
1756
  riskLevel: "low",
1611
1757
  },
1612
1758
  });
1613
1759
  });
1614
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
+ }
1615
1815
  /**
1616
1816
  * Ask the client to answer a question from the agent (legacy single-engine
1617
1817
  * path). This intentionally has no wall-clock timeout; Stop/cancel drains
@@ -47,10 +47,41 @@ export declare const ErrorCodes: {
47
47
  */
48
48
  readonly Cancelled: -32005;
49
49
  };
50
+ export type InputAttachmentKind = "image" | "file" | "directory";
51
+ export type InputAttachmentOrigin = "paste" | "os-drop" | "file-panel" | "picker" | "mention" | "generated" | "tool";
52
+ export interface InputAttachmentMeta {
53
+ id: string;
54
+ sessionId: string;
55
+ kind: InputAttachmentKind;
56
+ origin: InputAttachmentOrigin;
57
+ path: string;
58
+ absPath: string;
59
+ relPath?: string;
60
+ mime?: string;
61
+ size: number;
62
+ sha256: string;
63
+ originalName?: string;
64
+ createdAt: number;
65
+ sourcePath?: string;
66
+ width?: number;
67
+ height?: number;
68
+ vision?: {
69
+ include: boolean;
70
+ mediaPath?: string;
71
+ detail?: "low" | "standard" | "high";
72
+ };
73
+ directory?: {
74
+ treePath?: string;
75
+ truncated?: boolean;
76
+ entryCount?: number;
77
+ };
78
+ }
50
79
  /** Start an agent run with a user task. */
51
80
  export interface RunParams {
52
81
  sessionId: string;
53
82
  task: string;
83
+ /** Structured input attachments. Legacy `<codeshell-image>` task blocks remain supported. */
84
+ attachments?: InputAttachmentMeta[];
54
85
  /** Stable id for the user's submit intent; duplicate ids are idempotent. */
55
86
  clientMessageId?: string;
56
87
  /**
@@ -123,6 +154,10 @@ export interface CancelParams {
123
154
  export interface CloseSessionParams {
124
155
  sessionId: string;
125
156
  }
157
+ /** Reset a live session's workspace binding back to main. */
158
+ export interface ReleaseWorkspaceParams {
159
+ sessionId: string;
160
+ }
126
161
  /** Inject context into a session transcript. */
127
162
  export interface InjectParams {
128
163
  sessionId: string;
@@ -132,6 +167,8 @@ export interface InjectParams {
132
167
  export interface SteerParams {
133
168
  sessionId: string;
134
169
  text: string;
170
+ /** Structured input attachments that should ride with this queued steer. */
171
+ attachments?: InputAttachmentMeta[];
135
172
  /** Stable host-side id for this queued draft. Rides through to the
136
173
  * steer_injected event and is the handle Unsteer uses to revoke it. */
137
174
  id?: string;
@@ -260,6 +297,12 @@ export interface ApprovalRequestNotification {
260
297
  requestId: string;
261
298
  request: ApprovalRequest;
262
299
  }
300
+ /** Server tells clients a pending approval/ask has been resolved elsewhere. */
301
+ export interface ApprovalResolvedNotification {
302
+ /** Originating engine session when known. */
303
+ sessionId?: string;
304
+ requestId: string;
305
+ }
263
306
  /** Server status changed. */
264
307
  export interface StatusNotification {
265
308
  status: "ready" | "running" | "error" | "shutdown";
@@ -279,6 +322,8 @@ export declare const Methods: {
279
322
  readonly Unsteer: "agent/unsteer";
280
323
  /** Close (destroy) a session. */
281
324
  readonly CloseSession: "agent/closeSession";
325
+ /** Reset a session's workspace binding to main without closing it. */
326
+ readonly ReleaseWorkspace: "agent/releaseWorkspace";
282
327
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
283
328
  readonly GoalExtend: "agent/goalExtend";
284
329
  /** Clear a session's persisted active goal (CC /goal clear). */
@@ -293,6 +338,10 @@ export declare const Methods: {
293
338
  readonly BackgroundWork: "agent/backgroundWork";
294
339
  readonly StreamEvent: "agent/streamEvent";
295
340
  readonly ApprovalRequest: "agent/approvalRequest";
341
+ /** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
342
+ * AskUserQuestion that timed out) so every client can dismiss its stale card
343
+ * without a user decision. Same envelope the desktop main broadcasts. */
344
+ readonly ApprovalResolved: "agent/approvalResolved";
296
345
  readonly Status: "agent/status";
297
346
  };
298
347
  export declare function createRequest(method: string, params?: Record<string, unknown>): RpcRequest;
@@ -41,6 +41,8 @@ export const Methods = {
41
41
  Unsteer: "agent/unsteer",
42
42
  /** Close (destroy) a session. */
43
43
  CloseSession: "agent/closeSession",
44
+ /** Reset a session's workspace binding to main without closing it. */
45
+ ReleaseWorkspace: "agent/releaseWorkspace",
44
46
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
45
47
  GoalExtend: "agent/goalExtend",
46
48
  /** Clear a session's persisted active goal (CC /goal clear). */
@@ -56,6 +58,10 @@ export const Methods = {
56
58
  // Server → Client (notifications, no id)
57
59
  StreamEvent: "agent/streamEvent",
58
60
  ApprovalRequest: "agent/approvalRequest",
61
+ /** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
62
+ * AskUserQuestion that timed out) so every client can dismiss its stale card
63
+ * without a user decision. Same envelope the desktop main broadcasts. */
64
+ ApprovalResolved: "agent/approvalResolved",
59
65
  Status: "agent/status",
60
66
  };
61
67
  // ─── Helpers ────────────────────────────────────────────────────────
@@ -12,6 +12,8 @@
12
12
  import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, renameSync, rmSync, } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { homedir } from "node:os";
15
+ import { randomUUID } from "node:crypto";
16
+ import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
15
17
  export class FileRunStore {
16
18
  runsDir;
17
19
  constructor(storageDir) {
@@ -20,6 +22,7 @@ export class FileRunStore {
20
22
  }
21
23
  // ─── Helpers ───────────────────────────────────────────────────
22
24
  runDir(runId) {
25
+ assertSafeRunId(runId);
23
26
  return join(this.runsDir, runId);
24
27
  }
25
28
  ensureRunDir(runId) {
@@ -31,7 +34,7 @@ export class FileRunStore {
31
34
  return dir;
32
35
  }
33
36
  writeJson(filePath, data) {
34
- const tmp = filePath + ".tmp";
37
+ const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
35
38
  try {
36
39
  writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
37
40
  // Atomic rename — prevents partial writes on crash
@@ -153,6 +156,8 @@ export class FileRunStore {
153
156
  }
154
157
  // ─── Checkpoints ───────────────────────────────────────────────
155
158
  async saveCheckpoint(cp) {
159
+ assertSafeRunId(cp.runId);
160
+ assertSafeRunFileId(cp.checkpointId, "checkpoint id");
156
161
  const dir = this.runDir(cp.runId);
157
162
  this.writeJson(join(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
158
163
  }
@@ -175,10 +180,14 @@ export class FileRunStore {
175
180
  }
176
181
  // ─── Approvals ─────────────────────────────────────────────────
177
182
  async saveApproval(approval) {
183
+ assertSafeRunId(approval.runId);
184
+ assertSafeRunFileId(approval.approvalId, "approval id");
178
185
  const dir = this.runDir(approval.runId);
179
186
  this.writeJson(join(dir, "approvals", `${approval.approvalId}.json`), approval);
180
187
  }
181
188
  async getApproval(runId, approvalId) {
189
+ assertSafeRunId(runId);
190
+ assertSafeRunFileId(approvalId, "approval id");
182
191
  return this.readJson(join(this.runDir(runId), "approvals", `${approvalId}.json`));
183
192
  }
184
193
  async getPendingApproval(runId) {
@@ -11,6 +11,7 @@
11
11
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { homedir } from "node:os";
14
+ import { assertSafeRunId } from "./ids.js";
14
15
  export class Heartbeat {
15
16
  runsDir;
16
17
  intervalMs;
@@ -23,6 +24,12 @@ export class Heartbeat {
23
24
  * Start heartbeat for a run. Writes immediately, then repeats on interval.
24
25
  */
25
26
  start(runId) {
27
+ assertSafeRunId(runId);
28
+ const existing = this.timers.get(runId);
29
+ if (existing) {
30
+ clearInterval(existing);
31
+ this.timers.delete(runId);
32
+ }
26
33
  // Write first heartbeat immediately
27
34
  this.write(runId);
28
35
  // Schedule periodic heartbeats
@@ -37,6 +44,7 @@ export class Heartbeat {
37
44
  * Stop heartbeat for a run and remove the heartbeat file.
38
45
  */
39
46
  stop(runId) {
47
+ assertSafeRunId(runId);
40
48
  const timer = this.timers.get(runId);
41
49
  if (timer) {
42
50
  clearInterval(timer);
@@ -56,6 +64,7 @@ export class Heartbeat {
56
64
  * Read the last heartbeat for a run. Returns null if no heartbeat exists.
57
65
  */
58
66
  read(runId) {
67
+ assertSafeRunId(runId);
59
68
  const filePath = this.filePath(runId);
60
69
  if (!existsSync(filePath))
61
70
  return null;
@@ -71,6 +80,7 @@ export class Heartbeat {
71
80
  * Returns true if stale or missing, false if recent.
72
81
  */
73
82
  isStale(runId, thresholdMs) {
83
+ assertSafeRunId(runId);
74
84
  const threshold = thresholdMs ?? this.intervalMs * 3;
75
85
  const data = this.read(runId);
76
86
  if (!data)
@@ -81,6 +91,7 @@ export class Heartbeat {
81
91
  * Check if the process that wrote the heartbeat is still alive.
82
92
  */
83
93
  isProcessAlive(runId) {
94
+ assertSafeRunId(runId);
84
95
  const data = this.read(runId);
85
96
  if (!data)
86
97
  return false;
@@ -118,6 +129,7 @@ export class Heartbeat {
118
129
  }
119
130
  }
120
131
  filePath(runId) {
132
+ assertSafeRunId(runId);
121
133
  return join(this.runsDir, runId, "heartbeat");
122
134
  }
123
135
  }