gestalt-mobile 0.29.0 → 0.30.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.
@@ -16,8 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
16
16
  <link rel="icon" href="/icons/gestalt-mobile-192.png" />
17
17
  <link rel="apple-touch-icon" href="/icons/gestalt-mobile-180.png" />
18
18
  <title>Gestalt Mobile</title>
19
- <script type="module" crossorigin src="/assets/index-CRULnRRB.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-BtlbAtm9.css">
19
+ <script type="module" crossorigin src="/assets/index-mKzhyiTl.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-C79AKZvE.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="app"></div>
@@ -35,7 +35,7 @@ import { mapWithConcurrency } from './platform/runtime/concurrency.js';
35
35
  import { RelaySession, } from './features/sessions/model/relay-session.js';
36
36
  import { AgentActivityRegistry } from './features/agent-activity/registry.js';
37
37
  import { decodeAgentActivityFacts } from './platform/codex/activity-facts.js';
38
- import { resolvedServerRequestId, toPendingInteraction } from './platform/codex/server-request.js';
38
+ import { isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
39
39
  import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
40
40
  import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
41
41
  import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
@@ -51,9 +51,10 @@ import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-com
51
51
  import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
52
52
  import { SqliteAutopilotStore } from './platform/persistence/sqlite-autopilot-store.js';
53
53
  import { AutopilotCoordinator } from './features/autopilot/application/service.js';
54
- import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
54
+ import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, autopilotExecutorLaunchPrompt, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
55
55
  import { createRelyingPartyConfig } from './config.js';
56
56
  import { parseOrgPlanAttention } from '../shared/contracts/org-plan-attention.js';
57
+ import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
57
58
  const generatedProtocolVersion = 'codex-cli 0.144.3';
58
59
  export async function composeRelayApp(options) {
59
60
  const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
@@ -219,7 +220,7 @@ export async function composeRelayApp(options) {
219
220
  throw new Error('AUTOPILOT_START_UNAVAILABLE');
220
221
  await options.autopilotBeforeTurnAccepted?.({ sessionId, controlId });
221
222
  const continuationPrompt = launchIdentity
222
- ? `${AUTOPILOT_CONTINUATION_PROMPT} Launch task_name ${launchIdentity.taskName} for canonical ${launchIdentity.canonicalPosition}; retain the canonical label in status and review output.`
223
+ ? autopilotExecutorLaunchPrompt(launchIdentity)
223
224
  : AUTOPILOT_CONTINUATION_PROMPT;
224
225
  const started = await runtime.startTurn(writer.session, continuationPrompt, controlId, new Date().toISOString());
225
226
  if (!started.activeTurnId)
@@ -335,6 +336,7 @@ export async function composeRelayApp(options) {
335
336
  events.publish(journal.append(sessionId, 'interaction.resolved', { requestId, turnId: interaction?.turnId ?? null, resolvedAt: occurredAt, outcome }, occurredAt));
336
337
  if (interaction?.kind === 'orgPlanAttention')
337
338
  events.publish(journal.append(sessionId, 'org-plan.attention-resolved', { requestId, turnId: interaction.turnId ?? null, resolvedAt: occurredAt, outcome }, occurredAt));
339
+ autopilot.semanticEvent(sessionId, 'interactionChanged');
338
340
  };
339
341
  const publishAttentionSettlement = (sessionId, requestId, occurredAt, outcome) => {
340
342
  const remaining = interactions.list(sessionId);
@@ -428,8 +430,33 @@ export async function composeRelayApp(options) {
428
430
  }, (sessionId, request, origin) => {
429
431
  const rawInteraction = toPendingInteraction(request);
430
432
  const session = withPendingInteractions(sessions.find(sessionId));
431
- if (!rawInteraction || !session)
433
+ if (!session)
434
+ return false;
435
+ if (!rawInteraction && isAutopilotWaitLeaseCall(request)) {
436
+ const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
437
+ if (rootOwned)
438
+ autopilot.rejectProbe(sessionId);
439
+ return (runtime?.resolveServerRequest(sessionId, String(request.id), autopilotWaitLeaseToolResponse()) === true);
440
+ }
441
+ if (!rawInteraction)
432
442
  return false;
443
+ if (rawInteraction.kind === 'autopilotWaitLease') {
444
+ const lease = rawInteraction.payload;
445
+ const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
446
+ const accepted = rootOwned &&
447
+ autopilot.reportProbe(sessionId, {
448
+ id: lease.reportId,
449
+ kind: 'wait',
450
+ leaseId: lease.leaseId,
451
+ wakeConditions: lease.wakeConditions,
452
+ });
453
+ if (!accepted) {
454
+ if (rootOwned)
455
+ autopilot.rejectProbe(sessionId);
456
+ return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse()) === true);
457
+ }
458
+ return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse()) === true);
459
+ }
433
460
  if (rawInteraction.kind === 'orgPlanCheckpoint') {
434
461
  const checkpoint = parseOrgPlanCheckpoint(rawInteraction.payload);
435
462
  const retained = supervisedPlans.find(sessionId);
@@ -468,6 +495,7 @@ export async function composeRelayApp(options) {
468
495
  ? { attentionReason: interaction.payload.reason }
469
496
  : {}),
470
497
  });
