gestalt-mobile 0.28.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.
@@ -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',
@@ -24,8 +25,20 @@ export function parsePersistedSupervisedLifecycle(value) {
24
25
  if (blockingValue && !blocking)
25
26
  return undefined;
26
27
  const executorValue = record(root.executor);
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;
33
+ const checkpoints = checkpointsValue ? parseCheckpoints(checkpointsValue) : undefined;
34
+ if (checkpointsValue && !checkpoints)
35
+ return undefined;
27
36
  if (!executorValue)
28
- return blocking ? { blocking } : {};
37
+ return {
38
+ ...(blocking ? { blocking } : {}),
39
+ ...(supervision ? { supervision } : {}),
40
+ ...(checkpoints ? { checkpoints } : {}),
41
+ };
29
42
  const processesValue = executorValue.ownedProcesses;
30
43
  if (!Array.isArray(processesValue) || processesValue.length > 64)
31
44
  return undefined;
@@ -131,6 +144,42 @@ export function parsePersistedSupervisedLifecycle(value) {
131
144
  continuationCount,
132
145
  },
133
146
  ...(blocking ? { blocking } : {}),
147
+ ...(supervision ? { supervision } : {}),
148
+ ...(checkpoints ? { checkpoints } : {}),
149
+ };
150
+ }
151
+ function parseCheckpoints(value) {
152
+ const planIdentity = boundedText(value.planIdentity);
153
+ if (value.protocolVersion !== 1 || !planIdentity)
154
+ return undefined;
155
+ if (!Array.isArray(value.reportedL1Ids) || value.reportedL1Ids.length > 128)
156
+ return undefined;
157
+ const reportedL1Ids = value.reportedL1Ids
158
+ .map((id) => boundedText(id))
159
+ .filter(Boolean);
160
+ if (reportedL1Ids.length !== value.reportedL1Ids.length ||
161
+ new Set(reportedL1Ids).size !== reportedL1Ids.length)
162
+ return undefined;
163
+ if (!Array.isArray(value.acceptedKeys) || value.acceptedKeys.length > 128)
164
+ return undefined;
165
+ const acceptedKeys = value.acceptedKeys
166
+ .map((key) => boundedText(key))
167
+ .filter(Boolean);
168
+ if (acceptedKeys.length !== value.acceptedKeys.length ||
169
+ new Set(acceptedKeys).size !== acceptedKeys.length)
170
+ return undefined;
171
+ const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
172
+ if (pendingTurnId === undefined)
173
+ return undefined;
174
+ if (typeof value.terminalReviewAccepted !== 'boolean')
175
+ return undefined;
176
+ return {
177
+ protocolVersion: 1,
178
+ planIdentity,
179
+ reportedL1Ids,
180
+ acceptedKeys,
181
+ pendingTurnId,
182
+ terminalReviewAccepted: value.terminalReviewAccepted,
134
183
  };
135
184
  }
