@cjhyy/code-shell-core 0.8.13 → 0.9.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 (91) hide show
  1. package/dist/automation/desktop-authority-client.d.ts +9 -0
  2. package/dist/automation/desktop-authority-client.js +47 -0
  3. package/dist/automation/scheduler.d.ts +8 -0
  4. package/dist/automation/scheduler.js +37 -1
  5. package/dist/automation/store.js +14 -1
  6. package/dist/capabilities/index.d.ts +1 -0
  7. package/dist/cli/agent-server-stdio.js +8 -4
  8. package/dist/engine/engine-workspace-authority.d.ts +35 -0
  9. package/dist/engine/engine-workspace-authority.js +136 -0
  10. package/dist/engine/engine.d.ts +5 -10
  11. package/dist/engine/engine.js +108 -112
  12. package/dist/engine/input-attachments.d.ts +1 -0
  13. package/dist/engine/input-attachments.js +6 -2
  14. package/dist/engine/run-environment.d.ts +8 -3
  15. package/dist/engine/run-environment.js +33 -8
  16. package/dist/engine/run-image-input.d.ts +1 -0
  17. package/dist/engine/run-image-input.js +1 -0
  18. package/dist/engine/run-session-open.d.ts +2 -1
  19. package/dist/engine/run-session-open.js +7 -1
  20. package/dist/engine/run-setup.d.ts +1 -0
  21. package/dist/engine/run-setup.js +2 -1
  22. package/dist/engine/run-tooling.d.ts +2 -0
  23. package/dist/engine/run-tooling.js +3 -0
  24. package/dist/engine/run-types.d.ts +4 -0
  25. package/dist/engine/run-workspace.d.ts +6 -1
  26. package/dist/engine/run-workspace.js +47 -0
  27. package/dist/engine/subagent-spawner.d.ts +1 -0
  28. package/dist/engine/subagent-spawner.js +1 -0
  29. package/dist/engine/turn-loop.js +17 -0
  30. package/dist/engine/types.d.ts +2 -0
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.extension.d.ts +1 -1
  33. package/dist/index.internal.d.ts +3 -2
  34. package/dist/index.internal.js +2 -0
  35. package/dist/index.js +2 -2
  36. package/dist/llm/client-base.d.ts +2 -1
  37. package/dist/llm/client-base.js +3 -1
  38. package/dist/llm/providers/anthropic.js +24 -25
  39. package/dist/llm/providers/openai.d.ts +4 -1
  40. package/dist/llm/providers/openai.js +76 -13
  41. package/dist/panel-apps/index.d.ts +1 -1
  42. package/dist/panel-apps/index.js +1 -1
  43. package/dist/panel-apps/installer.d.ts +29 -0
  44. package/dist/panel-apps/installer.js +124 -15
  45. package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
  46. package/dist/plugins/pluginAutomationTemplates.js +4 -0
  47. package/dist/plugins/pluginCatalog.d.ts +6 -0
  48. package/dist/plugins/pluginCatalog.js +7 -2
  49. package/dist/plugins/pluginContent.d.ts +1 -1
  50. package/dist/plugins/pluginContent.js +2 -10
  51. package/dist/prompt/composer.d.ts +4 -1
  52. package/dist/prompt/composer.js +11 -3
  53. package/dist/protocol/chat-session-manager.d.ts +42 -1
  54. package/dist/protocol/chat-session-manager.js +167 -4
  55. package/dist/protocol/chat-session.d.ts +11 -1
  56. package/dist/protocol/chat-session.js +20 -2
  57. package/dist/protocol/mobile-remote-types.d.ts +15 -0
  58. package/dist/protocol/server.d.ts +1 -2
  59. package/dist/protocol/server.js +28 -51
  60. package/dist/protocol/session-workspace-rpc.d.ts +23 -0
  61. package/dist/protocol/session-workspace-rpc.js +171 -0
  62. package/dist/protocol/types.d.ts +47 -1
  63. package/dist/protocol/types.js +4 -0
  64. package/dist/session/session-manager.d.ts +25 -0
  65. package/dist/session/session-manager.js +106 -6
  66. package/dist/session/transcript.d.ts +9 -0
  67. package/dist/session/transcript.js +81 -0
  68. package/dist/settings/manager.d.ts +12 -0
  69. package/dist/settings/manager.js +31 -0
  70. package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
  71. package/dist/tool-system/builtin/configure-model-connection.js +396 -0
  72. package/dist/tool-system/builtin/cron.d.ts +11 -0
  73. package/dist/tool-system/builtin/cron.js +27 -2
  74. package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
  75. package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
  76. package/dist/tool-system/builtin/edit.js +5 -3
  77. package/dist/tool-system/builtin/index.js +14 -0
  78. package/dist/tool-system/builtin/install-capability.js +22 -8
  79. package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
  80. package/dist/tool-system/builtin/settings-changed.js +18 -0
  81. package/dist/tool-system/builtin/write.js +5 -3
  82. package/dist/tool-system/context.d.ts +12 -3
  83. package/dist/tool-system/executor.js +1 -1
  84. package/dist/tool-system/path-policy.d.ts +11 -3
  85. package/dist/tool-system/path-policy.js +56 -36
  86. package/dist/types.d.ts +11 -0
  87. package/dist/workspace/canonical-key.d.ts +7 -0
  88. package/dist/workspace/canonical-key.js +39 -0
  89. package/dist/workspace/workspace-context.d.ts +26 -0
  90. package/dist/workspace/workspace-context.js +112 -0
  91. package/package.json +1 -1
