@cjhyy/code-shell-core 0.6.0-rc.16 → 0.6.0-rc.18

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 (64) 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/cc-orchestrator/agent-adapter.d.ts +2 -0
  5. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  6. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  7. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  8. package/dist/cli/agent-server-stdio.js +2 -0
  9. package/dist/credentials/access.d.ts +56 -0
  10. package/dist/credentials/access.js +183 -0
  11. package/dist/credentials/index.d.ts +1 -0
  12. package/dist/credentials/index.js +1 -0
  13. package/dist/credentials/inject-credential-tool.js +5 -5
  14. package/dist/credentials/use-credential-tool.d.ts +8 -1
  15. package/dist/credentials/use-credential-tool.js +55 -45
  16. package/dist/engine/engine.d.ts +3 -0
  17. package/dist/engine/engine.js +40 -13
  18. package/dist/engine/image-policy.d.ts +6 -0
  19. package/dist/engine/image-policy.js +17 -6
  20. package/dist/engine/input-attachments.d.ts +13 -0
  21. package/dist/engine/input-attachments.js +255 -0
  22. package/dist/engine/model-facade.d.ts +5 -2
  23. package/dist/engine/model-facade.js +4 -4
  24. package/dist/engine/parse-task.d.ts +10 -0
  25. package/dist/engine/parse-task.js +5 -0
  26. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  27. package/dist/engine/streaming-tool-queue.js +11 -7
  28. package/dist/engine/turn-loop.d.ts +4 -0
  29. package/dist/engine/turn-loop.js +106 -25
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2 -2
  32. package/dist/logging/sanitize-messages.d.ts +10 -2
  33. package/dist/logging/sanitize-messages.js +21 -6
  34. package/dist/preset/index.js +10 -9
  35. package/dist/protocol/chat-session-manager.d.ts +1 -0
  36. package/dist/protocol/chat-session-manager.js +18 -2
  37. package/dist/protocol/chat-session.d.ts +3 -0
  38. package/dist/protocol/chat-session.js +1 -0
  39. package/dist/protocol/client.d.ts +9 -4
  40. package/dist/protocol/client.js +18 -1
  41. package/dist/protocol/server.d.ts +12 -0
  42. package/dist/protocol/server.js +116 -38
  43. package/dist/protocol/types.d.ts +37 -0
  44. package/dist/runtime/spawn-common.js +10 -0
  45. package/dist/tool-system/builtin/drive-claude-code.d.ts +1 -0
  46. package/dist/tool-system/builtin/drive-claude-code.js +137 -18
  47. package/dist/tool-system/builtin/index.d.ts +8 -3
  48. package/dist/tool-system/builtin/index.js +15 -13
  49. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  50. package/dist/tool-system/builtin/powershell.js +11 -7
  51. package/dist/tool-system/builtin/read.js +114 -5
  52. package/dist/tool-system/builtin/view-image.js +9 -0
  53. package/dist/tool-system/executor.d.ts +1 -5
  54. package/dist/tool-system/executor.js +94 -115
  55. package/dist/tool-system/mcp-manager.d.ts +2 -0
  56. package/dist/tool-system/mcp-manager.js +23 -8
  57. package/dist/tool-system/path-policy.js +13 -0
  58. package/dist/tool-system/permission.d.ts +28 -7
  59. package/dist/tool-system/permission.js +130 -49
  60. package/dist/tool-system/registry.js +11 -4
  61. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  62. package/dist/tool-system/tool-result-redaction.js +48 -0
  63. package/dist/types.d.ts +23 -4
  64. package/package.json +4 -3
