gestalt-mobile 0.41.2 → 0.42.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.
@@ -321,9 +321,9 @@ export class AutopilotCoordinator {
321
321
  consecutiveNoProgress: 0,
322
322
  nextEvaluationAt: null,
323
323
  stopReason: null,
324
- executor: undefined,
324
+ executor: !replacing && prior.state === 'attentionRequired' ? prior.executor : undefined,
325
325
  blocking: undefined,
326
- supervision: recoverSafetyPause(consumeObsoleteWait(replacing
326
+ supervision: recoverSafetyPause(consumeObsoleteWait(replacing || prior.supervision?.outcome === 'attentionRequired'
327
327
  ? startSupervisionProtocol(this.progressKey(sessionId))
328
328
  : (prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId))), this.progressKey(sessionId)), this.progressKey(sessionId)),
329
329
  updatedAt: now,
@@ -332,6 +332,64 @@ export class AutopilotCoordinator {
332
332
  this.evaluate(sessionId);
333
333
  return this.snapshot(sessionId);
334
334
  }
335
+ /** Converts one typed human permission into one exact durable physical replacement. */
336
+ authorizeExecutorReplacement(sessionId, canonicalTaskName) {
337
+ const state = this.deps.store.find(sessionId);
338
+ const retained = this.deps.plan(sessionId);
339
+ if (!state ||
340
+ state.state !== 'attentionRequired' ||
341
+ state.blocking?.reason !== 'permissionRequired')
342
+ return { accepted: false, reason: 'attentionRequired' };
343
+ if (!retained ||
344
+ state.planIdentity !== retained.identity ||
345
+ state.planFingerprint !== fingerprint(retained.plan))
346
+ return { accepted: false, reason: 'planMismatch' };
347
+ const executor = state.executor;
348
+ if (!executor || executor.canonicalTaskName !== canonicalTaskName || executor.l1State !== 'WIP')
349
+ return { accepted: false, reason: 'executorNotAssigned' };
350
+ if (executor.replacement && this.validReplacement(sessionId, executor.replacement))
351
+ return { accepted: true, identity: executor.replacement };
352
+ const activity = this.deps.activity(sessionId);
353
+ if (activity?.confidence !== 'fresh' || this.executorTurnActive(executor, activity))
354
+ return { accepted: false, reason: 'executorStillActive' };
355
+ const previous = executor.assignment ?? executorAssignment(executor);
356
+ const identity = executorIdentity(canonicalTaskName, previous.generation + 1);
357
+ const now = this.deps.now();
358
+ this.persist({
359
+ ...state,
360
+ executor: {
361
+ ...executor,
362
+ outcome: 'failed',
363
+ assignment: { ...previous, state: 'unavailable' },
364
+ replacement: {
365
+ ...identity,
366
+ planIdentity: retained.identity,
367
+ planFingerprint: fingerprint(retained.plan),
368
+ previous: { ...previous, state: 'unavailable' },
369
+ reason: 'humanPermissionGranted',
370
+ evidence: 'typed-attention-approval',
371
+ },
372
+ commands: executor.commands?.map((command) => ['scheduled', 'issued'].includes(command.status)
373
+ ? { ...command, status: 'superseded', updatedAt: now }
374
+ : command),
375
+ },
376
+ updatedAt: now,
377
+ }, undefined, [
378
+ {
379
+ sessionId,
380
+ type: 'autopilot.executor-replacement-authorized',
381
+ payload: {
382
+ canonicalTaskName,
383
+ previousTaskName: previous.taskName,
384
+ taskName: identity.taskName,
385
+ generation: identity.generation,
386
+ evidence: 'typed-attention-approval',
387
+ },
388
+ occurredAt: now,
389
+ },
390
+ ]);
391
+ return { accepted: true, identity };
392
+ }
335
393
  disable(sessionId) {
336
394
  const now = this.deps.now();
337
395
  const prior = this.deps.store.find(sessionId) ?? disabledAutopilot(sessionId, now);
@@ -421,6 +479,7 @@ export class AutopilotCoordinator {
421
479
  if (this.deps.executorController &&
422
480
  plan &&
423
481
  !this.requiresRootBoundary(prior, plan) &&
482
+ !this.executorDiagnosisRequired(prior) &&
424
483
  this.currentExecutor(sessionId, plan, prior.consecutiveNoProgress, prior.executor)) {
425
484
  this.enqueue(sessionId, async () => {
426
485
  await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
@@ -466,7 +525,12 @@ export class AutopilotCoordinator {
466
525
  {
467
526
  sessionId,
468
527
  type: 'autopilot.continuation-scheduled',
469
- payload: { controlId: scheduledControlId },
528
+ payload: {
529
+ controlId: scheduledControlId,
530
+ ...(prior.checkpoints?.activeHandoffId
531
+ ? { traceId: prior.checkpoints.activeHandoffId }
532
+ : {}),
533
+ },
470
534
  occurredAt: now,
471
535
  },
472
536
  ]);
@@ -580,7 +644,12 @@ export class AutopilotCoordinator {
580
644
  : pendingKind === 'terminalReviewAccepted'
581
645
  ? 'org-plan.terminal-review-reported'
582
646
  : 'org-plan.milestone-reported',
583
- payload: { turnId: state.checkpoints.pendingTurnId },
647
+ payload: {
648
+ turnId: state.checkpoints.pendingTurnId,
649
+ ...(state.checkpoints.activeHandoffId
650
+ ? { traceId: state.checkpoints.activeHandoffId }
651
+ : {}),
652
+ },
584
653
  occurredAt,
585
654
  },
586
655
  ]);
@@ -641,6 +710,7 @@ export class AutopilotCoordinator {
641
710
  ...(previous?.completionEpochs ?? []).filter((entry) => entry.target !== target),
642
711
  { target, epoch, reopened: false, completed: true },
643
712
  ];
713
+ const handoffId = `handoff-${key.slice(0, 24)}`;
644
714
  const checkpoints = {
645
715
  protocolVersion: 1,
646
716
  planIdentity: retained.identity,
@@ -651,6 +721,7 @@ export class AutopilotCoordinator {
651
721
  : reportedL1Ids,
652
722
  acceptedKeys: boundedUnique([...acceptedKeys, key], 768),
653
723
  pendingTurnId: turnId,
724
+ activeHandoffId: handoffId,
654
725
  pendingKind: checkpoint.kind,
655
726
  checkpointHandoffFailed: false,
656
727
  terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
@@ -687,10 +758,16 @@ export class AutopilotCoordinator {
687
758
  l2Id: checkpoint.l2Id,
688
759
  position: checkpoint.position,
689
760
  turnId,
761
+ traceId: handoffId,
690
762
  }
691
763
  : checkpoint.kind === 'l1Accepted'
692
- ? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
693
- : { turnId },
764
+ ? {
765
+ l1Id: checkpoint.l1Id,
766
+ position: checkpoint.position,
767
+ turnId,
768
+ traceId: handoffId,
769
+ }
770
+ : { turnId, traceId: handoffId },
694
771
  occurredAt,
695
772
  },
696
773
  ]);
@@ -885,15 +962,27 @@ export class AutopilotCoordinator {
885
962
  const cancelled = this.cancelScheduledControl(prior, now);
886
963
  const subagentsWorking = activity.aggregateSubagents === 'working' ||
887
964
  activity.aggregateSubagents === 'awaitingAgent';
965
+ const supervision = subagentsWorking &&
966
+ Boolean(this.deps.session(sessionId)?.activeTurnId) &&
967
+ prior.supervision?.outcome === 'probeRequired'
968
+ ? reportProbe(prior.supervision, {
969
+ id: `executor-actionable:${createHash('sha256')
970
+ .update(this.progressKey(sessionId))
971
+ .digest('hex')}`,
972
+ kind: 'actionable',
973
+ })
974
+ : prior.supervision;
888
975
  if (cancelled ||
889
976
  prior.state !== 'monitoring' ||
890
977
  prior.nextEvaluationAt ||
891
- (subagentsWorking && prior.consecutiveNoProgress > 0))
978
+ (subagentsWorking && prior.consecutiveNoProgress > 0) ||
979
+ supervision !== prior.supervision)
892
980
  this.persist({
893
981
  ...prior,
894
982
  state: 'monitoring',
895
983
  ...(cancelled ? { generation: prior.generation + 1, lastControlId: null } : {}),
896
984
  ...(subagentsWorking ? { consecutiveNoProgress: 0 } : {}),
985
+ ...(supervision ? { supervision } : {}),
897
986
  nextEvaluationAt: null,
898
987
  updatedAt: now,
899
988
  }, cancelled);
@@ -959,7 +1048,17 @@ export class AutopilotCoordinator {
959
1048
  const issuedControl = { ...priorControl, status: 'issued', updatedAt: now };
960
1049
  const events = [
961
1050
  ...this.snapshotEvents(next, issuedControl),
962
- { sessionId, type: 'autopilot.control-issued', payload: { controlId }, occurredAt: now },
1051
+ {
1052
+ sessionId,
1053
+ type: 'autopilot.control-issued',
1054
+ payload: {
1055
+ controlId,
1056
+ ...(prior.checkpoints?.activeHandoffId
1057
+ ? { traceId: prior.checkpoints.activeHandoffId }
1058
+ : {}),
1059
+ },
1060
+ occurredAt: now,
1061
+ },
963
1062
  ];
964
1063
  const control = this.deps.store.claimControlIssued
965
1064
  ? this.deps.store.claimControlIssued(sessionId, controlId, now, next, events)
@@ -1033,6 +1132,7 @@ export class AutopilotCoordinator {
1033
1132
  retained &&
1034
1133
  !current.executor?.replacement &&
1035
1134
  !this.requiresRootBoundary(current, retained.plan) &&
1135
+ !this.executorDiagnosisRequired(current) &&
1036
1136
  this.currentExecutor(sessionId, retained.plan, current.consecutiveNoProgress, current.executor)) {
1037
1137
  const now = this.deps.now();
1038
1138
  const cancelled = this.cancelScheduledControl(current, now);
@@ -1047,7 +1147,12 @@ export class AutopilotCoordinator {
1047
1147
  await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
1048
1148
  return;
1049
1149
  }
1050
- const protocol = recordAutomaticContinuation(current.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId));
1150
+ // Direct executor continuations account for their own accepted attempts.
1151
+ // Once that bounded loop requests diagnosis, preserve the probe state for
1152
+ // the root instead of counting the root control as another blind retry.
1153
+ const protocol = this.executorDiagnosisRequired(current)
1154
+ ? current.supervision
1155
+ : recordAutomaticContinuation(current.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId));
1051
1156
  if (protocol.outcome === 'probeRequired') {
1052
1157
  this.persist({ ...current, supervision: protocol, updatedAt: this.deps.now() }, undefined, [
1053
1158
  {
@@ -1224,6 +1329,8 @@ export class AutopilotCoordinator {
1224
1329
  case 'reinspect':
1225
1330
  return false;
1226
1331
  case 'resumeExecutor':
1332
+ if (this.executorDiagnosisRequired(this.deps.store.find(sessionId) ?? state))
1333
+ return false;
1227
1334
  this.armExecutorContinuation(sessionId, decision.action.delayMs, decision.action.threadId, decision.action.generation, { kind: 'partial' });
1228
1335
  return true;
1229
1336
  case 'monitorProcess': {
@@ -1258,7 +1365,7 @@ export class AutopilotCoordinator {
1258
1365
  resultArtifact: processAction.resultArtifact,
1259
1366
  });
1260
1367
  if (consumed && executor)
1261
- this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + 1, {
1368
+ this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + executor.continuationCount + 1, {
1262
1369
  kind: 'processExited',
1263
1370
  processId,
1264
1371
  resultArtifact: processAction.resultArtifact,
@@ -1279,7 +1386,7 @@ export class AutopilotCoordinator {
1279
1386
  reason: 'resourceBudget',
1280
1387
  });
1281
1388
  if (terminated && executor)
1282
- this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + 1, { kind: 'processResourceLimit', processId });
1389
+ this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + executor.continuationCount + 1, { kind: 'processResourceLimit', processId });
1283
1390
  else
1284
1391
  this.armExecutorRefresh(sessionId, this.deps.policy.processPollMs);
1285
1392
  return true;
@@ -1366,7 +1473,9 @@ export class AutopilotCoordinator {
1366
1473
  ownedProcesses: refreshPersistedProcesses(persisted?.canonicalPosition === canonicalPosition ? persisted.ownedProcesses : [], child.ownedProcesses ?? [], this.deps.now()),
1367
1474
  outcome: outcome.outcome,
1368
1475
  ...(outcome.blocking ? { blocking: outcome.blocking } : {}),
1369
- continuationGeneration: Math.max(child.continuationGeneration ?? 1, persisted?.canonicalPosition === canonicalPosition ? persisted.continuationGeneration : 1),
1476
+ // The roster value and assignment identify the physical l<a>_gN slot.
1477
+ // Logical follow-up attempts are tracked separately by continuationCount.
1478
+ continuationGeneration: replacementChild || !assigned ? (child.continuationGeneration ?? 1) : assigned.generation,
1370
1479
  continuationCount: persisted?.canonicalPosition === canonicalPosition
1371
1480
  ? persisted.continuationCount
1372
1481
  : continuationCount,
@@ -1892,23 +2001,24 @@ export class AutopilotCoordinator {
1892
2001
  const armedChild = this.deps
1893
2002
  .activity(sessionId)
1894
2003
  ?.subagents.find((child) => (child.threadId ?? child.id) === threadId);
1895
- const fence = state && retained
2004
+ const commandExecutor = state?.executor;
2005
+ const fence = state && retained && commandExecutor
1896
2006
  ? {
1897
2007
  sessionGeneration: state.generation,
1898
2008
  planIdentity: retained.identity,
1899
2009
  planFingerprint: fingerprint(retained.plan),
1900
2010
  canonicalPosition: armedChild?.canonicalPosition ?? state.executor?.canonicalPosition ?? null,
1901
2011
  executorThreadId: threadId,
1902
- executorGeneration: generation - 1,
2012
+ // The roster generation identifies the physical l<a>_gN slot.
2013
+ // A canonical executor can accept many logical follow-up turns, so
2014
+ // never derive this ownership fence from the follow-up sequence.
2015
+ executorGeneration: commandExecutor.assignment?.generation ?? commandExecutor.continuationGeneration,
1903
2016
  supervisionOutcome: state.supervision?.outcome ?? null,
1904
2017
  checkpoint: JSON.stringify(state.checkpoints ?? null),
1905
2018
  boundaryState: state.state,
1906
2019
  }
1907
2020
  : null;
1908
- if (!fence || this.deps.session(sessionId)?.activeTurnId)
1909
- return;
1910
- const commandExecutor = state?.executor;
1911
- if (!commandExecutor)
2021
+ if (!commandExecutor || !fence || this.deps.session(sessionId)?.activeTurnId)
1912
2022
  return;
1913
2023
  const requested = this.executorCommand(fence, commandExecutor, generation, trigger);
1914
2024
  const command = this.scheduleExecutorCommand(sessionId, requested);
@@ -1974,17 +2084,19 @@ export class AutopilotCoordinator {
1974
2084
  trigger: trigger.kind,
1975
2085
  });
1976
2086
  const latest = this.deps.store.find(sessionId);
1977
- if (latest?.requestedEnabled)
2087
+ if (latest?.requestedEnabled) {
2088
+ const progressKey = this.progressKey(sessionId);
2089
+ const supervision = recordAutomaticContinuation(latest.supervision ?? startSupervisionProtocol(progressKey), progressKey);
1978
2090
  this.persist({
1979
2091
  ...latest,
1980
2092
  state: 'monitoring',
2093
+ supervision,
1981
2094
  consecutiveNoProgress: latest.consecutiveNoProgress + 1,
1982
2095
  ...(latest.executor
1983
2096
  ? {
1984
2097
  executor: {
1985
2098
  ...latest.executor,
1986
2099
  outcome: 'partial',
1987
- continuationGeneration: generation,
1988
2100
  continuationCount: latest.executor.continuationCount + 1,
1989
2101
  lastActivityAt: this.deps.now(),
1990
2102
  },
@@ -1993,6 +2105,7 @@ export class AutopilotCoordinator {
1993
2105
  nextEvaluationAt: null,
1994
2106
  updatedAt: this.deps.now(),
1995
2107
  });
2108
+ }
1996
2109
  }
1997
2110
  catch (error) {
1998
2111
  // Only an explicit app-server rejection is safe to retry. A lost
@@ -2055,8 +2168,19 @@ export class AutopilotCoordinator {
2055
2168
  sessionId,
2056
2169
  type: eventType,
2057
2170
  payload: eventType === 'autopilot.turn-failed'
2058
- ? { controlId, code: failureCode }
2059
- : { controlId },
2171
+ ? {
2172
+ controlId,
2173
+ code: failureCode,
2174
+ ...(this.deps.store.find(sessionId)?.checkpoints?.activeHandoffId
2175
+ ? { traceId: this.deps.store.find(sessionId).checkpoints.activeHandoffId }
2176
+ : {}),
2177
+ }
2178
+ : {
2179
+ controlId,
2180
+ ...(this.deps.store.find(sessionId)?.checkpoints?.activeHandoffId
2181
+ ? { traceId: this.deps.store.find(sessionId).checkpoints.activeHandoffId }
2182
+ : {}),
2183
+ },
2060
2184
  occurredAt,
2061
2185
  },
2062
2186
  ]
@@ -2085,6 +2209,9 @@ export class AutopilotCoordinator {
2085
2209
  child.state === 'awaitingAgent' ||
2086
2210
  child.ownedProcesses?.some((process) => process.state === 'running' || process.state === 'detached-active')));
2087
2211
  }
2212
+ executorDiagnosisRequired(state) {
2213
+ return state.supervision?.outcome === 'probeRequired';
2214
+ }
2088
2215
  executorTurnActive(executor, activity) {
2089
2216
  if (activity?.confidence !== 'fresh')
2090
2217
  return false;
@@ -179,7 +179,8 @@ export function parsePersistedSupervisedLifecycle(value) {
179
179
  const replacementPrevious = replacementPreviousValue
180
180
  ? parseExecutorAssignment(replacementPreviousValue)
181
181
  : undefined;
182
- const replacementReason = replacementValue && stringValue(replacementValue.reason, ['explicitExecutorRejection']);
182
+ const replacementReason = replacementValue &&
183
+ stringValue(replacementValue.reason, ['explicitExecutorRejection', 'humanPermissionGranted']);
183
184
  const replacementEvidence = replacementValue && boundedText(replacementValue.evidence);
184
185
  if (!canonicalPosition ||
185
186
  !canonicalTaskName ||
@@ -200,7 +201,8 @@ export function parsePersistedSupervisedLifecycle(value) {
200
201
  !replacementPlanIdentity ||
201
202
  !replacementPlanFingerprint ||
202
203
  (replacementValue.previous !== undefined && !replacementPrevious) ||
203
- (replacementReason !== undefined && replacementReason !== 'explicitExecutorRejection') ||
204
+ (replacementReason !== undefined &&
205
+ !['explicitExecutorRejection', 'humanPermissionGranted'].includes(replacementReason)) ||
204
206
  (replacementValue.evidence !== undefined && !replacementEvidence))))
205
207
  return undefined;
206
208
  if (replacementValue) {
@@ -304,7 +306,10 @@ export function parsePersistedSupervisedLifecycle(value) {
304
306
  ownedProcesses,
305
307
  outcome,
306
308
  ...(executorBlocking ? { blocking: executorBlocking } : {}),
307
- continuationGeneration,
309
+ // Earlier releases incremented this value after every follow-up, even
310
+ // though the roster derives it from the immutable physical task name. A
311
+ // durable assignment is authoritative during reconciliation.
312
+ continuationGeneration: assignment?.generation ?? continuationGeneration,
308
313
  continuationCount,
309
314
  ...(resumeFailures ? { resumeFailures } : {}),
310
315
  ...(assignment ? { assignment } : {}),
@@ -385,6 +390,11 @@ function parseCheckpoints(value) {
385
390
  const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
386
391
  if (pendingTurnId === undefined)
387
392
  return undefined;
393
+ const activeHandoffId = value.activeHandoffId === undefined || value.activeHandoffId === null
394
+ ? null
395
+ : boundedText(value.activeHandoffId);
396
+ if (activeHandoffId === undefined)
397
+ return undefined;
388
398
  if (value.checkpointHandoffFailed !== undefined &&
389
399
  typeof value.checkpointHandoffFailed !== 'boolean')
390
400
  return undefined;
@@ -432,6 +442,7 @@ function parseCheckpoints(value) {
432
442
  reportedL1Ids,
433
443
  acceptedKeys,
434
444
  pendingTurnId,
445
+ ...(activeHandoffId ? { activeHandoffId } : {}),
435
446
  pendingKind: pendingKind,
436
447
  ...(value.checkpointHandoffFailed === undefined
437
448
  ? {}
@@ -537,7 +548,7 @@ export function decideSupervisedLifecycle(input) {
537
548
  action: {
538
549
  kind: 'resumeExecutor',
539
550
  threadId: input.executor.threadId,
540
- generation: input.executor.continuationGeneration + 1,
551
+ generation: input.executor.continuationGeneration + input.executor.continuationCount + 1,
541
552
  delayMs,
542
553
  },
543
554
  };
@@ -0,0 +1,90 @@
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
+ import { DatabaseSync } from 'node:sqlite';
7
+ const relevant = /^(?:org-plan\.|autopilot\.|agent\.activity\.|session\.status\.)/;
8
+ function objectPayload(value) {
9
+ try {
10
+ const parsed = JSON.parse(value);
11
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
12
+ ? parsed
13
+ : {};
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ function correlation(payload) {
20
+ for (const key of ['traceId', 'handoffId', 'controlId', 'turnId', 'requestId'])
21
+ if (typeof payload[key] === 'string' && payload[key])
22
+ return String(payload[key]);
23
+ return null;
24
+ }
25
+ function diagnose(events) {
26
+ const diagnoses = [];
27
+ if (!events.length)
28
+ return ['no control-plane events found for this session'];
29
+ const checkpointIndex = events.findLastIndex((event) => event.type.endsWith('-checkpointed'));
30
+ const currentHandoff = checkpointIndex < 0 ? events : events.slice(checkpointIndex);
31
+ const types = new Set(currentHandoff.map((event) => event.type));
32
+ if ([...types].some((type) => type.endsWith('-checkpointed')) &&
33
+ !types.has('autopilot.continuation-scheduled'))
34
+ diagnoses.push('checkpoint persisted, but no continuation was scheduled');
35
+ if (types.has('autopilot.continuation-scheduled') &&
36
+ !types.has('autopilot.control-issued') &&
37
+ !types.has('autopilot.executor-resumed'))
38
+ diagnoses.push('continuation scheduled, but no control or executor dispatch was observed');
39
+ if (types.has('autopilot.control-issued') &&
40
+ !types.has('autopilot.turn-started') &&
41
+ !types.has('autopilot.turn-failed'))
42
+ diagnoses.push('control issued, but no turn acceptance or failure was observed');
43
+ if (types.has('org-plan.checkpoint-handoff-failed'))
44
+ diagnoses.push('checkpoint response handoff failed; inspect the matching trace ID');
45
+ if (currentHandoff.some((event) => event.type === 'agent.activity.updated'))
46
+ diagnoses.push('backend agent projection was recorded; browser delivery still requires the client event cursor');
47
+ return diagnoses;
48
+ }
49
+ export function exportControlPlaneTrace(databasePath, sessionId, generatedAt = new Date().toISOString()) {
50
+ const database = new DatabaseSync(databasePath, { readOnly: true });
51
+ try {
52
+ const rows = database
53
+ .prepare('SELECT sequence,occurred_at,type,payload_json FROM session_events WHERE session_id = ? ORDER BY sequence')
54
+ .all(sessionId);
55
+ const events = rows
56
+ .filter((row) => relevant.test(row.type))
57
+ .map((row) => {
58
+ const payload = objectPayload(row.payload_json);
59
+ return {
60
+ sequence: row.sequence,
61
+ occurredAt: row.occurred_at,
62
+ type: row.type,
63
+ payload,
64
+ traceId: correlation(payload),
65
+ };
66
+ });
67
+ return { schemaVersion: 1, sessionId, generatedAt, events, diagnoses: diagnose(events) };
68
+ }
69
+ finally {
70
+ database.close();
71
+ }
72
+ }
73
+ export function formatControlPlaneTrace(trace) {
74
+ const lines = [
75
+ `Control-plane trace for ${trace.sessionId}`,
76
+ `Generated: ${trace.generatedAt}`,
77
+ '',
78
+ ];
79
+ for (const event of trace.events) {
80
+ const correlationLabel = event.traceId ? ` trace=${event.traceId}` : '';
81
+ lines.push(`${String(event.sequence).padStart(6)} ${event.occurredAt} ${event.type}${correlationLabel}`);
82
+ }
83
+ lines.push('', 'Diagnosis:');
84
+ if (!trace.diagnoses.length)
85
+ lines.push(' No incomplete control-plane transition detected.');
86
+ else
87
+ for (const diagnosis of trace.diagnoses)
88
+ lines.push(` - ${diagnosis}`);
89
+ return `${lines.join('\n')}\n`;
90
+ }
@@ -34,9 +34,11 @@ export function registerResolveOrgPlanAttention(app, deps) {
34
34
  ? 'ATTENTION_OPERATION_STALE'
35
35
  : outcome.kind === 'legacyUnsupported'
36
36
  ? 'ATTENTION_LEGACY_UNSUPPORTED'
37
- : outcome.kind === 'writerCleared'
38
- ? 'ATTENTION_WRITER_CLEARED'
39
- : 'ATTENTION_WRITER_UNAVAILABLE';
37
+ : outcome.kind === 'replacementRejected'
38
+ ? 'EXECUTOR_REPLACEMENT_NOT_AUTHORIZED'
39
+ : outcome.kind === 'writerCleared'
40
+ ? 'ATTENTION_WRITER_CLEARED'
41
+ : 'ATTENTION_WRITER_UNAVAILABLE';
40
42
  return reply.code(outcome.kind === 'noActive' ? 404 : 409).send({ code });
41
43
  });
42
44
  }
@@ -40,7 +40,8 @@ export function registerListAvailableSkills(app, deps) {
40
40
  return {
41
41
  source: project ? 'project' : 'native',
42
42
  errors: discovered.errors.map(({ message }) => ({ message })),
43
- skills: [...skills]
43
+ skills: skills
44
+ .filter((skill) => !isAlwaysAdvertisedSkill(skill))
44
45
  .sort((left, right) => left.path.localeCompare(right.path))
45
46
  .map((skill) => ({
46
47
  name: skill.name,
@@ -56,7 +56,10 @@ export const availableSkillSchema = z
56
56
  .strict();
57
57
  /** Gestalt workflow skills are session infrastructure, not optional profile entries. */
58
58
  export function isAlwaysAdvertisedSkill(skill) {
59
- return skill.name.startsWith('gestalt:');
59
+ return isGestaltSkillName(skill.name);
60
+ }
61
+ export function isGestaltSkillName(name) {
62
+ return name.startsWith('gestalt:');
60
63
  }
61
64
  const profileDocumentSchema = z
62
65
  .object({
@@ -195,8 +198,6 @@ export function reconcileSkillSelectionSnapshot(discovered, selection) {
195
198
  */
196
199
  export function compileSkillOverride(input) {
197
200
  const effective = selectEffectiveSkillSelection(input);
198
- if (effective.selection === undefined)
199
- return { source: 'native', skillsConfig: undefined, warnings: [] };
200
201
  const reconciled = reconcileSkillSelectionSnapshot(input.discovered, effective.selection);
201
202
  return {
202
203
  source: effective.source,
@@ -211,7 +212,10 @@ export function createSkillProfile(input) {
211
212
  return {
212
213
  version: 1,
213
214
  name: normalizeSkillProfileName(input.name),
214
- skills: createSkillSelection(input.skills),
215
+ // Gestalt skills are mandatory thread infrastructure. Legacy profiles are
216
+ // normalized on read so they cannot pin a stale plugin-cache version or
217
+ // expose a meaningless enable/disable control in the editor.
218
+ skills: createSkillSelection(input.skills.filter((skill) => !isGestaltSkillName(skill.name))),
215
219
  };
216
220
  }
217
221
  /** Parse the version-1 YAML document without performing filesystem access. */
@@ -3,16 +3,6 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
- export function profileAppServerCommand(skillsConfig) {
7
- const base = { command: 'codex', args: ['app-server', '--stdio'] };
8
- return skillsConfig === undefined
9
- ? base
10
- : {
11
- ...base,
12
- args: [
13
- ...base.args,
14
- '--config',
15
- `skills.config = [${skillsConfig.map((entry) => `{ path = ${JSON.stringify(entry.path)}, enabled = ${entry.enabled} }`).join(', ')}]`,
16
- ],
17
- };
6
+ export function profileAppServerCommand() {
7
+ return { command: 'codex', args: ['app-server', '--stdio'] };
18
8
  }
@@ -147,10 +147,12 @@ function collaborationAction(tool) {
147
147
  spawnAgent: 'spawn_agent',
148
148
  sendInput: 'send_input',
149
149
  resumeAgent: 'resume_agent',
150
+ followupTask: 'resume_agent',
150
151
  closeAgent: 'close_agent',
151
152
  spawn_agent: 'spawn_agent',
152
153
  send_input: 'send_input',
153
154
  resume_agent: 'resume_agent',
155
+ followup_task: 'resume_agent',
154
156
  close_agent: 'close_agent',
155
157
  wait: 'wait',
156
158
  }[tool];
@@ -7,7 +7,7 @@ import { spawn } from 'node:child_process';
7
7
  import { JsonRpcClient } from './json-rpc-client.js';
8
8
  import { profileAppServerCommand } from '../catalog/profile-command.js';
9
9
  export function launchCodexAppServer(input) {
10
- const launch = profileAppServerCommand(input.skillsConfig);
10
+ const launch = profileAppServerCommand();
11
11
  const child = spawn(launch.command, launch.args, {
12
12
  cwd: input.cwd,
13
13
  env: codexChildEnvironment(input.environment),