136
185
  export function classifyExecutorOutcome(input) {
@@ -0,0 +1,203 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ /** Facts which may wake a parked supervisor without inspecting conversation text. */
7
+ export const observableWakeConditions = [
8
+ 'planChanged',
9
+ 'reviewChanged',
10
+ 'checkpointChanged',
11
+ 'interactionChanged',
12
+ 'executorChanged',
13
+ 'processExited',
14
+ 'processResultAvailable',
15
+ 'processLimitBreached',
16
+ 'agentActivityChanged',
17
+ ];
18
+ /**
19
+ * A canonical, durable representation rather than a clock- or transcript-derived score.
20
+ * The string is intentionally opaque to callers; compare it only for equality.
21
+ */
22
+ export function semanticProgressKey(facts) {
23
+ return JSON.stringify({
24
+ plan: facts.plan,
25
+ review: facts.review,
26
+ checkpoint: facts.checkpoint,
27
+ pendingInteractions: [...facts.pendingInteractions]
28
+ .map(({ id, kind, state }) => ({ id, kind, state }))
29
+ .sort(compareBy('id')),
30
+ executor: facts.executor,
31
+ ownedProcesses: [...facts.ownedProcesses]
32
+ .map(({ id, state, ownerGeneration }) => ({ id, state, ownerGeneration }))
33
+ .sort(compareBy('id')),
34
+ agentActivity: latestAgentActivity(facts.agentActivity),
35
+ });
36
+ }
37
+ function latestAgentActivity(observations) {
38
+ const latest = new Map();
39
+ for (const observation of observations) {
40
+ const current = latest.get(observation.agentId);
41
+ if (!current ||
42
+ observation.sequence > current.sequence ||
43
+ (observation.sequence === current.sequence &&
44
+ observation.state.localeCompare(current.state) < 0))
45
+ latest.set(observation.agentId, observation);
46
+ }
47
+ return [...latest.values()]
48
+ .map(({ agentId, sequence, state }) => ({ agentId, sequence, state }))
49
+ .sort(compareBy('agentId'));
50
+ }
51
+ function compareBy(key) {
52
+ return (left, right) => left[key].localeCompare(right[key]);
53
+ }
54
+ export function startSupervisionProtocol(progressKey) {
55
+ return {
56
+ outcome: 'active',
57
+ progressKey,
58
+ unchangedContinuations: 0,
59
+ probeKey: null,
60
+ lastReportId: null,
61
+ waitLease: null,
62
+ retryKey: null,
63
+ safetyPauseReason: null,
64
+ };
65
+ }
66
+ /** Records an automatic continuation result; prose and timestamps are not inputs. */
67
+ export function recordAutomaticContinuation(state, progressKey) {
68
+ if (state.outcome === 'parked' ||
69
+ state.outcome === 'attentionRequired' ||
70
+ state.outcome === 'safetyPaused')
71
+ return state;
72
+ // The probe itself is the one continuation allowed while this key is
73
+ // unchanged. Reaching the scheduling boundary again without a structured
74
+ // report or semantic progress means that probe failed closed.
75
+ if (state.outcome === 'probeRequired' && progressKey === state.probeKey)
76
+ return { ...state, outcome: 'safetyPaused', safetyPauseReason: 'invalidProbeReport' };
77
+ if (state.outcome === 'retrying' && progressKey === state.retryKey)
78
+ return state.unchangedContinuations === 0
79
+ ? { ...state, unchangedContinuations: 1 }
80
+ : { ...state, outcome: 'safetyPaused', safetyPauseReason: 'retryRecurrence' };
81
+ if (progressKey !== state.progressKey)
82
+ return { ...startSupervisionProtocol(progressKey), lastReportId: state.lastReportId };
83
+ const unchangedContinuations = Math.min(3, state.unchangedContinuations + 1);
84
+ if (unchangedContinuations === 3 && state.probeKey !== progressKey)
85
+ return { ...state, unchangedContinuations, outcome: 'probeRequired', probeKey: progressKey };
86
+ return { ...state, unchangedContinuations };
87
+ }
88
+ /** Accepts only structured, bounded probe reports. Unsupported waits fail closed. */
89
+ export function reportProbe(state, report) {
90
+ if (state.lastReportId === report.id)
91
+ return state;
92
+ if (state.outcome !== 'probeRequired' || !state.probeKey)
93
+ return state;
94
+ if (report.kind === 'attention')
95
+ return { ...state, outcome: 'attentionRequired', lastReportId: report.id };
96
+ if (report.kind === 'actionable')
97
+ return {
98
+ ...startSupervisionProtocol(state.progressKey),
99
+ lastReportId: report.id,
100
+ };
101
+ const wakeConditions = uniqueSupportedWakeConditions(report.wakeConditions);
102
+ if (!report.leaseId || wakeConditions.length === 0)
103
+ return state;
104
+ return {
105
+ ...state,
106
+ outcome: 'parked',
107
+ lastReportId: report.id,
108
+ waitLease: { id: report.leaseId, probeKey: state.probeKey, wakeConditions },
109
+ };
110
+ }
111
+ /** A wake is useful only when it matches the durable lease and changed semantic state. */
112
+ export function consumeObservableWake(state, wake) {
113
+ const lease = state.waitLease;
114
+ if (state.outcome !== 'parked' ||
115
+ !lease ||
116
+ wake.leaseId !== lease.id ||
117
+ !lease.wakeConditions.includes(wake.condition) ||
118
+ wake.progressKey === lease.probeKey)
119
+ return state;
120
+ return {
121
+ ...state,
122
+ outcome: 'retrying',
123
+ progressKey: wake.progressKey,
124
+ unchangedContinuations: 0,
125
+ probeKey: null,
126
+ waitLease: null,
127
+ retryKey: wake.progressKey,
128
+ };
129
+ }
130
+ /** Manual recovery is the only transition out of durable safety pause. */
131
+ export function recoverSafetyPause(state, progressKey) {
132
+ return state.outcome === 'safetyPaused' ? startSupervisionProtocol(progressKey) : state;
133
+ }
134
+ /** Reject malformed persisted values instead of silently resuming automatic work. */
135
+ export function parseSupervisionProtocolState(value) {
136
+ if (!value || typeof value !== 'object' || Array.isArray(value))
137
+ return undefined;
138
+ const candidate = value;
139
+ const base = startSupervisionProtocol(typeof candidate.progressKey === 'string' ? candidate.progressKey : '');
140
+ if (!base.progressKey || !isProtocolOutcome(candidate.outcome))
141
+ return undefined;
142
+ const unchangedContinuations = candidate.unchangedContinuations;
143
+ if (unchangedContinuations !== 0 &&
144
+ unchangedContinuations !== 1 &&
145
+ unchangedContinuations !== 2 &&
146
+ unchangedContinuations !== 3)
147
+ return undefined;
148
+ const probeKey = nullableText(candidate.probeKey);
149
+ const lastReportId = nullableText(candidate.lastReportId);
150
+ const retryKey = nullableText(candidate.retryKey);
151
+ if (probeKey === undefined || lastReportId === undefined || retryKey === undefined)
152
+ return undefined;
153
+ const waitLease = parseWaitLease(candidate.waitLease);
154
+ if (waitLease === undefined)
155
+ return undefined;
156
+ if (candidate.safetyPauseReason !== null &&
157
+ candidate.safetyPauseReason !== 'retryRecurrence' &&
158
+ candidate.safetyPauseReason !== 'invalidProbeReport')
159
+ return undefined;
160
+ return {
161
+ ...base,
162
+ outcome: candidate.outcome,
163
+ unchangedContinuations,
164
+ probeKey,
165
+ lastReportId,
166
+ waitLease,
167
+ retryKey,
168
+ safetyPauseReason: candidate.safetyPauseReason,
169
+ };
170
+ }
171
+ function uniqueSupportedWakeConditions(conditions) {
172
+ return [...new Set(conditions)].filter((condition) => observableWakeConditions.includes(condition));
173
+ }
174
+ function isProtocolOutcome(value) {
175
+ return [
176
+ 'active',
177
+ 'probeRequired',
178
+ 'parked',
179
+ 'retrying',
180
+ 'attentionRequired',
181
+ 'safetyPaused',
182
+ ].includes(value);
183
+ }
184
+ function nullableText(value) {
185
+ return value === null ? null : typeof value === 'string' && value.length > 0 ? value : undefined;
186
+ }
187
+ function parseWaitLease(value) {
188
+ if (value === null)
189
+ return null;
190
+ if (!value || typeof value !== 'object' || Array.isArray(value))
191
+ return undefined;
192
+ const candidate = value;
193
+ if (typeof candidate.id !== 'string' ||
194
+ !candidate.id ||
195
+ typeof candidate.probeKey !== 'string' ||
196
+ !candidate.probeKey ||
197
+ !Array.isArray(candidate.wakeConditions))
198
+ return undefined;
199
+ const wakeConditions = uniqueSupportedWakeConditions(candidate.wakeConditions);
200
+ return wakeConditions.length === candidate.wakeConditions.length
201
+ ? { id: candidate.id, probeKey: candidate.probeKey, wakeConditions }
202
+ : undefined;
203
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ export function validOrgPlanCheckpoint(input) {
7
+ const { checkpoint, plan, planIdentity, rootOwned } = input;
8
+ if (!rootOwned || checkpoint.planIdentity !== planIdentity)
9
+ return false;
10
+ if (checkpoint.kind === 'terminalReviewAccepted')
11
+ return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED');
12
+ const l1 = plan.steps.find((step) => step.id === checkpoint.l1Id);
13
+ if (!l1)
14
+ return false;
15
+ const position = `L${plan.steps.indexOf(l1) + 1}`;
16
+ return (checkpoint.position === position &&
17
+ l1.state === 'DONE' &&
18
+ l1.reviewStatus === 'REVIEWED' &&
19
+ !input.hasActiveL1Writer(position));
20
+ }
@@ -83,6 +83,7 @@ function toActivityHistory(events, throughSequence) {
83
83
  label: payload.label,
84
84
  detail: payload.detail,
85
85
  ...(typeof payload.turnId === 'string' ? { turnId: payload.turnId } : {}),
86
+ ...(typeof payload.actorTurnId === 'string' ? { actorTurnId: payload.actorTurnId } : {}),
86
87
  ...(Number.isFinite(occurredAt) ? { occurredAt } : {}),
87
88
  ...(changes.length ? { changes } : {}),
88
89
  });
@@ -11,5 +11,7 @@ export function isInteractionKind(value) {
11
11
  'userInput',
12
12
  'quiz',
13
13
  'orgPlanAttention',
14
+ 'orgPlanCheckpoint',
15
+ 'autopilotWaitLease',
14
16
  ].includes(value);
15
17
  }
