@cjhyy/code-shell-core 0.8.20 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/automation/desktop-authority-client.d.ts +9 -0
- package/dist/automation/desktop-authority-client.js +47 -0
- package/dist/automation/scheduler.d.ts +8 -0
- package/dist/automation/scheduler.js +37 -1
- package/dist/automation/store.js +14 -1
- package/dist/capabilities/index.d.ts +1 -0
- package/dist/cli/agent-server-stdio.js +4 -1
- package/dist/engine/engine-workspace-authority.d.ts +50 -0
- package/dist/engine/engine-workspace-authority.js +167 -0
- package/dist/engine/engine.d.ts +7 -10
- package/dist/engine/engine.js +87 -101
- package/dist/engine/input-attachments.d.ts +1 -0
- package/dist/engine/input-attachments.js +6 -2
- package/dist/engine/run-environment.d.ts +8 -3
- package/dist/engine/run-environment.js +33 -8
- package/dist/engine/run-image-input.d.ts +1 -0
- package/dist/engine/run-image-input.js +1 -0
- package/dist/engine/run-session-open.d.ts +2 -1
- package/dist/engine/run-session-open.js +6 -0
- package/dist/engine/run-setup.d.ts +1 -0
- package/dist/engine/run-setup.js +2 -1
- package/dist/engine/run-tooling.d.ts +2 -0
- package/dist/engine/run-tooling.js +3 -0
- package/dist/engine/run-types.d.ts +2 -0
- package/dist/engine/run-workspace.d.ts +6 -1
- package/dist/engine/run-workspace.js +47 -0
- package/dist/engine/subagent-spawner.d.ts +1 -0
- package/dist/engine/subagent-spawner.js +1 -0
- package/dist/engine/types.d.ts +2 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.extension.d.ts +1 -1
- package/dist/index.internal.d.ts +2 -2
- package/dist/index.internal.js +1 -0
- package/dist/index.js +1 -1
- package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
- package/dist/plugins/pluginAutomationTemplates.js +4 -0
- package/dist/prompt/composer.d.ts +4 -1
- package/dist/prompt/composer.js +11 -3
- package/dist/protocol/background-result-wakeup.d.ts +18 -0
- package/dist/protocol/background-result-wakeup.js +101 -0
- package/dist/protocol/chat-session-manager.d.ts +42 -1
- package/dist/protocol/chat-session-manager.js +167 -4
- package/dist/protocol/chat-session.d.ts +9 -1
- package/dist/protocol/chat-session.js +20 -2
- package/dist/protocol/mobile-remote-types.d.ts +15 -0
- package/dist/protocol/server.d.ts +5 -6
- package/dist/protocol/server.js +62 -128
- package/dist/protocol/session-workspace-rpc.d.ts +23 -0
- package/dist/protocol/session-workspace-rpc.js +171 -0
- package/dist/protocol/types.d.ts +45 -1
- package/dist/protocol/types.js +4 -0
- package/dist/session/session-manager.d.ts +18 -0
- package/dist/session/session-manager.js +87 -0
- package/dist/settings/manager.d.ts +7 -0
- package/dist/settings/manager.js +64 -3
- package/dist/tool-system/builtin/agent-notifications.d.ts +8 -0
- package/dist/tool-system/builtin/agent-notifications.js +19 -0
- package/dist/tool-system/builtin/config.d.ts +11 -0
- package/dist/tool-system/builtin/config.js +55 -10
- package/dist/tool-system/builtin/cron.d.ts +11 -0
- package/dist/tool-system/builtin/cron.js +27 -2
- package/dist/tool-system/builtin/edit.js +5 -3
- package/dist/tool-system/builtin/view-image.js +8 -2
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +4 -1
- package/dist/tool-system/executor.js +1 -1
- package/dist/tool-system/path-policy.d.ts +11 -3
- package/dist/tool-system/path-policy.js +56 -36
- package/dist/types.d.ts +6 -0
- package/dist/workspace/canonical-key.d.ts +7 -0
- package/dist/workspace/canonical-key.js +39 -0
- package/dist/workspace/workspace-context.d.ts +35 -0
- package/dist/workspace/workspace-context.js +128 -0
- 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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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;
|
|
@@ -70,7 +71,7 @@ export interface TurnOpts {
|
|
|
70
71
|
*/
|
|
71
72
|
export declare class ChatSession {
|
|
72
73
|
readonly id: string;
|
|
73
|
-
|
|
74
|
+
private currentEngine;
|
|
74
75
|
/**
|
|
75
76
|
* Per-session approval callbacks and resolver-free metadata indexed by requestId.
|
|
76
77
|
* `readonly` guards the Map reference (preventing reassignment); the
|
|
@@ -111,6 +112,13 @@ export declare class ChatSession {
|
|
|
111
112
|
private settlePromise;
|
|
112
113
|
private resolveSettled;
|
|
113
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;
|
|
114
122
|
enqueueTurn(task: string, opts: TurnOpts): Promise<EngineResult>;
|
|
115
123
|
/**
|
|
116
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
|
-
|
|
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.
|
|
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 —
|
|
@@ -217,10 +218,10 @@ export declare class AgentServer {
|
|
|
217
218
|
* Guards:
|
|
218
219
|
* - chatManager path only (the legacy single-engine / headless path drives
|
|
219
220
|
* its own loop and has no idle-session-resume concept).
|
|
220
|
-
* - Session must exist
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
221
|
+
* - Session must exist. If it is busy, keep ownership of this wake request and
|
|
222
|
+
* wait for `settled` before draining. Merely returning here loses a wake when
|
|
223
|
+
* the bus event races the run-boundary re-check: both triggers can observe
|
|
224
|
+
* the other as in-flight and leave the result queued forever.
|
|
224
225
|
* - We `drainAll` exactly here and feed the items into the woken turn. This
|
|
225
226
|
* also merges a burst of near-simultaneous completions into one wakeup:
|
|
226
227
|
* the first drains all currently-pending items; subsequent bus events for
|
|
@@ -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,
|
package/dist/protocol/server.js
CHANGED
|
@@ -18,18 +18,20 @@ import { diskDefaultsFrom } from "../engine/engine.js";
|
|
|
18
18
|
import { ISOLATED_TASK_BEHAVIOR_MODE } from "../engine/run-types.js";
|
|
19
19
|
import { isProtectedSettingKey, SettingsManager } from "../settings/manager.js";
|
|
20
20
|
import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
21
|
-
import { agentNotificationBus, notificationQueue,
|
|
21
|
+
import { agentNotificationBus, notificationQueue, notificationEnvelopeToLegacyStreamEvent, } from "../tool-system/builtin/agent-notifications.js";
|
|
22
22
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
23
23
|
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 { wakeSessionForBackgroundResults } from "./background-result-wakeup.js";
|
|
27
28
|
import { assertSafeSessionId, SessionManager } from "../session/session-manager.js";
|
|
28
29
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
29
30
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
30
31
|
import { compileComposition } from "../composition/compiler.js";
|
|
31
32
|
import { attachProtocolContributions } from "../composition/protocol-attach.js";
|
|
32
33
|
import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
|
|
34
|
+
import { SessionWorkspaceRpcHandlers } from "./session-workspace-rpc.js";
|
|
33
35
|
import { describePluginCommands, expandPluginCommandBody, scanPluginCommands, MAX_PLUGIN_COMMAND_ARGUMENT_CHARS, } from "../plugins/pluginCommandsLoader.js";
|
|
34
36
|
function isValidRunAttachment(value) {
|
|
35
37
|
if (!value || typeof value !== "object")
|
|
@@ -380,6 +382,7 @@ export class AgentServer {
|
|
|
380
382
|
*/
|
|
381
383
|
bgAgentBusUnsubscribe = null;
|
|
382
384
|
wakeupsInFlight = new Set();
|
|
385
|
+
sessionWorkspaceRpc;
|
|
383
386
|
/**
|
|
384
387
|
* Effective session manager for the connection this server serves. Without
|
|
385
388
|
* a resolveIdentity hook this is exactly the host-supplied manager —
|
|
@@ -425,6 +428,14 @@ export class AgentServer {
|
|
|
425
428
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
426
429
|
}
|
|
427
430
|
this.transport = options.transport;
|
|
431
|
+
this.sessionWorkspaceRpc = new SessionWorkspaceRpcHandlers({
|
|
432
|
+
transport: this.transport,
|
|
433
|
+
getChatManager: () => this.chatManager,
|
|
434
|
+
getLegacyEngine: () => this.legacyEngine,
|
|
435
|
+
getLastSlice: (sessionId) => this.lastSliceBySession.get(sessionId),
|
|
436
|
+
rememberSessionSlice: (sessionId, slice) => this.rememberSessionSlice(sessionId, slice),
|
|
437
|
+
wireInteractiveSession: (session, sessionId) => this.wireInteractiveSession(session, sessionId),
|
|
438
|
+
});
|
|
428
439
|
// Protocol surface from the compiled composition. Each contributing
|
|
429
440
|
// module may attach one observer; the server calls them at every
|
|
430
441
|
// lifecycle hook point and isolates per-observer failures.
|
|
@@ -488,22 +499,29 @@ export class AgentServer {
|
|
|
488
499
|
// hands us.
|
|
489
500
|
this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((envelope) => {
|
|
490
501
|
const sessionId = envelope.to.sessionId;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
502
|
+
try {
|
|
503
|
+
const event = notificationEnvelopeToLegacyStreamEvent(envelope);
|
|
504
|
+
if (event)
|
|
505
|
+
this.notify(Methods.StreamEvent, { sessionId, event });
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
// Waking the model is the durable-consumption path; forwarding the
|
|
509
|
+
// legacy UI event is only an observation path. A renderer transport
|
|
510
|
+
// failure after accepting the event must not strand the result in the
|
|
511
|
+
// queue, so schedule the wake from finally.
|
|
512
|
+
if (envelope.kind === "result")
|
|
513
|
+
this.maybeWakeIdleSession(sessionId);
|
|
514
|
+
}
|
|
515
|
+
// Background work that finishes while the session is idle (a
|
|
495
516
|
// run_in_background Bash like a download, a background sub-agent, or a
|
|
496
517
|
// video poll — the engine no longer parks on any of them) would otherwise
|
|
497
518
|
// leave its completion sitting in the queue until the user manually sends.
|
|
498
519
|
// Wake the session with one run carrying the notification so the model
|
|
499
520
|
// reads "download complete" and continues on its own (the persisted goal
|
|
500
521
|
// is judged that turn). If the work finishes while a run is still in
|
|
501
|
-
// flight,
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
// classification needed).
|
|
505
|
-
if (envelope.kind === "result")
|
|
506
|
-
this.maybeWakeIdleSession(sessionId);
|
|
522
|
+
// flight, wakeIdleSession waits for that run to settle and then queues the
|
|
523
|
+
// continuation. A never-exiting dev server emits no completion, so it
|
|
524
|
+
// never wakes anything (no task/service classification needed).
|
|
507
525
|
});
|
|
508
526
|
// Notify client we're ready
|
|
509
527
|
this.notify(Methods.Status, { status: "ready" });
|
|
@@ -517,10 +535,10 @@ export class AgentServer {
|
|
|
517
535
|
* Guards:
|
|
518
536
|
* - chatManager path only (the legacy single-engine / headless path drives
|
|
519
537
|
* its own loop and has no idle-session-resume concept).
|
|
520
|
-
* - Session must exist
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
538
|
+
* - Session must exist. If it is busy, keep ownership of this wake request and
|
|
539
|
+
* wait for `settled` before draining. Merely returning here loses a wake when
|
|
540
|
+
* the bus event races the run-boundary re-check: both triggers can observe
|
|
541
|
+
* the other as in-flight and leave the result queued forever.
|
|
524
542
|
* - We `drainAll` exactly here and feed the items into the woken turn. This
|
|
525
543
|
* also merges a burst of near-simultaneous completions into one wakeup:
|
|
526
544
|
* the first drains all currently-pending items; subsequent bus events for
|
|
@@ -541,67 +559,22 @@ export class AgentServer {
|
|
|
541
559
|
this.maybeWakeIdleSession(sessionId);
|
|
542
560
|
}
|
|
543
561
|
})
|
|
544
|
-
.catch(() => {
|
|
562
|
+
.catch((error) => {
|
|
545
563
|
this.wakeupsInFlight.delete(sessionId);
|
|
564
|
+
logger.warn("bg_wakeup.failed", {
|
|
565
|
+
sessionId,
|
|
566
|
+
error: error instanceof Error ? error.message : String(error),
|
|
567
|
+
});
|
|
546
568
|
});
|
|
547
569
|
}
|
|
548
570
|
async wakeIdleSession(sessionId) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
557
|
-
// is gone, so there's no consumer for a woken continuation turn. Headless
|
|
558
|
-
// already drained its background sub-agents inside engine.run before
|
|
559
|
-
// returning; any remaining queued notification (video/shell) must NOT spin
|
|
560
|
-
// an orphan turn. Only the interactive path auto-continues.
|
|
561
|
-
if (session.engine.isHeadless())
|
|
562
|
-
return false;
|
|
563
|
-
// Don't resurrect a session the user just Stopped: cancel() leaves it idle
|
|
564
|
-
// (active=null) so isBusy() reads false, but auto-running a fresh turn here
|
|
565
|
-
// would defeat the Stop. The flag clears the moment the user sends again.
|
|
566
|
-
if (session.wasCancelledSinceLastTurn())
|
|
567
|
-
return false;
|
|
568
|
-
const pending = notificationQueue.drainAll(sessionId);
|
|
569
|
-
if (pending.length === 0)
|
|
570
|
-
return false;
|
|
571
|
-
const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
|
|
572
|
-
try {
|
|
573
|
-
await session.enqueueTurn(task, {
|
|
574
|
-
// Synthetic notification, not the user's own input: persisted with an
|
|
575
|
-
// `injected` flag so a disk rebuild doesn't render it as a phantom user
|
|
576
|
-
// bubble (the live UI shows only the woken assistant's reply).
|
|
577
|
-
injected: true,
|
|
578
|
-
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
579
|
-
approvalRouter: this.approvalRouter,
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
catch (err) {
|
|
583
|
-
// A wakeup turn failing must not crash the bus fan-out. The drained
|
|
584
|
-
// notifications are already in the transcript via the run's messages;
|
|
585
|
-
// log and move on.
|
|
586
|
-
logger.warn("bg_wakeup.turn_failed", {
|
|
587
|
-
sessionId,
|
|
588
|
-
error: err.message,
|
|
589
|
-
});
|
|
590
|
-
// Belt-and-braces, mirroring the send() path's run().then(clear-busy):
|
|
591
|
-
// the renderer set the composer "working" spinner on this run's
|
|
592
|
-
// session_started, and clears it on turn_complete/error. A failure
|
|
593
|
-
// BEFORE the turn-loop runs (e.g. a setup error) emits neither, which
|
|
594
|
-
// would leave the spinner stuck. Emit a terminal `error` (NOT a
|
|
595
|
-
// turn_complete) so the renderer clears busy AND a woken automation
|
|
596
|
-
// session's runStatus flips to "failed" rather than being mislabeled
|
|
597
|
-
// "completed". If the turn-loop already emitted its own `error`, this is
|
|
598
|
-
// a harmless duplicate (busy already cleared, status already failed).
|
|
599
|
-
this.notify(Methods.StreamEvent, {
|
|
600
|
-
sessionId,
|
|
601
|
-
event: { type: "error", error: err?.message ?? "background wakeup failed" },
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
return true;
|
|
571
|
+
return wakeSessionForBackgroundResults({
|
|
572
|
+
sessionId,
|
|
573
|
+
manager: this.chatManager,
|
|
574
|
+
rehydrate: (id) => this.rehydrateSessionForWake(id),
|
|
575
|
+
approvalRouter: this.approvalRouter,
|
|
576
|
+
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
577
|
+
});
|
|
605
578
|
}
|
|
606
579
|
async rehydrateSessionForWake(sessionId) {
|
|
607
580
|
if (!this.chatManager)
|
|
@@ -787,10 +760,16 @@ export class AgentServer {
|
|
|
787
760
|
await this.handleCloseSession(req);
|
|
788
761
|
break;
|
|
789
762
|
case Methods.ReleaseWorkspace:
|
|
790
|
-
this.
|
|
763
|
+
this.sessionWorkspaceRpc.release(req);
|
|
791
764
|
break;
|
|
792
765
|
case Methods.SetWorkspace:
|
|
793
|
-
this.
|
|
766
|
+
this.sessionWorkspaceRpc.set(req);
|
|
767
|
+
break;
|
|
768
|
+
case Methods.MigrateSessionMainRoot:
|
|
769
|
+
await this.sessionWorkspaceRpc.migrateMainRoot(req);
|
|
770
|
+
break;
|
|
771
|
+
case Methods.CompleteSessionMainRootMigration:
|
|
772
|
+
this.sessionWorkspaceRpc.completeMainRootMigration(req);
|
|
794
773
|
break;
|
|
795
774
|
case Methods.GoalExtend:
|
|
796
775
|
this.handleGoalExtend(req);
|
|
@@ -1153,6 +1132,7 @@ export class AgentServer {
|
|
|
1153
1132
|
}
|
|
1154
1133
|
const sessionConfig = {
|
|
1155
1134
|
cwd: params.cwd,
|
|
1135
|
+
workspaceContext: params.workspaceContext,
|
|
1156
1136
|
projectTrusted: params.projectTrusted,
|
|
1157
1137
|
preset: params.preset ??
|
|
1158
1138
|
(params.behaviorMode === ISOLATED_TASK_BEHAVIOR_MODE ? "general" : undefined),
|
|
@@ -1226,6 +1206,7 @@ export class AgentServer {
|
|
|
1226
1206
|
}
|
|
1227
1207
|
const run = session.enqueueTurn(params.task, {
|
|
1228
1208
|
cwd: params.cwd,
|
|
1209
|
+
workspaceContext: params.workspaceContext,
|
|
1229
1210
|
displayText: displayText || undefined,
|
|
1230
1211
|
injected: params.injected === true,
|
|
1231
1212
|
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
@@ -1268,11 +1249,12 @@ export class AgentServer {
|
|
|
1268
1249
|
petWorkDelegation: result.petWorkDelegation,
|
|
1269
1250
|
};
|
|
1270
1251
|
this.transport.send(createResponse(req.id, runResult));
|
|
1271
|
-
// Run-boundary re-check (trigger B):
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
// drained its sub-agents inside
|
|
1252
|
+
// Run-boundary re-check (trigger B): normally trigger A already owns a
|
|
1253
|
+
// completion that arrived while this run was busy and is waiting on the
|
|
1254
|
+
// session's `settled` promise. Keep this check as a recovery path for a
|
|
1255
|
+
// missed/delayed bus event and for results committed at the run boundary.
|
|
1256
|
+
// Interactive path only; headless already drained its sub-agents inside
|
|
1257
|
+
// engine.run before returning.
|
|
1276
1258
|
this.maybeWakeIdleSession(sid);
|
|
1277
1259
|
}
|
|
1278
1260
|
catch (err) {
|
|
@@ -1358,6 +1340,7 @@ export class AgentServer {
|
|
|
1358
1340
|
});
|
|
1359
1341
|
const result = await this.legacyEngine.run(params.task, {
|
|
1360
1342
|
cwd: params.cwd,
|
|
1343
|
+
workspaceContext: params.workspaceContext,
|
|
1361
1344
|
sessionId: params.sessionId,
|
|
1362
1345
|
displayText: displayText || undefined,
|
|
1363
1346
|
injected: params.injected === true,
|
|
@@ -1944,55 +1927,6 @@ export class AgentServer {
|
|
|
1944
1927
|
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
1945
1928
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
1946
1929
|
}
|
|
1947
|
-
// ─── ReleaseWorkspace ──────────────────────────────────────────
|
|
1948
|
-
handleReleaseWorkspace(req) {
|
|
1949
|
-
const params = (req.params ?? {});
|
|
1950
|
-
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
1951
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
if (this.chatManager) {
|
|
1955
|
-
const session = this.chatManager.get(params.sessionId);
|
|
1956
|
-
if (!session) {
|
|
1957
|
-
this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
const engine = session.engine;
|
|
1961
|
-
const workspace = engine.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
1962
|
-
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const engine = this.legacyEngine;
|
|
1966
|
-
const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
1967
|
-
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
1968
|
-
}
|
|
1969
|
-
handleSetWorkspace(req) {
|
|
1970
|
-
const params = (req.params ?? {});
|
|
1971
|
-
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
1972
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
1973
|
-
return;
|
|
1974
|
-
}
|
|
1975
|
-
if (!params.workspace ||
|
|
1976
|
-
typeof params.workspace !== "object" ||
|
|
1977
|
-
typeof params.workspace.root !== "string" ||
|
|
1978
|
-
params.workspace.root.length === 0 ||
|
|
1979
|
-
(params.workspace.kind !== "main" && params.workspace.kind !== "worktree")) {
|
|
1980
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid workspace is required"));
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
const engine = this.chatManager
|
|
1984
|
-
? this.chatManager.get(params.sessionId)?.engine
|
|
1985
|
-
: this.legacyEngine;
|
|
1986
|
-
if (!engine) {
|
|
1987
|
-
this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
const workspace = engine.setSessionWorkspace?.(params.sessionId, params.workspace);
|
|
1991
|
-
this.transport.send(createResponse(req.id, {
|
|
1992
|
-
ok: workspace !== undefined && workspace !== null,
|
|
1993
|
-
workspace: workspace ?? null,
|
|
1994
|
-
}));
|
|
1995
|
-
}
|
|
1996
1930
|
// ─── Configure ──────────────────────────────────────────────────
|
|
1997
1931
|
handleConfigure(req) {
|
|
1998
1932
|
const params = (req.params ?? {});
|