498
+ autopilot.semanticEvent(sessionId, 'interactionChanged');
471
499
  const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
472
500
  sessions.save(updated);
473
501
  events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
@@ -3,9 +3,16 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
- export const AUTOPILOT_PROMPT_VERSION = 'v3';
7
- export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. For a subagent dedicated to that position, use the collaboration-safe task_name l<a> or l<a>_<b> and refer to it by its canonical L label. Invoke gestalt_org_plan_attention only for a decision-table blocker; otherwise immediately perform the next legal lifecycle action. Do not send a status-only response.';
6
+ export const AUTOPILOT_PROMPT_VERSION = 'v5';
7
+ export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. For a subagent dedicated to that position, use the collaboration-safe task_name l<a> or l<a>_<b> and refer to it by its canonical L label. Invoke gestalt_org_plan_attention only for a decision-table blocker. When the Autopilot probe is active, register gestalt_autopilot_wait_lease with an observable wake condition, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
8
8
  export const AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT = 'Continue the same assigned Org L1 from its durable state. A prior turn ending did not complete the objective. Consume any supplied process result, take the next legal L2 action, and report only at the L1 review boundary or through structured attention.';
9
+ /** Builds a physical launch instruction while keeping durable L1 identity canonical. */
10
+ export function autopilotExecutorLaunchPrompt(identity) {
11
+ const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition}; retain the canonical label in status and review output.`;
12
+ if (identity.generation === 1)
13
+ return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch}`;
14
+ return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch} This is replacement generation ${identity.generation}: the durable ${identity.canonicalTaskName} slot may remain reserved. Do not reuse that task_name or attempt to change an existing agent's model in place. Interrupt the previous physical executor if it is still running, then spawn ${identity.taskName} with agent_type worker and an explicit model selected by the supervisor. Transfer sole ${identity.canonicalPosition} ownership and continue from its durable Org Plan and worktree state.`;
15
+ }
9
16
  export const defaultAutopilotPolicy = Object.freeze({
10
17
  quiescenceMs: 1_000,
11
18
  staleAfterMs: 30_000,
@@ -74,6 +81,8 @@ export function decideAutopilot(input) {
74
81
  // validated Org attention record may turn an incomplete plan into a human stop.
75
82
  if (hasPendingInteraction)
76
83
  return { kind: 'observe' };
84
+ if ((input.automaticActionCount ?? 0) > policy.actionLimit)
85
+ return { kind: 'safetyPause', reason: 'actionRateExceeded' };
77
86
  if (!activity || activity.confidence !== 'fresh')
78
87
  return { kind: 'reconcile' };
79
88
  if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
@@ -7,12 +7,15 @@ import { createHash } from 'node:crypto';
7
7
  import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
8
8
  import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, } from '../domain/supervised-lifecycle.js';
9
9
  import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
10
+ import { consumeObservableWake, recordAutomaticContinuation, recoverSafetyPause, reportProbe, semanticProgressKey, startSupervisionProtocol, } from '../domain/supervision-protocol.js';
10
11
  export class AutopilotCoordinator {
11
12
  deps;
12
13
  timers = new Map();
13
14
  completionTimers = new Map();
14
15
  executorTimers = new Map();
15
16
  publishedSnapshots = new Map();
17
+ planEventKeys = new Map();
18
+ activityEventKeys = new Map();
16
19
  /** Serializes asynchronous watchdog and timer work per relay session. */
17
20
  operations = new Map();
18
21
  constructor(deps) {
@@ -36,7 +39,7 @@ export class AutopilotCoordinator {
36
39
  !session ||
37
40
  !session.threadId ||
38
41
  !state.requestedEnabled ||
39
- ['disabled', 'attentionRequired', 'completed'].includes(state.state))
42
+ ['disabled', 'attentionRequired', 'completed', 'safetyPaused'].includes(state.state))
40
43
  return;
41
44
  // A relay restart deliberately drops its writer before rehydrating the
42
45
  // coordinator. The plan-status watcher is authoritative and asynchronous,
@@ -78,6 +81,88 @@ export class AutopilotCoordinator {
78
81
  }
79
82
  dispose(sessionId) {
80
83
  this.cancelTimer(sessionId);
84
+ this.planEventKeys.delete(sessionId);
85
+ this.activityEventKeys.delete(sessionId);
86
+ }
87
+ /** Accepts a session-owned structured probe response; no transcript text is inspected. */
88
+ reportProbe(sessionId, report) {
89
+ const state = this.deps.store.find(sessionId);
90
+ if (!state?.requestedEnabled)
91
+ return false;
92
+ const nextProtocol = reportProbe(state.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), report);
93
+ if (nextProtocol === state.supervision)
94
+ return false;
95
+ const now = this.deps.now();
96
+ this.persist({
97
+ ...state,
98
+ supervision: nextProtocol,
99
+ ...(nextProtocol.outcome === 'attentionRequired'
100
+ ? {
101
+ state: 'attentionRequired',
102
+ requestedEnabled: false,
103
+ stopReason: 'attentionRequired',
104
+ }
105
+ : {}),
106
+ updatedAt: now,
107
+ });
108
+ if (nextProtocol.outcome === 'parked' || nextProtocol.outcome === 'attentionRequired')
109
+ this.cancelTimer(sessionId);
110
+ return true;
111
+ }
112
+ /** Fails closed only for the currently active bounded probe. */
113
+ rejectProbe(sessionId) {
114
+ const state = this.deps.store.find(sessionId);
115
+ if (!state?.requestedEnabled || state.supervision?.outcome !== 'probeRequired')
116
+ return false;
117
+ const now = this.deps.now();
118
+ const cancelled = this.cancelScheduledControl(state, now);
119
+ this.cancelTimer(sessionId);
120
+ this.persist({
121
+ ...state,
122
+ state: 'safetyPaused',
123
+ requestedEnabled: false,
124
+ generation: state.generation + 1,
125
+ nextEvaluationAt: null,
126
+ ...(cancelled ? { lastControlId: null } : {}),
127
+ stopReason: 'safetyPaused',
128
+ supervision: {
129
+ ...state.supervision,
130
+ outcome: 'safetyPaused',
131
+ safetyPauseReason: 'invalidProbeReport',
132
+ },
133
+ updatedAt: now,
134
+ }, cancelled, [
135
+ {
136
+ sessionId,
137
+ type: 'autopilot.safety-paused',
138
+ payload: { reason: 'invalidProbeReport' },
139
+ occurredAt: now,
140
+ },
141
+ ]);
142
+ return true;
143
+ }
144
+ /** A durable matching wake grants the one retry and schedules no work by itself. */
145
+ observableWake(sessionId, wake) {
146
+ const state = this.deps.store.find(sessionId);
147
+ if (!state?.requestedEnabled || !state.supervision)
148
+ return false;
149
+ const nextProtocol = consumeObservableWake(state.supervision, wake);
150
+ if (nextProtocol === state.supervision)
151
+ return false;
152
+ this.persist({ ...state, supervision: nextProtocol, updatedAt: this.deps.now() });
153
+ this.evaluate(sessionId);
154
+ return true;
155
+ }
156
+ /** Wakes a parked lease only when this semantic event was explicitly requested. */
157
+ semanticEvent(sessionId, condition) {
158
+ const lease = this.deps.store.find(sessionId)?.supervision?.waitLease;
159
+ if (!lease || !lease.wakeConditions.includes(condition))
160
+ return false;
161
+ return this.observableWake(sessionId, {
162
+ leaseId: lease.id,
163
+ condition,
164
+ progressKey: this.progressKey(sessionId),
165
+ });
81
166
  }
82
167
  enable(sessionId) {
83
168
  const session = this.deps.session(sessionId);
@@ -107,6 +192,7 @@ export class AutopilotCoordinator {
107
192
  stopReason: null,
108
193
  executor: undefined,
109
194
  blocking: undefined,
195
+ supervision: recoverSafetyPause(prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId)),
110
196
  updatedAt: now,
111
197
  };
112
198
  this.persist(next);
@@ -137,6 +223,7 @@ export class AutopilotCoordinator {
137
223
  ...(cancelled ? { lastControlId: null } : {}),
138
224
  stopReason: 'manualDisabled',
139
225
  blocking: undefined,
226
+ supervision: recoverSafetyPause(prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId)),
140
227
  updatedAt: now,
141
228
  };
142
229
  this.persist(next, cancelled);
@@ -260,6 +347,17 @@ export class AutopilotCoordinator {
260
347
  updatedAt: now,
261
348
  };
262
349
  break;
350
+ case 'safetyPause':
351
+ next = {
352
+ ...prior,
353
+ state: 'safetyPaused',
354
+ requestedEnabled: false,
355
+ generation: prior.generation + 1,
356
+ nextEvaluationAt: null,
357
+ stopReason: 'safetyPaused',
358
+ updatedAt: now,
359
+ };
360
+ break;
263
361
  case 'disable':
264
362
  next = {
265
363
  ...prior,
@@ -275,12 +373,14 @@ export class AutopilotCoordinator {
275
373
  }
276
374
  if (decision.kind === 'requestAttention' ||
277
375
  decision.kind === 'complete' ||
278
- decision.kind === 'disable')
376
+ decision.kind === 'disable' ||
377
+ decision.kind === 'safetyPause')
279
378
  this.cancelTimer(sessionId);
280
379
  if (next !== prior) {
281
380
  const cancelled = decision.kind === 'requestAttention' ||
282
381
  decision.kind === 'complete' ||
283
- decision.kind === 'disable'
382
+ decision.kind === 'disable' ||
383
+ decision.kind === 'safetyPause'
284
384
  ? this.cancelScheduledControl(prior, now)
285
385
  : undefined;
286
386
  this.persist(cancelled ? { ...next, lastControlId: null } : next, cancelled);
@@ -375,6 +475,7 @@ export class AutopilotCoordinator {
375
475
  occurredAt,
376
476
  },
377
477
  ]);