@@ -11,7 +11,10 @@ export function isValidInteractionResponse(kind, value) {
11
11
  return false;
12
12
  if (kind === 'userInput')
13
13
  return isValidUserInputResponse(value);
14
- if (kind === 'quiz' || kind === 'orgPlanAttention')
14
+ if (kind === 'quiz' ||
15
+ kind === 'orgPlanAttention' ||
16
+ kind === 'orgPlanCheckpoint' ||
17
+ kind === 'autopilotWaitLease')
15
18
  return false;
16
19
  if (kind === 'permissionsApproval')
17
20
  return isRecord(value.permissions) && (value.scope === 'turn' || value.scope === 'session');
@@ -5,11 +5,13 @@
5
5
  */
6
6
  import { countDiffLines } from '../../../shared/contracts/file-change.js';
7
7
  import { relative } from 'node:path';
8
- export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath, activeTurnId) {
8
+ export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath, activeTurnId, origin = { kind: 'root' }) {
9
9
  const decoded = decodeNotification(notification);
10
10
  if (!decoded)
11
11
  return null;
12
- if (decoded.method === 'item/agentMessage/delta')
12
+ if (decoded.method === 'item/agentMessage/delta') {
13
+ if (origin.kind !== 'root')
14
+ return null;
13
15
  return {
14
16
  sessionId,
15
17
  sequence,
@@ -18,12 +20,11 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
18
20
  payload: {
19
21
  text: decoded.params.delta,
20
22
  ...(decoded.params.itemId ? { itemId: decoded.params.itemId } : {}),
21
- ...((decoded.params.turnId ?? activeTurnId)
22
- ? { turnId: decoded.params.turnId ?? activeTurnId }
23
- : {}),
23
+ ...(activeTurnId ? { turnId: activeTurnId } : {}),
24
24
  },
25
25
  };
26
- if (decoded.method === 'turn/completed')
26
+ }
27
+ if (decoded.method === 'turn/completed' && origin.kind === 'root')
27
28
  return {
28
29
  sessionId,
29
30
  sequence,
@@ -37,13 +38,15 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
37
38
  type: 'fileChange',
38
39
  status: 'in_progress',
39
40
  changes: decoded.params.changes,
40
- }, workspacePath, activeTurnId, decoded.params.turnId);
41
+ }, workspacePath, activeTurnId, decoded.params.turnId, origin);
41
42
  return activity
