@yu_robotics/remote-cli 1.6.102 → 1.6.106
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/client/MessageHandler.d.ts +4 -0
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +108 -17
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/client/WebSocketClient.d.ts.map +1 -1
- package/dist/client/WebSocketClient.js +7 -1
- package/dist/client/WebSocketClient.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +1 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/delegation/DelegatedWorkerLaneCleanup.d.ts +11 -0
- package/dist/delegation/DelegatedWorkerLaneCleanup.d.ts.map +1 -0
- package/dist/delegation/DelegatedWorkerLaneCleanup.js +91 -0
- package/dist/delegation/DelegatedWorkerLaneCleanup.js.map +1 -0
- package/dist/delegation/DelegatedWorkerSessionStore.d.ts +62 -0
- package/dist/delegation/DelegatedWorkerSessionStore.d.ts.map +1 -0
- package/dist/delegation/DelegatedWorkerSessionStore.js +276 -0
- package/dist/delegation/DelegatedWorkerSessionStore.js.map +1 -0
- package/dist/delegation/DelegationManager.d.ts +31 -3
- package/dist/delegation/DelegationManager.d.ts.map +1 -1
- package/dist/delegation/DelegationManager.js +359 -34
- package/dist/delegation/DelegationManager.js.map +1 -1
- package/dist/delegation/DelegationStatusFormatter.js +2 -2
- package/dist/delegation/DelegationStatusFormatter.js.map +1 -1
- package/dist/delegation/contract.d.ts +2 -2
- package/dist/delegation/contract.d.ts.map +1 -1
- package/dist/delegation/contract.js +2 -2
- package/dist/delegation/contract.js.map +1 -1
- package/dist/executor/AcpExecutor.d.ts.map +1 -1
- package/dist/executor/AcpExecutor.js +3 -0
- package/dist/executor/AcpExecutor.js.map +1 -1
- package/dist/executor/AgyExecutor.d.ts.map +1 -1
- package/dist/executor/AgyExecutor.js +1 -0
- package/dist/executor/AgyExecutor.js.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts +10 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.js +32 -1
- package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
- package/dist/executor/CodexAppServerClient.d.ts +2 -0
- package/dist/executor/CodexAppServerClient.d.ts.map +1 -1
- package/dist/executor/CodexAppServerClient.js +63 -9
- package/dist/executor/CodexAppServerClient.js.map +1 -1
- package/dist/executor/CodexAppServerExecutor.d.ts +5 -0
- package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
- package/dist/executor/CodexAppServerExecutor.js +24 -0
- package/dist/executor/CodexAppServerExecutor.js.map +1 -1
- package/dist/executor/IExecutor.d.ts +8 -0
- package/dist/executor/IExecutor.d.ts.map +1 -1
- package/dist/executor/PiExecutor.d.ts +6 -0
- package/dist/executor/PiExecutor.d.ts.map +1 -1
- package/dist/executor/PiExecutor.js +18 -0
- package/dist/executor/PiExecutor.js.map +1 -1
- package/dist/executor/index.js +3 -3
- package/dist/executor/index.js.map +1 -1
- package/dist/executor/pi/PiClient.d.ts +1 -0
- package/dist/executor/pi/PiClient.d.ts.map +1 -1
- package/dist/executor/pi/PiClient.js +9 -0
- package/dist/executor/pi/PiClient.js.map +1 -1
- package/dist/executor/pi/PiTypes.d.ts +2 -0
- package/dist/executor/pi/PiTypes.d.ts.map +1 -1
- package/dist/executor/pi/PiTypes.js.map +1 -1
- package/dist/thread/ThreadExecutorPool.d.ts.map +1 -1
- package/dist/thread/ThreadExecutorPool.js +10 -1
- package/dist/thread/ThreadExecutorPool.js.map +1 -1
- package/dist/thread/ThreadManager.d.ts +1 -1
- package/dist/thread/ThreadManager.d.ts.map +1 -1
- package/dist/thread/ThreadManager.js +2 -0
- package/dist/thread/ThreadManager.js.map +1 -1
- package/dist/thread/types.d.ts +6 -0
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +19 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DelegationManager = exports.DELEGATION_LIMITS = void 0;
|
|
6
|
+
exports.DelegationManager = exports.DELEGATION_TEXT_PROGRESS = exports.DELEGATION_LIMITS = void 0;
|
|
7
7
|
exports.workspacesOverlap = workspacesOverlap;
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -12,6 +12,8 @@ const ThreadExecutorPool_1 = require("../thread/ThreadExecutorPool");
|
|
|
12
12
|
const executor_1 = require("../executor");
|
|
13
13
|
const BackendRegistry_1 = require("./BackendRegistry");
|
|
14
14
|
const DelegationStore_1 = require("./DelegationStore");
|
|
15
|
+
const DelegatedWorkerSessionStore_1 = require("./DelegatedWorkerSessionStore");
|
|
16
|
+
const DelegatedWorkerLaneCleanup_1 = require("./DelegatedWorkerLaneCleanup");
|
|
15
17
|
const contract_1 = require("./contract");
|
|
16
18
|
const WorkerPolicy_1 = require("./WorkerPolicy");
|
|
17
19
|
const DelegationNotice_1 = require("./DelegationNotice");
|
|
@@ -20,6 +22,12 @@ exports.DELEGATION_LIMITS = { launches: 12, concurrent: 3,
|
|
|
20
22
|
idleTimeoutMs: 15 * 60_000, toolIdleTimeoutMs: 45 * 60_000,
|
|
21
23
|
resultBytes: 32 * 1024, continuationBytes: 64 * 1024,
|
|
22
24
|
storageTimeoutMs: 10_000, calls: 500 };
|
|
25
|
+
/** Bounds and cadence for optional display-only delegated-worker text. */
|
|
26
|
+
exports.DELEGATION_TEXT_PROGRESS = {
|
|
27
|
+
flushMs: 2_500,
|
|
28
|
+
bufferBytes: 4 * 1024,
|
|
29
|
+
latestTextBytes: 1_200,
|
|
30
|
+
};
|
|
23
31
|
function validDuration(value, fallback) {
|
|
24
32
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
25
33
|
}
|
|
@@ -65,6 +73,28 @@ function bounded(text, limit, preserveEnd = false) {
|
|
|
65
73
|
start++;
|
|
66
74
|
return { text: buffer.subarray(0, end).toString('utf8') + marker + buffer.subarray(start).toString('utf8'), truncated: true };
|
|
67
75
|
}
|
|
76
|
+
/** Keep the most recent UTF-8-safe tail without retaining a worker transcript. */
|
|
77
|
+
function tailText(text, limit) {
|
|
78
|
+
const buffer = Buffer.from(text);
|
|
79
|
+
if (buffer.length <= limit)
|
|
80
|
+
return text;
|
|
81
|
+
let start = Math.max(0, buffer.length - Math.max(0, limit - Buffer.byteLength('…')));
|
|
82
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80)
|
|
83
|
+
start++;
|
|
84
|
+
return `…${buffer.subarray(start).toString('utf8')}`;
|
|
85
|
+
}
|
|
86
|
+
/** Prefer a completed sentence or paragraph, but always provide a bounded recent tail. */
|
|
87
|
+
function latestDisplayText(text) {
|
|
88
|
+
const normalized = text.replace(/\r\n?/g, '\n').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '').trim();
|
|
89
|
+
if (!normalized)
|
|
90
|
+
return undefined;
|
|
91
|
+
let end = 0;
|
|
92
|
+
for (const match of normalized.matchAll(/[.!?。!?](?:\s|$)|\n+/g)) {
|
|
93
|
+
end = match.index + match[0].length;
|
|
94
|
+
}
|
|
95
|
+
const complete = end > 0 ? normalized.slice(0, end).trimEnd() : normalized;
|
|
96
|
+
return tailText(complete, exports.DELEGATION_TEXT_PROGRESS.latestTextBytes);
|
|
97
|
+
}
|
|
68
98
|
function boundedResults(terminal) {
|
|
69
99
|
let results = terminal;
|
|
70
100
|
// Keep every task's identity and terminal status, reducing only text.
|
|
@@ -96,16 +126,18 @@ class DelegationManager {
|
|
|
96
126
|
cleanupMs;
|
|
97
127
|
registry;
|
|
98
128
|
store;
|
|
129
|
+
laneStore;
|
|
99
130
|
running = 0;
|
|
100
131
|
workspaces = new Map();
|
|
101
132
|
scopes = new Map();
|
|
102
133
|
timeouts;
|
|
103
|
-
constructor(guard, factory = executor_1.createExecutor, registry = new BackendRegistry_1.BackendRegistry(), store = new DelegationStore_1.DelegationStore(), timeoutInput = exports.DELEGATION_LIMITS, cleanupMs = 10_000) {
|
|
134
|
+
constructor(guard, factory = executor_1.createExecutor, registry = new BackendRegistry_1.BackendRegistry(), store = new DelegationStore_1.DelegationStore(), timeoutInput = exports.DELEGATION_LIMITS, cleanupMs = 10_000, laneStore = new DelegatedWorkerSessionStore_1.DelegatedWorkerSessionStore()) {
|
|
104
135
|
this.guard = guard;
|
|
105
136
|
this.factory = factory;
|
|
106
137
|
this.cleanupMs = cleanupMs;
|
|
107
138
|
this.registry = registry;
|
|
108
139
|
this.store = store;
|
|
140
|
+
this.laneStore = laneStore;
|
|
109
141
|
this.timeouts = timeoutPolicy(timeoutInput);
|
|
110
142
|
}
|
|
111
143
|
begin(parent) {
|
|
@@ -153,15 +185,111 @@ class DelegationManager {
|
|
|
153
185
|
console.warn('[Delegation] Progress could not be delivered');
|
|
154
186
|
}
|
|
155
187
|
};
|
|
188
|
+
const reportProgress = (task, progress) => {
|
|
189
|
+
try {
|
|
190
|
+
return parent.onProgress?.({ taskId: task.record.id, backend: task.record.backend, ...progress }) === true;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
console.warn('[Delegation] Progress could not be delivered');
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const reportTextProgress = (task, latestText) => {
|
|
198
|
+
try {
|
|
199
|
+
return parent.onTextProgress?.({
|
|
200
|
+
taskId: task.record.id,
|
|
201
|
+
backend: task.record.backend,
|
|
202
|
+
phase: 'text',
|
|
203
|
+
latestText,
|
|
204
|
+
startedAt: task.record.startedAt,
|
|
205
|
+
}) === true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
console.warn('[Delegation] Display text could not be delivered');
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const clearTextProgress = (task, discardPending = false) => {
|
|
213
|
+
clearTimeout(task.textTimer);
|
|
214
|
+
task.textTimer = undefined;
|
|
215
|
+
if (discardPending)
|
|
216
|
+
task.textBuffer = undefined;
|
|
217
|
+
};
|
|
218
|
+
const flushTextProgress = (task) => {
|
|
219
|
+
if (closed || task.record.state !== 'running' || !task.textProgressEnabled)
|
|
220
|
+
return;
|
|
221
|
+
const latestText = task.textBuffer ? latestDisplayText(task.textBuffer) : undefined;
|
|
222
|
+
if (!latestText || latestText === task.lastReportedText)
|
|
223
|
+
return;
|
|
224
|
+
if (!reportTextProgress(task, latestText)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
task.lastReportedText = latestText;
|
|
228
|
+
};
|
|
229
|
+
const scheduleTextProgress = (task) => {
|
|
230
|
+
if (closed || task.record.state !== 'running' || !task.textProgressEnabled || task.textTimer)
|
|
231
|
+
return;
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
if (task.textTimer !== timer)
|
|
234
|
+
return;
|
|
235
|
+
task.textTimer = undefined;
|
|
236
|
+
flushTextProgress(task);
|
|
237
|
+
}, exports.DELEGATION_TEXT_PROGRESS.flushMs);
|
|
238
|
+
timer.unref?.();
|
|
239
|
+
task.textTimer = timer;
|
|
240
|
+
};
|
|
241
|
+
const recordDisplayText = (task, text) => {
|
|
242
|
+
if (!task.textProgressEnabled || !text)
|
|
243
|
+
return;
|
|
244
|
+
task.textBuffer = tailText(`${task.textBuffer ?? ''}${text}`, exports.DELEGATION_TEXT_PROGRESS.bufferBytes);
|
|
245
|
+
scheduleTextProgress(task);
|
|
246
|
+
};
|
|
247
|
+
const reportTerminalProgress = (task) => {
|
|
248
|
+
if (task.record.state === 'running')
|
|
249
|
+
return false;
|
|
250
|
+
return reportProgress(task, {
|
|
251
|
+
phase: task.record.state,
|
|
252
|
+
summary: task.record.output ? bounded(task.record.output, 1000, true).text : undefined,
|
|
253
|
+
error: task.record.error ? bounded(task.record.error, 1000).text : undefined,
|
|
254
|
+
startedAt: task.record.startedAt,
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const destroyWorker = async (worker) => {
|
|
258
|
+
await worker.destroy();
|
|
259
|
+
if (!worker.waitForExit) {
|
|
260
|
+
throw new Error('Worker backend cannot confirm process exit');
|
|
261
|
+
}
|
|
262
|
+
await worker.waitForExit();
|
|
263
|
+
};
|
|
264
|
+
const discardLane = async (lane) => {
|
|
265
|
+
try {
|
|
266
|
+
await (0, DelegatedWorkerLaneCleanup_1.cleanupDelegatedWorkerLane)(lane);
|
|
267
|
+
await this.laneStore.remove(lane.id);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const detail = error instanceof Error ? error.message : 'Local worker lane cleanup failed';
|
|
272
|
+
await this.laneStore.markCleanupFailure(lane.id, detail).catch(() => undefined);
|
|
273
|
+
console.warn('[Delegation] Worker lane cleanup failed:', detail);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
156
277
|
const stop = (task) => task.stop ??= (async () => {
|
|
157
|
-
|
|
278
|
+
clearTextProgress(task, true);
|
|
279
|
+
if (!task.executor) {
|
|
280
|
+
// A cancellation can race with durable lane acquisition. No worker
|
|
281
|
+
// process owns this lane yet, so it must not become a reusable empty
|
|
282
|
+
// session after the task finishes.
|
|
283
|
+
task.discardLane = true;
|
|
284
|
+
task.interrupt();
|
|
158
285
|
return;
|
|
286
|
+
}
|
|
159
287
|
try {
|
|
160
288
|
try {
|
|
161
289
|
await deadline(() => task.executor.abort(), Math.min(2000, this.cleanupMs));
|
|
162
290
|
}
|
|
163
291
|
catch { /* Force cleanup still runs. */ }
|
|
164
|
-
await deadline(
|
|
292
|
+
await deadline(() => destroyWorker(task.executor), this.cleanupMs);
|
|
165
293
|
}
|
|
166
294
|
finally {
|
|
167
295
|
task.interrupt();
|
|
@@ -175,6 +303,7 @@ class DelegationManager {
|
|
|
175
303
|
if (task.record.state !== 'running')
|
|
176
304
|
return;
|
|
177
305
|
clearTaskTimer(task);
|
|
306
|
+
clearTextProgress(task, true);
|
|
178
307
|
task.record.state = 'timed_out';
|
|
179
308
|
task.record.error = `Delegated task stopped after ${formatDuration(duration)} without tool activity`;
|
|
180
309
|
void stop(task).catch(() => undefined);
|
|
@@ -202,6 +331,7 @@ class DelegationManager {
|
|
|
202
331
|
const cancel = async (task) => {
|
|
203
332
|
if (task.record.state === 'running') {
|
|
204
333
|
clearTaskTimer(task);
|
|
334
|
+
clearTextProgress(task, true);
|
|
205
335
|
task.record.state = 'cancelled';
|
|
206
336
|
task.record.error = 'Cancelled; edits already made were not undone.';
|
|
207
337
|
await stop(task).catch(() => undefined);
|
|
@@ -211,47 +341,140 @@ class DelegationManager {
|
|
|
211
341
|
};
|
|
212
342
|
const run = async (task, configuration, objective) => {
|
|
213
343
|
const backend = task.record.backend;
|
|
344
|
+
const identity = {
|
|
345
|
+
threadId: parent.thread.id,
|
|
346
|
+
backend,
|
|
347
|
+
workingDirectory: cwd,
|
|
348
|
+
workspaceGeneration: parent.thread.delegationWorkspaceGeneration ?? 0,
|
|
349
|
+
};
|
|
350
|
+
const createWorker = async () => {
|
|
351
|
+
if (!task.lane) {
|
|
352
|
+
const acquired = await this.laneStore.acquire(identity);
|
|
353
|
+
task.lane = acquired.lane;
|
|
354
|
+
}
|
|
355
|
+
let worker;
|
|
356
|
+
try {
|
|
357
|
+
worker = this.factory(this.guard, configuration, cwd, task.lane.executorThreadId, (0, ThreadExecutorPool_1.resolveThreadModel)(parent.thread, configuration), parent.thread.efforts?.[backend], { lifecycleHooks: false, delegationWorker: true });
|
|
358
|
+
task.executor = worker;
|
|
359
|
+
if (fs_1.default.realpathSync(worker.getCurrentWorkingDirectory()) !== cwd) {
|
|
360
|
+
throw new Error('Worker refused the delegated workspace; refusing a fallback directory');
|
|
361
|
+
}
|
|
362
|
+
await this.laneStore.markRunning(task.lane.id);
|
|
363
|
+
return worker;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
// The factory either supplied an executor that final cleanup can stop,
|
|
367
|
+
// or failed before any worker process became manager-owned. Neither
|
|
368
|
+
// case warrants a workspace quarantine by itself. Do not reuse a lane
|
|
369
|
+
// whose setup did not reach a completed delegated turn.
|
|
370
|
+
task.discardLane = true;
|
|
371
|
+
if (!worker)
|
|
372
|
+
task.executor = undefined;
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const discardMissingSessionLane = async () => {
|
|
377
|
+
const lane = task.lane;
|
|
378
|
+
const worker = task.executor;
|
|
379
|
+
if (!lane || !worker)
|
|
380
|
+
return false;
|
|
381
|
+
try {
|
|
382
|
+
await deadline(() => destroyWorker(worker), this.cleanupMs);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
task.quarantineWorkspace = true;
|
|
386
|
+
const detail = error instanceof Error ? error.message : 'Worker exit could not be confirmed';
|
|
387
|
+
await this.laneStore.markDirty(lane.id, detail).catch(() => undefined);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
task.executor = undefined;
|
|
391
|
+
await this.laneStore.markDirty(lane.id, 'Native worker session was unavailable before the objective was dispatched.', true);
|
|
392
|
+
const discarded = await discardLane(lane);
|
|
393
|
+
task.lane = undefined;
|
|
394
|
+
return discarded;
|
|
395
|
+
};
|
|
214
396
|
try {
|
|
215
397
|
if (task.record.state !== 'running')
|
|
216
398
|
return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const result = await Promise.race([task.interrupted, task.executor.execute(`You are executing one delegated task in an independent session. Complete only this objective and return a concise result with verification and remaining issues. Do not delegate to other agents.\n\n${objective}`, {
|
|
399
|
+
let result;
|
|
400
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
401
|
+
const worker = await createWorker();
|
|
402
|
+
// Native session IDs are distinct; policy was resolved against the real
|
|
403
|
+
// parent thread before passing the synthetic worker identity to the factory.
|
|
404
|
+
const execution = worker.execute(`You are executing one delegated task in an isolated worker session. This session may contain context from earlier delegated tasks in this thread and workspace. Complete only this objective and return a concise result with verification and remaining issues. Do not delegate to other agents.
|
|
405
|
+
|
|
406
|
+
${objective}`, {
|
|
226
407
|
// The manager owns delegated-task liveness. Zero disables optional
|
|
227
408
|
// backend-local limits that would otherwise ignore tool callbacks.
|
|
228
409
|
timeout: 0,
|
|
229
410
|
inactivityTimeout: 0,
|
|
230
411
|
onStream: text => {
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// Intermediate output is not retained here; only relay input prompts.
|
|
412
|
+
// Generic streams may include reasoning or backend notices. Use
|
|
413
|
+
// this channel only to detect interactive input requests.
|
|
234
414
|
queueMicrotask(() => {
|
|
235
|
-
if (
|
|
236
|
-
|
|
415
|
+
if (closed || task.record.state !== 'running')
|
|
416
|
+
return;
|
|
417
|
+
if (task.executor?.isWaitingInput?.()) {
|
|
418
|
+
clearTextProgress(task, true);
|
|
419
|
+
const prompt = bounded(text, 4000).text;
|
|
420
|
+
if (task.waitingInputText === prompt)
|
|
421
|
+
return;
|
|
422
|
+
task.waitingInputText = prompt;
|
|
423
|
+
if (!reportProgress(task, { phase: 'waiting_input', summary: prompt, startedAt: task.record.startedAt })) {
|
|
424
|
+
notify(() => parent.onNotice(`
|
|
425
|
+
[${backend} delegated task]
|
|
426
|
+
${prompt}`));
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
237
429
|
}
|
|
238
430
|
});
|
|
239
431
|
},
|
|
432
|
+
onDisplayText: text => {
|
|
433
|
+
if (!closed && task.record.state === 'running' && !task.executor?.isWaitingInput?.()) {
|
|
434
|
+
recordDisplayText(task, text);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
240
437
|
onToolUse: tool => {
|
|
438
|
+
if (task.record.state !== 'running')
|
|
439
|
+
return;
|
|
440
|
+
clearTextProgress(task, true);
|
|
441
|
+
task.waitingInputText = undefined;
|
|
241
442
|
if (tool.id)
|
|
242
443
|
task.activeToolIds.add(tool.id);
|
|
243
444
|
noteToolActivity(task, 'tool_use');
|
|
445
|
+
reportProgress(task, { phase: 'tool_use', toolUse: { id: tool.id, name: tool.name, input: {} }, startedAt: task.record.startedAt });
|
|
244
446
|
},
|
|
245
|
-
onToolResult:
|
|
246
|
-
if (
|
|
247
|
-
|
|
447
|
+
onToolResult: toolResult => {
|
|
448
|
+
if (task.record.state !== 'running')
|
|
449
|
+
return;
|
|
450
|
+
clearTextProgress(task, true);
|
|
451
|
+
task.waitingInputText = undefined;
|
|
452
|
+
if (toolResult.tool_use_id)
|
|
453
|
+
task.activeToolIds.delete(toolResult.tool_use_id);
|
|
248
454
|
noteToolActivity(task, 'tool_result');
|
|
455
|
+
reportProgress(task, { phase: 'tool_result', toolResult: {
|
|
456
|
+
tool_use_id: toolResult.tool_use_id, content: '', is_error: toolResult.is_error,
|
|
457
|
+
}, startedAt: task.record.startedAt });
|
|
249
458
|
},
|
|
250
459
|
onApprovalRequest: request => !closed && task.record.state === 'running' && parent.onApproval({ ...request,
|
|
251
460
|
description: `[${backend} delegated task] ${request.description}`, canRemember: false }, task.executor),
|
|
252
461
|
onApprovalResolved: parent.onApprovalResolved,
|
|
253
|
-
})
|
|
254
|
-
|
|
462
|
+
});
|
|
463
|
+
// Start the inactivity window only once the worker has accepted the
|
|
464
|
+
// objective and can emit its first real progress callback.
|
|
465
|
+
armIdleTimer(task);
|
|
466
|
+
result = await Promise.race([task.interrupted, execution]);
|
|
467
|
+
if (!result.success && attempt === 0 && worker.consumeSessionResumeFailure?.()) {
|
|
468
|
+
clearTaskTimer(task);
|
|
469
|
+
clearTextProgress(task, true);
|
|
470
|
+
task.lastReportedText = undefined;
|
|
471
|
+
if (await discardMissingSessionLane())
|
|
472
|
+
continue;
|
|
473
|
+
throw new Error('The worker session was unavailable and its process could not be safely reset.');
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (task.record.state === 'running' && result) {
|
|
255
478
|
const output = bounded(result.output ?? '', exports.DELEGATION_LIMITS.resultBytes, true);
|
|
256
479
|
task.record.state = result.success ? 'succeeded' : 'failed';
|
|
257
480
|
task.record.output = output.text;
|
|
@@ -269,22 +492,49 @@ class DelegationManager {
|
|
|
269
492
|
}
|
|
270
493
|
finally {
|
|
271
494
|
clearTaskTimer(task);
|
|
272
|
-
|
|
495
|
+
clearTextProgress(task, true);
|
|
496
|
+
let released = !task.quarantineWorkspace;
|
|
273
497
|
try {
|
|
274
498
|
if (task.stop)
|
|
275
499
|
await task.stop;
|
|
276
500
|
else if (task.executor)
|
|
277
|
-
await deadline(
|
|
278
|
-
|
|
501
|
+
await deadline(() => destroyWorker(task.executor), this.cleanupMs);
|
|
502
|
+
else if (task.lane) {
|
|
503
|
+
// A scope can close after durable acquisition but before construction.
|
|
504
|
+
// No backend process was started, so discard the lane rather than
|
|
505
|
+
// reserving the workspace indefinitely.
|
|
506
|
+
task.discardLane = true;
|
|
507
|
+
}
|
|
508
|
+
if (released && task.lane && task.discardLane) {
|
|
509
|
+
const lane = task.lane;
|
|
510
|
+
await this.laneStore.markDirty(lane.id, 'Delegated worker setup ended before a reusable context was established.', true).catch(() => undefined);
|
|
511
|
+
await discardLane(lane);
|
|
512
|
+
task.lane = undefined;
|
|
513
|
+
}
|
|
514
|
+
if (released && task.lane) {
|
|
515
|
+
try {
|
|
516
|
+
await this.laneStore.markReady(task.lane.id);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
const detail = error instanceof Error ? error.message : 'Worker lane persistence failed';
|
|
520
|
+
await this.laneStore.markDirty(task.lane.id, detail, true).catch(() => undefined);
|
|
521
|
+
await discardLane(task.lane);
|
|
522
|
+
console.warn('[Delegation] Worker lane could not be retained:', detail);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
279
525
|
}
|
|
280
526
|
catch (error) {
|
|
527
|
+
released = false;
|
|
528
|
+
task.quarantineWorkspace = true;
|
|
529
|
+
if (task.lane) {
|
|
530
|
+
const detail = error instanceof Error ? error.message : 'Worker cleanup could not be confirmed';
|
|
531
|
+
await this.laneStore.markDirty(task.lane.id, detail).catch(() => undefined);
|
|
532
|
+
}
|
|
281
533
|
task.record.state = 'interrupted';
|
|
282
534
|
task.record.error = 'Worker cleanup could not be confirmed. This workspace is blocked until the worker is stopped and the CLI restarts.';
|
|
283
535
|
console.warn('[Delegation] Worker cleanup failed:', error instanceof Error ? error.message : 'Cleanup failure');
|
|
284
536
|
}
|
|
285
|
-
if (
|
|
286
|
-
await deadline(() => task.executor?.deleteThreadData?.(`delegate-${task.record.id}`), this.cleanupMs)
|
|
287
|
-
.catch(() => console.warn('[Delegation] Worker session cleanup did not finish'));
|
|
537
|
+
if (released) {
|
|
288
538
|
this.running--;
|
|
289
539
|
this.workspaces.delete(cwd);
|
|
290
540
|
}
|
|
@@ -293,8 +543,10 @@ class DelegationManager {
|
|
|
293
543
|
task.record.finishedAt = Date.now();
|
|
294
544
|
await deadline(async () => { await save(task); await this.store.prune(); }, this.cleanupMs)
|
|
295
545
|
.catch(() => console.warn('[Delegation] Final task metadata could not be saved or pruned'));
|
|
296
|
-
|
|
297
|
-
|
|
546
|
+
if (!reportTerminalProgress(task)) {
|
|
547
|
+
notify(() => parent.onToolResult({ tool_use_id: task.record.id, content: JSON.stringify(view(task)), is_error: task.record.state !== 'succeeded' }));
|
|
548
|
+
notify(() => parent.onNotice((0, DelegationNotice_1.formatDelegationNotice)(task.record)));
|
|
549
|
+
}
|
|
298
550
|
task.settle();
|
|
299
551
|
}
|
|
300
552
|
};
|
|
@@ -397,6 +649,7 @@ class DelegationManager {
|
|
|
397
649
|
const task = { record: { id: (0, crypto_1.randomUUID)(), threadId: parent.thread.id,
|
|
398
650
|
parentMessageId: parent.messageId, backend, objective: objective.slice(0, 1000),
|
|
399
651
|
state: 'running', startedAt: Date.now() }, resultDelivered: false,
|
|
652
|
+
textProgressEnabled: parent.onTextProgress !== undefined,
|
|
400
653
|
activeToolIds: new Set(), done: new Promise(resolve => { settle = resolve; }), settle, interrupted, interrupt };
|
|
401
654
|
tasks.set(task.record.id, task);
|
|
402
655
|
try {
|
|
@@ -410,12 +663,35 @@ class DelegationManager {
|
|
|
410
663
|
task.settle();
|
|
411
664
|
throw error;
|
|
412
665
|
}
|
|
666
|
+
try {
|
|
667
|
+
const lane = await this.laneStore.acquire({
|
|
668
|
+
threadId: parent.thread.id,
|
|
669
|
+
backend,
|
|
670
|
+
workingDirectory: cwd,
|
|
671
|
+
workspaceGeneration: parent.thread.delegationWorkspaceGeneration ?? 0,
|
|
672
|
+
});
|
|
673
|
+
task.lane = lane.lane;
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
task.record.state = 'failed';
|
|
677
|
+
task.record.error = bounded(error instanceof Error ? error.message : 'Worker lane storage failed', 4000).text;
|
|
678
|
+
task.record.finishedAt = Date.now();
|
|
679
|
+
await this.store.write(task.record).catch(() => undefined);
|
|
680
|
+
tasks.delete(task.record.id);
|
|
681
|
+
this.running--;
|
|
682
|
+
this.workspaces.delete(cwd);
|
|
683
|
+
launches--;
|
|
684
|
+
task.settle();
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
413
687
|
if (closed) {
|
|
414
688
|
task.record.state = 'cancelled';
|
|
415
689
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
690
|
+
if (!reportProgress(task, { phase: 'started', objective: task.record.objective, startedAt: task.record.startedAt })) {
|
|
691
|
+
notify(() => parent.onToolUse({ id: task.record.id, name: 'Task', input: {
|
|
692
|
+
description: `${backend}: ${objective.slice(0, 120)}`, prompt: objective.slice(0, 1000), subagent_type: backend,
|
|
693
|
+
} }));
|
|
694
|
+
}
|
|
419
695
|
void run(task, configuration, objective).catch(() => undefined);
|
|
420
696
|
return receive(task, requestedDuring);
|
|
421
697
|
}
|
|
@@ -480,6 +756,55 @@ class DelegationManager {
|
|
|
480
756
|
return scope;
|
|
481
757
|
}
|
|
482
758
|
waitingExecutor(threadId) { return this.scopes.get(threadId)?.waitingExecutor(); }
|
|
759
|
+
hasActiveTasks(threadId) { return this.scopes.get(threadId)?.hasTasks() ?? false; }
|
|
760
|
+
/** Retry previously confirmed cleanup requests without touching uncertain interrupted workers. */
|
|
761
|
+
async reconcilePendingWorkerLanes() {
|
|
762
|
+
return this.removeWorkerLanes(await this.laneStore.cleanupCandidates(), true);
|
|
763
|
+
}
|
|
764
|
+
/** Remove all lanes older than the new workspace generation after a successful `/cd`. */
|
|
765
|
+
async invalidateWorkspaceGeneration(threadId, currentGeneration) {
|
|
766
|
+
if (this.hasActiveTasks(threadId)) {
|
|
767
|
+
throw new Error('Cannot change working directory while a delegated worker is running. Wait for it to finish or send /abort first.');
|
|
768
|
+
}
|
|
769
|
+
return this.removeWorkerLanes(await this.laneStore.invalidateGeneration(threadId, currentGeneration), true);
|
|
770
|
+
}
|
|
771
|
+
/** Forget one backend lane or all lanes for an idle parent thread. */
|
|
772
|
+
async resetWorkerLanes(threadId, backend) {
|
|
773
|
+
if (this.hasActiveTasks(threadId)) {
|
|
774
|
+
throw new Error('Cannot reset delegated worker context while a delegated worker is running. Wait for it to finish or send /abort first.');
|
|
775
|
+
}
|
|
776
|
+
const result = await this.removeWorkerLanes(await this.laneStore.markForReset(threadId, backend));
|
|
777
|
+
return result.removed;
|
|
778
|
+
}
|
|
779
|
+
/** Thread deletion is executor-free so it works whether delegation is currently enabled or not. */
|
|
780
|
+
async deleteWorkerLanes(threadId) {
|
|
781
|
+
if (this.hasActiveTasks(threadId)) {
|
|
782
|
+
throw new Error('Cannot delete delegated worker context while a delegated worker is running.');
|
|
783
|
+
}
|
|
784
|
+
const result = await this.removeWorkerLanes(await this.laneStore.markForReset(threadId));
|
|
785
|
+
return result.removed;
|
|
786
|
+
}
|
|
787
|
+
async removeWorkerLanes(lanes, tolerateFailures = false) {
|
|
788
|
+
let removed = 0;
|
|
789
|
+
let failed = 0;
|
|
790
|
+
for (const lane of lanes) {
|
|
791
|
+
try {
|
|
792
|
+
await (0, DelegatedWorkerLaneCleanup_1.cleanupDelegatedWorkerLane)(lane);
|
|
793
|
+
await this.laneStore.remove(lane.id);
|
|
794
|
+
removed++;
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
failed++;
|
|
798
|
+
const detail = error instanceof Error ? error.message : 'Local worker lane cleanup failed';
|
|
799
|
+
await this.laneStore.markCleanupFailure(lane.id, detail).catch(() => undefined);
|
|
800
|
+
console.warn('[Delegation] Worker lane cleanup failed:', detail);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (failed && !tolerateFailures) {
|
|
804
|
+
throw new Error(`Delegated worker cleanup failed for ${failed} of ${lanes.length} lane(s). ${removed} lane(s) were removed; retry after checking filesystem permissions.`);
|
|
805
|
+
}
|
|
806
|
+
return { removed, failed };
|
|
807
|
+
}
|
|
483
808
|
blocksWorkspace(cwd, threadId) {
|
|
484
809
|
if (this.workspaces.size === 0)
|
|
485
810
|
return false;
|