gestalt-mobile 0.25.2 → 0.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{index-BkpRh3eb.js → index-CFw8G97O.js} +18 -18
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +52 -62
- package/dist/server/server/composition.js +63 -4
- package/dist/server/server/features/agent-activity/model.js +27 -0
- package/dist/server/server/features/agent-activity/registry.js +24 -0
- package/dist/server/server/features/autopilot/application/policy.js +14 -12
- package/dist/server/server/features/autopilot/application/service.js +339 -17
- package/dist/server/server/features/autopilot/domain/autopilot-session.js +2 -0
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +255 -0
- package/dist/server/server/platform/codex/session-runtime.js +163 -0
- package/dist/server/server/platform/persistence/migrate.js +7 -2
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +21 -3
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +5 -0
- package/dist/server/shared/contracts/autopilot-audit.js +12 -0
- package/dist/server/shared/org-plan-position.js +12 -2
- package/package.json +1 -1
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
|
|
7
|
+
import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, } from '../domain/supervised-lifecycle.js';
|
|
7
8
|
import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
|
|
8
9
|
export class AutopilotCoordinator {
|
|
9
10
|
deps;
|
|
10
11
|
timers = new Map();
|
|
11
12
|
completionTimers = new Map();
|
|
13
|
+
executorTimers = new Map();
|
|
12
14
|
publishedSnapshots = new Map();
|
|
13
15
|
/** Serializes asynchronous watchdog and timer work per relay session. */
|
|
14
16
|
operations = new Map();
|
|
@@ -57,13 +59,14 @@ export class AutopilotCoordinator {
|
|
|
57
59
|
}
|
|
58
60
|
this.persist({
|
|
59
61
|
...state,
|
|
60
|
-
state: '
|
|
61
|
-
requestedEnabled:
|
|
62
|
+
state: 'monitoring',
|
|
63
|
+
requestedEnabled: true,
|
|
62
64
|
generation: state.generation + 1,
|
|
63
65
|
nextEvaluationAt: null,
|
|
64
66
|
stopReason: 'reconcileFailed',
|
|
65
67
|
updatedAt: this.deps.now(),
|
|
66
68
|
});
|
|
69
|
+
this.armExecutorRefresh(sessionId, this.deps.policy.executorContinuationMaxMs);
|
|
67
70
|
return;
|
|
68
71
|
}
|
|
69
72
|
if (state.state === 'backoff' && state.nextEvaluationAt) {
|
|
@@ -101,6 +104,8 @@ export class AutopilotCoordinator {
|
|
|
101
104
|
consecutiveNoProgress: 0,
|
|
102
105
|
nextEvaluationAt: null,
|
|
103
106
|
stopReason: null,
|
|
107
|
+
executor: undefined,
|
|
108
|
+
blocking: undefined,
|
|
104
109
|
updatedAt: now,
|
|
105
110
|
};
|
|
106
111
|
this.persist(next);
|
|
@@ -130,6 +135,7 @@ export class AutopilotCoordinator {
|
|
|
130
135
|
nextEvaluationAt: null,
|
|
131
136
|
...(cancelled ? { lastControlId: null } : {}),
|
|
132
137
|
stopReason: 'manualDisabled',
|
|
138
|
+
blocking: undefined,
|
|
133
139
|
updatedAt: now,
|
|
134
140
|
};
|
|
135
141
|
this.persist(next, cancelled);
|
|
@@ -150,6 +156,7 @@ export class AutopilotCoordinator {
|
|
|
150
156
|
nextEvaluationAt: null,
|
|
151
157
|
...(cancelled ? { lastControlId: null } : {}),
|
|
152
158
|
stopReason: reason,
|
|
159
|
+
blocking: undefined,
|
|
153
160
|
updatedAt: now,
|
|
154
161
|
}, cancelled);
|
|
155
162
|
this.cancelTimer(sessionId);
|
|
@@ -173,6 +180,14 @@ export class AutopilotCoordinator {
|
|
|
173
180
|
case 'scheduleContinuation':
|
|
174
181
|
if (session?.activeTurnId || this.deps.pendingInteraction(sessionId))
|
|
175
182
|
break;
|
|
183
|
+
if (this.deps.executorController &&
|
|
184
|
+
this.deps.plan(sessionId)?.plan &&
|
|
185
|
+
this.currentExecutor(sessionId, this.deps.plan(sessionId).plan, prior.consecutiveNoProgress, prior.executor)) {
|
|
186
|
+
this.enqueue(sessionId, async () => {
|
|
187
|
+
await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
176
191
|
const existingControl = prior.lastControlId
|
|
177
192
|
? this.deps.store.findControl(sessionId, prior.lastControlId)
|
|
178
193
|
: null;
|
|
@@ -216,7 +231,10 @@ export class AutopilotCoordinator {
|
|
|
216
231
|
next = prior;
|
|
217
232
|
this.arm(sessionId, prior.generation, decision.at);
|
|
218
233
|
break;
|
|
219
|
-
case 'requestAttention':
|
|
234
|
+
case 'requestAttention': {
|
|
235
|
+
const blocking = this.deps.attention?.(sessionId) ?? undefined;
|
|
236
|
+
if (!validStructuredBlock(blocking))
|
|
237
|
+
break;
|
|
220
238
|
next = {
|
|
221
239
|
...prior,
|
|
222
240
|
state: 'attentionRequired',
|
|
@@ -224,9 +242,11 @@ export class AutopilotCoordinator {
|
|
|
224
242
|
generation: prior.generation + 1,
|
|
225
243
|
nextEvaluationAt: null,
|
|
226
244
|
stopReason: decision.reason,
|
|
245
|
+
blocking,
|
|
227
246
|
updatedAt: now,
|
|
228
247
|
};
|
|
229
248
|
break;
|
|
249
|
+
}
|
|
230
250
|
case 'complete':
|
|
231
251
|
next = {
|
|
232
252
|
...prior,
|
|
@@ -235,6 +255,7 @@ export class AutopilotCoordinator {
|
|
|
235
255
|
generation: prior.generation + 1,
|
|
236
256
|
nextEvaluationAt: null,
|
|
237
257
|
stopReason: 'planComplete',
|
|
258
|
+
blocking: undefined,
|
|
238
259
|
updatedAt: now,
|
|
239
260
|
};
|
|
240
261
|
break;
|
|
@@ -246,6 +267,7 @@ export class AutopilotCoordinator {
|
|
|
246
267
|
generation: prior.generation + 1,
|
|
247
268
|
nextEvaluationAt: null,
|
|
248
269
|
stopReason: decision.reason,
|
|
270
|
+
blocking: undefined,
|
|
249
271
|
updatedAt: now,
|
|
250
272
|
};
|
|
251
273
|
break;
|
|
@@ -264,8 +286,35 @@ export class AutopilotCoordinator {
|
|
|
264
286
|
}
|
|
265
287
|
return this.snapshot(sessionId);
|
|
266
288
|
}
|
|
289
|
+
/** Returns whether an incomplete supervised plan may be treated as terminal by the relay. */
|
|
267
290
|
turnCompleted(sessionId) {
|
|
268
|
-
this.
|
|
291
|
+
const state = this.deps.store.find(sessionId);
|
|
292
|
+
const plan = this.deps.plan(sessionId)?.plan;
|
|
293
|
+
const explicitlyStopped = state?.stopReason === 'manualDisabled' ||
|
|
294
|
+
state?.stopReason === 'planRemoved' ||
|
|
295
|
+
state?.stopReason === 'planReplaced' ||
|
|
296
|
+
state?.stopReason === 'sessionEnded';
|
|
297
|
+
const finalAllowed = !state ||
|
|
298
|
+
!plan ||
|
|
299
|
+
explicitlyStopped ||
|
|
300
|
+
executionComplete(plan) ||
|
|
301
|
+
validStructuredBlock(state.blocking);
|
|
302
|
+
if (!finalAllowed) {
|
|
303
|
+
const occurredAt = this.deps.now();
|
|
304
|
+
this.commit({
|
|
305
|
+
events: [
|
|
306
|
+
{
|
|
307
|
+
sessionId,
|
|
308
|
+
type: 'autopilot.final-rejected',
|
|
309
|
+
payload: { reason: 'incompletePlan' },
|
|
310
|
+
occurredAt,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
this.flushOutbox(sessionId);
|
|
315
|
+
}
|
|
316
|
+
this.activitySettled(sessionId, 'rootFinalAttempt');
|
|
317
|
+
return finalAllowed;
|
|
269
318
|
}
|
|
270
319
|
/** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
|
|
271
320
|
planStatusChanged(sessionId) {
|
|
@@ -319,6 +368,8 @@ export class AutopilotCoordinator {
|
|
|
319
368
|
nextEvaluationAt: null,
|
|
320
369
|
...(cancelled ? { lastControlId: null } : {}),
|
|
321
370
|
stopReason: null,
|
|
371
|
+
executor: undefined,
|
|
372
|
+
blocking: undefined,
|
|
322
373
|
updatedAt: now,
|
|
323
374
|
};
|
|
324
375
|
this.persist(next, cancelled);
|
|
@@ -361,9 +412,9 @@ export class AutopilotCoordinator {
|
|
|
361
412
|
return;
|
|
362
413
|
}
|
|
363
414
|
if (disposition === 'settled')
|
|
364
|
-
this.activitySettled(sessionId);
|
|
415
|
+
this.activitySettled(sessionId, 'stateChanged');
|
|
365
416
|
}
|
|
366
|
-
activitySettled(sessionId) {
|
|
417
|
+
activitySettled(sessionId, event = 'stateChanged') {
|
|
367
418
|
const prior = this.deps.store.find(sessionId);
|
|
368
419
|
if (!prior?.requestedEnabled || prior.state !== 'monitoring')
|
|
369
420
|
return;
|
|
@@ -373,7 +424,10 @@ export class AutopilotCoordinator {
|
|
|
373
424
|
const current = this.deps.store.find(sessionId);
|
|
374
425
|
if (!current?.requestedEnabled || current.state !== 'monitoring')
|
|
375
426
|
return;
|
|
376
|
-
this.
|
|
427
|
+
this.enqueue(sessionId, async () => {
|
|
428
|
+
if (!(await this.enforceSupervisedLifecycle(sessionId, event)))
|
|
429
|
+
this.evaluate(sessionId);
|
|
430
|
+
});
|
|
377
431
|
}, this.deps.policy.quiescenceMs));
|
|
378
432
|
}
|
|
379
433
|
manualSend(sessionId) {
|
|
@@ -457,13 +511,30 @@ export class AutopilotCoordinator {
|
|
|
457
511
|
this.evaluate(sessionId);
|
|
458
512
|
return;
|
|
459
513
|
}
|
|
514
|
+
const retained = this.deps.plan(sessionId);
|
|
515
|
+
if (this.deps.executorController &&
|
|
516
|
+
retained &&
|
|
517
|
+
this.currentExecutor(sessionId, retained.plan, current.consecutiveNoProgress, current.executor)) {
|
|
518
|
+
const now = this.deps.now();
|
|
519
|
+
const cancelled = this.cancelScheduledControl(current, now);
|
|
520
|
+
this.persist({
|
|
521
|
+
...current,
|
|
522
|
+
state: 'monitoring',
|
|
523
|
+
generation: current.generation + 1,
|
|
524
|
+
nextEvaluationAt: null,
|
|
525
|
+
lastControlId: null,
|
|
526
|
+
updatedAt: now,
|
|
527
|
+
}, cancelled);
|
|
528
|
+
await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
460
531
|
const id = current.lastControlId;
|
|
461
532
|
if (!id)
|
|
462
533
|
return;
|
|
463
534
|
if (!this.recordControlIssued(sessionId, id))
|
|
464
535
|
return;
|
|
465
536
|
try {
|
|
466
|
-
await this.deps.turnStarter.start(sessionId, id, generation);
|
|
537
|
+
await this.deps.turnStarter.start(sessionId, id, generation, this.freshExecutorIdentity(sessionId));
|
|
467
538
|
this.updateControl(sessionId, id, 'started', null, this.deps.session(sessionId)?.activeTurnId ?? null, 'autopilot.turn-started');
|
|
468
539
|
}
|
|
469
540
|
catch (error) {
|
|
@@ -477,21 +548,20 @@ export class AutopilotCoordinator {
|
|
|
477
548
|
}
|
|
478
549
|
const failureCode = startFailureCode(error);
|
|
479
550
|
this.updateControl(sessionId, id, 'failed', failureCode, null, 'autopilot.turn-failed');
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
// human stop; unknown transport failures still consume the retry budget.
|
|
551
|
+
// A transport/runtime failure is not a decision-table blocker. Keep the
|
|
552
|
+
// supervised lifecycle active and re-inspect explicit runtime state.
|
|
483
553
|
if (failureCode === 'START_UNAVAILABLE') {
|
|
484
554
|
const latest = this.deps.store.find(sessionId);
|
|
485
555
|
if (latest?.requestedEnabled)
|
|
486
556
|
this.persist({
|
|
487
557
|
...latest,
|
|
488
|
-
state: '
|
|
489
|
-
requestedEnabled: false,
|
|
558
|
+
state: 'monitoring',
|
|
490
559
|
generation: latest.generation + 1,
|
|
491
560
|
nextEvaluationAt: null,
|
|
492
561
|
stopReason: 'startUnavailable',
|
|
493
562
|
updatedAt: this.deps.now(),
|
|
494
563
|
});
|
|
564
|
+
this.armExecutorRefresh(sessionId, this.deps.policy.executorContinuationMaxMs);
|
|
495
565
|
return;
|
|
496
566
|
}
|
|
497
567
|
this.evaluate(sessionId);
|
|
@@ -502,6 +572,235 @@ export class AutopilotCoordinator {
|
|
|
502
572
|
this.timers.delete(sessionId);
|
|
503
573
|
this.completionTimers.get(sessionId)?.();
|
|
504
574
|
this.completionTimers.delete(sessionId);
|
|
575
|
+
this.executorTimers.get(sessionId)?.();
|
|
576
|
+
this.executorTimers.delete(sessionId);
|
|
577
|
+
}
|
|
578
|
+
async enforceSupervisedLifecycle(sessionId, event) {
|
|
579
|
+
const controller = this.deps.executorController;
|
|
580
|
+
const retained = this.deps.plan(sessionId);
|
|
581
|
+
const state = this.deps.store.find(sessionId);
|
|
582
|
+
if (!controller ||
|
|
583
|
+
!retained ||
|
|
584
|
+
!state?.requestedEnabled ||
|
|
585
|
+
this.deps.pendingInteraction(sessionId))
|
|
586
|
+
return false;
|
|
587
|
+
const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor);
|
|
588
|
+
if (executor)
|
|
589
|
+
this.persistExecutor(sessionId, executor);
|
|
590
|
+
const decision = decideSupervisedLifecycle({
|
|
591
|
+
plan: retained.plan,
|
|
592
|
+
event,
|
|
593
|
+
...(executor ? { executor } : {}),
|
|
594
|
+
now: this.deps.now(),
|
|
595
|
+
policy: {
|
|
596
|
+
continuationBaseDelayMs: this.deps.policy.executorContinuationBaseMs,
|
|
597
|
+
continuationMaxDelayMs: this.deps.policy.executorContinuationMaxMs,
|
|
598
|
+
processPollMs: this.deps.policy.processPollMs,
|
|
599
|
+
processMaxElapsedMs: this.deps.policy.processMaxElapsedMs,
|
|
600
|
+
processMaxRssBytes: this.deps.policy.processMaxRssBytes,
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
switch (decision.action.kind) {
|
|
604
|
+
case 'allowFinal':
|
|
605
|
+
case 'invokeAttention':
|
|
606
|
+
case 'continueSupervisor':
|
|
607
|
+
case 'reinspect':
|
|
608
|
+
return false;
|
|
609
|
+
case 'resumeExecutor':
|
|
610
|
+
this.armExecutorContinuation(sessionId, decision.action.delayMs, decision.action.threadId, decision.action.generation, { kind: 'partial' });
|
|
611
|
+
return true;
|
|
612
|
+
case 'monitorProcess': {
|
|
613
|
+
const processId = decision.action.process.processId;
|
|
614
|
+
controller.transferProcess(sessionId, decision.action.process.ownerThreadId, processId);
|
|
615
|
+
if (executor)
|
|
616
|
+
this.persistExecutor(sessionId, {
|
|
617
|
+
...executor,
|
|
618
|
+
ownedProcesses: executor.ownedProcesses.map((process) => process.processId === processId
|
|
619
|
+
? { ...process, ownership: 'supervisor', state: 'detached-active' }
|
|
620
|
+
: process),
|
|
621
|
+
});
|
|
622
|
+
this.audit(sessionId, 'autopilot.process-monitoring', {
|
|
623
|
+
threadId: decision.action.process.ownerThreadId,
|
|
624
|
+
processId,
|
|
625
|
+
});
|
|
626
|
+
this.armExecutorRefresh(sessionId, decision.action.pollAfterMs);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
case 'consumeProcessResult': {
|
|
630
|
+
const processId = decision.action.processId;
|
|
631
|
+
controller.consumeProcess(sessionId, decision.action.threadId, processId);
|
|
632
|
+
if (executor)
|
|
633
|
+
this.persistExecutor(sessionId, {
|
|
634
|
+
...executor,
|
|
635
|
+
ownedProcesses: executor.ownedProcesses.map((process) => process.processId === processId ? { ...process, state: 'result-consumed' } : process),
|
|
636
|
+
});
|
|
637
|
+
this.audit(sessionId, 'autopilot.process-result-consumed', {
|
|
638
|
+
threadId: decision.action.threadId,
|
|
639
|
+
processId,
|
|
640
|
+
resultArtifact: decision.action.resultArtifact,
|
|
641
|
+
});
|
|
642
|
+
if (executor)
|
|
643
|
+
this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + 1, {
|
|
644
|
+
kind: 'processExited',
|
|
645
|
+
processId,
|
|
646
|
+
resultArtifact: decision.action.resultArtifact,
|
|
647
|
+
});
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
case 'terminateProcess': {
|
|
651
|
+
const processId = decision.action.processId;
|
|
652
|
+
const terminated = await controller.terminateProcess(sessionId, decision.action.threadId, processId);
|
|
653
|
+
if (terminated)
|
|
654
|
+
this.audit(sessionId, 'autopilot.process-terminated', {
|
|
655
|
+
threadId: decision.action.threadId,
|
|
656
|
+
processId,
|
|
657
|
+
reason: 'resourceBudget',
|
|
658
|
+
});
|
|
659
|
+
if (terminated && executor)
|
|
660
|
+
this.persistExecutor(sessionId, {
|
|
661
|
+
...executor,
|
|
662
|
+
ownedProcesses: executor.ownedProcesses.map((process) => process.processId === processId
|
|
663
|
+
? { ...process, state: 'terminated-for-budget' }
|
|
664
|
+
: process),
|
|
665
|
+
});
|
|
666
|
+
if (terminated && executor)
|
|
667
|
+
this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + 1, { kind: 'processResourceLimit', processId });
|
|
668
|
+
else
|
|
669
|
+
this.armExecutorRefresh(sessionId, this.deps.policy.processPollMs);
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
currentExecutor(sessionId, plan, continuationCount, persisted) {
|
|
675
|
+
const stepIndex = plan.steps.findIndex((step) => step.id === plan.currentStepId || step.state === 'WIP');
|
|
676
|
+
const index = stepIndex >= 0 ? stepIndex : plan.steps.findIndex((step) => step.state !== 'DONE');
|
|
677
|
+
if (index < 0)
|
|
678
|
+
return undefined;
|
|
679
|
+
const step = plan.steps[index];
|
|
680
|
+
const canonicalPosition = `L${index + 1}`;
|
|
681
|
+
const child = this.deps
|
|
682
|
+
.activity(sessionId)
|
|
683
|
+
?.subagents.filter((candidate) => candidate.canonicalPosition === canonicalPosition)
|
|
684
|
+
.sort((left, right) => (right.continuationGeneration ?? 1) - (left.continuationGeneration ?? 1) ||
|
|
685
|
+
Date.parse(right.lastActivityAt) - Date.parse(left.lastActivityAt))[0];
|
|
686
|
+
if (!child?.taskPath ||
|
|
687
|
+
!child.canonicalTaskName ||
|
|
688
|
+
child.state === 'disconnected' ||
|
|
689
|
+
child.outcome === 'cancelled' ||
|
|
690
|
+
child.outcome === 'failed') {
|
|
691
|
+
if (persisted?.canonicalPosition !== canonicalPosition ||
|
|
692
|
+
persisted.outcome === 'cancelled' ||
|
|
693
|
+
persisted.outcome === 'failed')
|
|
694
|
+
return undefined;
|
|
695
|
+
return {
|
|
696
|
+
...persisted,
|
|
697
|
+
l1State: step.state,
|
|
698
|
+
continuationCount: persisted.continuationCount,
|
|
699
|
+
ownedProcesses: refreshPersistedProcesses(persisted.ownedProcesses, [], this.deps.now()),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
const activeL2 = step.children.find((candidate) => candidate.state === 'WIP');
|
|
703
|
+
const outcome = classifyExecutorOutcome({
|
|
704
|
+
objectiveComplete: step.state === 'DONE',
|
|
705
|
+
reportedOutcome: child.outcome,
|
|
706
|
+
});
|
|
707
|
+
return {
|
|
708
|
+
canonicalPosition,
|
|
709
|
+
canonicalTaskName: child.canonicalTaskName,
|
|
710
|
+
taskPath: child.taskPath,
|
|
711
|
+
threadId: child.threadId ?? child.id,
|
|
712
|
+
l1State: step.state,
|
|
713
|
+
...(activeL2 ? { l2State: activeL2.state } : {}),
|
|
714
|
+
lastActivityAt: child.lastActivityAt,
|
|
715
|
+
ownedProcesses: refreshPersistedProcesses(persisted?.canonicalPosition === canonicalPosition ? persisted.ownedProcesses : [], child.ownedProcesses ?? [], this.deps.now()),
|
|
716
|
+
outcome: outcome.outcome,
|
|
717
|
+
...(outcome.blocking ? { blocking: outcome.blocking } : {}),
|
|
718
|
+
continuationGeneration: Math.max(child.continuationGeneration ?? 1, persisted?.canonicalPosition === canonicalPosition ? persisted.continuationGeneration : 1),
|
|
719
|
+
continuationCount: persisted?.canonicalPosition === canonicalPosition
|
|
720
|
+
? persisted.continuationCount
|
|
721
|
+
: continuationCount,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
persistExecutor(sessionId, executor) {
|
|
725
|
+
const current = this.deps.store.find(sessionId);
|
|
726
|
+
if (!current || JSON.stringify(current.executor) === JSON.stringify(executor))
|
|
727
|
+
return;
|
|
728
|
+
this.persist({ ...current, executor, updatedAt: this.deps.now() });
|
|
729
|
+
}
|
|
730
|
+
freshExecutorIdentity(sessionId) {
|
|
731
|
+
const plan = this.deps.plan(sessionId)?.plan;
|
|
732
|
+
if (!plan)
|
|
733
|
+
return undefined;
|
|
734
|
+
const stepIndex = plan.steps.findIndex((step) => step.id === plan.currentStepId || step.state === 'WIP');
|
|
735
|
+
const index = stepIndex >= 0 ? stepIndex : plan.steps.findIndex((step) => step.state !== 'DONE');
|
|
736
|
+
if (index < 0)
|
|
737
|
+
return undefined;
|
|
738
|
+
const canonicalTaskName = `l${index + 1}`;
|
|
739
|
+
const generations = this.deps
|
|
740
|
+
.activity(sessionId)
|
|
741
|
+
?.subagents.filter((child) => child.canonicalTaskName === canonicalTaskName)
|
|
742
|
+
.map((child) => child.continuationGeneration ?? 1) ?? [];
|
|
743
|
+
return executorIdentity(canonicalTaskName, Math.max(0, ...generations) + 1);
|
|
744
|
+
}
|
|
745
|
+
armExecutorRefresh(sessionId, delayMs) {
|
|
746
|
+
if (this.executorTimers.has(sessionId))
|
|
747
|
+
return;
|
|
748
|
+
this.executorTimers.set(sessionId, this.deps.schedule(() => {
|
|
749
|
+
this.executorTimers.delete(sessionId);
|
|
750
|
+
this.enqueue(sessionId, async () => {
|
|
751
|
+
await this.deps.executorController?.refresh(sessionId);
|
|
752
|
+
if (!(await this.enforceSupervisedLifecycle(sessionId, 'processObserved')))
|
|
753
|
+
this.evaluate(sessionId);
|
|
754
|
+
});
|
|
755
|
+
}, delayMs));
|
|
756
|
+
}
|
|
757
|
+
armExecutorContinuation(sessionId, delayMs, threadId, generation, trigger) {
|
|
758
|
+
if (this.executorTimers.has(sessionId))
|
|
759
|
+
return;
|
|
760
|
+
this.executorTimers.set(sessionId, this.deps.schedule(() => {
|
|
761
|
+
this.executorTimers.delete(sessionId);
|
|
762
|
+
this.enqueue(sessionId, async () => {
|
|
763
|
+
const current = this.deps.store.find(sessionId);
|
|
764
|
+
if (!current?.requestedEnabled || this.deps.pendingInteraction(sessionId))
|
|
765
|
+
return;
|
|
766
|
+
try {
|
|
767
|
+
await this.deps.executorController?.resume(sessionId, threadId, generation, trigger);
|
|
768
|
+
this.audit(sessionId, 'autopilot.executor-resumed', {
|
|
769
|
+
threadId,
|
|
770
|
+
generation,
|
|
771
|
+
trigger: trigger.kind,
|
|
772
|
+
});
|
|
773
|
+
const latest = this.deps.store.find(sessionId);
|
|
774
|
+
if (latest?.requestedEnabled)
|
|
775
|
+
this.persist({
|
|
776
|
+
...latest,
|
|
777
|
+
state: 'monitoring',
|
|
778
|
+
consecutiveNoProgress: latest.consecutiveNoProgress + 1,
|
|
779
|
+
...(latest.executor
|
|
780
|
+
? {
|
|
781
|
+
executor: {
|
|
782
|
+
...latest.executor,
|
|
783
|
+
outcome: 'partial',
|
|
784
|
+
continuationGeneration: generation,
|
|
785
|
+
continuationCount: latest.executor.continuationCount + 1,
|
|
786
|
+
lastActivityAt: this.deps.now(),
|
|
787
|
+
},
|
|
788
|
+
}
|
|
789
|
+
: {}),
|
|
790
|
+
nextEvaluationAt: null,
|
|
791
|
+
updatedAt: this.deps.now(),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
this.armExecutorRefresh(sessionId, this.deps.policy.executorContinuationMaxMs);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
}, delayMs));
|
|
799
|
+
}
|
|
800
|
+
audit(sessionId, type, payload) {
|
|
801
|
+
const occurredAt = this.deps.now();
|
|
802
|
+
this.commit({ events: [{ sessionId, type, payload, occurredAt }] });
|
|
803
|
+
this.flushOutbox(sessionId);
|
|
505
804
|
}
|
|
506
805
|
cancelScheduledControl(state, updatedAt) {
|
|
507
806
|
if (!state.lastControlId)
|
|
@@ -608,7 +907,7 @@ export class AutopilotCoordinator {
|
|
|
608
907
|
plan: plan?.plan ?? null,
|
|
609
908
|
activity: this.deps.activity(sessionId),
|
|
610
909
|
hasPendingInteraction: this.deps.pendingInteraction(sessionId),
|
|
611
|
-
hasActiveAttention:
|
|
910
|
+
hasActiveAttention: validStructuredBlock(this.deps.attention?.(sessionId) ?? state.blocking),
|
|
612
911
|
lastTurnOutcome: this.lastTurnOutcome(sessionId, state.lastControlId),
|
|
613
912
|
automaticActionCount: this.deps.store.automaticActionsSince?.(sessionId, new Date(Date.parse(now) - this.deps.policy.actionWindowMs).toISOString()) ?? 0,
|
|
614
913
|
now,
|
|
@@ -625,7 +924,10 @@ export class AutopilotCoordinator {
|
|
|
625
924
|
// Do not feed a still-stale projection straight back into reconciliation:
|
|
626
925
|
// that would create an unbounded microtask loop with no timer or event to
|
|
627
926
|
// yield to. A later activity/watchdog event will evaluate it again.
|
|
628
|
-
|
|
927
|
+
const activity = this.deps.activity(sessionId);
|
|
928
|
+
if (activity?.confidence === 'fresh' &&
|
|
929
|
+
Date.parse(this.deps.now()) - Date.parse(activity.root.lastActivityAt) <=
|
|
930
|
+
this.deps.policy.staleAfterMs)
|
|
629
931
|
this.evaluate(sessionId);
|
|
630
932
|
}
|
|
631
933
|
catch {
|
|
@@ -634,13 +936,13 @@ export class AutopilotCoordinator {
|
|
|
634
936
|
return;
|
|
635
937
|
this.persist({
|
|
636
938
|
...current,
|
|
637
|
-
state: '
|
|
638
|
-
requestedEnabled: false,
|
|
939
|
+
state: 'monitoring',
|
|
639
940
|
generation: current.generation + 1,
|
|
640
941
|
nextEvaluationAt: null,
|
|
641
942
|
stopReason: 'reconcileFailed',
|
|
642
943
|
updatedAt: this.deps.now(),
|
|
643
944
|
});
|
|
945
|
+
this.armExecutorRefresh(sessionId, this.deps.policy.executorContinuationMaxMs);
|
|
644
946
|
}
|
|
645
947
|
}
|
|
646
948
|
enqueue(sessionId, operation) {
|
|
@@ -674,3 +976,23 @@ function fingerprint(plan) {
|
|
|
674
976
|
step.children.map((child) => [child.id, child.state]),
|
|
675
977
|
]));
|
|
676
978
|
}
|
|
979
|
+
function refreshPersistedProcesses(persisted, observed, now) {
|
|
980
|
+
if (!observed.length)
|
|
981
|
+
return persisted;
|
|
982
|
+
const priorById = new Map(persisted.map((process) => [process.processId, process]));
|
|
983
|
+
return observed.map((process) => {
|
|
984
|
+
const prior = priorById.get(process.processId);
|
|
985
|
+
if (!prior)
|
|
986
|
+
return process;
|
|
987
|
+
const observedAt = prior.observedAt;
|
|
988
|
+
return {
|
|
989
|
+
...process,
|
|
990
|
+
observedAt,
|
|
991
|
+
elapsedMs: Math.max(process.elapsedMs, Date.parse(now) - Date.parse(observedAt)),
|
|
992
|
+
ownership: prior.ownership === 'supervisor' ? 'supervisor' : process.ownership,
|
|
993
|
+
state: prior.ownership === 'supervisor' && process.state === 'running'
|
|
994
|
+
? 'detached-active'
|
|
995
|
+
: process.state,
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
}
|
|
@@ -35,6 +35,8 @@ export function autopilotSnapshot(state, retryLimit) {
|
|
|
35
35
|
}
|
|
36
36
|
: {}),
|
|
37
37
|
...(state.nextEvaluationAt ? { nextEvaluationAt: state.nextEvaluationAt } : {}),
|
|
38
|
+
...(state.executor ? { executor: state.executor } : {}),
|
|
39
|
+
...(state.blocking ? { blocking: state.blocking } : {}),
|
|
38
40
|
updatedAt: state.updatedAt,
|
|
39
41
|
};
|
|
40
42
|
}
|