@@ -34,6 +34,9 @@ export class ChatSessionManager {
34
34
  closingSessions = new Map();
35
35
  closedSessions = new Set();
36
36
  sessionGeneration = new Map();
37
+ sessionSlices = new Map();
38
+ migrationClaims = new Map();
39
+ residentMigrations = new Map();
37
40
  runtime;
38
41
  /** Identity scope this manager serves ("local" unless injected). */
39
42
  identity;
@@ -78,11 +81,133 @@ export class ChatSessionManager {
78
81
  });
79
82
  }
80
83
  async getOrCreate(sessionId, slice) {
81
- const closing = this.closingSessions.get(sessionId);
82
- if (closing) {
83
- await closing;
84
+ // A non-resident migration claim is a short ownership handoff to Main.
85
+ // Wait rather than fail the user's run: once Main atomically commits (or
86
+ // aborts) and releases the token, the Engine is created from current disk.
87
+ for (;;) {
88
+ const residentMigration = this.residentMigrations.get(sessionId);
89
+ if (residentMigration) {
90
+ await residentMigration.released;
91
+ continue;
92
+ }
93
+ const claim = this.migrationClaims.get(sessionId);
94
+ if (claim) {
95
+ await claim.released;
96
+ continue;
97
+ }
98
+ const closing = this.closingSessions.get(sessionId);
99
+ if (closing) {
100
+ await closing;
101
+ continue;
102
+ }
103
+ // No await separates the final claim check from getOrCreateNow. On this
104
+ // process's event loop, either this creates the resident owner first or
105
+ // beginSessionMigration installs the fence first; both cannot win.
106
+ return this.getOrCreateNow(sessionId, slice);
107
+ }
108
+ }
109
+ /**
110
+ * Prove whether this worker currently owns a resident Engine, or fence the
111
+ * Session so Main can perform one durable migration without a re-resume race.
112
+ */
113
+ beginSessionMigration(sessionId, ownershipToken) {
114
+ if (this.residentMigrations.has(sessionId)) {
115
+ return { status: "failed", error: `Session ${sessionId} migration is already in progress` };
116
+ }
117
+ const resident = this.sessions.get(sessionId);
118
+ if (resident)
119
+ return { status: "resident", session: resident };
120
+ if (this.closingSessions.has(sessionId)) {
121
+ return { status: "failed", error: `Session ${sessionId} is closing` };
122
+ }
123
+ if (this.migrationClaims.has(sessionId)) {
124
+ return { status: "failed", error: `Session ${sessionId} migration is already in progress` };
125
+ }
126
+ let release;
127
+ const released = new Promise((resolve) => {
128
+ release = resolve;
129
+ });
130
+ this.migrationClaims.set(sessionId, { ownershipToken, released, release });
131
+ return { status: "not-resident", ownershipToken };
132
+ }
133
+ /** Release only the exact claim minted for this Session. */
134
+ completeSessionMigration(sessionId, ownershipToken) {
135
+ const claim = this.migrationClaims.get(sessionId);
136
+ if (!claim || claim.ownershipToken !== ownershipToken)
137
+ return false;
138
+ this.migrationClaims.delete(sessionId);
139
+ claim.release();
140
+ return true;
141
+ }
142
+ /**
143
+ * Rebuild an idle resident Engine against a new authoritative main root.
144
+ *
145
+ * Transaction order is intentional:
146
+ * 1. construct and restore the candidate while durable state still points at
147
+ * the old owner (factory/restore failure is therefore a no-op),
148
+ * 2. atomically commit durable state through the old owning Engine,
149
+ * 3. synchronously swap the Engine inside the existing ChatSession,
150
+ * 4. dispose the unreachable old Engine before reporting success.
151
+ *
152
+ * No await occurs between the idle check, durable commit and owner swap.
153
+ */
154
+ async migrateResidentSessionMainRoot(sessionId, target) {
155
+ const session = this.sessions.get(sessionId);
156
+ if (!session)
157
+ throw new Error(`Session ${sessionId} is not resident`);
158
+ if (session.isBusy() || session.queueDepth() > 0) {
159
+ throw new Error(`Session ${sessionId} is running or has queued turns`);
160
+ }
161
+ if (this.residentMigrations.has(sessionId) || this.migrationClaims.has(sessionId)) {
162
+ throw new Error(`Session ${sessionId} migration is already in progress`);
163
+ }
164
+ let release;
165
+ const released = new Promise((resolve) => {
166
+ release = resolve;
167
+ });
168
+ this.residentMigrations.set(sessionId, { released, release });
169
+ const previous = session.engine;
170
+ let candidate;
171
+ let committed = false;
172
+ try {
173
+ const previousSlice = this.sessionSlices.get(sessionId) ?? {};
174
+ const permissionMode = previous.getPermissionMode?.() ?? previousSlice.permissionMode;
175
+ const nextSlice = {
176
+ ...previousSlice,
177
+ cwd: target.mainRoot,
178
+ workspaceContext: target.workspaceContext,
179
+ projectTrusted: target.projectTrusted,
180
+ ...(permissionMode ? { permissionMode } : {}),
181
+ };
182
+ const built = this.factory(nextSlice);
183
+ if (built === previous) {
184
+ throw new Error(`Session ${sessionId} Engine factory reused the resident owner`);
185
+ }
186
+ candidate = built;
187
+ if (permissionMode && candidate.getPermissionMode?.() !== permissionMode) {
188
+ candidate.setPermissionMode?.(permissionMode);
189
+ }
190
+ const candidateGeneration = this.engineSessionManager(candidate)?.registerSessionGeneration(sessionId) ??
191
+ this.sessionGeneration.get(sessionId) ??
192
+ 1;
193
+ candidate.restoreSessionModel?.(sessionId);
194
+ const workspace = previous.migrateSessionMainRoot(sessionId, target.project, target.mainRoot);
195
+ session.replaceEngine(previous, candidate);
196
+ this.sessionGeneration.set(sessionId, candidateGeneration);
197
+ this.sessionSlices.set(sessionId, nextSlice);
198
+ committed = true;
199
+ await this.disposeEngine(previous, sessionId);
200
+ return workspace;
201
+ }
202
+ catch (error) {
203
+ if (!committed && candidate)
204
+ await this.disposeEngine(candidate, sessionId);
205
+ throw error;
206
+ }
207
+ finally {
208
+ this.residentMigrations.delete(sessionId);
209
+ release();
84
210
  }
85
- return this.getOrCreateNow(sessionId, slice);
86
211
  }
87
212
  /**
88
213
  * Cold-resume a persisted session using its own cwd. The first detached
@@ -129,6 +254,7 @@ export class ChatSessionManager {
129
254
  const sessionManager = this.engineSessionManager(engine);
130
255
  const generation = sessionManager?.registerSessionGeneration(sessionId) ?? 1;
131
256
  this.sessionGeneration.set(sessionId, generation);
257
+ this.sessionSlices.set(sessionId, { ...slice });
132
258
  this.sessions.set(sessionId, session);
133
259
  // A direct user run is an explicit resume/open and may clear the tombstone.
134
260
  // Background wakeups must check isUnavailable() before reaching this path.
@@ -201,6 +327,10 @@ export class ChatSessionManager {
201
327
  return this.closeSession(sessionId, true);
202
328
  }
203
329
  closeSession(sessionId, markClosed) {
330
+ const migration = this.residentMigrations.get(sessionId);
331
+ if (migration) {
332
+ return migration.released.then(() => this.closeSession(sessionId, markClosed));
333
+ }
204
334
  const alreadyClosing = this.closingSessions.get(sessionId);
205
335
  if (alreadyClosing)
206
336
  return alreadyClosing;
@@ -237,6 +367,7 @@ export class ChatSessionManager {
237
367
  else
238
368
  this.closedSessions.delete(sessionId);
239
369
  this.sessionGeneration.delete(sessionId);
370
+ this.sessionSlices.delete(sessionId);
240
371
  };
241
372
  if (!s.isBusy()) {
242
373
  finishClose();
@@ -278,6 +409,9 @@ export class ChatSessionManager {
278
409
  * immediately afterward (the TUI REPL) doesn't orphan detached dev servers.
279
410
  */
280
411
  async closeAllAsync() {
412
+ for (const [sessionId, claim] of [...this.migrationClaims]) {
413
+ this.completeSessionMigration(sessionId, claim.ownershipToken);
414
+ }
281
415
  await Promise.all([...this.sessions.keys()].map((id) => this.close(id)));
282
416
  // App/worker shutdown — reap every background shell so a detached
283
417
  // `npm run dev` doesn't outlive the process as an orphan holding a port
@@ -335,6 +469,35 @@ export class ChatSessionManager {
335
469
  });
336
470
  });
