gestalt-mobile 0.28.0 → 0.29.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-Bz23puhw.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-B7x1bL59.css">
19
+ <script type="module" crossorigin src="/assets/index-CRULnRRB.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-BtlbAtm9.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="app"></div>
@@ -36,6 +36,8 @@ 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
38
  import { resolvedServerRequestId, toPendingInteraction } from './platform/codex/server-request.js';
39
+ import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
40
+ import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
39
41
  import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
40
42
  import { promoteRecentThread } from './features/sessions/promote-recent-thread/use-case.js';
41
43
  import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
@@ -379,7 +381,7 @@ export async function composeRelayApp(options) {
379
381
  autopilot.planStatusChanged(sessionId);
380
382
  };
381
383
  runtime = options.startAppServers
382
- ? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification) => {
384
+ ? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification, origin) => {
383
385
  const occurredAt = new Date().toISOString();
384
386
  for (const activityFact of decodeAgentActivityFacts(sessionId, occurredAt, notification))
385
387
  activity.observe(activityFact);
@@ -395,7 +397,18 @@ export async function composeRelayApp(options) {
395
397
  }
396
398
  }
397
399
  const currentSession = sessions.find(sessionId);
398
- const normalized = normalizeCodexNotification(sessionId, 0, occurredAt, notification, currentSession?.workspacePath, currentSession?.activeTurnId);
400
+ const completionTurnId = notification.method === 'turn/completed' &&
401
+ notification.params &&
402
+ typeof notification.params === 'object' &&
403
+ typeof notification.params.turn?.id === 'string'
404
+ ? notification.params.turn.id
405
+ : undefined;
406
+ // A completion with no thread identity is only root-owned when it exactly
407
+ // matches the durable active root turn; unknown child turns never fall back.
408
+ const resolvedOrigin = origin.kind === 'unknown' && completionTurnId === currentSession?.activeTurnId
409
+ ? { ...origin, kind: 'root' }
410
+ : origin;
411
+ const normalized = normalizeCodexNotification(sessionId, 0, occurredAt, notification, currentSession?.workspacePath, currentSession?.activeTurnId, resolvedOrigin);
399
412
  if (!normalized)
400
413
  return;
401
414
  let completedSession;