42
43
  ? { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity }
43
44
  : null;
44
45
  }
45
46
  if (decoded.method === 'item/started' || decoded.method === 'item/completed') {
46
- const message = safeAgentMessage(decoded.params.item, activeTurnId, decoded.params.turnId);
47
+ const message = origin.kind === 'root'
48
+ ? safeAgentMessage(decoded.params.item, activeTurnId, decoded.params.turnId)
49
+ : null;
47
50
  if (message)
48
51
  return {
49
52
  sessionId,
@@ -52,7 +55,7 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
52
55
  type: decoded.method === 'item/started' ? 'agentMessageStarted' : 'agentMessageCompleted',
53
56
  payload: message,
54
57
  };
55
- const activity = safeActivity(decoded.params.item, workspacePath, activeTurnId, decoded.params.turnId);
58
+ const activity = safeActivity(decoded.params.item, workspacePath, activeTurnId, decoded.params.turnId, origin);
56
59
  if (activity)
57
60
  return { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity };
58
61
  }
@@ -120,19 +123,21 @@ export function decodeNotification(input) {
120
123
  }
121
124
  return null;
122
125
  }
123
- function safeActivity(item, workspacePath, activeTurnId, notificationTurnId) {
126
+ function safeActivity(item, workspacePath, activeTurnId, notificationTurnId, origin = { kind: 'root' }) {
124
127
  if (!item || typeof item !== 'object')
125
128
  return null;
126
129
  const value = item;
127
130
  if (typeof value.id !== 'string' || typeof value.type !== 'string')
128
131
  return null;
129
- const owner = notificationTurnId
130
- ? { turnId: notificationTurnId }
131
- : safeId(value.turnId)
132
- ? { turnId: value.turnId }
133
- : activeTurnId
134
- ? { turnId: activeTurnId }
135
- : {};
132
+ const owner = origin.kind === 'child' && activeTurnId
133
+ ? { turnId: activeTurnId, actorTurnId: origin.physicalTurnId }
134
+ : notificationTurnId && origin.kind === 'root'
135
+ ? { turnId: notificationTurnId }
136
+ : safeId(value.turnId)
137
+ ? { turnId: value.turnId }
138
+ : activeTurnId && origin.kind === 'root'
139
+ ? { turnId: activeTurnId }
140
+ : {};
136
141
  const status = typeof value.status === 'string' ? ` · ${value.status}` : '';
137
142
  if (value.type === 'commandExecution' && typeof value.command === 'string')
138
143
  return { id: value.id, label: `Command${status}`, detail: value.command, ...owner };
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { GESTALT_QUIZ_TOOL_NAME, parseQuiz } from '../../../shared/contracts/quiz.js';
7
7
  import { GESTALT_ORG_PLAN_ATTENTION_TOOL_NAME, parseOrgPlanAttention, } from '../../../shared/contracts/org-plan-attention.js';
8
+ import { GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME, parseOrgPlanCheckpoint, } from '../../../shared/contracts/org-plan-checkpoint.js';
9
+ import { GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME, parseAutopilotWaitLease, } from '../../../shared/contracts/autopilot-wait-lease.js';
8
10
  export function toPendingInteraction(input) {
9
11
  if (!Number.isSafeInteger(input.id) || !isRecord(input.params))
10
12
  return null;
@@ -19,6 +21,18 @@ export function toPendingInteraction(input) {
19
21
  ? { requestId: String(input.id), kind: 'orgPlanAttention', payload: attention }
20
22
  : null;
21
23
  }
24
+ if (input.params.tool === GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME) {
25
+ const checkpoint = parseOrgPlanCheckpoint(input.params.arguments);
26
+ return checkpoint
27
+ ? { requestId: String(input.id), kind: 'orgPlanCheckpoint', payload: checkpoint }
28
+ : null;
29
+ }
30
+ if (input.params.tool === GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME) {
31
+ const lease = parseAutopilotWaitLease(input.params.arguments);
32
+ return lease
33
+ ? { requestId: String(input.id), kind: 'autopilotWaitLease', payload: lease }
34
+ : null;
35
+ }
22
36
  return null;
23
37
  }
24
38
  const kind = {
@@ -38,6 +52,12 @@ export function resolvedServerRequestId(input) {
38
52
  return null;
39
53
  return String(requestId);
40
54
  }
55
+ export function isAutopilotWaitLeaseCall(input) {
56
+ return (Number.isSafeInteger(input.id) &&
57
+ input.method === 'item/tool/call' &&
58
+ isRecord(input.params) &&
59
+ input.params.tool === GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME);
60
+ }
41
61
  function isRecord(value) {
42
62
  return typeof value === 'object' && value !== null && !Array.isArray(value);
43
63
  }
@@ -9,6 +9,8 @@ import { createPlanMeasurementSnapshot, } from '../../features/plans/application
9
9
  import { canRebindMissingRollout, rebindMissingRollout, } from '../../features/sessions/restore-session/use-case.js';
10
10
  import { gestaltQuizDynamicTool } from '../../../shared/contracts/quiz.js';
11
11
  import { gestaltOrgPlanAttentionDynamicTool } from '../../../shared/contracts/org-plan-attention.js';
12
+ import { gestaltOrgPlanCheckpointDynamicTool } from '../../../shared/contracts/org-plan-checkpoint.js';
13
+ import { gestaltAutopilotWaitLeaseDynamicTool } from '../../../shared/contracts/autopilot-wait-lease.js';
12
14
  import { countDiffLines } from '../../../shared/contracts/file-change.js';
13
15
  import { threadPlanName } from './thread-plan-name.js';
14
16
  import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
@@ -32,6 +34,8 @@ class SessionResource {
32
34
  spawnedAgentModels = new Map();
33
35
  attemptedAgentModelRecovery = new Set();
34
36
  ownedChildProcesses = new Map();
37
+ turnThreads = new Map();
38
+ childThreads = new Set();
35
39
  constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
36
40
  this.sessionId = sessionId;
37
41
  this.process = process;
@@ -561,7 +565,12 @@ export class CodexSessionRuntime {
561
565
  ? { approvalPolicy: session.executionPolicy.approvalPolicy }
562
566
  : {}),
563
567
  ...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
564
- dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
568
+ dynamicTools: [
569
+ gestaltQuizDynamicTool,
570
+ gestaltOrgPlanAttentionDynamicTool,
571
+ gestaltOrgPlanCheckpointDynamicTool,
572
+ gestaltAutopilotWaitLeaseDynamicTool,
573
+ ],
565
574
  });
566
575
  result = {
567
576
  session: RelaySession.rehydrate(session).restore(now).supportsAttentionTool(now).snapshot,
@@ -629,7 +638,8 @@ export class CodexSessionRuntime {
629
638
  resource.pendingRequests.set(requestId, { resolve, reject });
630
639
  let accepted = false;
631
640
  try {
632
- accepted = this.onServerRequest?.(resource.sessionId, request) === true;
641
+ accepted =
642
+ this.onServerRequest?.(resource.sessionId, request, this.resolveNotificationOrigin(resource, request)) === true;
633
643
  }
634
644
  catch {
635
645
  // Publication failures are protocol failures, not permission to retain
@@ -646,7 +656,12 @@ export class CodexSessionRuntime {
646
656
  return {
647
657
  cwd: session.workspacePath,
648
658
  approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
649
- dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
659
+ dynamicTools: [
660
+ gestaltQuizDynamicTool,
661
+ gestaltOrgPlanAttentionDynamicTool,
662
+ gestaltOrgPlanCheckpointDynamicTool,
663
+ gestaltAutopilotWaitLeaseDynamicTool,
664
+ ],
650
665
  ...(session.model ? { model: session.model } : {}),
651
666
  ...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
652
667
  };
@@ -709,7 +724,7 @@ export class CodexSessionRuntime {
709
724
  // The notification callback reconciles the durable interaction record.
710
725
  pending?.reject(new Error('CODEX_SERVER_REQUEST_CLEARED'));
711
726
  }
712
- this.onNotification?.(session.id, notification);
727
+ this.onNotification?.(session.id, notification, this.resolveNotificationOrigin(resource, notification));
713
728
  });
714
729
  const requestUnsubscribe = process.rpc.onServerRequest((request) => this.holdServerRequest(resource, request));
715
730
  if (!resource.active) {
@@ -726,6 +741,48 @@ export class CodexSessionRuntime {
726
741
  throw error;
727
742
  }
728
743
  }
744
+ resolveNotificationOrigin(resource, notification) {
745
+ const params = asRecord(notification.params);
746
+ if (!params)
747
+ return { kind: 'unknown' };
748
+ const item = asRecord(params.item);
749
+ if (item?.type === 'collabAgentToolCall' || item?.type === 'collabToolCall') {
750
+ for (const value of [item.receiverThreadId, item.newThreadId])
751
+ if (boundedString(value, 256) && resource.childThreads.size < 64)
752
+ resource.childThreads.add(boundedString(value, 256));
753
+ if (Array.isArray(item.receiverThreadIds))
754
+ for (const value of item.receiverThreadIds)
755
+ if (boundedString(value, 256) && resource.childThreads.size < 64)
756
+ resource.childThreads.add(boundedString(value, 256));
757
+ const states = asRecord(item.agentsStates);
758
+ if (states)
759
+ for (const childId of Object.keys(states))
760
+ if (boundedString(childId, 256) && resource.childThreads.size < 64)
761
+ resource.childThreads.add(childId);
762
+ }
763
+ const turn = asRecord(params.turn);
764
+ const turnId = boundedString(params.turnId, 256) ??
765
+ boundedString(turn?.id, 256) ??
766
+ boundedString(item?.turnId, 256);
767
+ if (notification.method === 'turn/started' && turnId && boundedString(params.threadId, 256)) {
768
+ if (resource.turnThreads.has(turnId) || resource.turnThreads.size < 256)
769
+ resource.turnThreads.set(turnId, boundedString(params.threadId, 256));
770
+ }
771
+ const resolvedThreadId = boundedString(params.threadId, 256) ??
772
+ (turnId ? resource.turnThreads.get(turnId) : undefined);
773
+ const origin = {
774
+ kind: resolvedThreadId && resolvedThreadId === resource.threadId
775
+ ? 'root'
776
+ : resolvedThreadId && resource.childThreads.has(resolvedThreadId)
777
+ ? 'child'
778
+ : 'unknown',
779
+ ...(turnId ? { physicalTurnId: turnId } : {}),
780
+ ...(resolvedThreadId ? { physicalThreadId: resolvedThreadId } : {}),
781
+ };
782
+ if (notification.method === 'turn/completed' && turnId)
783
+ resource.turnThreads.delete(turnId);
784
+ return origin;
785
+ }
729
786
  }
730
787
  function isMethodNotFound(error) {
731
788
  return Boolean(error && typeof error === 'object' && error.code === -32601);
@@ -16,7 +16,14 @@ export class SqliteAutopilotStore {
16
16
  if (!row)
17
17
  return null;
18
18
  const lifecycle = parseLifecycle(row.lifecycle_json);
19
- if (!['disabled', 'monitoring', 'backoff', 'attentionRequired', 'completed'].includes(String(row.state)) ||
19
+ if (![
20
+ 'disabled',
21
+ 'monitoring',
22
+ 'backoff',
23
+ 'attentionRequired',
24
+ 'completed',
25
+ 'safetyPaused',
26
+ ].includes(String(row.state)) ||
20
27
  ![
21
28
  'manualDisabled',
22
29
  'planRequired',
@@ -30,6 +37,7 @@ export class SqliteAutopilotStore {
30
37
  'planRemoved',
31
38
  'planReplaced',
32
39
  'sessionEnded',
40
+ 'safetyPaused',
33
41
  'null',
34
42
  ].includes(String(row.stop_reason)) ||
35
43
  !Number.isSafeInteger(Number(row.generation)) ||
@@ -57,8 +65,13 @@ export class SqliteAutopilotStore {
57
65
  save(state) {
58
66
  this.db
59
67
  .prepare('INSERT INTO autopilot_sessions (session_id,state,requested_enabled,plan_identity,plan_fingerprint,generation,no_progress_count,next_evaluation_at,last_control_id,stop_reason,lifecycle_json,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET state=excluded.state,requested_enabled=excluded.requested_enabled,plan_identity=excluded.plan_identity,plan_fingerprint=excluded.plan_fingerprint,generation=excluded.generation,no_progress_count=excluded.no_progress_count,next_evaluation_at=excluded.next_evaluation_at,last_control_id=excluded.last_control_id,stop_reason=excluded.stop_reason,lifecycle_json=excluded.lifecycle_json,updated_at=excluded.updated_at')
60
- .run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking
61
- ? JSON.stringify({ executor: state.executor, blocking: state.blocking })
68
+ .run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking || state.supervision || state.checkpoints
69
+ ? JSON.stringify({
70
+ executor: state.executor,
71
+ blocking: state.blocking,
72
+ supervision: state.supervision,
73
+ checkpoints: state.checkpoints,
74
+ })
62
75
  : null, state.updatedAt);
63
76
  }
64
77
  findControl(sessionId, controlId) {