@yu_robotics/remote-cli 1.6.128 → 1.6.133
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/README.md +15 -2
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +37 -3
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/client/TaskRecovery.d.ts.map +1 -1
- package/dist/client/TaskRecovery.js +5 -0
- package/dist/client/TaskRecovery.js.map +1 -1
- package/dist/delegation/DelegatedWorkerSessionStore.d.ts +4 -1
- package/dist/delegation/DelegatedWorkerSessionStore.d.ts.map +1 -1
- package/dist/delegation/DelegatedWorkerSessionStore.js +6 -7
- package/dist/delegation/DelegatedWorkerSessionStore.js.map +1 -1
- package/dist/delegation/DelegatedWorkspaceManager.d.ts +91 -0
- package/dist/delegation/DelegatedWorkspaceManager.d.ts.map +1 -0
- package/dist/delegation/DelegatedWorkspaceManager.js +377 -0
- package/dist/delegation/DelegatedWorkspaceManager.js.map +1 -0
- package/dist/delegation/DelegationManager.d.ts +13 -4
- package/dist/delegation/DelegationManager.d.ts.map +1 -1
- package/dist/delegation/DelegationManager.js +388 -114
- package/dist/delegation/DelegationManager.js.map +1 -1
- package/dist/delegation/DelegationNotice.d.ts.map +1 -1
- package/dist/delegation/DelegationNotice.js +2 -0
- package/dist/delegation/DelegationNotice.js.map +1 -1
- package/dist/delegation/DelegationStore.d.ts +6 -2
- package/dist/delegation/DelegationStore.d.ts.map +1 -1
- package/dist/delegation/DelegationStore.js +12 -5
- package/dist/delegation/DelegationStore.js.map +1 -1
- package/dist/delegation/GitCheckpoint.d.ts +27 -0
- package/dist/delegation/GitCheckpoint.d.ts.map +1 -0
- package/dist/delegation/GitCheckpoint.js +226 -0
- package/dist/delegation/GitCheckpoint.js.map +1 -0
- package/dist/delegation/contract.d.ts +18 -4
- package/dist/delegation/contract.d.ts.map +1 -1
- package/dist/delegation/contract.js +13 -4
- package/dist/delegation/contract.js.map +1 -1
- package/dist/executor/AcpExecutor.d.ts +2 -0
- package/dist/executor/AcpExecutor.d.ts.map +1 -1
- package/dist/executor/AcpExecutor.js +22 -2
- package/dist/executor/AcpExecutor.js.map +1 -1
- package/dist/executor/AgyExecutor.d.ts +1 -0
- package/dist/executor/AgyExecutor.d.ts.map +1 -1
- package/dist/executor/AgyExecutor.js +4 -0
- package/dist/executor/AgyExecutor.js.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts +2 -0
- package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.js +19 -0
- package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
- package/dist/executor/CodexAppServerExecutor.d.ts +4 -0
- package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
- package/dist/executor/CodexAppServerExecutor.js +32 -0
- package/dist/executor/CodexAppServerExecutor.js.map +1 -1
- package/dist/executor/ExecutionMetadata.d.ts +9 -0
- package/dist/executor/ExecutionMetadata.d.ts.map +1 -0
- package/dist/executor/ExecutionMetadata.js +57 -0
- package/dist/executor/ExecutionMetadata.js.map +1 -0
- package/dist/executor/IExecutor.d.ts +4 -1
- package/dist/executor/IExecutor.d.ts.map +1 -1
- package/dist/executor/PiExecutor.d.ts +3 -0
- package/dist/executor/PiExecutor.d.ts.map +1 -1
- package/dist/executor/PiExecutor.js +36 -0
- package/dist/executor/PiExecutor.js.map +1 -1
- package/dist/maintenance/release-notes.json +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,9 @@ exports.workspacesOverlap = workspacesOverlap;
|
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const DirectoryGuard_1 = require("../security/DirectoryGuard");
|
|
11
12
|
const ThreadExecutorPool_1 = require("../thread/ThreadExecutorPool");
|
|
13
|
+
const ExecutionMetadata_1 = require("../executor/ExecutionMetadata");
|
|
12
14
|
const executor_1 = require("../executor");
|
|
13
15
|
const BackendRegistry_1 = require("./BackendRegistry");
|
|
14
16
|
const DelegationStore_1 = require("./DelegationStore");
|
|
@@ -18,8 +20,10 @@ const contract_1 = require("./contract");
|
|
|
18
20
|
const WorkerPolicy_1 = require("./WorkerPolicy");
|
|
19
21
|
const DelegationNotice_1 = require("./DelegationNotice");
|
|
20
22
|
const WorkingDirectory_1 = require("../utils/WorkingDirectory");
|
|
21
|
-
|
|
23
|
+
const DelegatedWorkspaceManager_1 = require("./DelegatedWorkspaceManager");
|
|
24
|
+
exports.DELEGATION_LIMITS = { launches: 12, concurrent: 5,
|
|
22
25
|
idleTimeoutMs: 15 * 60_000, toolIdleTimeoutMs: 45 * 60_000,
|
|
26
|
+
queueTimeoutMs: 60 * 60_000,
|
|
23
27
|
resultBytes: 32 * 1024, continuationBytes: 64 * 1024,
|
|
24
28
|
storageTimeoutMs: 10_000, calls: 500 };
|
|
25
29
|
/** Bounds and cadence for optional display-only delegated-worker text. */
|
|
@@ -34,11 +38,12 @@ function validDuration(value, fallback) {
|
|
|
34
38
|
function timeoutPolicy(input) {
|
|
35
39
|
if (typeof input === 'number') {
|
|
36
40
|
const duration = validDuration(input, exports.DELEGATION_LIMITS.idleTimeoutMs);
|
|
37
|
-
return { idleTimeoutMs: duration, toolIdleTimeoutMs: duration };
|
|
41
|
+
return { idleTimeoutMs: duration, toolIdleTimeoutMs: duration, queueTimeoutMs: exports.DELEGATION_LIMITS.queueTimeoutMs };
|
|
38
42
|
}
|
|
39
43
|
return {
|
|
40
44
|
idleTimeoutMs: validDuration(input.idleTimeoutMs, exports.DELEGATION_LIMITS.idleTimeoutMs),
|
|
41
45
|
toolIdleTimeoutMs: validDuration(input.toolIdleTimeoutMs, exports.DELEGATION_LIMITS.toolIdleTimeoutMs),
|
|
46
|
+
queueTimeoutMs: validDuration(input.queueTimeoutMs, exports.DELEGATION_LIMITS.queueTimeoutMs),
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
function formatDuration(milliseconds) {
|
|
@@ -122,17 +127,20 @@ class DelegationManager {
|
|
|
122
127
|
registry;
|
|
123
128
|
store;
|
|
124
129
|
laneStore;
|
|
130
|
+
workspaceManager;
|
|
125
131
|
running = 0;
|
|
126
132
|
workspaces = new Map();
|
|
133
|
+
pumps = new Set();
|
|
127
134
|
scopes = new Map();
|
|
128
135
|
timeouts;
|
|
129
|
-
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()) {
|
|
136
|
+
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(), workspaceManager = new DelegatedWorkspaceManager_1.DelegatedWorkspaceManager()) {
|
|
130
137
|
this.guard = guard;
|
|
131
138
|
this.factory = factory;
|
|
132
139
|
this.cleanupMs = cleanupMs;
|
|
133
140
|
this.registry = registry;
|
|
134
141
|
this.store = store;
|
|
135
142
|
this.laneStore = laneStore;
|
|
143
|
+
this.workspaceManager = workspaceManager;
|
|
136
144
|
this.timeouts = timeoutPolicy(timeoutInput);
|
|
137
145
|
}
|
|
138
146
|
begin(parent) {
|
|
@@ -147,12 +155,32 @@ class DelegationManager {
|
|
|
147
155
|
let closed = false;
|
|
148
156
|
let launches = 0;
|
|
149
157
|
let launchRevision = 0;
|
|
150
|
-
let
|
|
158
|
+
let admissions = 0;
|
|
159
|
+
let admissionTail = Promise.resolve();
|
|
160
|
+
const activeTasks = new Set();
|
|
161
|
+
let integrating = false;
|
|
162
|
+
let integrationPending;
|
|
163
|
+
let source;
|
|
164
|
+
let sourcePromise;
|
|
165
|
+
let baseline;
|
|
166
|
+
let maxChildren = 1;
|
|
167
|
+
const workspaceOwner = { id: (0, crypto_1.randomUUID)(), threadId: parent.thread.id, quarantined: false };
|
|
168
|
+
const workspaceGeneration = parent.thread.delegationWorkspaceGeneration ?? 0;
|
|
151
169
|
let execution = 0;
|
|
152
170
|
let acceptingResults = true;
|
|
153
171
|
const stagedResults = new Set();
|
|
154
172
|
const cwd = fs_1.default.realpathSync(this.guard.resolveWorkingDirectory(parent.cwd));
|
|
173
|
+
let leasePath = cwd;
|
|
155
174
|
this.guard.resolveWorkingDirectory(cwd);
|
|
175
|
+
const discoverWorkspace = () => sourcePromise ??= this.workspaceManager.discover(cwd).then(found => {
|
|
176
|
+
if (found) {
|
|
177
|
+
// A full repository checkpoint must not read outside the directory-selection policy.
|
|
178
|
+
this.guard.resolveWorkingDirectory(found.root);
|
|
179
|
+
source = found;
|
|
180
|
+
leasePath = found.root;
|
|
181
|
+
maxChildren = exports.DELEGATION_LIMITS.concurrent;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
156
184
|
const getTask = (value) => {
|
|
157
185
|
const task = typeof value === 'string' ? tasks.get(value) : undefined;
|
|
158
186
|
if (!task)
|
|
@@ -160,12 +188,15 @@ class DelegationManager {
|
|
|
160
188
|
return task;
|
|
161
189
|
};
|
|
162
190
|
const view = (task) => ({ taskId: task.record.id, backend: task.record.backend,
|
|
163
|
-
state: task.record.finishedAt ? task.record.state : 'running',
|
|
164
|
-
output: task.record.finishedAt ? task.record.output : undefined,
|
|
165
|
-
error: task.record.finishedAt ? task.record.error : undefined,
|
|
166
|
-
truncated: task.record.truncated ?? false
|
|
191
|
+
state: task.record.finishedAt !== undefined ? task.record.state : task.record.startedAt === undefined ? 'queued' : 'running',
|
|
192
|
+
output: task.record.finishedAt !== undefined ? task.record.output : undefined,
|
|
193
|
+
error: task.record.finishedAt !== undefined ? task.record.error : undefined,
|
|
194
|
+
truncated: task.record.truncated ?? false,
|
|
195
|
+
...(task.executionMetadata ? { executionMetadata: { ...task.executionMetadata } } : {}),
|
|
196
|
+
...(task.artifact ? { artifact: { taskId: task.artifact.taskId,
|
|
197
|
+
disposition: task.artifact.disposition, outputCommit: task.artifact.outputCommit } } : {}) });
|
|
167
198
|
const receive = (task, requestedDuring) => {
|
|
168
|
-
if (task.record.finishedAt && acceptingResults && requestedDuring === execution)
|
|
199
|
+
if (task.record.finishedAt !== undefined && acceptingResults && requestedDuring === execution)
|
|
169
200
|
stagedResults.add(task.record.id);
|
|
170
201
|
return view(task);
|
|
171
202
|
};
|
|
@@ -180,9 +211,35 @@ class DelegationManager {
|
|
|
180
211
|
console.warn('[Delegation] Progress could not be delivered');
|
|
181
212
|
}
|
|
182
213
|
};
|
|
214
|
+
const releaseWorkspaceIfIdle = () => {
|
|
215
|
+
if (integrating || admissions || activeTasks.size || [...tasks.values()].some(task => !task.settled))
|
|
216
|
+
return;
|
|
217
|
+
// A new idle cohort must observe coordinator edits even if the last task was read-only.
|
|
218
|
+
baseline = undefined;
|
|
219
|
+
if (this.workspaces.get(leasePath) === workspaceOwner && !workspaceOwner.quarantined)
|
|
220
|
+
this.workspaces.delete(leasePath);
|
|
221
|
+
};
|
|
222
|
+
const assertWorkspace = () => {
|
|
223
|
+
(0, WorkingDirectory_1.assertWorkingDirectoryExists)(parent.cwd);
|
|
224
|
+
if (fs_1.default.realpathSync(this.guard.resolveWorkingDirectory(parent.cwd)) !== cwd
|
|
225
|
+
|| (parent.thread.delegationWorkspaceGeneration ?? 0) !== workspaceGeneration) {
|
|
226
|
+
throw new Error('Delegated workspace changed before the task could start');
|
|
227
|
+
}
|
|
228
|
+
if (this.workspaces.get(leasePath) !== workspaceOwner || workspaceOwner.quarantined || parent.isWorkspaceBusy?.(leasePath)) {
|
|
229
|
+
throw new Error('Delegation capacity or workspace is busy; task was not started');
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const captureWorkerMetadata = (task) => {
|
|
233
|
+
if (!task.executor || task.record.startedAt === undefined)
|
|
234
|
+
return;
|
|
235
|
+
task.executionMetadata = (0, ExecutionMetadata_1.mergeReportedExecutionMetadata)(task.executionMetadata, (0, ExecutionMetadata_1.captureExecutionMetadata)(task.executor, task.record.backend));
|
|
236
|
+
};
|
|
183
237
|
const reportProgress = (task, progress) => {
|
|
184
238
|
try {
|
|
185
|
-
|
|
239
|
+
if (task.record.state === 'running')
|
|
240
|
+
captureWorkerMetadata(task);
|
|
241
|
+
return parent.onProgress?.({ taskId: task.record.id, backend: task.record.backend, ...progress,
|
|
242
|
+
...(task.executionMetadata ? { executionMetadata: { ...task.executionMetadata } } : {}) }) === true;
|
|
186
243
|
}
|
|
187
244
|
catch {
|
|
188
245
|
console.warn('[Delegation] Progress could not be delivered');
|
|
@@ -191,12 +248,14 @@ class DelegationManager {
|
|
|
191
248
|
};
|
|
192
249
|
const reportTextProgress = (task, latestText) => {
|
|
193
250
|
try {
|
|
251
|
+
captureWorkerMetadata(task);
|
|
194
252
|
return parent.onTextProgress?.({
|
|
195
253
|
taskId: task.record.id,
|
|
196
254
|
backend: task.record.backend,
|
|
197
255
|
phase: 'text',
|
|
198
256
|
latestText,
|
|
199
257
|
startedAt: task.record.startedAt,
|
|
258
|
+
...(task.executionMetadata ? { executionMetadata: { ...task.executionMetadata } } : {}),
|
|
200
259
|
}) === true;
|
|
201
260
|
}
|
|
202
261
|
catch {
|
|
@@ -240,7 +299,7 @@ class DelegationManager {
|
|
|
240
299
|
scheduleTextProgress(task);
|
|
241
300
|
};
|
|
242
301
|
const reportTerminalProgress = (task) => {
|
|
243
|
-
if (task.record.state ===
|
|
302
|
+
if (!(0, DelegationStore_1.isTerminalTaskState)(task.record.state) || task.record.startedAt === undefined)
|
|
244
303
|
return false;
|
|
245
304
|
return reportProgress(task, {
|
|
246
305
|
phase: task.record.state,
|
|
@@ -258,6 +317,8 @@ class DelegationManager {
|
|
|
258
317
|
};
|
|
259
318
|
const discardLane = async (lane) => {
|
|
260
319
|
try {
|
|
320
|
+
if (source)
|
|
321
|
+
await this.workspaceManager.preserveLane(lane.id);
|
|
261
322
|
await (0, DelegatedWorkerLaneCleanup_1.cleanupDelegatedWorkerLane)(lane);
|
|
262
323
|
await this.laneStore.remove(lane.id);
|
|
263
324
|
return true;
|
|
@@ -280,6 +341,7 @@ class DelegationManager {
|
|
|
280
341
|
return;
|
|
281
342
|
}
|
|
282
343
|
try {
|
|
344
|
+
captureWorkerMetadata(task);
|
|
283
345
|
try {
|
|
284
346
|
await deadline(() => task.executor.abort(), Math.min(2000, this.cleanupMs));
|
|
285
347
|
}
|
|
@@ -323,8 +385,45 @@ class DelegationManager {
|
|
|
323
385
|
task.record.lastActivityKind = kind;
|
|
324
386
|
armIdleTimer(task);
|
|
325
387
|
};
|
|
388
|
+
const finishTask = async (task) => {
|
|
389
|
+
clearTimeout(task.queueTimer);
|
|
390
|
+
task.record.finishedAt = Date.now();
|
|
391
|
+
await deadline(async () => { await save(task); await this.store.prune(); }, this.cleanupMs)
|
|
392
|
+
.catch(() => console.warn('[Delegation] Final task metadata could not be saved or pruned'));
|
|
393
|
+
if (!reportTerminalProgress(task)) {
|
|
394
|
+
// A never-started task has no tool/progress card to close on older peers.
|
|
395
|
+
if (task.record.startedAt !== undefined) {
|
|
396
|
+
notify(() => parent.onToolResult({ tool_use_id: task.record.id, content: JSON.stringify(view(task)), is_error: task.record.state !== 'succeeded' }));
|
|
397
|
+
}
|
|
398
|
+
notify(() => parent.onNotice((0, DelegationNotice_1.formatDelegationNotice)(task.record)));
|
|
399
|
+
}
|
|
400
|
+
task.settle();
|
|
401
|
+
activeTasks.delete(task);
|
|
402
|
+
releaseWorkspaceIfIdle();
|
|
403
|
+
for (const start of this.pumps)
|
|
404
|
+
start();
|
|
405
|
+
};
|
|
406
|
+
const completeUnstarted = (task, state, error) => {
|
|
407
|
+
if (task.finishUnstarted)
|
|
408
|
+
return task.finishUnstarted;
|
|
409
|
+
task.record.state = state;
|
|
410
|
+
task.record.error = error;
|
|
411
|
+
return task.finishUnstarted = finishTask(task);
|
|
412
|
+
};
|
|
413
|
+
const armQueueTimer = (task) => {
|
|
414
|
+
const remaining = Math.max(0, task.record.acceptedAt + this.timeouts.queueTimeoutMs - Date.now());
|
|
415
|
+
task.queueTimer = setTimeout(() => {
|
|
416
|
+
if (task.record.state !== 'queued' || task.ownsSlot)
|
|
417
|
+
return;
|
|
418
|
+
void completeUnstarted(task, 'timed_out', `Task was not started within ${formatDuration(this.timeouts.queueTimeoutMs)} of admission.`);
|
|
419
|
+
}, remaining);
|
|
420
|
+
};
|
|
326
421
|
const cancel = async (task) => {
|
|
327
|
-
if (task.record.state === '
|
|
422
|
+
if (task.record.state === 'queued' && !task.ownsSlot) {
|
|
423
|
+
await completeUnstarted(task, 'cancelled', 'Cancelled before the worker started.');
|
|
424
|
+
}
|
|
425
|
+
else if (!(0, DelegationStore_1.isTerminalTaskState)(task.record.state)) {
|
|
426
|
+
clearTimeout(task.queueTimer);
|
|
328
427
|
clearTaskTimer(task);
|
|
329
428
|
clearTextProgress(task, true);
|
|
330
429
|
task.record.state = 'cancelled';
|
|
@@ -334,27 +433,53 @@ class DelegationManager {
|
|
|
334
433
|
await task.done;
|
|
335
434
|
return view(task);
|
|
336
435
|
};
|
|
337
|
-
const run = async (task
|
|
436
|
+
const run = async (task) => {
|
|
437
|
+
const { configuration, objective } = task;
|
|
438
|
+
let metadataCaptured = false;
|
|
338
439
|
const backend = task.record.backend;
|
|
339
440
|
const identity = {
|
|
340
441
|
threadId: parent.thread.id,
|
|
341
442
|
backend,
|
|
342
443
|
workingDirectory: cwd,
|
|
343
|
-
workspaceGeneration
|
|
444
|
+
workspaceGeneration,
|
|
445
|
+
};
|
|
446
|
+
const assertStartable = () => {
|
|
447
|
+
if (closed || (0, DelegationStore_1.isTerminalTaskState)(task.record.state) || task.quarantineWorkspace) {
|
|
448
|
+
throw new Error('Delegated task ended before execution could start');
|
|
449
|
+
}
|
|
450
|
+
assertWorkspace();
|
|
344
451
|
};
|
|
345
452
|
const createWorker = async () => {
|
|
453
|
+
assertStartable();
|
|
346
454
|
if (!task.lane) {
|
|
347
|
-
const acquired = await this.laneStore.acquire(identity
|
|
455
|
+
const acquired = await this.laneStore.acquire(identity, source
|
|
456
|
+
? { pooled: true, excluded: await this.workspaceManager.unavailableLanes() } : undefined);
|
|
348
457
|
task.lane = acquired.lane;
|
|
458
|
+
if (task.settled || task.quarantineWorkspace) {
|
|
459
|
+
// Setup can finish after its deadline, but must never start a late
|
|
460
|
+
// worker or leave that acquisition resumable.
|
|
461
|
+
await this.laneStore.markDirty(acquired.lane.id, 'Worker setup completed after its task ended.', true).catch(() => undefined);
|
|
462
|
+
}
|
|
349
463
|
}
|
|
350
464
|
let worker;
|
|
351
465
|
try {
|
|
352
|
-
|
|
466
|
+
assertStartable();
|
|
467
|
+
if (source && !task.workspace) {
|
|
468
|
+
baseline ??= this.workspaceManager.baseline(source);
|
|
469
|
+
const input = await baseline;
|
|
470
|
+
assertStartable();
|
|
471
|
+
task.workspace = await this.workspaceManager.prepare(task.lane, source, input, task.record.id);
|
|
472
|
+
assertStartable();
|
|
473
|
+
}
|
|
474
|
+
const executionCwd = task.workspace?.cwd ?? cwd;
|
|
475
|
+
const workerGuard = task.workspace ? new DirectoryGuard_1.DirectoryGuard([...this.guard.getAllowedDirectories(), executionCwd]) : this.guard;
|
|
476
|
+
worker = this.factory(workerGuard, configuration, executionCwd, task.lane.executorThreadId, (0, ThreadExecutorPool_1.resolveThreadModel)(parent.thread, configuration), parent.thread.efforts?.[backend], { lifecycleHooks: false, delegationWorker: true });
|
|
353
477
|
task.executor = worker;
|
|
354
|
-
if (fs_1.default.realpathSync(worker.getCurrentWorkingDirectory()) !==
|
|
478
|
+
if (fs_1.default.realpathSync(worker.getCurrentWorkingDirectory()) !== fs_1.default.realpathSync(executionCwd)) {
|
|
355
479
|
throw new Error('Worker refused the delegated workspace; refusing a fallback directory');
|
|
356
480
|
}
|
|
357
481
|
await this.laneStore.markRunning(task.lane.id);
|
|
482
|
+
assertStartable();
|
|
358
483
|
return worker;
|
|
359
484
|
}
|
|
360
485
|
catch (error) {
|
|
@@ -386,17 +511,42 @@ class DelegationManager {
|
|
|
386
511
|
await this.laneStore.markDirty(lane.id, 'Native worker session was unavailable before the objective was dispatched.', true);
|
|
387
512
|
const discarded = await discardLane(lane);
|
|
388
513
|
task.lane = undefined;
|
|
514
|
+
task.workspace = undefined;
|
|
389
515
|
return discarded;
|
|
390
516
|
};
|
|
391
517
|
try {
|
|
392
|
-
if (task.record.state
|
|
518
|
+
if ((0, DelegationStore_1.isTerminalTaskState)(task.record.state))
|
|
393
519
|
return;
|
|
394
520
|
let result;
|
|
395
521
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
396
|
-
|
|
522
|
+
let setupPending = true;
|
|
523
|
+
const setup = createWorker().finally(() => { setupPending = false; });
|
|
524
|
+
let worker;
|
|
525
|
+
try {
|
|
526
|
+
worker = await deadline(() => setup, exports.DELEGATION_LIMITS.storageTimeoutMs, 'Worker setup timed out');
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (setupPending)
|
|
530
|
+
task.quarantineWorkspace = true;
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
assertStartable();
|
|
534
|
+
task.executionMetadata = (0, ExecutionMetadata_1.captureExecutionMetadata)(worker, backend);
|
|
535
|
+
metadataCaptured = false;
|
|
536
|
+
if (task.record.startedAt === undefined) {
|
|
537
|
+
task.record.state = 'running';
|
|
538
|
+
task.record.startedAt = Date.now();
|
|
539
|
+
void save(task);
|
|
540
|
+
if (!reportProgress(task, { phase: 'started', objective: task.record.objective, startedAt: task.record.startedAt })) {
|
|
541
|
+
notify(() => parent.onToolUse({ id: task.record.id, name: 'Task', input: {
|
|
542
|
+
description: `${backend}: ${objective.slice(0, 120)}`, prompt: objective.slice(0, 1000), subagent_type: backend,
|
|
543
|
+
} }));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
397
546
|
// Native session IDs are distinct; policy was resolved against the real
|
|
398
547
|
// parent thread before passing the synthetic worker identity to the factory.
|
|
399
|
-
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.
|
|
548
|
+
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.${task.workspace
|
|
549
|
+
? `\nThis task runs in an owned Git worktree at ${task.workspace.cwd}, with a fresh detached task checkout based on checkpoint ${task.workspace.input.commit}. Previous task changes are NOT implicitly present. Re-read current files; earlier conversation context may be stale. Stay within this checkout. Do not merge into or edit the delivery directory, push, change branches, or remove the worktree. Your file changes will be captured as an artifact for explicit coordinator integration. Ignored files and dependencies are not copied from the delivery directory. A worktree is not an OS sandbox.` : ''}
|
|
400
550
|
|
|
401
551
|
${objective}`, {
|
|
402
552
|
// The manager owns delegated-task liveness. Zero disables optional
|
|
@@ -462,8 +612,11 @@ ${prompt}`));
|
|
|
462
612
|
});
|
|
463
613
|
// Start the inactivity window only once the worker has accepted the
|
|
464
614
|
// objective and can emit its first real progress callback.
|
|
615
|
+
task.signalDispatch();
|
|
465
616
|
armIdleTimer(task);
|
|
466
617
|
result = await Promise.race([task.interrupted, execution]);
|
|
618
|
+
captureWorkerMetadata(task);
|
|
619
|
+
metadataCaptured = true;
|
|
467
620
|
if (!result.success && attempt === 0 && worker.consumeSessionResumeFailure?.()) {
|
|
468
621
|
clearTaskTimer(task);
|
|
469
622
|
clearTextProgress(task, true);
|
|
@@ -484,70 +637,120 @@ ${prompt}`));
|
|
|
484
637
|
}
|
|
485
638
|
}
|
|
486
639
|
catch (error) {
|
|
487
|
-
if (task.record.state
|
|
640
|
+
if (!(0, DelegationStore_1.isTerminalTaskState)(task.record.state)) {
|
|
488
641
|
task.record.state = 'failed';
|
|
489
642
|
task.record.error = bounded(error instanceof Error ? error.message : 'Worker failed', 4000).text;
|
|
490
643
|
}
|
|
491
644
|
this.registry.invalidate();
|
|
492
645
|
}
|
|
493
646
|
finally {
|
|
647
|
+
if (!metadataCaptured)
|
|
648
|
+
captureWorkerMetadata(task);
|
|
494
649
|
clearTaskTimer(task);
|
|
495
650
|
clearTextProgress(task, true);
|
|
651
|
+
if (task.record.startedAt === undefined)
|
|
652
|
+
task.discardLane = true;
|
|
496
653
|
let released = !task.quarantineWorkspace;
|
|
497
654
|
try {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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);
|
|
655
|
+
await deadline(async () => {
|
|
656
|
+
if (task.stop)
|
|
657
|
+
await task.stop;
|
|
658
|
+
else if (task.executor)
|
|
659
|
+
await deadline(() => destroyWorker(task.executor), this.cleanupMs);
|
|
660
|
+
else if (task.lane) {
|
|
661
|
+
// A scope can close after durable acquisition but before construction.
|
|
662
|
+
// No backend process was started, so discard the lane rather than
|
|
663
|
+
// reserving the workspace indefinitely.
|
|
664
|
+
task.discardLane = true;
|
|
517
665
|
}
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
await this.laneStore.markDirty(
|
|
521
|
-
await discardLane(
|
|
522
|
-
|
|
666
|
+
if (released && task.lane && task.discardLane) {
|
|
667
|
+
const lane = task.lane;
|
|
668
|
+
await this.laneStore.markDirty(lane.id, 'Delegated worker setup ended before a reusable context was established.', true).catch(() => undefined);
|
|
669
|
+
await discardLane(lane);
|
|
670
|
+
task.lane = undefined;
|
|
523
671
|
}
|
|
524
|
-
|
|
672
|
+
if (released && task.lane) {
|
|
673
|
+
try {
|
|
674
|
+
if (task.workspace) {
|
|
675
|
+
task.artifact = await this.workspaceManager.collect(task.workspace, task.record.state === 'succeeded');
|
|
676
|
+
}
|
|
677
|
+
await this.laneStore.markReady(task.lane.id);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
const detail = error instanceof Error ? error.message : 'Worker lane persistence failed';
|
|
681
|
+
if (task.workspace) {
|
|
682
|
+
// Exit was confirmed. Preserve files and context for recovery without blocking unrelated lanes.
|
|
683
|
+
await this.workspaceManager.preserveLane(task.lane.id).catch(() => undefined);
|
|
684
|
+
await this.laneStore.markDirty(task.lane.id, detail).catch(() => undefined);
|
|
685
|
+
if (task.record.state === 'succeeded')
|
|
686
|
+
task.record.state = 'failed';
|
|
687
|
+
task.record.error = bounded(`Worker artifact could not be finalized: ${detail}. Files were retained for manual recovery.`, 4000).text;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
await this.laneStore.markDirty(task.lane.id, detail, true).catch(() => undefined);
|
|
691
|
+
await discardLane(task.lane);
|
|
692
|
+
}
|
|
693
|
+
console.warn('[Delegation] Worker lane could not be retained:', detail);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}, this.cleanupMs);
|
|
525
697
|
}
|
|
526
698
|
catch (error) {
|
|
527
699
|
released = false;
|
|
528
700
|
task.quarantineWorkspace = true;
|
|
529
701
|
if (task.lane) {
|
|
530
702
|
const detail = error instanceof Error ? error.message : 'Worker cleanup could not be confirmed';
|
|
531
|
-
await this.laneStore.markDirty(task.lane.id, detail).catch(() => undefined);
|
|
703
|
+
await deadline(() => this.laneStore.markDirty(task.lane.id, detail), this.cleanupMs).catch(() => undefined);
|
|
532
704
|
}
|
|
533
705
|
task.record.state = 'interrupted';
|
|
534
706
|
task.record.error = 'Worker cleanup could not be confirmed. This workspace is blocked until the worker is stopped and the CLI restarts.';
|
|
535
707
|
console.warn('[Delegation] Worker cleanup failed:', error instanceof Error ? error.message : 'Cleanup failure');
|
|
536
708
|
}
|
|
537
|
-
if (released) {
|
|
709
|
+
if (released && task.ownsSlot) {
|
|
710
|
+
task.ownsSlot = false;
|
|
538
711
|
this.running--;
|
|
539
|
-
this.workspaces.delete(cwd);
|
|
540
712
|
}
|
|
541
|
-
else
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
.catch(() => console.warn('[Delegation] Final task metadata could not be saved or pruned'));
|
|
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)));
|
|
713
|
+
else if (!released) {
|
|
714
|
+
workspaceOwner.quarantined = true;
|
|
715
|
+
task.record.state = 'interrupted';
|
|
716
|
+
task.record.error = 'Worker cleanup could not be confirmed. This workspace is blocked until the worker is stopped and the CLI restarts.';
|
|
549
717
|
}
|
|
550
|
-
task
|
|
718
|
+
await finishTask(task);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const startTask = (task) => {
|
|
722
|
+
clearTimeout(task.queueTimer);
|
|
723
|
+
activeTasks.add(task);
|
|
724
|
+
task.ownsSlot = true;
|
|
725
|
+
this.running++;
|
|
726
|
+
void run(task).catch(() => undefined);
|
|
727
|
+
};
|
|
728
|
+
const pump = () => {
|
|
729
|
+
if (closed || integrating || activeTasks.size >= maxChildren)
|
|
730
|
+
return;
|
|
731
|
+
const queued = [...tasks.values()].filter(task => task.record.state === 'queued' && !task.ownsSlot && !task.finishUnstarted);
|
|
732
|
+
if (workspaceOwner.quarantined) {
|
|
733
|
+
for (const task of queued)
|
|
734
|
+
void completeUnstarted(task, 'interrupted', 'Task was not started because the previous worker exit could not be confirmed.');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const task = queued[0];
|
|
738
|
+
if (!task)
|
|
739
|
+
return;
|
|
740
|
+
if (Date.now() >= task.record.acceptedAt + this.timeouts.queueTimeoutMs) {
|
|
741
|
+
void completeUnstarted(task, 'timed_out', `Task was not started within ${formatDuration(this.timeouts.queueTimeoutMs)} of admission.`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
assertWorkspace();
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
void completeUnstarted(task, 'failed', bounded(error instanceof Error ? error.message : 'Workspace unavailable', 4000).text);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (this.running < exports.DELEGATION_LIMITS.concurrent) {
|
|
752
|
+
startTask(task);
|
|
753
|
+
pump();
|
|
551
754
|
}
|
|
552
755
|
};
|
|
553
756
|
const invoke = async (name, args, callId) => {
|
|
@@ -567,6 +770,7 @@ ${prompt}`));
|
|
|
567
770
|
throw new Error('Delegation tool-call limit reached');
|
|
568
771
|
const result = (async () => {
|
|
569
772
|
if (name === 'remote_cli_list_backends') {
|
|
773
|
+
await discoverWorkspace();
|
|
570
774
|
const backends = await this.registry.list(parent.config);
|
|
571
775
|
return { backends: backends.map(item => {
|
|
572
776
|
const configure = (mode) => (0, WorkerPolicy_1.workerConfiguration)(parent.config, this.guard, parent.thread.id, parent.backend, item.backend, mode);
|
|
@@ -586,14 +790,16 @@ ${prompt}`));
|
|
|
586
790
|
}
|
|
587
791
|
}
|
|
588
792
|
return { ...item, readOnly };
|
|
589
|
-
}), workspace: cwd, maxConcurrentChildren:
|
|
793
|
+
}), workspace: cwd, maxConcurrentChildren: maxChildren,
|
|
794
|
+
maxTasksPerRequest: exports.DELEGATION_LIMITS.launches, scheduling: source ? 'isolated-worktrees' : 'serial',
|
|
795
|
+
queueTimeoutSeconds: this.timeouts.queueTimeoutMs / 1000 };
|
|
590
796
|
}
|
|
591
797
|
if (name === 'remote_cli_result') {
|
|
592
798
|
const task = getTask(args.taskId);
|
|
593
799
|
const seconds = args.waitSeconds ?? 25;
|
|
594
800
|
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds < 0 || seconds > 25)
|
|
595
801
|
throw new Error('waitSeconds must be between 0 and 25');
|
|
596
|
-
if (
|
|
802
|
+
if (task.record.finishedAt === undefined && seconds > 0) {
|
|
597
803
|
let timer;
|
|
598
804
|
try {
|
|
599
805
|
await Promise.race([task.done, new Promise(resolve => { timer = setTimeout(resolve, seconds * 1000); })]);
|
|
@@ -609,14 +815,42 @@ ${prompt}`));
|
|
|
609
815
|
await cancel(task);
|
|
610
816
|
return receive(task, requestedDuring);
|
|
611
817
|
}
|
|
818
|
+
if (name === 'remote_cli_integrate') {
|
|
819
|
+
if (!acceptingResults)
|
|
820
|
+
throw new Error('The coordinator execution has ended');
|
|
821
|
+
await discoverWorkspace();
|
|
822
|
+
if (!source)
|
|
823
|
+
throw new Error('Non-Git delegation uses the shared workspace and has no worktree artifacts');
|
|
824
|
+
if (integrating || admissions || activeTasks.size || [...tasks.values()].some(task => !task.settled)) {
|
|
825
|
+
throw new Error('Wait for all accepted workers to finish before integrating artifacts');
|
|
826
|
+
}
|
|
827
|
+
if (workspaceOwner.quarantined || [...this.workspaces].some(([active, owner]) => owner !== workspaceOwner && workspacesOverlap(active, leasePath)) || parent.isWorkspaceBusy?.(leasePath)) {
|
|
828
|
+
throw new Error('Delivery workspace is busy; integration was not started');
|
|
829
|
+
}
|
|
830
|
+
integrating = true;
|
|
831
|
+
this.workspaces.set(leasePath, workspaceOwner);
|
|
832
|
+
try {
|
|
833
|
+
assertWorkspace();
|
|
834
|
+
integrationPending = this.workspaceManager.integrate({ threadId: parent.thread.id, cwd, workspaceGeneration,
|
|
835
|
+
checkActive: () => { if (closed)
|
|
836
|
+
throw new Error('Integration was cancelled before delivery'); assertWorkspace(); } }, String(args.taskId ?? ''), args.action ?? 'inspect', args.expectedRevision);
|
|
837
|
+
const result = await integrationPending;
|
|
838
|
+
baseline = undefined;
|
|
839
|
+
const task = tasks.get(String(args.taskId));
|
|
840
|
+
if (task)
|
|
841
|
+
task.artifact = await this.workspaceManager.describe(task.record.id);
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
finally {
|
|
845
|
+
integrationPending = undefined;
|
|
846
|
+
integrating = false;
|
|
847
|
+
releaseWorkspaceIfIdle();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
612
850
|
if (name !== 'remote_cli_delegate')
|
|
613
851
|
throw new Error('Unknown delegation tool');
|
|
614
852
|
if (!acceptingResults)
|
|
615
853
|
throw new Error('The coordinator execution has ended');
|
|
616
|
-
if (starting || [...tasks.values()].some(task => !task.record.finishedAt))
|
|
617
|
-
throw new Error('Wait for the current delegated task before starting another');
|
|
618
|
-
if (launches >= exports.DELEGATION_LIMITS.launches)
|
|
619
|
-
throw new Error('Delegated task limit reached');
|
|
620
854
|
if (!contract_1.DELEGATION_BACKENDS.includes(args.backend))
|
|
621
855
|
throw new Error('Unsupported worker backend');
|
|
622
856
|
if (typeof args.objective !== 'string' || !args.objective.trim() || args.objective.length > 24_000)
|
|
@@ -627,77 +861,107 @@ ${prompt}`));
|
|
|
627
861
|
throw new Error('Unsupported execution mode');
|
|
628
862
|
const backend = args.backend;
|
|
629
863
|
const configuration = (0, WorkerPolicy_1.workerConfiguration)(parent.config, this.guard, parent.thread.id, parent.backend, backend, mode);
|
|
864
|
+
if (integrating)
|
|
865
|
+
throw new Error('Artifact integration is in progress; wait before delegating');
|
|
630
866
|
launchRevision++;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
867
|
+
admissions++;
|
|
868
|
+
const discovery = this.registry.get(backend, parent.config);
|
|
869
|
+
void discovery.catch(() => undefined);
|
|
870
|
+
// Serialize admission, not worker execution. Replayed calls share the
|
|
871
|
+
// original promise; simultaneous new calls cannot overdraw the quota.
|
|
872
|
+
const admitted = admissionTail.then(async () => {
|
|
873
|
+
if (closed)
|
|
874
|
+
throw new Error('Delegation turn has ended');
|
|
875
|
+
if (launches >= exports.DELEGATION_LIMITS.launches)
|
|
876
|
+
throw new Error('Delegated task limit reached');
|
|
877
|
+
const available = await discovery;
|
|
634
878
|
if (!available.installed)
|
|
635
879
|
throw new Error(`${backend}: ${available.reason}`);
|
|
880
|
+
await discoverWorkspace();
|
|
881
|
+
if (integrating)
|
|
882
|
+
throw new Error('Artifact integration is in progress; wait before delegating');
|
|
636
883
|
await deadline(() => this.store.initialize(), exports.DELEGATION_LIMITS.storageTimeoutMs, 'Task record initialization timed out');
|
|
637
884
|
if (closed)
|
|
638
885
|
throw new Error('Delegation turn has ended');
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
886
|
+
const idle = !activeTasks.size && ![...tasks.values()].some(task => !task.settled);
|
|
887
|
+
const first = activeTasks.size < maxChildren && this.running < exports.DELEGATION_LIMITS.concurrent
|
|
888
|
+
&& ![...tasks.values()].some(task => task.record.state === 'queued' && !task.ownsSlot && !task.finishUnstarted);
|
|
889
|
+
if ((idle && this.running >= exports.DELEGATION_LIMITS.concurrent)
|
|
890
|
+
|| workspaceOwner.quarantined
|
|
891
|
+
|| [...this.workspaces].some(([active, owner]) => owner !== workspaceOwner && workspacesOverlap(active, leasePath))
|
|
892
|
+
|| parent.isWorkspaceBusy?.(leasePath))
|
|
642
893
|
throw new Error('Delegation capacity or workspace is busy; wait before retrying');
|
|
894
|
+
this.workspaces.set(leasePath, workspaceOwner);
|
|
895
|
+
assertWorkspace();
|
|
643
896
|
launches++;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
let settle;
|
|
897
|
+
let resolveDone;
|
|
898
|
+
let resolveDispatch;
|
|
647
899
|
let interrupt;
|
|
648
900
|
const interrupted = new Promise(resolve => { interrupt = () => resolve({ success: false }); });
|
|
649
901
|
const task = { record: { id: (0, crypto_1.randomUUID)(), threadId: parent.thread.id,
|
|
650
902
|
parentMessageId: parent.messageId, backend, objective: objective.slice(0, 1000),
|
|
651
|
-
state: '
|
|
903
|
+
state: 'queued', acceptedAt: Date.now() }, configuration, objective,
|
|
904
|
+
resultDelivered: false, settled: false, ownsSlot: first,
|
|
652
905
|
textProgressEnabled: parent.onTextProgress !== undefined,
|
|
653
|
-
activeToolIds: new Set(), done: new Promise(resolve => {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
this.running--;
|
|
661
|
-
this.workspaces.delete(cwd);
|
|
662
|
-
launches--;
|
|
663
|
-
task.settle();
|
|
664
|
-
throw error;
|
|
906
|
+
activeToolIds: new Set(), done: new Promise(resolve => { resolveDone = resolve; }),
|
|
907
|
+
dispatched: new Promise(resolve => { resolveDispatch = resolve; }), signalDispatch: () => resolveDispatch(),
|
|
908
|
+
settle: () => { if (task.settled)
|
|
909
|
+
return; task.settled = true; resolveDone(); resolveDispatch(); }, interrupted, interrupt };
|
|
910
|
+
if (first) {
|
|
911
|
+
activeTasks.add(task);
|
|
912
|
+
this.running++;
|
|
665
913
|
}
|
|
666
914
|
try {
|
|
667
|
-
|
|
668
|
-
threadId: parent.thread.id,
|
|
669
|
-
backend,
|
|
670
|
-
workingDirectory: cwd,
|
|
671
|
-
workspaceGeneration: parent.thread.delegationWorkspaceGeneration ?? 0,
|
|
672
|
-
});
|
|
673
|
-
task.lane = lane.lane;
|
|
915
|
+
await deadline(() => this.store.write(task.record), exports.DELEGATION_LIMITS.storageTimeoutMs, 'Initial task record write timed out');
|
|
674
916
|
}
|
|
675
917
|
catch (error) {
|
|
676
|
-
task.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
this.running--;
|
|
682
|
-
this.workspaces.delete(cwd);
|
|
918
|
+
if (task.ownsSlot) {
|
|
919
|
+
this.running--;
|
|
920
|
+
task.ownsSlot = false;
|
|
921
|
+
activeTasks.delete(task);
|
|
922
|
+
}
|
|
683
923
|
launches--;
|
|
684
924
|
task.settle();
|
|
685
925
|
throw error;
|
|
686
926
|
}
|
|
927
|
+
// The durable queued record is the acceptance boundary. Every task
|
|
928
|
+
// after it consumes quota, including setup failures and cancellation.
|
|
929
|
+
tasks.set(task.record.id, task);
|
|
687
930
|
if (closed) {
|
|
688
|
-
task.
|
|
931
|
+
if (task.ownsSlot) {
|
|
932
|
+
this.running--;
|
|
933
|
+
task.ownsSlot = false;
|
|
934
|
+
activeTasks.delete(task);
|
|
935
|
+
}
|
|
936
|
+
await completeUnstarted(task, 'cancelled', 'Cancelled before the worker started.');
|
|
689
937
|
}
|
|
690
|
-
if (
|
|
691
|
-
|
|
692
|
-
description: `${backend}: ${objective.slice(0, 120)}`, prompt: objective.slice(0, 1000), subagent_type: backend,
|
|
693
|
-
} }));
|
|
938
|
+
else if (task.ownsSlot) {
|
|
939
|
+
void run(task).catch(() => undefined);
|
|
694
940
|
}
|
|
695
|
-
|
|
696
|
-
|
|
941
|
+
else {
|
|
942
|
+
armQueueTimer(task);
|
|
943
|
+
notify(() => parent.onNotice(`\n\n⏳ ${backend} delegated task ${task.record.id.slice(0, 8)} queued. It will start after earlier tasks finish; queue wait is limited to ${formatDuration(this.timeouts.queueTimeoutMs)}.\n\n`));
|
|
944
|
+
pump();
|
|
945
|
+
}
|
|
946
|
+
return task;
|
|
947
|
+
});
|
|
948
|
+
admissionTail = admitted.catch(() => undefined);
|
|
949
|
+
let task;
|
|
950
|
+
try {
|
|
951
|
+
task = await admitted;
|
|
697
952
|
}
|
|
698
953
|
finally {
|
|
699
|
-
|
|
954
|
+
admissions--;
|
|
955
|
+
releaseWorkspaceIfIdle();
|
|
956
|
+
for (const start of this.pumps)
|
|
957
|
+
start();
|
|
700
958
|
}
|
|
959
|
+
if (task.ownsSlot) {
|
|
960
|
+
// Preserve the existing first-task response after dispatch/setup,
|
|
961
|
+
// without holding up admission of followers while setup is pending.
|
|
962
|
+
await task.dispatched;
|
|
963
|
+
}
|
|
964
|
+
return receive(task, requestedDuring);
|
|
701
965
|
})();
|
|
702
966
|
calls.set(callId, { signature, result });
|
|
703
967
|
return result;
|
|
@@ -705,7 +969,7 @@ ${prompt}`));
|
|
|
705
969
|
const scope = {
|
|
706
970
|
invoke,
|
|
707
971
|
isClosed: () => closed,
|
|
708
|
-
hasTasks: () =>
|
|
972
|
+
hasTasks: () => admissions > 0 || tasks.size > 0,
|
|
709
973
|
getLaunchRevision: () => launchRevision,
|
|
710
974
|
beginExecution: (taskIds = []) => {
|
|
711
975
|
if (closed)
|
|
@@ -714,7 +978,7 @@ ${prompt}`));
|
|
|
714
978
|
acceptingResults = true;
|
|
715
979
|
stagedResults.clear();
|
|
716
980
|
for (const id of taskIds) {
|
|
717
|
-
if (tasks.get(id)?.record.finishedAt)
|
|
981
|
+
if (tasks.get(id)?.record.finishedAt !== undefined)
|
|
718
982
|
stagedResults.add(id);
|
|
719
983
|
}
|
|
720
984
|
},
|
|
@@ -726,9 +990,9 @@ ${prompt}`));
|
|
|
726
990
|
}
|
|
727
991
|
stagedResults.clear();
|
|
728
992
|
},
|
|
729
|
-
hasPendingResults: () =>
|
|
993
|
+
hasPendingResults: () => admissions > 0 || [...tasks.values()].some(task => !task.resultDelivered && !stagedResults.has(task.record.id)),
|
|
730
994
|
getRetainedResults: () => boundedResults([...tasks.values()]
|
|
731
|
-
.filter(task => task.record.finishedAt && !task.resultDelivered).map(view)),
|
|
995
|
+
.filter(task => task.record.finishedAt !== undefined && !task.resultDelivered).map(view)),
|
|
732
996
|
collectPendingResults: async () => {
|
|
733
997
|
if (closed)
|
|
734
998
|
throw new Error('Delegation was cancelled');
|
|
@@ -743,19 +1007,28 @@ ${prompt}`));
|
|
|
743
1007
|
throw new Error('Delegation was cancelled');
|
|
744
1008
|
return boundedResults(pending.map(view));
|
|
745
1009
|
},
|
|
746
|
-
waitingExecutor: () =>
|
|
1010
|
+
waitingExecutor: () => {
|
|
1011
|
+
const waiting = [...activeTasks].filter(task => task.record.state === 'running' && task.executor?.isWaitingInput?.());
|
|
1012
|
+
return !closed && waiting.length === 1 ? waiting[0].executor : undefined;
|
|
1013
|
+
},
|
|
1014
|
+
waitingInputCount: () => closed ? 0 : [...activeTasks].filter(task => task.record.state === 'running' && task.executor?.isWaitingInput?.()).length,
|
|
747
1015
|
close: async () => {
|
|
748
1016
|
closed = true;
|
|
1017
|
+
this.pumps.delete(pump);
|
|
749
1018
|
await Promise.allSettled([...tasks.values()].map(cancel));
|
|
1019
|
+
await integrationPending?.catch(() => undefined);
|
|
1020
|
+
releaseWorkspaceIfIdle();
|
|
750
1021
|
// A version probe may still be pending. Its closed check prevents launch.
|
|
751
1022
|
if (this.scopes.get(parent.thread.id) === scope)
|
|
752
1023
|
this.scopes.delete(parent.thread.id);
|
|
753
1024
|
},
|
|
754
1025
|
};
|
|
1026
|
+
this.pumps.add(pump);
|
|
755
1027
|
this.scopes.set(parent.thread.id, scope);
|
|
756
1028
|
return scope;
|
|
757
1029
|
}
|
|
758
1030
|
waitingExecutor(threadId) { return this.scopes.get(threadId)?.waitingExecutor(); }
|
|
1031
|
+
hasAmbiguousInput(threadId) { return (this.scopes.get(threadId)?.waitingInputCount() ?? 0) > 1; }
|
|
759
1032
|
hasActiveTasks(threadId) { return this.scopes.get(threadId)?.hasTasks() ?? false; }
|
|
760
1033
|
/** Retry previously confirmed cleanup requests without touching uncertain interrupted workers. */
|
|
761
1034
|
async reconcilePendingWorkerLanes() {
|
|
@@ -789,6 +1062,7 @@ ${prompt}`));
|
|
|
789
1062
|
let failed = 0;
|
|
790
1063
|
for (const lane of lanes) {
|
|
791
1064
|
try {
|
|
1065
|
+
await this.workspaceManager.preserveLane(lane.id);
|
|
792
1066
|
await (0, DelegatedWorkerLaneCleanup_1.cleanupDelegatedWorkerLane)(lane);
|
|
793
1067
|
await this.laneStore.remove(lane.id);
|
|
794
1068
|
removed++;
|
|
@@ -809,7 +1083,7 @@ ${prompt}`));
|
|
|
809
1083
|
if (this.workspaces.size === 0)
|
|
810
1084
|
return false;
|
|
811
1085
|
const canonical = fs_1.default.realpathSync(cwd);
|
|
812
|
-
return [...this.workspaces].some(([active, owner]) => owner !== threadId && workspacesOverlap(active, canonical));
|
|
1086
|
+
return [...this.workspaces].some(([active, owner]) => (owner.quarantined || owner.threadId !== threadId) && workspacesOverlap(active, canonical));
|
|
813
1087
|
}
|
|
814
1088
|
async cancelThread(threadId) { await this.scopes.get(threadId)?.close(); }
|
|
815
1089
|
async destroy() { await Promise.allSettled([...this.scopes.values()].map(scope => scope.close())); }
|