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.
@@ -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: 'attentionRequired',
61
- requestedEnabled: false,
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.activitySettled(sessionId);
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.evaluate(sessionId);
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
- // Availability, permission, and dependency failures cannot be repaired
481
- // by another identical synthetic turn. They remain a durable, explicit
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: 'attentionRequired',
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: state.state === 'attentionRequired',
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
- if (this.deps.activity(sessionId)?.confidence === 'fresh')
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: 'attentionRequired',
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
  }