praxis-agent 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/session-service.d.ts +8 -4
- package/dist/application/session-service.js +528 -434
- package/dist/application/subagent-service.js +358 -177
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +33 -0
- package/dist/application/transcript-projection.js +110 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.js +264 -204
- package/dist/cli/tui/claude-style.js +154 -130
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +9 -5
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli-runtime.d.ts +2 -2
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/runtime.d.ts +5 -0
- package/dist/core/runtime.js +15 -1
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { mkdir, readFile, readdir, realpath, rm } from 'node:fs/promises';
|
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { resolveClaudePaths } from '../compatibility/claude/paths.js';
|
|
8
|
+
import { isTerminalLifecycleState } from '../core/agent-orchestration.js';
|
|
8
9
|
import { ClaudeJobStore, isProcessAlive, newClaudeJobIdentity, } from '../persistence/claude-job-store.js';
|
|
9
10
|
import { resolveDataPlanePaths, } from '../persistence/data-plane.js';
|
|
10
11
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
@@ -235,6 +236,7 @@ export class TopLevelAgentManager {
|
|
|
235
236
|
const cwd = await canonicalDirectory(options.cwd ?? this.options.cwd);
|
|
236
237
|
let identity;
|
|
237
238
|
let state;
|
|
239
|
+
let execution;
|
|
238
240
|
for (let attempt = 0; attempt < JOB_CREATE_ATTEMPTS; attempt += 1) {
|
|
239
241
|
const generated = newClaudeJobIdentity();
|
|
240
242
|
const sessionId = options.sourceSessionId
|
|
@@ -272,7 +274,7 @@ export class TopLevelAgentManager {
|
|
|
272
274
|
socketPath: socketPath(this.options.configRoot, generated.id),
|
|
273
275
|
controlToken: randomBytes(24).toString('hex'),
|
|
274
276
|
};
|
|
275
|
-
|
|
277
|
+
const created = await this.store.createExecution(candidate, {
|
|
276
278
|
version: 1,
|
|
277
279
|
argv: [...options.argv],
|
|
278
280
|
resume: options.resumeSessionId !== undefined,
|
|
@@ -283,15 +285,53 @@ export class TopLevelAgentManager {
|
|
|
283
285
|
...(options.sourceCheckpoint === undefined
|
|
284
286
|
? {}
|
|
285
287
|
: { sourceCheckpoint: options.sourceCheckpoint }),
|
|
286
|
-
})
|
|
288
|
+
});
|
|
289
|
+
if (created) {
|
|
287
290
|
identity = { id: generated.id, sessionId };
|
|
288
291
|
state = candidate;
|
|
292
|
+
execution = created;
|
|
289
293
|
break;
|
|
290
294
|
}
|
|
291
295
|
}
|
|
292
|
-
if (!identity || !state)
|
|
296
|
+
if (!identity || !state || !execution)
|
|
293
297
|
throw new Error('Could not allocate agent ID');
|
|
298
|
+
const allocatedExecution = execution;
|
|
294
299
|
let child;
|
|
300
|
+
const failLaunch = async (error) => {
|
|
301
|
+
const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(this.options.environment ?? process.env));
|
|
302
|
+
const secondary = [];
|
|
303
|
+
if (child?.pid !== undefined) {
|
|
304
|
+
try {
|
|
305
|
+
child.kill('SIGTERM');
|
|
306
|
+
}
|
|
307
|
+
catch (killError) {
|
|
308
|
+
secondary.push(killError);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
await allocatedExecution.finish('failed', (current) => {
|
|
313
|
+
const now = new Date().toISOString();
|
|
314
|
+
return {
|
|
315
|
+
...clearWorkerFields(current),
|
|
316
|
+
state: 'failed',
|
|
317
|
+
detail: message,
|
|
318
|
+
tempo: 'idle',
|
|
319
|
+
updatedAt: now,
|
|
320
|
+
firstTerminalAt: current.firstTerminalAt ?? now,
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (finishError) {
|
|
325
|
+
secondary.push(finishError);
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
await allocatedExecution.release();
|
|
329
|
+
}
|
|
330
|
+
catch (releaseError) {
|
|
331
|
+
secondary.push(releaseError);
|
|
332
|
+
}
|
|
333
|
+
throw new AggregateError([error, ...secondary], 'Could not start background agent');
|
|
334
|
+
};
|
|
295
335
|
try {
|
|
296
336
|
child = spawn(this.options.executablePath ?? process.execPath, [this.options.cliPath, '__background-worker', identity.id], {
|
|
297
337
|
cwd,
|
|
@@ -299,76 +339,73 @@ export class TopLevelAgentManager {
|
|
|
299
339
|
detached: true,
|
|
300
340
|
stdio: 'ignore',
|
|
301
341
|
});
|
|
342
|
+
const spawnedChild = child;
|
|
302
343
|
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
303
344
|
const spawned = () => {
|
|
304
|
-
|
|
345
|
+
spawnedChild.removeListener('error', failed);
|
|
305
346
|
resolveSpawn();
|
|
306
347
|
};
|
|
307
348
|
const failed = (error) => {
|
|
308
|
-
|
|
349
|
+
spawnedChild.removeListener('spawn', spawned);
|
|
309
350
|
rejectSpawn(error);
|
|
310
351
|
};
|
|
311
|
-
|
|
312
|
-
|
|
352
|
+
spawnedChild.once('spawn', spawned);
|
|
353
|
+
spawnedChild.once('error', failed);
|
|
313
354
|
});
|
|
314
355
|
}
|
|
315
356
|
catch (error) {
|
|
316
|
-
|
|
317
|
-
const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(this.options.environment ?? process.env));
|
|
318
|
-
await this.store.update(identity.id, (current) => ({
|
|
319
|
-
...clearWorkerFields(current),
|
|
320
|
-
state: 'failed',
|
|
321
|
-
detail: message,
|
|
322
|
-
tempo: 'idle',
|
|
323
|
-
updatedAt: failedAt,
|
|
324
|
-
firstTerminalAt: failedAt,
|
|
325
|
-
}));
|
|
326
|
-
throw new Error('Could not start background agent', { cause: error });
|
|
357
|
+
return failLaunch(error);
|
|
327
358
|
}
|
|
328
|
-
const childPid = child
|
|
359
|
+
const childPid = child?.pid;
|
|
329
360
|
if (!childPid) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
361
|
+
return failLaunch(new Error('worker failed to start'));
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await execution.update((current) => ({
|
|
365
|
+
...current,
|
|
366
|
+
pid: childPid,
|
|
335
367
|
updatedAt: new Date().toISOString(),
|
|
336
|
-
firstTerminalAt: new Date().toISOString(),
|
|
337
368
|
}));
|
|
338
|
-
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return failLaunch(error);
|
|
339
372
|
}
|
|
340
373
|
child.unref();
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
try {
|
|
375
|
+
await execution.handoff();
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const secondary = [];
|
|
379
|
+
try {
|
|
380
|
+
child.kill('SIGTERM');
|
|
381
|
+
}
|
|
382
|
+
catch (killError) {
|
|
383
|
+
secondary.push(killError);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await execution.release();
|
|
387
|
+
}
|
|
388
|
+
catch (releaseError) {
|
|
389
|
+
secondary.push(releaseError);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const reconciliation = await this.store.reconcileOwnerLoss(identity.id);
|
|
393
|
+
if (reconciliation.owned) {
|
|
394
|
+
secondary.push(new Error(`Background agent handoff owner is still held for ${identity.id}`));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (reconcileError) {
|
|
398
|
+
secondary.push(reconcileError);
|
|
399
|
+
}
|
|
400
|
+
throw new AggregateError([error, ...secondary], 'Background agent handoff failed');
|
|
401
|
+
}
|
|
346
402
|
return identity;
|
|
347
403
|
}
|
|
348
404
|
async list(options) {
|
|
349
405
|
const cwd = options.cwd === undefined
|
|
350
406
|
? undefined
|
|
351
407
|
: await canonicalDirectory(options.cwd);
|
|
352
|
-
const
|
|
353
|
-
const reconciled = await Promise.all(states.map(async (state) => {
|
|
354
|
-
if (state.state !== 'working' || (await this.workerAlive(state))) {
|
|
355
|
-
return state;
|
|
356
|
-
}
|
|
357
|
-
return this.store.update(state.daemonShort, (current) => {
|
|
358
|
-
if (current.state !== 'working' || current.pid !== state.pid) {
|
|
359
|
-
return current;
|
|
360
|
-
}
|
|
361
|
-
const now = new Date().toISOString();
|
|
362
|
-
return {
|
|
363
|
-
...clearWorkerFields(current),
|
|
364
|
-
state: 'failed',
|
|
365
|
-
detail: 'worker exited unexpectedly',
|
|
366
|
-
tempo: 'idle',
|
|
367
|
-
updatedAt: now,
|
|
368
|
-
firstTerminalAt: current.firstTerminalAt ?? now,
|
|
369
|
-
};
|
|
370
|
-
});
|
|
371
|
-
}));
|
|
408
|
+
const reconciled = await Promise.all((await this.store.listWithLifecycle()).map(async (view) => (await this.readLifecycleView(view.state.daemonShort, view)).state));
|
|
372
409
|
const praxis = reconciled
|
|
373
410
|
.filter((state) => cwd === undefined || state.cwd === cwd)
|
|
374
411
|
.filter((state) => options.all || state.state === 'working')
|
|
@@ -397,6 +434,40 @@ export class TopLevelAgentManager {
|
|
|
397
434
|
const native = await this.nativeSessions(cwd, new Set(reconciled.map((state) => state.sessionId)), options.all);
|
|
398
435
|
return [...praxis, ...native].sort((left, right) => right.startedAt - left.startedAt);
|
|
399
436
|
}
|
|
437
|
+
async readLifecycleView(id, known, reconcileLegacy = true) {
|
|
438
|
+
const view = known ?? (await this.store.readWithLifecycle(id));
|
|
439
|
+
if (view.legacy || !view.lifecycle) {
|
|
440
|
+
if (!reconcileLegacy)
|
|
441
|
+
return view;
|
|
442
|
+
if (view.state.state !== 'working' ||
|
|
443
|
+
(await this.workerAlive(view.state)))
|
|
444
|
+
return view;
|
|
445
|
+
const state = await this.store.update(id, (current) => {
|
|
446
|
+
if (current.state !== 'working' || current.pid !== view.state.pid)
|
|
447
|
+
return current;
|
|
448
|
+
const now = new Date().toISOString();
|
|
449
|
+
return {
|
|
450
|
+
...clearWorkerFields(current),
|
|
451
|
+
state: 'failed',
|
|
452
|
+
detail: 'worker exited unexpectedly',
|
|
453
|
+
tempo: 'idle',
|
|
454
|
+
updatedAt: now,
|
|
455
|
+
firstTerminalAt: current.firstTerminalAt ?? now,
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
return { state, lifecycleState: 'failed', lifecycle: null, legacy: true };
|
|
459
|
+
}
|
|
460
|
+
if (isTerminalLifecycleState(view.lifecycle.state))
|
|
461
|
+
return view;
|
|
462
|
+
if (view.lifecycle.state === 'queued' &&
|
|
463
|
+
view.state.pid !== undefined &&
|
|
464
|
+
Number.isFinite(Date.parse(view.state.updatedAt)) &&
|
|
465
|
+
Date.now() - Date.parse(view.state.updatedAt) <
|
|
466
|
+
WORKER_REGISTRATION_GRACE_MS &&
|
|
467
|
+
(await this.workerAlive(view.state)))
|
|
468
|
+
return view;
|
|
469
|
+
return (await this.store.reconcileOwnerLoss(id)).view;
|
|
470
|
+
}
|
|
400
471
|
async nativeSessions(cwd, knownSessionIds, all) {
|
|
401
472
|
const registryRoot = topLevelAgentProcessRegistryRoot(this.options.configRoot, this.options.dataPlane);
|
|
402
473
|
let files;
|
|
@@ -462,10 +533,13 @@ export class TopLevelAgentManager {
|
|
|
462
533
|
}
|
|
463
534
|
}
|
|
464
535
|
async stop(id) {
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
throw new Error(`Agent ${id} is not running (state: ${
|
|
536
|
+
let view = await this.readLifecycleView(id);
|
|
537
|
+
if (view.lifecycle && isTerminalLifecycleState(view.lifecycleState)) {
|
|
538
|
+
throw new Error(`Agent ${id} is not running (state: ${view.lifecycleState})`);
|
|
468
539
|
}
|
|
540
|
+
if (!view.lifecycle && view.state.state !== 'working')
|
|
541
|
+
throw new Error(`Agent ${id} is not running (state: ${view.state.state})`);
|
|
542
|
+
const state = view.state;
|
|
469
543
|
let stopped = false;
|
|
470
544
|
if (state.socketPath && state.controlToken) {
|
|
471
545
|
try {
|
|
@@ -481,20 +555,59 @@ export class TopLevelAgentManager {
|
|
|
481
555
|
(await this.workerAlive(state))) {
|
|
482
556
|
process.kill(state.pid, 'SIGTERM');
|
|
483
557
|
}
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
558
|
+
const deadline = Date.now() + STARTUP_CONTROL_WAIT_MS;
|
|
559
|
+
for (;;) {
|
|
560
|
+
view = await this.readLifecycleView(id);
|
|
561
|
+
if (view.lifecycle) {
|
|
562
|
+
if (view.lifecycleState === 'cancelled')
|
|
563
|
+
return;
|
|
564
|
+
if (isTerminalLifecycleState(view.lifecycleState))
|
|
565
|
+
throw new Error(`Agent ${id} stop ended in state ${view.lifecycleState}`);
|
|
566
|
+
}
|
|
567
|
+
else if (stopped || view.state.state !== 'working') {
|
|
568
|
+
if (stopped || view.state.state === 'stopped') {
|
|
569
|
+
if (stopped && view.state.state === 'working') {
|
|
570
|
+
const now = new Date().toISOString();
|
|
571
|
+
await this.store.update(id, (current) => ({
|
|
572
|
+
...clearWorkerFields(current),
|
|
573
|
+
state: 'stopped',
|
|
574
|
+
detail: 'stopped',
|
|
575
|
+
tempo: 'idle',
|
|
576
|
+
updatedAt: now,
|
|
577
|
+
firstTerminalAt: current.firstTerminalAt ?? now,
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
throw new Error(`Agent ${id} stop ended in state ${view.state.state}`);
|
|
583
|
+
}
|
|
584
|
+
if (Date.now() >= deadline) {
|
|
585
|
+
if (view.legacy) {
|
|
586
|
+
const now = new Date().toISOString();
|
|
587
|
+
await this.store.update(id, (current) => ({
|
|
588
|
+
...clearWorkerFields(current),
|
|
589
|
+
state: 'stopped',
|
|
590
|
+
detail: 'stopped',
|
|
591
|
+
tempo: 'idle',
|
|
592
|
+
updatedAt: now,
|
|
593
|
+
firstTerminalAt: current.firstTerminalAt ?? now,
|
|
594
|
+
}));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
throw new Error(`Agent ${id} cancellation is still in progress`);
|
|
598
|
+
}
|
|
599
|
+
await waitForSocketRetry();
|
|
600
|
+
}
|
|
493
601
|
}
|
|
494
602
|
async attach(id, input, output, signal) {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
603
|
+
const view = await this.readLifecycleView(id, undefined, false);
|
|
604
|
+
const state = view.state;
|
|
605
|
+
if ((view.lifecycle &&
|
|
606
|
+
!['queued', 'running', 'waiting'].includes(view.lifecycleState)) ||
|
|
607
|
+
(!view.lifecycle && state.state !== 'working') ||
|
|
608
|
+
!state.socketPath ||
|
|
609
|
+
!state.controlToken) {
|
|
610
|
+
throw new Error(`Agent ${id} is not attachable (state: ${view.lifecycleState})`);
|
|
498
611
|
}
|
|
499
612
|
const socket = await connectToWorker(state.socketPath, ATTACH_READY_WAIT_MS, signal);
|
|
500
613
|
let readyResolve;
|
|
@@ -597,17 +710,25 @@ export class TopLevelAgentManager {
|
|
|
597
710
|
async request(path, message, timeoutMs) {
|
|
598
711
|
return new Promise((resolveRequest, reject) => {
|
|
599
712
|
const socket = createConnection(path);
|
|
713
|
+
let settled = false;
|
|
600
714
|
const timer = setTimeout(() => {
|
|
715
|
+
if (settled)
|
|
716
|
+
return;
|
|
717
|
+
settled = true;
|
|
601
718
|
socket.destroy();
|
|
602
719
|
reject(new Error('Agent control request timed out'));
|
|
603
720
|
}, timeoutMs);
|
|
604
721
|
timer.unref?.();
|
|
605
722
|
const finish = (operation) => {
|
|
723
|
+
if (settled)
|
|
724
|
+
return;
|
|
725
|
+
settled = true;
|
|
606
726
|
clearTimeout(timer);
|
|
607
727
|
socket.end();
|
|
608
728
|
operation();
|
|
609
729
|
};
|
|
610
730
|
socket.once('error', (error) => finish(() => reject(error)));
|
|
731
|
+
socket.once('close', () => finish(() => reject(new Error('Agent control request closed'))));
|
|
611
732
|
socket.once('connect', () => writeWire(socket, message));
|
|
612
733
|
lines(socket, (response) => finish(() => resolveRequest(response)));
|
|
613
734
|
});
|
|
@@ -625,387 +746,888 @@ export class TopLevelAgentManager {
|
|
|
625
746
|
value.sessionId === state.sessionId);
|
|
626
747
|
}
|
|
627
748
|
catch {
|
|
628
|
-
const
|
|
629
|
-
return (Number.isFinite(
|
|
630
|
-
Date.now() -
|
|
749
|
+
const updatedAt = Date.parse(state.updatedAt);
|
|
750
|
+
return (Number.isFinite(updatedAt) &&
|
|
751
|
+
Date.now() - updatedAt < WORKER_REGISTRATION_GRACE_MS);
|
|
631
752
|
}
|
|
632
753
|
}
|
|
633
754
|
}
|
|
634
755
|
export async function runTopLevelAgentWorker(options) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
let
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (!initial.socketPath || !initial.controlToken) {
|
|
644
|
-
throw new Error(`Agent ${options.id} has no control endpoint`);
|
|
645
|
-
}
|
|
646
|
-
const socketFile = initial.socketPath;
|
|
647
|
-
const processFile = join(topLevelAgentProcessRegistryRoot(options.configRoot, options.dataPlane), `${process.pid}.json`);
|
|
648
|
-
const processStartedAt = Date.now();
|
|
649
|
-
const writeProcessStatus = (status) => {
|
|
650
|
-
const now = Date.now();
|
|
651
|
-
return writeFileAtomically(processFile, `${JSON.stringify({
|
|
652
|
-
pid: process.pid,
|
|
653
|
-
sessionId: initial.sessionId,
|
|
654
|
-
cwd: initial.cwd,
|
|
655
|
-
startedAt: processStartedAt,
|
|
656
|
-
version: initial.cliVersion,
|
|
657
|
-
kind: 'bg',
|
|
658
|
-
entrypoint: 'cli',
|
|
659
|
-
name: initial.intent,
|
|
660
|
-
jobId: options.id,
|
|
661
|
-
status,
|
|
662
|
-
updatedAt: now,
|
|
663
|
-
statusUpdatedAt: now,
|
|
664
|
-
})}\n`);
|
|
756
|
+
let stopRequested = false;
|
|
757
|
+
let requestCancellation = async () => undefined;
|
|
758
|
+
let cancellationFinalizer;
|
|
759
|
+
let activeController;
|
|
760
|
+
const captureStop = () => {
|
|
761
|
+
stopRequested = true;
|
|
762
|
+
void requestCancellation().catch(() => undefined);
|
|
763
|
+
void cancellationFinalizer?.().catch(() => undefined);
|
|
665
764
|
};
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
765
|
+
process.once('SIGTERM', captureStop);
|
|
766
|
+
process.once('SIGINT', captureStop);
|
|
767
|
+
const removeCaptureListeners = () => {
|
|
768
|
+
process.removeListener('SIGTERM', captureStop);
|
|
769
|
+
process.removeListener('SIGINT', captureStop);
|
|
770
|
+
};
|
|
771
|
+
const store = new ClaudeJobStore(options.configRoot, join(options.configRoot, options.dataPlane === 'native' ? 'state' : 'praxis'));
|
|
772
|
+
let execution;
|
|
773
|
+
let cleanupClaimed;
|
|
774
|
+
let executionReleased = false;
|
|
775
|
+
const releaseExecution = async () => {
|
|
776
|
+
if (executionReleased || !execution)
|
|
777
|
+
return;
|
|
778
|
+
const failures = [];
|
|
779
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
780
|
+
try {
|
|
781
|
+
await execution.release();
|
|
782
|
+
executionReleased = true;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
failures.push(error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
throw new AggregateError(failures, 'Could not release agent execution');
|
|
672
790
|
};
|
|
673
|
-
let
|
|
674
|
-
let
|
|
675
|
-
let
|
|
676
|
-
let outputWrites = Promise.resolve();
|
|
677
|
-
let runtime;
|
|
791
|
+
let claimed = false;
|
|
792
|
+
let postClaimFailure;
|
|
793
|
+
let deferredFailure;
|
|
678
794
|
try {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
795
|
+
const initialView = await store.readWithLifecycle(options.id);
|
|
796
|
+
if (initialView.lifecycle &&
|
|
797
|
+
isTerminalLifecycleState(initialView.lifecycleState) &&
|
|
798
|
+
initialView.lifecycleState !== 'orphaned')
|
|
799
|
+
return;
|
|
800
|
+
if (!initialView.lifecycle && initialView.state.state !== 'working')
|
|
801
|
+
return;
|
|
802
|
+
const initial = initialView.state;
|
|
803
|
+
let dispatch = await store.readDispatch(options.id);
|
|
804
|
+
const sensitiveValues = sensitiveEnvironmentValues(process.env);
|
|
805
|
+
const safeText = (text) => redactSensitiveText(text, sensitiveValues);
|
|
806
|
+
const safeErrorMessage = (error) => safeText(error instanceof Error ? error.message : String(error));
|
|
807
|
+
const hasOwnedContention = (error) => error instanceof Error &&
|
|
808
|
+
error.message ===
|
|
809
|
+
`Agent ${options.id} lifecycle execution is already owned`;
|
|
810
|
+
const claimDeadline = Date.now() + WORKER_REGISTRATION_GRACE_MS;
|
|
811
|
+
for (;;) {
|
|
812
|
+
try {
|
|
813
|
+
execution = await store.claimExecution(options.id);
|
|
814
|
+
claimed = true;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
if (!hasOwnedContention(error) || Date.now() >= claimDeadline)
|
|
819
|
+
throw error;
|
|
820
|
+
await waitForSocketRetry();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const claimedExecution = execution;
|
|
824
|
+
if (!claimedExecution)
|
|
825
|
+
throw new Error('Worker execution was not claimed');
|
|
826
|
+
let runtime;
|
|
827
|
+
let runtimeInitialization;
|
|
828
|
+
let runtimeInitializationError;
|
|
829
|
+
let runtimeClosed = false;
|
|
830
|
+
let server;
|
|
831
|
+
const clients = new Set();
|
|
832
|
+
let stopClient;
|
|
833
|
+
let outputWriteError;
|
|
834
|
+
let outputWrites = Promise.resolve();
|
|
835
|
+
let liveTurnText = '';
|
|
836
|
+
let sessionStarted = dispatch.resume || dispatch.handoffComplete === true;
|
|
837
|
+
let closing = false;
|
|
838
|
+
let setupStep = Promise.resolve();
|
|
839
|
+
const runSetupStep = (operation) => {
|
|
840
|
+
const next = setupStep.then(operation);
|
|
841
|
+
setupStep = next.then(() => undefined, () => undefined);
|
|
842
|
+
return next;
|
|
843
|
+
};
|
|
844
|
+
let listenPending = false;
|
|
845
|
+
let listenCancelled = false;
|
|
846
|
+
let rejectPendingListen;
|
|
847
|
+
let serverClosePromise;
|
|
848
|
+
let resolveServerClose;
|
|
849
|
+
let rejectServerClose;
|
|
850
|
+
let serverCloseRequested = false;
|
|
851
|
+
let serverCloseStarted = false;
|
|
852
|
+
let serverCloseError;
|
|
853
|
+
let invokeServerClose;
|
|
854
|
+
const settleServerClose = (error) => {
|
|
855
|
+
if (error !== undefined)
|
|
856
|
+
rejectServerClose?.(error);
|
|
857
|
+
else
|
|
858
|
+
resolveServerClose?.();
|
|
859
|
+
resolveServerClose = undefined;
|
|
860
|
+
rejectServerClose = undefined;
|
|
861
|
+
};
|
|
862
|
+
const closeServer = () => {
|
|
863
|
+
if (!server)
|
|
864
|
+
return Promise.resolve();
|
|
865
|
+
const currentServer = server;
|
|
866
|
+
if (!serverClosePromise) {
|
|
867
|
+
serverClosePromise = new Promise((resolveClose, rejectClose) => {
|
|
868
|
+
resolveServerClose = resolveClose;
|
|
869
|
+
rejectServerClose = rejectClose;
|
|
687
870
|
});
|
|
688
|
-
|
|
871
|
+
const closeNow = () => {
|
|
872
|
+
if (serverCloseStarted)
|
|
873
|
+
return;
|
|
874
|
+
serverCloseStarted = true;
|
|
875
|
+
try {
|
|
876
|
+
currentServer.close((error) => {
|
|
877
|
+
if (error) {
|
|
878
|
+
serverCloseError = error;
|
|
879
|
+
settleServerClose(error);
|
|
880
|
+
}
|
|
881
|
+
else
|
|
882
|
+
settleServerClose();
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
if (error.code === 'ERR_SERVER_NOT_RUNNING')
|
|
887
|
+
settleServerClose();
|
|
888
|
+
else {
|
|
889
|
+
serverCloseError = error;
|
|
890
|
+
settleServerClose(error);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
invokeServerClose = closeNow;
|
|
895
|
+
if (currentServer.listening)
|
|
896
|
+
closeNow();
|
|
897
|
+
else if (!listenPending)
|
|
898
|
+
settleServerClose();
|
|
899
|
+
else
|
|
900
|
+
serverCloseRequested = true;
|
|
689
901
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
902
|
+
else if (currentServer.listening && serverCloseRequested) {
|
|
903
|
+
invokeServerClose?.();
|
|
904
|
+
}
|
|
905
|
+
return serverClosePromise;
|
|
906
|
+
};
|
|
907
|
+
const closePendingListen = () => {
|
|
908
|
+
if (!listenPending)
|
|
909
|
+
return;
|
|
910
|
+
listenCancelled = true;
|
|
911
|
+
const closePromise = closeServer();
|
|
912
|
+
rejectPendingListen?.(new Error('Agent listen cancelled'));
|
|
913
|
+
void closePromise.catch(() => undefined);
|
|
914
|
+
};
|
|
915
|
+
let turnQueue = Promise.resolve();
|
|
916
|
+
let finishWorker;
|
|
917
|
+
const finished = new Promise((resolveFinished) => {
|
|
918
|
+
finishWorker = resolveFinished;
|
|
701
919
|
});
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
920
|
+
const socketFile = initial.socketPath ?? socketPath(options.configRoot, options.id);
|
|
921
|
+
const processFile = join(topLevelAgentProcessRegistryRoot(options.configRoot, options.dataPlane), `${process.pid}.json`);
|
|
922
|
+
const processStartedAt = Date.now();
|
|
923
|
+
const writeProcessStatus = (status) => {
|
|
924
|
+
const now = Date.now();
|
|
925
|
+
return writeFileAtomically(processFile, `${JSON.stringify({
|
|
926
|
+
pid: process.pid,
|
|
927
|
+
sessionId: initial.sessionId,
|
|
928
|
+
cwd: initial.cwd,
|
|
929
|
+
startedAt: processStartedAt,
|
|
930
|
+
version: initial.cliVersion,
|
|
931
|
+
kind: 'bg',
|
|
932
|
+
entrypoint: 'cli',
|
|
933
|
+
name: initial.intent,
|
|
934
|
+
jobId: options.id,
|
|
935
|
+
status,
|
|
936
|
+
updatedAt: now,
|
|
937
|
+
statusUpdatedAt: now,
|
|
938
|
+
})}\n`);
|
|
939
|
+
};
|
|
940
|
+
const closeRuntime = async () => {
|
|
941
|
+
if (runtimeClosed)
|
|
942
|
+
return;
|
|
943
|
+
runtimeClosed = true;
|
|
944
|
+
if (runtimeInitialization) {
|
|
945
|
+
try {
|
|
946
|
+
runtime = await runtimeInitialization;
|
|
728
947
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
client.destroy();
|
|
732
|
-
return;
|
|
948
|
+
catch (error) {
|
|
949
|
+
runtimeInitializationError = error;
|
|
733
950
|
}
|
|
734
|
-
authenticated = true;
|
|
735
|
-
clients.add(client);
|
|
736
|
-
void store.output(options.id).then((history) => {
|
|
737
|
-
writeWire(client, {
|
|
738
|
-
type: 'ready',
|
|
739
|
-
text: history,
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
return;
|
|
743
951
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
952
|
+
await runtime?.close?.();
|
|
953
|
+
};
|
|
954
|
+
const closeControl = (destroy = false) => {
|
|
955
|
+
for (const client of clients) {
|
|
956
|
+
if (destroy)
|
|
957
|
+
client.destroy();
|
|
958
|
+
else
|
|
959
|
+
client.end();
|
|
747
960
|
}
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
961
|
+
if (server?.listening || listenPending)
|
|
962
|
+
void closeServer().catch(() => undefined);
|
|
963
|
+
};
|
|
964
|
+
cleanupClaimed = async () => {
|
|
965
|
+
const failures = [];
|
|
966
|
+
closePendingListen();
|
|
967
|
+
try {
|
|
968
|
+
await setupStep;
|
|
751
969
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
state: 'failed',
|
|
803
|
-
detail: message,
|
|
804
|
-
text: message,
|
|
805
|
-
});
|
|
806
|
-
await store.update(options.id, (state) => ({
|
|
807
|
-
...clearWorkerFields(state),
|
|
808
|
-
state: 'failed',
|
|
809
|
-
detail: message,
|
|
810
|
-
tempo: 'idle',
|
|
811
|
-
updatedAt: now,
|
|
812
|
-
firstTerminalAt: state.firstTerminalAt ?? now,
|
|
813
|
-
}));
|
|
814
|
-
}
|
|
815
|
-
finally {
|
|
816
|
-
broadcast({ type: 'failed', message });
|
|
817
|
-
closeControl();
|
|
818
|
-
}
|
|
819
|
-
};
|
|
820
|
-
async function runTurn(prompt, resume, requestId, client) {
|
|
821
|
-
if (closing)
|
|
822
|
-
return;
|
|
823
|
-
liveTurnText = '';
|
|
824
|
-
outputWriteError = undefined;
|
|
825
|
-
activeController = new AbortController();
|
|
826
|
-
const now = new Date().toISOString();
|
|
827
|
-
const activated = await store.update(options.id, (state) => {
|
|
828
|
-
if (state.state !== 'working')
|
|
829
|
-
return state;
|
|
830
|
-
const next = {
|
|
831
|
-
...state,
|
|
832
|
-
detail: safeText(prompt),
|
|
833
|
-
tempo: 'active',
|
|
834
|
-
inFlight: { tasks: 1, queued: 0, kinds: ['prompt'] },
|
|
835
|
-
updatedAt: now,
|
|
970
|
+
catch (error) {
|
|
971
|
+
failures.push(error);
|
|
972
|
+
}
|
|
973
|
+
activeController?.abort();
|
|
974
|
+
try {
|
|
975
|
+
await outputWrites;
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
failures.push(error);
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
await closeRuntime();
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
failures.push(error);
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
closeControl(true);
|
|
988
|
+
await closeServer();
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
failures.push(error);
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
await rm(socketFile, { force: true });
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
failures.push(error);
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
await rm(processFile, { force: true });
|
|
1001
|
+
}
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
failures.push(error);
|
|
1004
|
+
}
|
|
1005
|
+
stopClient?.destroy();
|
|
1006
|
+
stopClient = undefined;
|
|
1007
|
+
if (failures.length > 0)
|
|
1008
|
+
throw new AggregateError(failures, 'Agent fallback cleanup failed');
|
|
1009
|
+
};
|
|
1010
|
+
const broadcast = (message) => {
|
|
1011
|
+
for (const client of clients)
|
|
1012
|
+
writeWire(client, message);
|
|
1013
|
+
};
|
|
1014
|
+
const lifecycleWrite = (() => {
|
|
1015
|
+
let chain = Promise.resolve();
|
|
1016
|
+
return (operation) => {
|
|
1017
|
+
const next = chain.then(operation);
|
|
1018
|
+
chain = next.then(() => undefined, () => undefined);
|
|
1019
|
+
return next;
|
|
836
1020
|
};
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1021
|
+
})();
|
|
1022
|
+
let cancellationTransition;
|
|
1023
|
+
requestCancellation = () => {
|
|
1024
|
+
if (!cancellationTransition) {
|
|
1025
|
+
cancellationTransition = lifecycleWrite(async () => {
|
|
1026
|
+
if (['queued', 'running', 'waiting'].includes(claimedExecution.snapshot.state))
|
|
1027
|
+
await claimedExecution.beginCancellation();
|
|
1028
|
+
}).then(() => undefined);
|
|
1029
|
+
}
|
|
1030
|
+
return cancellationTransition;
|
|
1031
|
+
};
|
|
1032
|
+
const finalization = (outcome, error, skipTurnDrain = false) => {
|
|
1033
|
+
if (finalizationPromise)
|
|
1034
|
+
return finalizationPromise;
|
|
1035
|
+
const operation = (async () => {
|
|
1036
|
+
const cancellationAtStart = outcome === 'cancelled' ||
|
|
1037
|
+
stopRequested ||
|
|
1038
|
+
claimedExecution.snapshot.state === 'cancelling';
|
|
1039
|
+
const detail = cancellationAtStart ? 'stopped' : safeErrorMessage(error);
|
|
1040
|
+
const failures = [];
|
|
1041
|
+
try {
|
|
1042
|
+
if (cancellationAtStart)
|
|
1043
|
+
await requestCancellation();
|
|
848
1044
|
}
|
|
849
|
-
|
|
850
|
-
|
|
1045
|
+
catch (failure) {
|
|
1046
|
+
failures.push(failure);
|
|
851
1047
|
}
|
|
852
|
-
|
|
853
|
-
|
|
1048
|
+
closing = true;
|
|
1049
|
+
activeController?.abort();
|
|
1050
|
+
closePendingListen();
|
|
1051
|
+
try {
|
|
1052
|
+
await setupStep;
|
|
854
1053
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1054
|
+
catch (failure) {
|
|
1055
|
+
if (!(cancellationAtStart && listenCancelled))
|
|
1056
|
+
failures.push(failure);
|
|
1057
|
+
}
|
|
1058
|
+
if (!skipTurnDrain) {
|
|
1059
|
+
try {
|
|
1060
|
+
await turnQueue;
|
|
1061
|
+
}
|
|
1062
|
+
catch (failure) {
|
|
1063
|
+
failures.push(failure);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
await outputWrites;
|
|
1068
|
+
// A streaming write failure is already the primary fatal error when
|
|
1069
|
+
// runTurn routes that same object through failed finalization. It is
|
|
1070
|
+
// still a cancellation cleanup failure, and distinct queued errors
|
|
1071
|
+
// remain cleanup failures.
|
|
1072
|
+
if (outputWriteError &&
|
|
1073
|
+
(cancellationAtStart || outputWriteError !== error))
|
|
1074
|
+
failures.push(outputWriteError);
|
|
1075
|
+
}
|
|
1076
|
+
catch (failure) {
|
|
1077
|
+
failures.push(failure);
|
|
1078
|
+
}
|
|
1079
|
+
if (!cancellationAtStart) {
|
|
1080
|
+
try {
|
|
1081
|
+
await store.appendOutput(options.id, `${detail}\n`);
|
|
1082
|
+
await store.trimOutput(options.id, MAX_JOB_OUTPUT_BYTES);
|
|
1083
|
+
}
|
|
1084
|
+
catch (failure) {
|
|
1085
|
+
failures.push(failure);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
await closeRuntime();
|
|
1090
|
+
}
|
|
1091
|
+
catch (failure) {
|
|
1092
|
+
failures.push(failure);
|
|
1093
|
+
}
|
|
1094
|
+
if (cancellationAtStart && runtimeInitializationError)
|
|
1095
|
+
failures.push(runtimeInitializationError);
|
|
1096
|
+
try {
|
|
1097
|
+
const deferServerDrain = cancellationAtStart && stopClient !== undefined;
|
|
1098
|
+
closeControl(true);
|
|
1099
|
+
// Starting the close synchronously stops new accepts. For an
|
|
1100
|
+
// authenticated stop, retain the drain promise until after ack.
|
|
1101
|
+
await Promise.resolve();
|
|
1102
|
+
if (serverCloseError)
|
|
1103
|
+
throw serverCloseError;
|
|
1104
|
+
if (!deferServerDrain)
|
|
1105
|
+
await closeServer();
|
|
1106
|
+
}
|
|
1107
|
+
catch (failure) {
|
|
1108
|
+
failures.push(failure);
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
await rm(socketFile, { force: true });
|
|
1112
|
+
}
|
|
1113
|
+
catch (failure) {
|
|
1114
|
+
failures.push(failure);
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
await rm(processFile, { force: true });
|
|
1118
|
+
}
|
|
1119
|
+
catch (failure) {
|
|
1120
|
+
failures.push(failure);
|
|
1121
|
+
}
|
|
1122
|
+
const cancellation = cancellationAtStart ||
|
|
1123
|
+
stopRequested ||
|
|
1124
|
+
String(claimedExecution.snapshot.state) === 'cancelling';
|
|
1125
|
+
if (cancellation && !cancellationAtStart) {
|
|
1126
|
+
try {
|
|
1127
|
+
await requestCancellation();
|
|
1128
|
+
}
|
|
1129
|
+
catch (failure) {
|
|
1130
|
+
failures.push(failure);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (failures.length > 0) {
|
|
1134
|
+
const diagnostic = safeText(failures.map((failure) => safeErrorMessage(failure)).join('; '));
|
|
1135
|
+
try {
|
|
1136
|
+
if (!isTerminalLifecycleState(claimedExecution.snapshot.state))
|
|
1137
|
+
await lifecycleWrite(() => claimedExecution.update((state) => ({
|
|
1138
|
+
...clearWorkerFields(state),
|
|
1139
|
+
detail: diagnostic,
|
|
1140
|
+
tempo: 'idle',
|
|
1141
|
+
updatedAt: new Date().toISOString(),
|
|
1142
|
+
})));
|
|
1143
|
+
}
|
|
1144
|
+
catch (diagnosticError) {
|
|
1145
|
+
failures.push(diagnosticError);
|
|
1146
|
+
}
|
|
1147
|
+
throw new AggregateError(failures, 'Agent cleanup failed');
|
|
1148
|
+
}
|
|
1149
|
+
if (isTerminalLifecycleState(claimedExecution.snapshot.state)) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const terminal = await lifecycleWrite(() => claimedExecution.finish(cancellation ? 'cancelled' : 'failed', (state) => ({
|
|
1153
|
+
...clearWorkerFields(state),
|
|
1154
|
+
...(cancellation
|
|
1155
|
+
? { detail, tempo: 'idle' }
|
|
1156
|
+
: {
|
|
1157
|
+
state: 'failed',
|
|
1158
|
+
detail,
|
|
1159
|
+
tempo: 'idle',
|
|
1160
|
+
firstTerminalAt: state.firstTerminalAt ?? new Date().toISOString(),
|
|
1161
|
+
}),
|
|
1162
|
+
updatedAt: new Date().toISOString(),
|
|
1163
|
+
})));
|
|
1164
|
+
let timelineError;
|
|
1165
|
+
try {
|
|
1166
|
+
await store.appendTimeline(options.id, {
|
|
1167
|
+
at: new Date().toISOString(),
|
|
1168
|
+
state: cancellation ? 'stopped' : 'failed',
|
|
1169
|
+
detail,
|
|
1170
|
+
text: detail,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
catch (failure) {
|
|
1174
|
+
timelineError = failure;
|
|
1175
|
+
}
|
|
1176
|
+
if (cancellation && terminal.state === 'cancelled') {
|
|
1177
|
+
broadcast({ type: 'stopped' });
|
|
1178
|
+
if (stopClient) {
|
|
1179
|
+
writeWire(stopClient, { type: 'stopped' });
|
|
1180
|
+
stopClient.end();
|
|
1181
|
+
stopClient = undefined;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
await closeServer();
|
|
1185
|
+
}
|
|
1186
|
+
catch (failure) {
|
|
1187
|
+
throw new AggregateError([failure], 'Agent server close failed after cancellation');
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
else if (!cancellation) {
|
|
1191
|
+
broadcast({ type: 'failed', message: detail });
|
|
1192
|
+
}
|
|
1193
|
+
if (timelineError)
|
|
1194
|
+
throw new AggregateError([timelineError], 'Terminal lifecycle timeline projection failed');
|
|
1195
|
+
})();
|
|
1196
|
+
const finalPromise = operation
|
|
1197
|
+
.catch((failure) => {
|
|
1198
|
+
finalizationError = failure;
|
|
1199
|
+
throw failure;
|
|
1200
|
+
})
|
|
1201
|
+
.finally(() => {
|
|
1202
|
+
closeControl(true);
|
|
1203
|
+
stopClient?.destroy();
|
|
1204
|
+
stopClient = undefined;
|
|
1205
|
+
finishWorker?.();
|
|
890
1206
|
});
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1207
|
+
finalizationPromise = finalPromise;
|
|
1208
|
+
return finalPromise;
|
|
1209
|
+
};
|
|
1210
|
+
let finalizationError;
|
|
1211
|
+
let finalizationPromise;
|
|
1212
|
+
cancellationFinalizer = () => finalization('cancelled');
|
|
1213
|
+
const runTurn = async (prompt, resume, requestId, client) => {
|
|
1214
|
+
if (closing || stopRequested)
|
|
1215
|
+
return;
|
|
1216
|
+
liveTurnText = '';
|
|
1217
|
+
outputWriteError = undefined;
|
|
1218
|
+
activeController = new AbortController();
|
|
1219
|
+
const now = new Date().toISOString();
|
|
1220
|
+
if (!['running', 'waiting'].includes(claimedExecution.snapshot.state) ||
|
|
1221
|
+
closing ||
|
|
1222
|
+
stopRequested)
|
|
1223
|
+
return;
|
|
1224
|
+
const activate = (state) => {
|
|
894
1225
|
const next = {
|
|
895
1226
|
...state,
|
|
896
|
-
detail:
|
|
897
|
-
tempo: '
|
|
898
|
-
inFlight: { tasks:
|
|
899
|
-
|
|
900
|
-
updatedAt: completedAt,
|
|
1227
|
+
detail: safeText(prompt),
|
|
1228
|
+
tempo: 'active',
|
|
1229
|
+
inFlight: { tasks: 1, queued: 0, kinds: ['prompt'] },
|
|
1230
|
+
updatedAt: now,
|
|
901
1231
|
};
|
|
902
1232
|
delete next.needs;
|
|
903
1233
|
return next;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
1234
|
+
};
|
|
1235
|
+
let resumeTurn = resume;
|
|
1236
|
+
try {
|
|
1237
|
+
if (claimedExecution.snapshot.state === 'waiting')
|
|
1238
|
+
await lifecycleWrite(() => claimedExecution.running(activate));
|
|
1239
|
+
else
|
|
1240
|
+
await lifecycleWrite(() => claimedExecution.update(activate));
|
|
1241
|
+
if (closing || stopRequested)
|
|
1242
|
+
return;
|
|
1243
|
+
await writeProcessStatus('working');
|
|
1244
|
+
if (!sessionStarted && dispatch.sourceSessionId) {
|
|
1245
|
+
if (!runtime?.ensureFork && !runtime?.fork)
|
|
1246
|
+
throw new Error('Background handoff runtime cannot fork sessions');
|
|
1247
|
+
if (runtime.ensureFork)
|
|
1248
|
+
await runtime.ensureFork(dispatch.sourceSessionId, initial.sessionId, dispatch.sourceCheckpoint);
|
|
1249
|
+
else
|
|
1250
|
+
await runtime.fork?.(dispatch.sourceSessionId, initial.sessionId);
|
|
1251
|
+
dispatch = await store.updateDispatch(options.id, (current) => {
|
|
1252
|
+
const next = {
|
|
1253
|
+
...current,
|
|
1254
|
+
resume: true,
|
|
1255
|
+
handoffComplete: true,
|
|
1256
|
+
};
|
|
1257
|
+
delete next.sourceSessionId;
|
|
1258
|
+
delete next.sourceCheckpoint;
|
|
1259
|
+
return next;
|
|
1260
|
+
});
|
|
1261
|
+
sessionStarted = true;
|
|
1262
|
+
resumeTurn = true;
|
|
1263
|
+
}
|
|
1264
|
+
const currentRuntime = runtime;
|
|
1265
|
+
const currentController = activeController;
|
|
1266
|
+
if (!currentRuntime || !currentController)
|
|
1267
|
+
throw new Error('Background agent runtime is not ready');
|
|
1268
|
+
const result = resumeTurn
|
|
1269
|
+
? await currentRuntime.resume(initial.sessionId, prompt, currentController.signal)
|
|
1270
|
+
: await currentRuntime.run(prompt, currentController.signal, initial.sessionId);
|
|
1271
|
+
sessionStarted = true;
|
|
1272
|
+
if (closing || stopRequested)
|
|
1273
|
+
return;
|
|
1274
|
+
const completedAt = new Date().toISOString();
|
|
1275
|
+
await outputWrites;
|
|
1276
|
+
if (outputWriteError)
|
|
1277
|
+
throw outputWriteError;
|
|
1278
|
+
const resultText = safeText(result.text);
|
|
1279
|
+
if (liveTurnText.length === 0 && resultText.length > 0) {
|
|
1280
|
+
broadcast({ type: 'output', text: resultText });
|
|
1281
|
+
await store.appendOutput(options.id, resultText);
|
|
1282
|
+
}
|
|
1283
|
+
await store.appendOutput(options.id, '\n');
|
|
1284
|
+
await store.trimOutput(options.id, MAX_JOB_OUTPUT_BYTES);
|
|
1285
|
+
await store.appendTimeline(options.id, {
|
|
1286
|
+
at: completedAt,
|
|
1287
|
+
state: 'working',
|
|
1288
|
+
detail: resultText,
|
|
1289
|
+
text: resultText,
|
|
1290
|
+
});
|
|
1291
|
+
const idled = await lifecycleWrite(() => claimedExecution.waiting((state) => {
|
|
1292
|
+
const next = {
|
|
1293
|
+
...state,
|
|
1294
|
+
detail: resultText,
|
|
1295
|
+
tempo: 'idle',
|
|
1296
|
+
inFlight: { tasks: 0, queued: 0, kinds: [] },
|
|
1297
|
+
tokens: state.tokens +
|
|
1298
|
+
result.usage.inputTokens +
|
|
1299
|
+
result.usage.outputTokens,
|
|
1300
|
+
updatedAt: completedAt,
|
|
1301
|
+
};
|
|
1302
|
+
delete next.needs;
|
|
1303
|
+
return next;
|
|
1304
|
+
}));
|
|
1305
|
+
if (!closing && idled.state === 'waiting')
|
|
1306
|
+
await writeProcessStatus('idle');
|
|
1307
|
+
if (requestId && client)
|
|
1308
|
+
writeWire(client, { type: 'turn-complete', requestId });
|
|
907
1309
|
}
|
|
908
|
-
|
|
909
|
-
|
|
1310
|
+
catch (turnError) {
|
|
1311
|
+
if (closing || activeController?.signal.aborted || stopRequested)
|
|
1312
|
+
return;
|
|
1313
|
+
const message = safeErrorMessage(turnError);
|
|
1314
|
+
const failedAt = new Date().toISOString();
|
|
1315
|
+
if (resumeTurn) {
|
|
1316
|
+
try {
|
|
1317
|
+
await store.appendOutput(options.id, `${message}\n`);
|
|
1318
|
+
await store.trimOutput(options.id, MAX_JOB_OUTPUT_BYTES);
|
|
1319
|
+
await store.appendTimeline(options.id, {
|
|
1320
|
+
at: failedAt,
|
|
1321
|
+
state: 'working',
|
|
1322
|
+
detail: message,
|
|
1323
|
+
text: message,
|
|
1324
|
+
});
|
|
1325
|
+
const idled = await lifecycleWrite(() => claimedExecution.waiting((state) => {
|
|
1326
|
+
const next = {
|
|
1327
|
+
...state,
|
|
1328
|
+
detail: message,
|
|
1329
|
+
tempo: 'idle',
|
|
1330
|
+
inFlight: { tasks: 0, queued: 0, kinds: [] },
|
|
1331
|
+
updatedAt: failedAt,
|
|
1332
|
+
};
|
|
1333
|
+
delete next.needs;
|
|
1334
|
+
return next;
|
|
1335
|
+
}));
|
|
1336
|
+
if (!closing && idled.state === 'waiting')
|
|
1337
|
+
await writeProcessStatus('idle');
|
|
1338
|
+
if (requestId && client)
|
|
1339
|
+
writeWire(client, { type: 'turn-error', requestId, message });
|
|
1340
|
+
}
|
|
1341
|
+
catch (persistenceError) {
|
|
1342
|
+
await finalization('failed', persistenceError, true).catch(() => undefined);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
await finalization('failed', turnError, true).catch(() => undefined);
|
|
1347
|
+
}
|
|
910
1348
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1349
|
+
finally {
|
|
1350
|
+
activeController = undefined;
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
try {
|
|
1354
|
+
if (!initial.socketPath || !initial.controlToken) {
|
|
1355
|
+
const token = initial.controlToken ?? randomBytes(24).toString('hex');
|
|
1356
|
+
await runSetupStep(() => lifecycleWrite(() => claimedExecution.update((state) => ({
|
|
1357
|
+
...state,
|
|
1358
|
+
socketPath: socketFile,
|
|
1359
|
+
controlToken: token,
|
|
1360
|
+
updatedAt: new Date().toISOString(),
|
|
1361
|
+
}))));
|
|
1362
|
+
}
|
|
1363
|
+
const current = await runSetupStep(() => store.read(options.id));
|
|
1364
|
+
const controlToken = current.controlToken;
|
|
1365
|
+
if (!controlToken)
|
|
1366
|
+
throw new Error(`Agent ${options.id} has no control endpoint`);
|
|
1367
|
+
if (stopRequested || closing) {
|
|
1368
|
+
await finalization('cancelled');
|
|
914
1369
|
return;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1370
|
+
}
|
|
1371
|
+
await runSetupStep(() => lifecycleWrite(() => claimedExecution.update((state) => ({
|
|
1372
|
+
...state,
|
|
1373
|
+
pid: process.pid,
|
|
1374
|
+
updatedAt: new Date().toISOString(),
|
|
1375
|
+
}))));
|
|
1376
|
+
if (stopRequested || closing) {
|
|
1377
|
+
await finalization('cancelled');
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
await runSetupStep(async () => {
|
|
1381
|
+
await mkdir(dirname(socketFile), { recursive: true });
|
|
1382
|
+
await rm(socketFile, { force: true });
|
|
1383
|
+
});
|
|
1384
|
+
if (stopRequested || closing) {
|
|
1385
|
+
await finalization('cancelled');
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (claimedExecution.snapshot.state === 'queued') {
|
|
1389
|
+
await runSetupStep(() => lifecycleWrite(() => claimedExecution.running((state) => ({
|
|
1390
|
+
...state,
|
|
1391
|
+
tempo: 'active',
|
|
1392
|
+
updatedAt: new Date().toISOString(),
|
|
1393
|
+
}))));
|
|
1394
|
+
if (dispatch.deferInitialTurn)
|
|
1395
|
+
await runSetupStep(() => lifecycleWrite(() => claimedExecution.waiting((state) => {
|
|
930
1396
|
const next = {
|
|
931
1397
|
...state,
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
updatedAt:
|
|
1398
|
+
tempo: dispatch.handoffComplete === true
|
|
1399
|
+
? 'idle'
|
|
1400
|
+
: 'blocked',
|
|
1401
|
+
updatedAt: new Date().toISOString(),
|
|
936
1402
|
};
|
|
937
|
-
|
|
1403
|
+
if (dispatch.handoffComplete === true)
|
|
1404
|
+
delete next.needs;
|
|
1405
|
+
else
|
|
1406
|
+
next.needs = 'send a prompt to start';
|
|
938
1407
|
return next;
|
|
1408
|
+
})));
|
|
1409
|
+
}
|
|
1410
|
+
if (stopRequested || closing) {
|
|
1411
|
+
await finalization('cancelled');
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
runtimeInitialization = runSetupStep(() => Promise.resolve().then(() => options.createRuntime((event) => {
|
|
1415
|
+
if (event.type === 'text-delta') {
|
|
1416
|
+
const delta = safeText(event.delta);
|
|
1417
|
+
liveTurnText += delta;
|
|
1418
|
+
outputWrites = outputWrites
|
|
1419
|
+
.then(() => store.appendOutput(options.id, delta))
|
|
1420
|
+
.catch((error) => {
|
|
1421
|
+
outputWriteError = error;
|
|
939
1422
|
});
|
|
940
|
-
|
|
941
|
-
|
|
1423
|
+
broadcast({ type: 'output', text: delta });
|
|
1424
|
+
}
|
|
1425
|
+
}, dispatch)));
|
|
1426
|
+
try {
|
|
1427
|
+
runtime = await runtimeInitialization;
|
|
1428
|
+
}
|
|
1429
|
+
catch (runtimeError) {
|
|
1430
|
+
if (stopRequested || closing)
|
|
1431
|
+
await finalization('cancelled', runtimeError);
|
|
1432
|
+
else
|
|
1433
|
+
await finalization('failed', runtimeError);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (stopRequested || closing) {
|
|
1437
|
+
await finalization('cancelled');
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
server = createServer((client) => {
|
|
1441
|
+
let authenticated = false;
|
|
1442
|
+
lines(client, (message) => {
|
|
1443
|
+
if (!authenticated) {
|
|
1444
|
+
if (message.type === 'stop' && message.token === controlToken) {
|
|
1445
|
+
authenticated = true;
|
|
1446
|
+
stopClient = client;
|
|
1447
|
+
void finalization('cancelled')
|
|
1448
|
+
.catch(() => undefined)
|
|
1449
|
+
.finally(() => {
|
|
1450
|
+
if (stopClient === client)
|
|
1451
|
+
client.destroy();
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (message.type !== 'attach' || message.token !== controlToken) {
|
|
1456
|
+
client.destroy();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
authenticated = true;
|
|
1460
|
+
clients.add(client);
|
|
1461
|
+
void store.output(options.id).then((history) => {
|
|
1462
|
+
writeWire(client, { type: 'ready', text: history });
|
|
1463
|
+
});
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (message.type === 'detach') {
|
|
1467
|
+
client.end();
|
|
1468
|
+
return;
|
|
942
1469
|
}
|
|
943
|
-
if (
|
|
944
|
-
|
|
1470
|
+
if (message.type === 'prompt' && message.text && message.requestId) {
|
|
1471
|
+
const { text, requestId } = message;
|
|
1472
|
+
turnQueue = turnQueue.then(() => runTurn(text, sessionStarted, requestId, client));
|
|
945
1473
|
}
|
|
1474
|
+
});
|
|
1475
|
+
client.once('close', () => clients.delete(client));
|
|
1476
|
+
});
|
|
1477
|
+
listenPending = true;
|
|
1478
|
+
listenCancelled = false;
|
|
1479
|
+
await runSetupStep(() => new Promise((resolveListen, rejectListen) => {
|
|
1480
|
+
let settled = false;
|
|
1481
|
+
rejectPendingListen = (listenError) => {
|
|
1482
|
+
if (settled)
|
|
1483
|
+
return;
|
|
1484
|
+
settled = true;
|
|
1485
|
+
listenPending = false;
|
|
1486
|
+
rejectListen(listenError);
|
|
1487
|
+
};
|
|
1488
|
+
const listening = () => {
|
|
1489
|
+
if (listenCancelled) {
|
|
1490
|
+
void closeServer().catch(() => undefined);
|
|
1491
|
+
void rm(socketFile, { force: true }).catch(() => undefined);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (settled)
|
|
1495
|
+
return;
|
|
1496
|
+
settled = true;
|
|
1497
|
+
listenPending = false;
|
|
1498
|
+
rejectPendingListen = undefined;
|
|
1499
|
+
server?.removeListener('error', failed);
|
|
1500
|
+
server?.removeListener('close', closed);
|
|
1501
|
+
resolveListen();
|
|
1502
|
+
};
|
|
1503
|
+
const closed = () => {
|
|
1504
|
+
if (serverClosePromise)
|
|
1505
|
+
settleServerClose();
|
|
1506
|
+
if (settled)
|
|
1507
|
+
return;
|
|
1508
|
+
settled = true;
|
|
1509
|
+
listenPending = false;
|
|
1510
|
+
rejectPendingListen = undefined;
|
|
1511
|
+
server?.removeListener('listening', listening);
|
|
1512
|
+
server?.removeListener('error', failed);
|
|
1513
|
+
rejectListen(new Error('Agent server closed before listening'));
|
|
1514
|
+
};
|
|
1515
|
+
const failed = (listenError) => {
|
|
1516
|
+
if (serverClosePromise)
|
|
1517
|
+
settleServerClose();
|
|
1518
|
+
if (settled)
|
|
1519
|
+
return;
|
|
1520
|
+
settled = true;
|
|
1521
|
+
listenPending = false;
|
|
1522
|
+
rejectPendingListen = undefined;
|
|
1523
|
+
server?.removeListener('listening', listening);
|
|
1524
|
+
server?.removeListener('close', closed);
|
|
1525
|
+
rejectListen(listenError);
|
|
1526
|
+
};
|
|
1527
|
+
server?.once('listening', listening);
|
|
1528
|
+
server?.once('close', closed);
|
|
1529
|
+
server?.once('error', failed);
|
|
1530
|
+
try {
|
|
1531
|
+
server?.listen(socketFile);
|
|
946
1532
|
}
|
|
947
|
-
catch (
|
|
948
|
-
|
|
1533
|
+
catch (listenError) {
|
|
1534
|
+
failed(listenError);
|
|
949
1535
|
}
|
|
1536
|
+
}));
|
|
1537
|
+
if (stopRequested || closing) {
|
|
1538
|
+
closeControl(true);
|
|
1539
|
+
await rm(socketFile, { force: true }).catch(() => undefined);
|
|
1540
|
+
await finalization('cancelled');
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
server.on('error', (serverError) => {
|
|
1544
|
+
void finalization('failed', serverError).catch(() => undefined);
|
|
1545
|
+
});
|
|
1546
|
+
const waitingForHandoff = dispatch.deferInitialTurn === true && dispatch.handoffComplete !== true;
|
|
1547
|
+
const registered = await runSetupStep(() => lifecycleWrite(() => claimedExecution.update((state) => {
|
|
1548
|
+
const next = {
|
|
1549
|
+
...state,
|
|
1550
|
+
pid: process.pid,
|
|
1551
|
+
detail: dispatch.deferInitialTurn ? state.detail : 'starting',
|
|
1552
|
+
tempo: dispatch.deferInitialTurn
|
|
1553
|
+
? waitingForHandoff
|
|
1554
|
+
? 'blocked'
|
|
1555
|
+
: 'idle'
|
|
1556
|
+
: 'active',
|
|
1557
|
+
...(waitingForHandoff ? { needs: 'send a prompt to start' } : {}),
|
|
1558
|
+
socketPath: socketFile,
|
|
1559
|
+
controlToken,
|
|
1560
|
+
updatedAt: new Date().toISOString(),
|
|
1561
|
+
};
|
|
1562
|
+
if (!waitingForHandoff)
|
|
1563
|
+
delete next.needs;
|
|
1564
|
+
return next;
|
|
1565
|
+
})));
|
|
1566
|
+
if (registered.state !== 'working' || stopRequested || closing) {
|
|
1567
|
+
await finalization('cancelled');
|
|
1568
|
+
return;
|
|
950
1569
|
}
|
|
951
|
-
|
|
952
|
-
|
|
1570
|
+
await runSetupStep(() => writeProcessStatus(dispatch.deferInitialTurn ? 'idle' : 'working'));
|
|
1571
|
+
if (stopRequested || closing) {
|
|
1572
|
+
closeControl(true);
|
|
1573
|
+
await rm(socketFile, { force: true }).catch(() => undefined);
|
|
1574
|
+
await rm(processFile, { force: true }).catch(() => undefined);
|
|
1575
|
+
await finalization('cancelled');
|
|
1576
|
+
return;
|
|
953
1577
|
}
|
|
1578
|
+
if (!dispatch.deferInitialTurn)
|
|
1579
|
+
turnQueue = turnQueue.then(() => runTurn(initial.intent, dispatch.resume));
|
|
1580
|
+
await finished;
|
|
1581
|
+
if (finalizationError)
|
|
1582
|
+
throw finalizationError;
|
|
954
1583
|
}
|
|
955
|
-
|
|
956
|
-
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
if (finalizationPromise) {
|
|
1586
|
+
try {
|
|
1587
|
+
await finalizationPromise;
|
|
1588
|
+
}
|
|
1589
|
+
catch (finalizationFailure) {
|
|
1590
|
+
postClaimFailure = finalizationFailure;
|
|
1591
|
+
throw finalizationFailure;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
else if (!closing) {
|
|
1595
|
+
try {
|
|
1596
|
+
await finalization(stopRequested ? 'cancelled' : 'failed', error);
|
|
1597
|
+
}
|
|
1598
|
+
catch (finalizationFailure) {
|
|
1599
|
+
postClaimFailure = finalizationFailure;
|
|
1600
|
+
throw finalizationFailure;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
957
1603
|
}
|
|
958
1604
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
server.once('error', reject);
|
|
962
|
-
server.listen(socketFile, resolveListen);
|
|
963
|
-
});
|
|
964
|
-
const waitingForHandoff = dispatch.deferInitialTurn === true && dispatch.handoffComplete !== true;
|
|
965
|
-
const registered = await store.update(options.id, (state) => {
|
|
966
|
-
if (state.state !== 'working')
|
|
967
|
-
return state;
|
|
968
|
-
const next = {
|
|
969
|
-
...state,
|
|
970
|
-
pid: process.pid,
|
|
971
|
-
detail: dispatch.deferInitialTurn ? state.detail : 'starting',
|
|
972
|
-
tempo: dispatch.deferInitialTurn
|
|
973
|
-
? waitingForHandoff
|
|
974
|
-
? 'blocked'
|
|
975
|
-
: 'idle'
|
|
976
|
-
: 'active',
|
|
977
|
-
...(waitingForHandoff ? { needs: 'send a prompt to start' } : {}),
|
|
978
|
-
socketPath: socketFile,
|
|
979
|
-
updatedAt: new Date().toISOString(),
|
|
980
|
-
};
|
|
981
|
-
if (!waitingForHandoff)
|
|
982
|
-
delete next.needs;
|
|
983
|
-
return next;
|
|
984
|
-
});
|
|
985
|
-
if (registered.state !== 'working') {
|
|
986
|
-
server.close();
|
|
987
|
-
await runtime.close?.();
|
|
988
|
-
await rm(socketFile, { force: true });
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
await writeProcessStatus(dispatch.deferInitialTurn ? 'idle' : 'working');
|
|
992
|
-
const stop = () => void setStopped('stopped').catch(() => undefined);
|
|
993
|
-
process.once('SIGTERM', stop);
|
|
994
|
-
process.once('SIGINT', stop);
|
|
995
|
-
if (!dispatch.deferInitialTurn) {
|
|
996
|
-
turnQueue = turnQueue.then(() => runTurn(initial.intent, dispatch.resume));
|
|
997
|
-
}
|
|
998
|
-
try {
|
|
999
|
-
await finished;
|
|
1605
|
+
catch (error) {
|
|
1606
|
+
deferredFailure = error;
|
|
1000
1607
|
}
|
|
1001
1608
|
finally {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1609
|
+
removeCaptureListeners();
|
|
1610
|
+
if (claimed) {
|
|
1611
|
+
const failures = [];
|
|
1612
|
+
try {
|
|
1613
|
+
await cleanupClaimed?.();
|
|
1614
|
+
}
|
|
1615
|
+
catch (error) {
|
|
1616
|
+
failures.push(error);
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
await releaseExecution();
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
failures.push(error);
|
|
1623
|
+
}
|
|
1624
|
+
if (postClaimFailure !== undefined)
|
|
1625
|
+
failures.unshift(postClaimFailure);
|
|
1626
|
+
if (failures.length > 0)
|
|
1627
|
+
deferredFailure = new AggregateError(failures, 'Agent worker cleanup failed');
|
|
1628
|
+
}
|
|
1009
1629
|
}
|
|
1630
|
+
if (deferredFailure !== undefined)
|
|
1631
|
+
throw deferredFailure;
|
|
1010
1632
|
}
|
|
1011
1633
|
//# sourceMappingURL=top-level-agent-manager.js.map
|