@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.
Files changed (73) hide show
  1. package/dist/client/MessageHandler.d.ts +4 -0
  2. package/dist/client/MessageHandler.d.ts.map +1 -1
  3. package/dist/client/MessageHandler.js +108 -17
  4. package/dist/client/MessageHandler.js.map +1 -1
  5. package/dist/client/WebSocketClient.d.ts.map +1 -1
  6. package/dist/client/WebSocketClient.js +7 -1
  7. package/dist/client/WebSocketClient.js.map +1 -1
  8. package/dist/commands/start.d.ts.map +1 -1
  9. package/dist/commands/start.js +1 -0
  10. package/dist/commands/start.js.map +1 -1
  11. package/dist/delegation/DelegatedWorkerLaneCleanup.d.ts +11 -0
  12. package/dist/delegation/DelegatedWorkerLaneCleanup.d.ts.map +1 -0
  13. package/dist/delegation/DelegatedWorkerLaneCleanup.js +91 -0
  14. package/dist/delegation/DelegatedWorkerLaneCleanup.js.map +1 -0
  15. package/dist/delegation/DelegatedWorkerSessionStore.d.ts +62 -0
  16. package/dist/delegation/DelegatedWorkerSessionStore.d.ts.map +1 -0
  17. package/dist/delegation/DelegatedWorkerSessionStore.js +276 -0
  18. package/dist/delegation/DelegatedWorkerSessionStore.js.map +1 -0
  19. package/dist/delegation/DelegationManager.d.ts +31 -3
  20. package/dist/delegation/DelegationManager.d.ts.map +1 -1
  21. package/dist/delegation/DelegationManager.js +359 -34
  22. package/dist/delegation/DelegationManager.js.map +1 -1
  23. package/dist/delegation/DelegationStatusFormatter.js +2 -2
  24. package/dist/delegation/DelegationStatusFormatter.js.map +1 -1
  25. package/dist/delegation/contract.d.ts +2 -2
  26. package/dist/delegation/contract.d.ts.map +1 -1
  27. package/dist/delegation/contract.js +2 -2
  28. package/dist/delegation/contract.js.map +1 -1
  29. package/dist/executor/AcpExecutor.d.ts.map +1 -1
  30. package/dist/executor/AcpExecutor.js +3 -0
  31. package/dist/executor/AcpExecutor.js.map +1 -1
  32. package/dist/executor/AgyExecutor.d.ts.map +1 -1
  33. package/dist/executor/AgyExecutor.js +1 -0
  34. package/dist/executor/AgyExecutor.js.map +1 -1
  35. package/dist/executor/ClaudePersistentExecutor.d.ts +10 -1
  36. package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
  37. package/dist/executor/ClaudePersistentExecutor.js +32 -1
  38. package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
  39. package/dist/executor/CodexAppServerClient.d.ts +2 -0
  40. package/dist/executor/CodexAppServerClient.d.ts.map +1 -1
  41. package/dist/executor/CodexAppServerClient.js +63 -9
  42. package/dist/executor/CodexAppServerClient.js.map +1 -1
  43. package/dist/executor/CodexAppServerExecutor.d.ts +5 -0
  44. package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
  45. package/dist/executor/CodexAppServerExecutor.js +24 -0
  46. package/dist/executor/CodexAppServerExecutor.js.map +1 -1
  47. package/dist/executor/IExecutor.d.ts +8 -0
  48. package/dist/executor/IExecutor.d.ts.map +1 -1
  49. package/dist/executor/PiExecutor.d.ts +6 -0
  50. package/dist/executor/PiExecutor.d.ts.map +1 -1
  51. package/dist/executor/PiExecutor.js +18 -0
  52. package/dist/executor/PiExecutor.js.map +1 -1
  53. package/dist/executor/index.js +3 -3
  54. package/dist/executor/index.js.map +1 -1
  55. package/dist/executor/pi/PiClient.d.ts +1 -0
  56. package/dist/executor/pi/PiClient.d.ts.map +1 -1
  57. package/dist/executor/pi/PiClient.js +9 -0
  58. package/dist/executor/pi/PiClient.js.map +1 -1
  59. package/dist/executor/pi/PiTypes.d.ts +2 -0
  60. package/dist/executor/pi/PiTypes.d.ts.map +1 -1
  61. package/dist/executor/pi/PiTypes.js.map +1 -1
  62. package/dist/thread/ThreadExecutorPool.d.ts.map +1 -1
  63. package/dist/thread/ThreadExecutorPool.js +10 -1
  64. package/dist/thread/ThreadExecutorPool.js.map +1 -1
  65. package/dist/thread/ThreadManager.d.ts +1 -1
  66. package/dist/thread/ThreadManager.d.ts.map +1 -1
  67. package/dist/thread/ThreadManager.js +2 -0
  68. package/dist/thread/ThreadManager.js.map +1 -1
  69. package/dist/thread/types.d.ts +6 -0
  70. package/dist/thread/types.d.ts.map +1 -1
  71. package/dist/types/index.d.ts +19 -1
  72. package/dist/types/index.d.ts.map +1 -1
  73. 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
- if (!task.executor)
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(async () => { await task.executor.destroy(); await task.executor.waitForExit?.(); }, this.cleanupMs);
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
- const childId = `delegate-${task.record.id}`;
218
- task.executor = this.factory(this.guard, configuration, cwd, childId, (0, ThreadExecutorPool_1.resolveThreadModel)(parent.thread, configuration), parent.thread.efforts?.[backend], { lifecycleHooks: false, delegationWorker: true });
219
- if (fs_1.default.realpathSync(task.executor.getCurrentWorkingDirectory()) !== cwd) {
220
- throw new Error('Worker refused the delegated workspace; refusing a fallback directory');
221
- }
222
- // Native session IDs are distinct; policy was resolved against the real
223
- // parent thread before passing the synthetic child identity to the factory.
224
- armIdleTimer(task);
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
- // Text streaming is intentionally not liveness activity; it can be
232
- // frequent without a tool making measurable progress.
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 (!closed && task.record.state === 'running' && task.executor?.isWaitingInput?.()) {
236
- notify(() => parent.onNotice(`\n[${backend} delegated task]\n${bounded(text, 4000).text}`));
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: result => {
246
- if (result.tool_use_id)
247
- task.activeToolIds.delete(result.tool_use_id);
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
- if (task.record.state === 'running') {
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
- let cleaned = false;
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(async () => { await task.executor.destroy(); await task.executor.waitForExit?.(); }, this.cleanupMs);
278
- cleaned = true;
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 (cleaned) {
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
- notify(() => parent.onToolResult({ tool_use_id: task.record.id, content: JSON.stringify(view(task)), is_error: task.record.state !== 'succeeded' }));
297
- notify(() => parent.onNotice((0, DelegationNotice_1.formatDelegationNotice)(task.record)));
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
- notify(() => parent.onToolUse({ id: task.record.id, name: 'Task', input: {
417
- description: `${backend}: ${objective.slice(0, 120)}`, prompt: objective.slice(0, 1000), subagent_type: backend,
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;