478
+ this.semanticEvent(sessionId, 'checkpointChanged');
378
479
  return true;
379
480
  }
380
481
  /** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
@@ -391,8 +492,16 @@ export class AutopilotCoordinator {
391
492
  this.cancel(sessionId, 'planReplaced');
392
493
  return;
393
494
  }
394
- if (executionComplete(plan.plan))
495
+ if (this.semanticEvent(sessionId, 'planChanged') ||
496
+ this.semanticEvent(sessionId, 'reviewChanged'))
497
+ return;
498
+ if (executionComplete(plan.plan)) {
499
+ const eventKey = `plan:${fingerprint(plan.plan)}`;
500
+ if (this.planEventKeys.get(sessionId) === eventKey)
501
+ return;
502
+ this.planEventKeys.set(sessionId, eventKey);
395
503
  this.evaluate(sessionId);
504
+ }
396
505
  }
397
506
  /**
398
507
  * Records a validated, session-private supervision request. It intentionally
@@ -447,6 +556,25 @@ export class AutopilotCoordinator {
447
556
  const activity = this.deps.activity(sessionId);
448
557
  if (!activity || activity.confidence !== 'fresh')
449
558
  return;
559
+ const eventKey = JSON.stringify({
560
+ root: [activity.root.state, activity.root.reason],
561
+ children: activity.subagents.map((child) => [
562
+ child.id,
563
+ child.state,
564
+ child.reason,
565
+ child.continuationGeneration ?? 0,
566
+ (child.ownedProcesses ?? []).map((process) => [
567
+ process.processId,
568
+ process.state,
569
+ process.ownership,
570
+ ]),
571
+ ]),
572
+ });
573
+ if (this.activityEventKeys.get(sessionId) === eventKey)
574
+ return;
575
+ this.activityEventKeys.set(sessionId, eventKey);
576
+ if (this.semanticEvent(sessionId, 'agentActivityChanged'))
577
+ return;
450
578
  const disposition = classifyAgentActivity(activity);
451
579
  if (disposition === 'attention') {
452
580
  this.evaluate(sessionId);
@@ -589,6 +717,39 @@ export class AutopilotCoordinator {
589
717
  await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
590
718
  return;
591
719
  }
720
+ const protocol = recordAutomaticContinuation(current.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId));
721
+ if (protocol.outcome === 'probeRequired') {
722
+ this.persist({ ...current, supervision: protocol, updatedAt: this.deps.now() }, undefined, [
723
+ {
724
+ sessionId,
725
+ type: 'autopilot.probe-required',
726
+ payload: { progressKey: protocol.progressKey },
727
+ occurredAt: this.deps.now(),
728
+ },
729
+ ]);
730
+ }
731
+ if (protocol.outcome === 'safetyPaused') {
732
+ const now = this.deps.now();
733
+ const cancelled = this.cancelScheduledControl(current, now);
734
+ this.persist({
735
+ ...current,
736
+ state: 'safetyPaused',
737
+ requestedEnabled: false,
738
+ stopReason: 'safetyPaused',
739
+ ...(cancelled ? { generation: current.generation + 1, lastControlId: null } : {}),
740
+ nextEvaluationAt: null,
741
+ supervision: protocol,
742
+ updatedAt: now,
743
+ }, cancelled, [
744
+ {
745
+ sessionId,
746
+ type: 'autopilot.safety-paused',
747
+ payload: { progressKey: protocol.progressKey },
748
+ occurredAt: now,
749
+ },
750
+ ]);
751
+ return;
752
+ }
592
753
  const id = current.lastControlId;
593
754
  if (!id)
594
755
  return;
@@ -646,8 +807,9 @@ export class AutopilotCoordinator {
646
807
  this.deps.pendingInteraction(sessionId))
647
808
  return false;
648
809
  const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor);
649
- if (executor)
650
- this.persistExecutor(sessionId, executor);
810
+ const executorChanged = executor ? this.persistExecutor(sessionId, executor) : false;
811
+ if (executorChanged && this.wakeForExecutorChange(sessionId, executor))
812
+ return true;
651
813
  const decision = decideSupervisedLifecycle({
652
814
  plan: retained.plan,
653
815
  event,
@@ -672,7 +834,11 @@ export class AutopilotCoordinator {
672
834
  return true;
673
835
  case 'monitorProcess': {
674
836
  const processId = decision.action.process.processId;
675
- controller.transferProcess(sessionId, decision.action.process.ownerThreadId, processId);
837
+ const alreadyMonitoring = executor?.ownedProcesses.some((process) => process.processId === processId &&
838
+ process.ownership === 'supervisor' &&
839
+ process.state === 'detached-active');
840
+ if (!alreadyMonitoring)
841
+ controller.transferProcess(sessionId, decision.action.process.ownerThreadId, processId);
676
842
  if (executor)
677
843
  this.persistExecutor(sessionId, {
678
844
  ...executor,
@@ -680,10 +846,11 @@ export class AutopilotCoordinator {
680
846
  ? { ...process, ownership: 'supervisor', state: 'detached-active' }
681
847
  : process),
682
848
  });
683
- this.audit(sessionId, 'autopilot.process-monitoring', {
684
- threadId: decision.action.process.ownerThreadId,
685
- processId,
686
- });
849
+ if (!alreadyMonitoring)
850
+ this.audit(sessionId, 'autopilot.process-monitoring', {
851
+ threadId: decision.action.process.ownerThreadId,
852
+ processId,
853
+ });
687
854
  this.armExecutorRefresh(sessionId, decision.action.pollAfterMs);
688
855
  return true;
689
856
  }
@@ -785,8 +952,22 @@ export class AutopilotCoordinator {
785
952
  persistExecutor(sessionId, executor) {
786
953
  const current = this.deps.store.find(sessionId);
787
954
  if (!current || JSON.stringify(current.executor) === JSON.stringify(executor))
788
- return;
955
+ return false;
789
956
  this.persist({ ...current, executor, updatedAt: this.deps.now() });
957
+ return true;
958
+ }
959
+ wakeForExecutorChange(sessionId, executor) {
960
+ const processes = executor.ownedProcesses;
961
+ const conditions = [
962
+ ...(processes.some((process) => process.state === 'exited-awaiting-result')
963
+ ? ['processExited', 'processResultAvailable']
964
+ : []),
965
+ ...(processes.some((process) => process.state === 'terminated-for-budget')
966
+ ? ['processLimitBreached']
967
+ : []),
968
+ 'executorChanged',
969
+ ];
970
+ return conditions.some((condition) => this.semanticEvent(sessionId, condition));
790
971
  }
791
972
  freshExecutorIdentity(sessionId) {
792
973
  const plan = this.deps.plan(sessionId)?.plan;
@@ -961,6 +1142,8 @@ export class AutopilotCoordinator {
961
1142
  return 'unknown';
962
1143
  }
963
1144
  decision(sessionId, state) {
1145
+ if (state.supervision?.outcome === 'parked')
1146
+ return { kind: 'observe' };
964
1147
  const plan = this.deps.plan(sessionId);
965
1148
  const now = this.deps.now();
966
1149
  return decideAutopilot({
@@ -975,6 +1158,48 @@ export class AutopilotCoordinator {
975
1158
  policy: this.deps.policy,
976
1159
  });
977
1160
  }
1161
+ progressKey(sessionId) {
1162
+ const retained = this.deps.plan(sessionId);
1163
+ const activity = this.deps.activity(sessionId);
1164
+ const interactions = this.deps.pendingInteraction(sessionId)
1165
+ ? [{ id: 'pending', kind: 'session', state: 'pending' }]
1166
+ : [];
1167
+ return semanticProgressKey({
1168
+ plan: {
1169
+ identity: retained?.identity ?? 'none',
1170
+ fingerprint: retained ? fingerprint(retained.plan) : 'none',
1171
+ currentPosition: retained?.plan.currentStepId ?? null,
1172
+ },
1173
+ review: { status: null },
1174
+ checkpoint: {
1175
+ pendingTurnId: this.deps.store.find(sessionId)?.checkpoints?.pendingTurnId ?? null,
1176
+ terminalReviewAccepted: this.deps.store.find(sessionId)?.checkpoints?.terminalReviewAccepted ?? false,
1177
+ },
1178
+ pendingInteractions: interactions,
1179
+ executor: {
1180
+ generation: this.deps.store.find(sessionId)?.executor?.continuationGeneration ?? 0,
1181
+ state: this.deps.store.find(sessionId)?.executor?.outcome ?? null,
1182
+ },
1183
+ ownedProcesses: (this.deps.store.find(sessionId)?.executor?.ownedProcesses ?? []).map((process) => ({
1184
+ id: process.processId,
1185
+ state: process.state,
1186
+ ownerGeneration: this.deps.store.find(sessionId)?.executor?.continuationGeneration ?? 0,
1187
+ })),
1188
+ agentActivity: [
1189
+ ...(activity
1190
+ ? [
1191
+ {
1192
+ agentId: 'root',
1193
+ // Activity time is liveness-only; semantic progress uses the
1194
+ // sequenced state projection, never the observation clock.
1195
+ sequence: 0,
1196
+ state: activity.root.state,
1197
+ },
1198
+ ]
1199
+ : []),
1200
+ ],
1201
+ });
1202
+ }
978
1203
  async reconcile(sessionId, generation) {
979
1204
  try {
980
1205
  const result = await this.deps.reconcile(sessionId);
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
6
  import { orgPlanAgentDisplayName } from '../../../../shared/org-plan-position.js';
7
+ import { parseSupervisionProtocolState, } from './supervision-protocol.js';
7
8
  export const blockingResumeConditions = {
8
9
  planChange: 'planRevision',
9
10
  hardBlock: 'externalStateChanged',
@@ -25,11 +26,19 @@ export function parsePersistedSupervisedLifecycle(value) {
25
26
  return undefined;
26
27
  const executorValue = record(root.executor);
27
28
  const checkpointsValue = record(root.checkpoints);
29
+ const supervisionValue = root.supervision;
30
+ const supervision = supervisionValue === undefined ? undefined : parseSupervisionProtocolState(supervisionValue);
31
+ if (supervisionValue !== undefined && !supervision)
32
+ return undefined;
28
33
  const checkpoints = checkpointsValue ? parseCheckpoints(checkpointsValue) : undefined;
29
34
  if (checkpointsValue && !checkpoints)
30
35
  return undefined;
31
36
  if (!executorValue)
32
- return { ...(blocking ? { blocking } : {}), ...(checkpoints ? { checkpoints } : {}) };
37
+ return {
38
+ ...(blocking ? { blocking } : {}),
39
+ ...(supervision ? { supervision } : {}),
40
+ ...(checkpoints ? { checkpoints } : {}),
41
+ };
33
42
  const processesValue = executorValue.ownedProcesses;
34
43
  if (!Array.isArray(processesValue) || processesValue.length > 64)
35
44
  return undefined;
@@ -135,6 +144,7 @@ export function parsePersistedSupervisedLifecycle(value) {
135
144
  continuationCount,
136
145
  },
137
146
  ...(blocking ? { blocking } : {}),
147
+ ...(supervision ? { supervision } : {}),
138
148
  ...(checkpoints ? { checkpoints } : {}),
139
149
  };
140
150
  }