337
471
  }
472
+ async disposeEngine(engine, sessionId) {
473
+ const dispose = engine.dispose;
474
+ if (typeof dispose === "function") {
475
+ try {
476
+ await dispose.call(engine);
477
+ }
478
+ catch (error) {
479
+ logger.warn("chat_session.engine_dispose_failed", {
480
+ sessionId,
481
+ identity: this.identity,
482
+ error: error instanceof Error ? error.message : String(error),
483
+ });
484
+ }
485
+ return;
486
+ }
487
+ const mcpPool = this.runtime.mcpPool;
488
+ if (typeof mcpPool?.unregisterOwner !== "function")
489
+ return;
490
+ try {
491
+ await mcpPool.unregisterOwner(engine);
492
+ }
493
+ catch (error) {
494
+ logger.warn("chat_session.mcp_owner_unregister_failed", {
495
+ sessionId,
496
+ identity: this.identity,
497
+ error: error instanceof Error ? error.message : String(error),
498
+ });
499
+ }
500
+ }
338
501
  engineSessionManager(engine) {
339
502
  const candidate = engine;
340
503
  const manager = candidate.getSessionManager?.();
@@ -21,6 +21,7 @@ export interface TurnOpts {
21
21
  /** Working directory override for this turn. If omitted, Engine uses its
22
22
  * configured cwd. */
23
23
  cwd?: string;
24
+ workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
24
25
  onStream?: (event: StreamEvent) => void;
25
26
  /** User-facing text persisted beside the full model-facing task. */
26
27
  displayText?: string;
@@ -37,6 +38,8 @@ export interface TurnOpts {
37
38
  archiveBeforeCurrentTurn?: {
38
39
  fromClientMessageId?: string;
39
40
  segmentId?: string;
41
+ /** Host-authored replacement for the archived span; skips the summarizer. */
42
+ summary?: string;
40
43
  };
41
44
  /** Structured input attachments for this turn. */
42
45
  attachments?: InputAttachmentMeta[];
@@ -68,7 +71,7 @@ export interface TurnOpts {
68
71
  */
69
72
  export declare class ChatSession {
70
73
  readonly id: string;
71
- readonly engine: Engine;
74
+ private currentEngine;
72
75
  /**
73
76
  * Per-session approval callbacks and resolver-free metadata indexed by requestId.
74
77
  * `readonly` guards the Map reference (preventing reassignment); the
@@ -109,6 +112,13 @@ export declare class ChatSession {
109
112
  private settlePromise;
110
113
  private resolveSettled;
111
114
  constructor(opts: ChatSessionOptions);
115
+ get engine(): Engine;
116
+ /**
117
+ * Swap the Engine at an idle run boundary without replacing the ChatSession
118
+ * that owns the queue, stream callback, approvals, idle timestamp and id.
119
+ * The expected-owner check makes a stale migration fail closed.
120
+ */
121
+ replaceEngine(expected: Engine, replacement: Engine): void;
112
122
  enqueueTurn(task: string, opts: TurnOpts): Promise<EngineResult>;
113
123
  /**
114
124
  * Run session maintenance (for example context-package summarization) under
@@ -7,7 +7,7 @@ import { isSameGoalInstance } from "../goal/lifecycle.js";
7
7
  */
8
8
  export class ChatSession {
9
9
  id;
10
- engine;
10
+ currentEngine;
11
11
  /**
12
12
  * Per-session approval callbacks and resolver-free metadata indexed by requestId.
13
13
  * `readonly` guards the Map reference (preventing reassignment); the
@@ -49,9 +49,26 @@ export class ChatSession {
49
49
  resolveSettled = null;
50
50
  constructor(opts) {
51
51
  this.id = opts.id;
52
- this.engine = opts.engine;
52
+ this.currentEngine = opts.engine;
53
53
  this.defaultOnStream = opts.onStream;
54
54
  }
55
+ get engine() {
56
+ return this.currentEngine;
57
+ }
58
+ /**
59
+ * Swap the Engine at an idle run boundary without replacing the ChatSession
60
+ * that owns the queue, stream callback, approvals, idle timestamp and id.
61
+ * The expected-owner check makes a stale migration fail closed.
62
+ */
63
+ replaceEngine(expected, replacement) {
64
+ if (this.active || this.exclusiveOperation || this.queue.length > 0) {
65
+ throw new Error(`Session ${this.id} is running or has queued turns`);
66
+ }
67
+ if (this.currentEngine !== expected) {
68
+ throw new Error(`Session ${this.id} Engine ownership changed during migration`);
69
+ }
70
+ this.currentEngine = replacement;
71
+ }
55
72
  enqueueTurn(task, opts) {
56
73
  this.lastActivityAt = Date.now();
57
74
  // The user (or a wakeup the guard already let through) is starting a turn —
@@ -283,6 +300,7 @@ export class ChatSession {
283
300
  const onStream = next.opts.onStream ?? this.defaultOnStream;
284
301
  const result = await this.engine.run(next.task, {
285
302
  cwd: next.opts.cwd,
303
+ workspaceContext: next.opts.workspaceContext,
286
304
  sessionId: this.id,
287
305
  displayText: next.opts.displayText,
288
306
  signal: this.controller.signal,
@@ -66,10 +66,18 @@ export interface MobilePermissionModeSnapshotEntry {
66
66
  mode: PermissionMode;
67
67
  }
68
68
  export interface MobileProjectMeta {
69
+ id?: string;
69
70
  path: string;
70
71
  name: string;
71
72
  addedAt?: number;
72
73
  pinned?: boolean;
74
+ roots?: Array<{
75
+ id: string;
76
+ path: string;
77
+ name: string;
78
+ role: "primary" | "secondary";
79
+ }>;
80
+ primaryRootId?: string;
73
81
  }
74
82
  export type MobileImageMime = "image/png" | "image/jpeg" | "image/webp" | "image/gif";
75
83
  export interface MobileImageBase {
@@ -112,6 +120,11 @@ export type MobileClientEvent = {
112
120
  sessionId: string;
113
121
  } | {
114
122
  type: "session.create";
123
+ /** Stable V2 project identity. null explicitly selects the no-repo workspace. */
124
+ projectId?: string | null;
125
+ /** Stable V2 root identity. Omit to use the project's current primary root. */
126
+ rootId?: string;
127
+ /** Legacy client compatibility only; Main validates it against mounted roots. */
115
128
  cwd?: string | null;
116
129
  name?: string;
117
130
  } | {
@@ -230,6 +243,8 @@ export type MobileServerEvent = {
230
243
  type: "chat.accepted";
231
244
  sessionId?: string;
232
245
  cwd?: string | null;
246
+ projectId?: string | null;
247
+ rootId?: string;
233
248
  clientMessageId?: string;
234
249
  attachments?: MobileAttachmentSummary[];
235
250
  } | {
@@ -198,6 +198,7 @@ export declare class AgentServer {
198
198
  */
199
199
  private bgAgentBusUnsubscribe;
200
200
  private readonly wakeupsInFlight;
201
+ private readonly sessionWorkspaceRpc;
201
202
  /**
202
203
  * Effective session manager for the connection this server serves. Without
203
204
  * a resolveIdentity hook this is exactly the host-supplied manager —
@@ -310,8 +311,6 @@ export declare class AgentServer {
310
311
  */
311
312
  private handleBackgroundWork;
312
313
  private handleCloseSession;
313
- private handleReleaseWorkspace;
314
- private handleSetWorkspace;
315
314
  private handleConfigure;
316
315
  /**
317
316
  * Resolve the Engine that owns a session-scoped query (compact, archive_range,
@@ -30,6 +30,7 @@ import { redactSecrets } from "../logging/sanitize-messages.js";
30
30
  import { compileComposition } from "../composition/compiler.js";
31
31
  import { attachProtocolContributions } from "../composition/protocol-attach.js";
32
32
  import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
33
+ import { SessionWorkspaceRpcHandlers } from "./session-workspace-rpc.js";
33
34
  import { describePluginCommands, expandPluginCommandBody, scanPluginCommands, MAX_PLUGIN_COMMAND_ARGUMENT_CHARS, } from "../plugins/pluginCommandsLoader.js";
34
35
  function isValidRunAttachment(value) {
35
36
  if (!value || typeof value !== "object")
@@ -85,6 +86,13 @@ function runInputError(params) {
85
86
  return `archiveBeforeCurrentTurn.${field} must be a bounded non-empty string`;
86
87
  }
87
88
  }
89
+ if (archive.summary !== undefined &&
90
+ (typeof archive.summary !== "string" ||
91
+ archive.summary.trim().length === 0 ||
92
+ archive.summary.length > 4_000 ||
93
+ /[\u0000]/u.test(archive.summary))) {
94
+ return "archiveBeforeCurrentTurn.summary must be a non-empty string up to 4000 characters";
95
+ }
88
96
  }
89
97
  if (params.behaviorMode !== undefined &&
90
98
  (typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
@@ -373,6 +381,7 @@ export class AgentServer {
373
381
  */
374
382
  bgAgentBusUnsubscribe = null;
375
383
  wakeupsInFlight = new Set();
384
+ sessionWorkspaceRpc;
376
385
  /**
377
386
  * Effective session manager for the connection this server serves. Without
378
387
  * a resolveIdentity hook this is exactly the host-supplied manager —
@@ -418,6 +427,14 @@ export class AgentServer {
418
427
  throw new Error("AgentServer: either chatManager or engine must be supplied");
419
428
  }
420
429
  this.transport = options.transport;
430
+ this.sessionWorkspaceRpc = new SessionWorkspaceRpcHandlers({
431
+ transport: this.transport,
432
+ getChatManager: () => this.chatManager,
433
+ getLegacyEngine: () => this.legacyEngine,
434
+ getLastSlice: (sessionId) => this.lastSliceBySession.get(sessionId),
435
+ rememberSessionSlice: (sessionId, slice) => this.rememberSessionSlice(sessionId, slice),
436
+ wireInteractiveSession: (session, sessionId) => this.wireInteractiveSession(session, sessionId),
437
+ });
421
438
  // Protocol surface from the compiled composition. Each contributing
422
439
  // module may attach one observer; the server calls them at every
423
440
  // lifecycle hook point and isolates per-observer failures.
@@ -780,10 +797,16 @@ export class AgentServer {
780
797
  await this.handleCloseSession(req);
781
798
  break;
782
799
  case Methods.ReleaseWorkspace:
783
- this.handleReleaseWorkspace(req);
800
+ this.sessionWorkspaceRpc.release(req);
784
801
  break;
785
802
  case Methods.SetWorkspace:
786
- this.handleSetWorkspace(req);
803
+ this.sessionWorkspaceRpc.set(req);
804
+ break;
805
+ case Methods.MigrateSessionMainRoot:
806
+ await this.sessionWorkspaceRpc.migrateMainRoot(req);
807
+ break;
808
+ case Methods.CompleteSessionMainRootMigration:
809
+ this.sessionWorkspaceRpc.completeMainRootMigration(req);
787
810
  break;
788
811
  case Methods.GoalExtend:
789
812
  this.handleGoalExtend(req);
@@ -1146,6 +1169,7 @@ export class AgentServer {
1146
1169
  }
1147
1170
  const sessionConfig = {
1148
1171
  cwd: params.cwd,
1172
+ workspaceContext: params.workspaceContext,
1149
1173
  projectTrusted: params.projectTrusted,
1150
1174
  preset: params.preset ??
1151
1175
  (params.behaviorMode === ISOLATED_TASK_BEHAVIOR_MODE ? "general" : undefined),
@@ -1219,6 +1243,7 @@ export class AgentServer {
1219
1243
  }
1220
1244
  const run = session.enqueueTurn(params.task, {
1221
1245
  cwd: params.cwd,
1246
+ workspaceContext: params.workspaceContext,
1222
1247
  displayText: displayText || undefined,
1223
1248
  injected: params.injected === true,
1224
1249
  attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
@@ -1351,6 +1376,7 @@ export class AgentServer {
1351
1376
  });
1352
1377
  const result = await this.legacyEngine.run(params.task, {
1353
1378
  cwd: params.cwd,
1379
+ workspaceContext: params.workspaceContext,
1354
1380
  sessionId: params.sessionId,
1355
1381
  displayText: displayText || undefined,
1356
1382
  injected: params.injected === true,
@@ -1937,55 +1963,6 @@ export class AgentServer {
1937
1963
  backgroundJobRegistry.dropForSession(params.sessionId);
1938
1964
  this.transport.send(createResponse(req.id, { ok: true }));
1939
1965
  }
1940
- // ─── ReleaseWorkspace ──────────────────────────────────────────
1941
- handleReleaseWorkspace(req) {
1942
- const params = (req.params ?? {});
1943
- if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
1944
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1945
- return;
1946
- }
1947
- if (this.chatManager) {
1948
- const session = this.chatManager.get(params.sessionId);
1949
- if (!session) {
1950
- this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
1951
- return;
1952
- }
1953
- const engine = session.engine;
1954
- const workspace = engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
1955
- this.transport.send(createResponse(req.id, { ok: true, workspace }));
1956
- return;
1957
- }
1958
- const engine = this.legacyEngine;
1959
- const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
1960
- this.transport.send(createResponse(req.id, { ok: true, workspace }));
1961
- }
1962
- handleSetWorkspace(req) {
1963
- const params = (req.params ?? {});
1964
- if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
1965
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1966
- return;
1967
- }
1968
- if (!params.workspace ||
1969
- typeof params.workspace !== "object" ||
1970
- typeof params.workspace.root !== "string" ||
1971
- params.workspace.root.length === 0 ||
1972
- (params.workspace.kind !== "main" && params.workspace.kind !== "worktree")) {
1973
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid workspace is required"));
1974
- return;
1975
- }
1976
- const engine = this.chatManager
1977
- ? this.chatManager.get(params.sessionId)?.engine
1978
- : this.legacyEngine;
1979
- if (!engine) {
1980
- this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
1981
- return;
1982
- }
1983
- const workspace = engine.setSessionWorkspace?.(params.sessionId, params.workspace);
1984
- this.transport.send(createResponse(req.id, {
1985
- ok: workspace !== undefined && workspace !== null,
1986
- workspace: workspace ?? null,
1987
- }));
1988
- }
1989
1966
  // ─── Configure ──────────────────────────────────────────────────
1990
1967
  handleConfigure(req) {
1991
1968
  const params = (req.params ?? {});
@@ -0,0 +1,23 @@
1
+ import type { Engine } from "../engine/engine.js";
2
+ import type { ChatSession } from "./chat-session.js";
3
+ import type { ChatSessionManager, EngineConfigSlice } from "./chat-session-manager.js";
4
+ import type { Transport } from "./transport.js";
5
+ import { type RpcRequest } from "./types.js";
6
+ interface SessionWorkspaceRpcDependencies {
7
+ transport: Transport;
8
+ getChatManager(): ChatSessionManager | null;
9
+ getLegacyEngine(): Engine | null;
10
+ getLastSlice(sessionId: string): EngineConfigSlice | undefined;
11
+ rememberSessionSlice(sessionId: string, slice: EngineConfigSlice): void;
12
+ wireInteractiveSession(session: ChatSession, sessionId: string): void;
13
+ }
14
+ /** Protocol boundary for Session workspace ownership and root migration. */
15
+ export declare class SessionWorkspaceRpcHandlers {
16
+ private readonly deps;
17
+ constructor(deps: SessionWorkspaceRpcDependencies);
18
+ release(req: RpcRequest): void;
19
+ set(req: RpcRequest): void;
20
+ migrateMainRoot(req: RpcRequest): Promise<void>;
21
+ completeMainRootMigration(req: RpcRequest): void;
22
+ }
23
+ export {};