@@ -75,9 +75,9 @@ const GENERAL_BUILTIN_TOOLS = [
75
75
  // what lets the LLM list/read/save/delete memories during a normal session,
76
76
  // AND what makes the end-of-session auto-dream consolidation work — its
77
77
  // tool-call loop pulls these from the registry, so without them every dream
78
- // run bailed with "missing memory tools". Save/Delete in the user scope stay
79
- // permission-gated (the tools declare permissionDefault: "ask"); dream-scope
80
- // writes go through freely.
78
+ // run bailed with "missing memory tools". Save/Delete in the user scope have
79
+ // no explicit allow rule, so the default-mode classifier fallback asks;
80
+ // dream-scope writes go through freely.
81
81
  "MemoryList",
82
82
  "MemoryRead",
83
83
  "MemorySave",
@@ -93,7 +93,7 @@ const GENERAL_BUILTIN_TOOLS = [
93
93
  // AI 增/改模型 catalog(写 ~/.code-shell/model-catalog.user.json)。同 BashOutput/
94
94
  // UseCredential 一样的 whitelist 要求:registerBuiltins 按 preset 集过滤 BUILTIN_TOOLS,
95
95
  // 名单里没有它 → 即使工具已注册,AI 列表里也没有 → 用户实测「找不到这个工具」。
96
- // permissionDefault: "ask",列入只是让 AI 看得到,调用仍走审批,无条件列入安全。
96
+ // 列入只是让 AI 看得到;执行期没有显式 allow rule,默认 classifier fallback 会审批。
97
97
  "EditModelCatalog",
98
98
  // Goal-mode control tools. Same whitelist requirement as the rest:
99
99
  // registerBuiltins filters BUILTIN_TOOLS by the preset set, so a goal tool
@@ -134,18 +134,19 @@ const GENERAL_PERMISSION_RULES = [
134
134
  { tool: "Skill", decision: "allow" },
135
135
  // ListMcpResources only enumerates resource names (and the executor filters
136
136
  // the list to this session's enabled servers). ReadMcpResource pulls actual
137
- // content and is intentionally NOT auto-allowed here — it falls back to its
138
- // tool-level "ask" default so a default-mode session confirms the read.
137
+ // content and is intentionally NOT auto-allowed here — with no explicit rule,
138
+ // default-mode classifier fallback asks the user. The tool's
139
+ // permissionDefault is only UI/metadata, not classifier input.
139
140
  { tool: "ListMcpResources", decision: "allow" },
140
141
  // Reading memory is always safe; writes (MemorySave/MemoryDelete) stay gated
141
- // by the tools' own permissionDefault so the user confirms each change.
142
+ // by explicit rules below plus default-mode ask fallback.
142
143
  { tool: "MemoryList", decision: "allow" },
143
144
  { tool: "MemoryRead", decision: "allow" },
144
145
  // Browser automation (3 semantic tools). Rules are first-match-wins, ordered
145
146
  // by specificity — so the action-gating rule for browser_act MUST come first:
146
147
  // click/type/select mutate the page/form → "ask"; all other actions (hover/
147
- // scroll/wait/press_key/list_tabs/switch_tab) fall through to browser_act's
148
- // tool-level "allow". observe (read-only) and navigate auto-allow.
148
+ // scroll/wait/press_key/list_tabs/switch_tab) fall through to the default
149
+ // allow rule below. observe (read-only) and navigate auto-allow.
149
150
  // (Main-side sensitive-action + domain-whitelist enforcement also applies.)
150
151
  {
151
152
  tool: "browser_act",
@@ -25,6 +25,7 @@ export declare class ChatSessionManager {
25
25
  constructor(opts: ChatSessionManagerOptions);
26
26
  getOrCreate(sessionId: string, slice: EngineConfigSlice): ChatSession;
27
27
  get(sessionId: string): ChatSession | undefined;
28
+ sessionExistsOnDisk(sessionId: string, slice: EngineConfigSlice): boolean;
28
29
  /**
29
30
  * Iterate every live session once. Public iterator so callers (e.g. the
30
31
  * protocol server's config hot-reload / sessions query) don't reach into
@@ -1,10 +1,12 @@
1
1
  import { ChatSession } from "./chat-session.js";
2
2
  import { backgroundShellManager } from "../runtime/background-shell.js";
3
3
  import { clearAgentOutputFiles } from "../tool-system/builtin/agent-output-file.js";
4
+ import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
4
5
  import { clearCredentialSessionAllow } from "../credentials/use-credential-tool.js";
5
6
  import { clearInjectCredentialSessionAllow } from "../credentials/inject-credential-tool.js";
6
7
  import { logger } from "../logging/logger.js";
7
8
  import { clearSessionPathApprovals, openSessionPathApprovals } from "../tool-system/path-policy.js";
9
+ import { clearInteractiveApprovalSession, openInteractiveApprovalSession, } from "../tool-system/permission.js";
8
10
  export class ChatSessionManager {
9
11
  sessions = new Map();
10
12
  runtime;
@@ -20,6 +22,7 @@ export class ChatSessionManager {
20
22
  }
21
23
  getOrCreate(sessionId, slice) {
22
24
  openSessionPathApprovals(sessionId);
25
+ openInteractiveApprovalSession(sessionId);
23
26
  const existing = this.sessions.get(sessionId);
24
27
  if (existing) {
25
28
  existing.lastActivityAt = Date.now();
@@ -49,6 +52,13 @@ export class ChatSessionManager {
49
52
  get(sessionId) {
50
53
  return this.sessions.get(sessionId);
51
54
  }
55
+ sessionExistsOnDisk(sessionId, slice) {
56
+ const existing = this.sessions.get(sessionId);
57
+ if (existing)
58
+ return true;
59
+ const probeEngine = this.factory(slice);
60
+ return probeEngine.sessionExistsOnDisk(sessionId);
61
+ }
52
62
  /**
53
63
  * Iterate every live session once. Public iterator so callers (e.g. the
54
64
  * protocol server's config hot-reload / sessions query) don't reach into
@@ -65,6 +75,7 @@ export class ChatSessionManager {
65
75
  return;
66
76
  s.cancel();
67
77
  clearSessionPathApprovals(sessionId);
78
+ clearInteractiveApprovalSession(sessionId);
68
79
  clearCredentialSessionAllow(sessionId);
69
80
  clearInjectCredentialSessionAllow(sessionId);
70
81
  this.unregisterMcpOwner(s);
@@ -103,8 +114,13 @@ export class ChatSessionManager {
103
114
  sweepIdle() {
104
115
  const cutoff = Date.now() - this.idleTtlMs;
105
116
  for (const [id, s] of [...this.sessions]) {
106
- if (s.lastActivityAt < cutoff && !s.isBusy())
107
- this.close(id);
117
+ if (s.lastActivityAt >= cutoff)
118
+ continue;
119
+ if (s.isBusy())
120
+ continue;
121
+ if (backgroundJobRegistry.hasRunningForSession(id))
122
+ continue;
123
+ this.close(id);
108
124
  }
109
125
  }
110
126
  startIdleSweeper(intervalMs = 60_000) {
@@ -1,6 +1,7 @@
1
1
  import type { Engine, EngineResult } from "../engine/engine.js";
2
2
  import type { ModelEntry } from "../llm/model-pool.js";
3
3
  import type { StreamEvent } from "../types.js";
4
+ import type { InputAttachmentMeta } from "./types.js";
4
5
  export interface ChatSessionOptions {
5
6
  id: string;
6
7
  engine: Engine;
@@ -20,6 +21,8 @@ export interface TurnOpts {
20
21
  injected?: boolean;
21
22
  /** Stable id for this user-intent; forwarded to Engine.run for idempotency. */
22
23
  clientMessageId?: string;
24
+ /** Structured input attachments for this turn. */
25
+ attachments?: InputAttachmentMeta[];
23
26
  }
24
27
  /**
25
28
  * One ChatSession per UI chat tab. Owns a single Engine, an AbortController
@@ -170,6 +170,7 @@ export class ChatSession {
170
170
  goal: next.opts.goal,
171
171
  injected: next.opts.injected,
172
172
  clientMessageId: next.opts.clientMessageId,
173
+ attachments: next.opts.attachments,
173
174
  });
174
175
  this.lastActivityAt = Date.now();
175
176
  next.resolve(result);
@@ -11,12 +11,15 @@
11
11
  * const result = await client.run("fix the bug");
12
12
  */
13
13
  import type { Transport } from "./transport.js";
14
- import { type RunParams, type RunResult, type AgentStreamEventNotification, type ConfigureParams, type QueryParams, type QueryResult } from "./types.js";
14
+ import { type RunParams, type RunResult, type AgentStreamEventNotification, type ConfigureParams, type QueryParams, type QueryResult, type ApprovalRequestNotification, type ApprovalResolvedNotification } from "./types.js";
15
15
  import type { ApprovalRequest, ApprovalResult, PermissionMode, BackgroundAgentCompletedEvent } from "../types.js";
16
+ export type ApprovalRequestMeta = Pick<ApprovalRequestNotification, "sessionId">;
17
+ export type ApprovalResolvedEvent = ApprovalResolvedNotification;
16
18
  export interface AgentClientEvents {
17
19
  /** Multi-session envelope: carries sessionId + event. */
18
20
  stream: (envelope: AgentStreamEventNotification) => void;
19
- approvalRequest: (requestId: string, request: ApprovalRequest) => void;
21
+ approvalRequest: (requestId: string, request: ApprovalRequest, meta?: ApprovalRequestMeta) => void;
22
+ approvalResolved: (event: ApprovalResolvedEvent) => void;
20
23
  status: (status: string, message?: string) => void;
21
24
  }
22
25
  export interface AgentRunOptions {
@@ -115,8 +118,10 @@ export declare class AgentClient {
115
118
  */
116
119
  onStreamEvent(handler: (envelope: AgentStreamEventNotification) => void): void;
117
120
  offStreamEvent(handler: (envelope: AgentStreamEventNotification) => void): void;
118
- onApprovalRequest(handler: (requestId: string, request: ApprovalRequest) => void): void;
119
- offApprovalRequest(handler: (requestId: string, request: ApprovalRequest) => void): void;
121
+ onApprovalRequest(handler: (requestId: string, request: ApprovalRequest, meta?: ApprovalRequestMeta) => void): void;
122
+ offApprovalRequest(handler: (requestId: string, request: ApprovalRequest, meta?: ApprovalRequestMeta) => void): void;
123
+ onApprovalResolved(handler: (event: ApprovalResolvedEvent) => void): void;
124
+ offApprovalResolved(handler: (event: ApprovalResolvedEvent) => void): void;
120
125
  onStatus(handler: (status: string, message?: string) => void): void;
121
126
  offStatus(handler: (status: string, message?: string) => void): void;
122
127
  /**
@@ -211,6 +211,12 @@ export class AgentClient {
211
211
  offApprovalRequest(handler) {
212
212
  this.emitter.off("approvalRequest", handler);
213
213
  }
214
+ onApprovalResolved(handler) {
215
+ this.emitter.on("approvalResolved", handler);
216
+ }
217
+ offApprovalResolved(handler) {
218
+ this.emitter.off("approvalResolved", handler);
219
+ }
214
220
  onStatus(handler) {
215
221
  this.emitter.on("status", handler);
216
222
  }
@@ -279,7 +285,18 @@ export class AgentClient {
279
285
  const requestId = params.requestId;
280
286
  const request = params.request;
281
287
  if (requestId && request) {
282
- this.emitter.emit("approvalRequest", requestId, request);
288
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
289
+ const meta = sessionId === undefined ? {} : { sessionId };
290
+ this.emitter.emit("approvalRequest", requestId, request, meta);
291
+ }
292
+ break;
293
+ }
294
+ case Methods.ApprovalResolved: {
295
+ const requestId = params.requestId;
296
+ if (requestId) {
297
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
298
+ const event = sessionId === undefined ? { requestId } : { sessionId, requestId };
299
+ this.emitter.emit("approvalResolved", event);
283
300
  }
284
301
  break;
285
302
  }
@@ -67,6 +67,14 @@ export declare class AgentServer {
67
67
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
68
68
  private readonly readActiveGoalFromDisk;
69
69
  private readonly workspaceBridgeEnabled;
70
+ /**
71
+ * Last per-session EngineConfigSlice supplied by agent/run. Kept even after
72
+ * idle eviction so background-completion wakeups can rebuild the ChatSession
73
+ * with the same cwd/permission/trust inputs before draining the queue.
74
+ */
75
+ private readonly lastSliceBySession;
76
+ /** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
77
+ private diskSessionReader;
70
78
  /**
71
79
  * Monotonic config-reload version, bumped per reloadSettings request so each
72
80
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -137,6 +145,10 @@ export declare class AgentServer {
137
145
  * "wakeup in flight" guard flag.
138
146
  */
139
147
  private maybeWakeIdleSession;
148
+ private rehydrateSessionForWake;
149
+ private sliceForWakeRehydrate;
150
+ private rememberSessionSlice;
151
+ private wireInteractiveSession;
140
152
  private handleRequest;
141
153
  private handleRun;
142
154
  private handleRunMulti;
@@ -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([
@@ -49,6 +50,14 @@ export class AgentServer {
49
50
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
50
51
  readActiveGoalFromDisk;
51
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;
52
61
  /**
53
62
  * Monotonic config-reload version, bumped per reloadSettings request so each
54
63
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -179,7 +188,7 @@ export class AgentServer {
179
188
  maybeWakeIdleSession(sessionId) {
180
189
  if (!this.chatManager)
181
190
  return;
182
- const session = this.chatManager.get(sessionId);
191
+ const session = this.chatManager.get(sessionId) ?? this.rehydrateSessionForWake(sessionId);
183
192
  if (!session || session.isBusy())
184
193
  return;
185
194
  // Headless / automation runs are one-shot: the caller takes result.text and
@@ -239,6 +248,76 @@ export class AgentServer {
239
248
  this.maybeWakeIdleSession(sessionId);
240
249
  });
241
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
+ }
242
321
  // ─── Request Dispatch ───────────────────────────────────────────
243
322
  async handleRequest(req) {
244
323
  switch (req.method) {
@@ -311,28 +390,39 @@ export class AgentServer {
311
390
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "task is required"));
312
391
  return;
313
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
+ }
314
416
  let session;
315
417
  try {
316
- session = cm.getOrCreate(params.sessionId, {
317
- permissionMode: params.permissionMode,
318
- cwd: params.cwd,
319
- projectTrusted: params.projectTrusted,
320
- });
418
+ session = cm.getOrCreate(params.sessionId, sessionConfig);
321
419
  }
322
420
  catch (err) {
323
421
  const code = err.code ?? ErrorCodes.InternalError;
324
422
  this.transport.send(createErrorResponse(req.id, code, err.message));
325
423
  return;
326
424
  }
327
- // `requireExisting`: reject if the target session isn't on disk rather than
328
- // running the prompt against a freshly-created blank session. A cron
329
- // "continue this conversation" job whose session the user deleted must fail
330
- // loudly here (SessionNotFound) so the scheduler can auto-disable it,
331
- // instead of silently executing with no transcript/goal/context.
332
- if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
333
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
334
- return;
335
- }
425
+ this.rememberSessionSlice(params.sessionId, sessionConfig);
336
426
  if (params.model !== undefined) {
337
427
  if (typeof params.model !== "string" || params.model.length === 0) {
338
428
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
@@ -350,32 +440,14 @@ export class AgentServer {
350
440
  session.engine.setPlanMode(params.planMode);
351
441
  }
352
442
  const sid = params.sessionId;
353
- // Wire AskUserQuestion for this interactive session. The chatManager path
354
- // builds a fresh per-session Engine via engineFactory, which (unlike the
355
- // legacy single-engine path) never had askUser wired so AskUserQuestion
356
- // always fell into its "not available in headless mode" branch in normal
357
- // chat (and in a resumed automation session). Route it to the client with
358
- // this session's id so the renderer attributes the question to the right
359
- // tab, resolving via the session's own pendingApprovals. Skip genuinely
360
- // headless engines (no human to answer).
361
- if (!session.engine.isHeadless()) {
362
- session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
363
- // Browser automation bridge: each method routes a browser action to the
364
- // client (Electron main drives the webview via CDP) over the SAME
365
- // request/response channel as askUser (pendingApprovals + requestId).
366
- // Browser actions still keep their own bounded timeout; AskUserQuestion
367
- // waits for a real answer or Stop/cancel.
368
- session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
369
- // Cookie→browser injection (InjectCredential tool): same cross-process
370
- // channel; main restores the cookie jar into the built-in browser.
371
- session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
372
- if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
373
- session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
374
- }
375
- }
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);
376
447
  try {
377
448
  const result = await session.enqueueTurn(params.task, {
378
449
  cwd: params.cwd,
450
+ attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
379
451
  goal: typeof params.goal === "string" ||
380
452
  (params.goal != null && typeof params.goal === "object")
381
453
  ? params.goal
@@ -637,6 +709,12 @@ export class AgentServer {
637
709
  : params.sessionId
638
710
  ? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
639
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
+ }
640
718
  this.transport.send(createResponse(req.id, { ok: true, cleared }));
641
719
  }
642
720
  /**
@@ -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
  /**
@@ -264,6 +295,12 @@ export interface ApprovalRequestNotification {
264
295
  requestId: string;
265
296
  request: ApprovalRequest;
266
297
  }
298
+ /** Server tells clients a pending approval/ask has been resolved elsewhere. */
299
+ export interface ApprovalResolvedNotification {
300
+ /** Originating engine session when known. */
301
+ sessionId?: string;
302
+ requestId: string;
303
+ }
267
304
  /** Server status changed. */
268
305
  export interface StatusNotification {
269
306
  status: "ready" | "running" | "error" | "shutdown";
@@ -159,6 +159,13 @@ export function resolveGitBash() {
159
159
  const override = process.env.CODE_SHELL_GIT_BASH_PATH;
160
160
  if (override && existsSync(override))
161
161
  return (gitBashCache = override);
162
+ // Test-only hermetic switch: skip auto-discovery entirely. Clearing PATH is
163
+ // NOT enough on Windows — CreateProcess always searches System32, so `where`
164
+ // still spawns and a CI runner's preinstalled Git for Windows would resolve a
165
+ // real bash.exe and defeat the fallback tests. This env flag makes the
166
+ // "Git Bash absent" scenario reproducible on a real Windows host.
167
+ if (process.env.CODE_SHELL_NO_SHELL_DISCOVERY)
168
+ return (gitBashCache = null) ?? undefined;
162
169
  const candidates = [];
163
170
  // (2) derive from `where git`. Git installs git.exe under either \cmd\ or
164
171
  // \bin\; bash.exe lives under the sibling \bin\. Walk up to the Git root.
@@ -194,6 +201,9 @@ export function resolvePowerShell() {
194
201
  const override = process.env.CODE_SHELL_POWERSHELL_PATH;
195
202
  if (override && existsSync(override))
196
203
  return (powerShellCache = override);
204
+ // Test-only hermetic switch (see resolveGitBash for why PATH-clearing fails).
205
+ if (process.env.CODE_SHELL_NO_SHELL_DISCOVERY)
206
+ return (powerShellCache = null) ?? undefined;
197
207
  const candidates = [];
198
208
  for (const exe of ["pwsh", "powershell"]) {
199
209
  try {
@@ -14,6 +14,7 @@ type Runner = (opts: {
14
14
  cwd: string;
15
15
  permissionMode?: PermMode;
16
16
  signal?: AbortSignal;
17
+ imagePaths?: string[];
17
18
  }) => Promise<AgentRunResult>;
18
19
  type SessionStore = {
19
20
  get(cli: DriveCli, sessionId: string): ExternalAgentSessionBinding | undefined;