@@ -412,11 +425,35 @@ export async function composeRelayApp(options) {
412
425
  events.publish(journal.append(sessionId, normalized.type, normalized.payload, normalized.occurredAt));
413
426
  if (completedSession)
414
427
  events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
415
- }, (sessionId, request) => {
428
+ }, (sessionId, request, origin) => {
416
429
  const rawInteraction = toPendingInteraction(request);
417
430
  const session = withPendingInteractions(sessions.find(sessionId));
418
431
  if (!rawInteraction || !session)
419
432
  return false;
433
+ if (rawInteraction.kind === 'orgPlanCheckpoint') {
434
+ const checkpoint = parseOrgPlanCheckpoint(rawInteraction.payload);
435
+ const retained = supervisedPlans.find(sessionId);
436
+ const identity = supervisedPlans.identity(sessionId);
437
+ const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
438
+ if (!checkpoint ||
439
+ !retained ||
440
+ !identity ||
441
+ checkpoint.planIdentity !== identity ||
442
+ !rootOwned)
443
+ return false;
444
+ const valid = validOrgPlanCheckpoint({
445
+ checkpoint,
446
+ plan: retained,
447
+ planIdentity: identity,
448
+ rootOwned,
449
+ hasActiveL1Writer: (position) => hasActiveL1Writer(activity.snapshot(sessionId, new Date().toISOString()), position),
450
+ });
451
+ if (!valid)
452
+ return false;
453
+ if (!autopilot.checkpointAccepted(sessionId, checkpoint, session.activeTurnId, new Date().toISOString()))
454
+ return false;
455
+ return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse()) === true);
456
+ }
420
457
  const interaction = {
421
458
  ...rawInteraction,
422
459
  turnId: session.activeTurnId,
@@ -959,3 +996,7 @@ function resolveStateDatabasePath(root, stateHome) {
959
996
  const legacyPath = legacyRelayStatePath(root, stateHome);
960
997
  return existsSync(legacyPath) ? legacyPath : currentPath;
961
998
  }
999
+ function hasActiveL1Writer(snapshot, position) {
1000
+ return snapshot.subagents.some((child) => child.canonicalPosition === position &&
1001
+ ['working', 'awaitingAgent', 'awaitingHuman'].includes(child.state));
1002
+ }
@@ -59,8 +59,17 @@ export function decideAutopilot(input) {
59
59
  return { kind: 'disable', reason: 'manualDisabled' };
60
60
  if (!plan)
61
61
  return { kind: 'disable', reason: 'planRequired' };
62
- if (executionComplete(plan))
62
+ if (executionComplete(plan)) {
63
+ // Checkpoint-aware sessions deliberately reserve a later root turn for
64
+ // terminal whole-branch review. The Org projection remains authoritative:
65
+ // this only controls whether a completed L1 may end the current report turn.
66
+ if (state.checkpoints?.protocolVersion === 1 && !state.checkpoints.terminalReviewAccepted)
67
+ return {
68
+ kind: 'scheduleContinuation',
69
+ at: new Date(Date.parse(now) + policy.backoffMs(state.consecutiveNoProgress)).toISOString(),
70
+ };
63
71
  return { kind: 'complete' };
72
+ }
64
73
  // Quiz, approval, and other held requests are ordinary session work. Only a
65
74
  // validated Org attention record may turn an incomplete plan into a human stop.
66
75
  if (hasPendingInteraction)
@@ -3,6 +3,7 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
+ import { createHash } from 'node:crypto';
6
7
  import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
7
8
  import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, } from '../domain/supervised-lifecycle.js';
8
9
  import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
@@ -294,11 +295,31 @@ export class AutopilotCoordinator {
294
295
  state?.stopReason === 'planRemoved' ||
295
296
  state?.stopReason === 'planReplaced' ||
296
297
  state?.stopReason === 'sessionEnded';
298
+ const checkpointBoundary = Boolean(state?.checkpoints?.pendingTurnId &&
299
+ state.checkpoints.planIdentity === this.deps.plan(sessionId)?.identity);
297
300
  const finalAllowed = !state ||
298
301
  !plan ||
299
302
  explicitlyStopped ||
300
- executionComplete(plan) ||
303
+ checkpointBoundary ||
304
+ (!state.checkpoints && executionComplete(plan)) ||
301
305
  validStructuredBlock(state.blocking);
306
+ if (checkpointBoundary && state?.checkpoints) {
307
+ const occurredAt = this.deps.now();
308
+ this.persist({
309
+ ...state,
310
+ checkpoints: { ...state.checkpoints, pendingTurnId: null },
311
+ updatedAt: occurredAt,
312
+ }, undefined, [
313
+ {
314
+ sessionId,
315
+ type: state.checkpoints.terminalReviewAccepted
316
+ ? 'org-plan.terminal-review-reported'
317
+ : 'org-plan.milestone-reported',
318
+ payload: { turnId: state.checkpoints.pendingTurnId },
319
+ occurredAt,
320
+ },
321
+ ]);
322
+ }
302
323
  if (!finalAllowed) {
303
324
  const occurredAt = this.deps.now();
304
325
  this.commit({
@@ -316,6 +337,46 @@ export class AutopilotCoordinator {
316
337
  this.activitySettled(sessionId, 'rootFinalAttempt');
317
338
  return finalAllowed;
318
339
  }
340
+ /** Records an already validated root-only checkpoint; it never changes Org state. */
341
+ checkpointAccepted(sessionId, checkpoint, turnId, occurredAt) {
342
+ const prior = this.deps.store.find(sessionId);
343
+ const retained = this.deps.plan(sessionId);
344
+ if (!prior || !retained || !turnId || checkpoint.planIdentity !== retained.identity)
345
+ return false;
346
+ const previous = prior.checkpoints;
347
+ if (previous && previous.planIdentity !== retained.identity)
348
+ return false;
349
+ const reported = previous?.reportedL1Ids ?? [];
350
+ const key = createHash('sha256').update(JSON.stringify(checkpoint)).digest('hex');
351
+ const acceptedKeys = previous?.acceptedKeys ?? [];
352
+ if (acceptedKeys.includes(key))
353
+ return true;
354
+ if (checkpoint.kind === 'l1Accepted' && reported.includes(checkpoint.l1Id))
355
+ return false;
356
+ if (checkpoint.kind === 'terminalReviewAccepted' && previous?.terminalReviewAccepted)
357
+ return false;
358
+ const checkpoints = {
359
+ protocolVersion: 1,
360
+ planIdentity: retained.identity,
361
+ reportedL1Ids: checkpoint.kind === 'l1Accepted' ? [...reported, checkpoint.l1Id] : reported,
362
+ acceptedKeys: [...acceptedKeys, key],
363
+ pendingTurnId: turnId,
364
+ terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
365
+ };
366
+ this.persist({ ...prior, checkpoints, updatedAt: occurredAt }, undefined, [
367
+ {
368
+ sessionId,
369
+ type: checkpoint.kind === 'l1Accepted'
370
+ ? 'org-plan.milestone-checkpointed'
371
+ : 'org-plan.terminal-review-checkpointed',
372
+ payload: checkpoint.kind === 'l1Accepted'
373
+ ? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
374
+ : { turnId },
375
+ occurredAt,
376
+ },
377
+ ]);
378
+ return true;
379
+ }
319
380
  /** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
320
381
  planStatusChanged(sessionId) {
321
382
  const prior = this.deps.store.find(sessionId);
@@ -24,8 +24,12 @@ export function parsePersistedSupervisedLifecycle(value) {
24
24
  if (blockingValue && !blocking)
25
25
  return undefined;
26
26
  const executorValue = record(root.executor);
27
+ const checkpointsValue = record(root.checkpoints);
28
+ const checkpoints = checkpointsValue ? parseCheckpoints(checkpointsValue) : undefined;
29
+ if (checkpointsValue && !checkpoints)
30
+ return undefined;
27
31
  if (!executorValue)
28
- return blocking ? { blocking } : {};
32
+ return { ...(blocking ? { blocking } : {}), ...(checkpoints ? { checkpoints } : {}) };
29
33
  const processesValue = executorValue.ownedProcesses;
30
34
  if (!Array.isArray(processesValue) || processesValue.length > 64)
31
35
  return undefined;
@@ -131,6 +135,41 @@ export function parsePersistedSupervisedLifecycle(value) {
131
135
  continuationCount,
132
136
  },
133
137
  ...(blocking ? { blocking } : {}),
138
+ ...(checkpoints ? { checkpoints } : {}),
139
+ };
140
+ }
141
+ function parseCheckpoints(value) {
142
+ const planIdentity = boundedText(value.planIdentity);
143
+ if (value.protocolVersion !== 1 || !planIdentity)
144
+ return undefined;
145
+ if (!Array.isArray(value.reportedL1Ids) || value.reportedL1Ids.length > 128)
146
+ return undefined;
147
+ const reportedL1Ids = value.reportedL1Ids
148
+ .map((id) => boundedText(id))
149
+ .filter(Boolean);
150
+ if (reportedL1Ids.length !== value.reportedL1Ids.length ||
151
+ new Set(reportedL1Ids).size !== reportedL1Ids.length)
152
+ return undefined;
153
+ if (!Array.isArray(value.acceptedKeys) || value.acceptedKeys.length > 128)
154
+ return undefined;
155
+ const acceptedKeys = value.acceptedKeys
156
+ .map((key) => boundedText(key))
157
+ .filter(Boolean);
158
+ if (acceptedKeys.length !== value.acceptedKeys.length ||
159
+ new Set(acceptedKeys).size !== acceptedKeys.length)
160
+ return undefined;
161
+ const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
162
+ if (pendingTurnId === undefined)
163
+ return undefined;
164
+ if (typeof value.terminalReviewAccepted !== 'boolean')
165
+ return undefined;
166
+ return {
167
+ protocolVersion: 1,
168
+ planIdentity,
169
+ reportedL1Ids,
170
+ acceptedKeys,
171
+ pendingTurnId,
172
+ terminalReviewAccepted: value.terminalReviewAccepted,
134
173
  };
135
174
  }
136
175
  export function classifyExecutorOutcome(input) {
@@ -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,6 @@ export function isInteractionKind(value) {
11
11
  'userInput',
12
12
  'quiz',
13
13
  'orgPlanAttention',
14
+ 'orgPlanCheckpoint',
14
15
  ].includes(value);
15
16
  }
@@ -11,7 +11,7 @@ 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' || kind === 'orgPlanAttention' || kind === 'orgPlanCheckpoint')
15
15
  return false;
16
16
  if (kind === 'permissionsApproval')
17
17
  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,7 @@
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';
8
9
  export function toPendingInteraction(input) {
9
10
  if (!Number.isSafeInteger(input.id) || !isRecord(input.params))
10
11
  return null;
@@ -19,6 +20,12 @@ export function toPendingInteraction(input) {
19
20
  ? { requestId: String(input.id), kind: 'orgPlanAttention', payload: attention }
20
21
  : null;
21
22
  }
23
+ if (input.params.tool === GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME) {
24
+ const checkpoint = parseOrgPlanCheckpoint(input.params.arguments);
25
+ return checkpoint
26
+ ? { requestId: String(input.id), kind: 'orgPlanCheckpoint', payload: checkpoint }
27
+ : null;
28
+ }
22
29
  return null;
23
30
  }
24
31
  const kind = {
@@ -9,6 +9,7 @@ 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';
12
13
  import { countDiffLines } from '../../../shared/contracts/file-change.js';
13
14
  import { threadPlanName } from './thread-plan-name.js';
14
15
  import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
@@ -32,6 +33,8 @@ class SessionResource {
32
33
  spawnedAgentModels = new Map();
33
34
  attemptedAgentModelRecovery = new Set();
34
35
  ownedChildProcesses = new Map();
36
+ turnThreads = new Map();
37
+ childThreads = new Set();
35
38
  constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
36
39
  this.sessionId = sessionId;
37
40
  this.process = process;
@@ -561,7 +564,11 @@ export class CodexSessionRuntime {
561
564
  ? { approvalPolicy: session.executionPolicy.approvalPolicy }
562
565
  : {}),
563
566
  ...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
564
- dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
567
+ dynamicTools: [
568
+ gestaltQuizDynamicTool,
569
+ gestaltOrgPlanAttentionDynamicTool,
570
+ gestaltOrgPlanCheckpointDynamicTool,
571
+ ],
565
572
  });
566
573
  result = {
567
574
  session: RelaySession.rehydrate(session).restore(now).supportsAttentionTool(now).snapshot,
@@ -629,7 +636,8 @@ export class CodexSessionRuntime {
629
636
  resource.pendingRequests.set(requestId, { resolve, reject });
630
637
  let accepted = false;
631
638
  try {
632
- accepted = this.onServerRequest?.(resource.sessionId, request) === true;
639
+ accepted =
640
+ this.onServerRequest?.(resource.sessionId, request, this.resolveNotificationOrigin(resource, request)) === true;
633
641
  }
634
642
  catch {
635
643
  // Publication failures are protocol failures, not permission to retain
@@ -646,7 +654,11 @@ export class CodexSessionRuntime {
646
654
  return {
647
655
  cwd: session.workspacePath,
648
656
  approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
649
- dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
657
+ dynamicTools: [
658
+ gestaltQuizDynamicTool,
659
+ gestaltOrgPlanAttentionDynamicTool,
660
+ gestaltOrgPlanCheckpointDynamicTool,
661
+ ],
650
662
  ...(session.model ? { model: session.model } : {}),
651
663
  ...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
652
664
  };
@@ -709,7 +721,7 @@ export class CodexSessionRuntime {
709
721
  // The notification callback reconciles the durable interaction record.
710
722
  pending?.reject(new Error('CODEX_SERVER_REQUEST_CLEARED'));
711
723
  }
712
- this.onNotification?.(session.id, notification);
724
+ this.onNotification?.(session.id, notification, this.resolveNotificationOrigin(resource, notification));
713
725
  });
714
726
  const requestUnsubscribe = process.rpc.onServerRequest((request) => this.holdServerRequest(resource, request));
715
727
  if (!resource.active) {
@@ -726,6 +738,48 @@ export class CodexSessionRuntime {
726
738
  throw error;
727
739
  }
728
740
  }
741
+ resolveNotificationOrigin(resource, notification) {
742
+ const params = asRecord(notification.params);
743
+ if (!params)
744
+ return { kind: 'unknown' };
745
+ const item = asRecord(params.item);
746
+ if (item?.type === 'collabAgentToolCall' || item?.type === 'collabToolCall') {
747
+ for (const value of [item.receiverThreadId, item.newThreadId])
748
+ if (boundedString(value, 256) && resource.childThreads.size < 64)
749
+ resource.childThreads.add(boundedString(value, 256));
750
+ if (Array.isArray(item.receiverThreadIds))
751
+ for (const value of item.receiverThreadIds)
752
+ if (boundedString(value, 256) && resource.childThreads.size < 64)
753
+ resource.childThreads.add(boundedString(value, 256));
754
+ const states = asRecord(item.agentsStates);
755
+ if (states)
756
+ for (const childId of Object.keys(states))
757
+ if (boundedString(childId, 256) && resource.childThreads.size < 64)
758
+ resource.childThreads.add(childId);
759
+ }
760
+ const turn = asRecord(params.turn);
761
+ const turnId = boundedString(params.turnId, 256) ??
762
+ boundedString(turn?.id, 256) ??
763
+ boundedString(item?.turnId, 256);
764
+ if (notification.method === 'turn/started' && turnId && boundedString(params.threadId, 256)) {
765
+ if (resource.turnThreads.has(turnId) || resource.turnThreads.size < 256)
766
+ resource.turnThreads.set(turnId, boundedString(params.threadId, 256));
767
+ }
768
+ const resolvedThreadId = boundedString(params.threadId, 256) ??
769
+ (turnId ? resource.turnThreads.get(turnId) : undefined);
770
+ const origin = {
771
+ kind: resolvedThreadId && resolvedThreadId === resource.threadId
772
+ ? 'root'
773
+ : resolvedThreadId && resource.childThreads.has(resolvedThreadId)
774
+ ? 'child'
775
+ : 'unknown',
776
+ ...(turnId ? { physicalTurnId: turnId } : {}),
777
+ ...(resolvedThreadId ? { physicalThreadId: resolvedThreadId } : {}),
778
+ };
779
+ if (notification.method === 'turn/completed' && turnId)
780
+ resource.turnThreads.delete(turnId);
781
+ return origin;
782
+ }
729
783
  }
730
784
  function isMethodNotFound(error) {
731
785
  return Boolean(error && typeof error === 'object' && error.code === -32601);
@@ -57,8 +57,12 @@ export class SqliteAutopilotStore {
57
57
  save(state) {
58
58
  this.db
59
59
  .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 })
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 || state.checkpoints
61
+ ? JSON.stringify({
62
+ executor: state.executor,
63
+ blocking: state.blocking,
64
+ checkpoints: state.checkpoints,
65
+ })
62
66
  : null, state.updatedAt);
63
67
  }
64
68
  findControl(sessionId, controlId) {