gestalt-mobile 0.18.6 → 0.18.7

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,7 +16,7 @@ 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-DoUX4ONm.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-DgSHf9eV.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-CgSuVqbd.css">
21
21
  </head>
22
22
  <body>
@@ -90,7 +90,7 @@ export async function composeRelayApp(options) {
90
90
  const planMeasurementHelperPath = options.planMeasurementHelperPath ?? process.env.GESTALT_MOBILE_ORG_PLAN_HELPER;
91
91
  const withPendingInteractions = (session) => (session ? { ...session, pendingInteractions: interactions.list(session.id) } : null);
92
92
  const events = new SessionEventBus();
93
- let notifyAutopilotSettled = () => undefined;
93
+ let notifyAutopilotActivity = () => undefined;
94
94
  const attentionTransitions = {
95
95
  subscribe: (sessionId, listener) => events.subscribe(sessionId, (event) => {
96
96
  if (event.type !== 'org-plan.attention-required' &&
@@ -113,10 +113,7 @@ export async function composeRelayApp(options) {
113
113
  options.onAttentionTransitions?.(attentionTransitions);
114
114
  const activity = new AgentActivityRegistry((snapshot, occurredAt) => {
115
115
  events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', snapshot, occurredAt));
116
- if (snapshot.confidence === 'fresh' &&
117
- ['idle', 'blocked'].includes(snapshot.root.state) &&
118
- ['idle', 'blocked'].includes(snapshot.aggregateSubagents))
119
- notifyAutopilotSettled(snapshot.sessionId);
116
+ notifyAutopilotActivity(snapshot.sessionId);
120
117
  }, {
121
118
  // Evidence arms one bounded reconciliation; healthy sessions are never polled.
122
119
  schedule: options.activitySchedule ??
@@ -216,7 +213,7 @@ export async function composeRelayApp(options) {
216
213
  events.publish(journal.append(sessionId, type, payload, occurredAt, outboxId));
217
214
  },
218
215
  });
219
- notifyAutopilotSettled = (sessionId) => autopilot.activitySettled(sessionId);
216
+ notifyAutopilotActivity = (sessionId) => autopilot.activityChanged(sessionId);
220
217
  options.onAutopilotCoordinator?.(autopilot);
221
218
  const workspaces = new FilesystemWorkspaceCatalog(root);
222
219
  const models = new CodexModelCatalog(root, options.launchAppServer ?? launchCodexAppServer);
@@ -298,7 +295,7 @@ export async function composeRelayApp(options) {
298
295
  }
299
296
  events.publish(journal.append(sessionId, 'plan.updated', { plan: update.plan, reason: update.reason }, occurredAt));
300
297
  }
301
- autopilot.planUpdated(sessionId);
298
+ autopilot.planStatusChanged(sessionId);
302
299
  };
303
300
  runtime = options.startAppServers
304
301
  ? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification) => {
@@ -9,6 +9,8 @@ export const defaultAutopilotPolicy = Object.freeze({
9
9
  quiescenceMs: 1_000,
10
10
  staleAfterMs: 30_000,
11
11
  retryLimit: 3,
12
+ actionLimit: 12,
13
+ actionWindowMs: 10 * 60_000,
12
14
  backoffMs: (attempt) => Math.min(60_000, 1_000 * 2 ** Math.max(0, attempt)),
13
15
  promptVersion: AUTOPILOT_PROMPT_VERSION,
14
16
  });
@@ -18,32 +20,56 @@ export function executionComplete(plan) {
18
20
  step.reviewStatus === 'REVIEWED' &&
19
21
  step.children.every((child) => child.state === 'DONE')));
20
22
  }
23
+ /** Classifies fresh actor topology; the caller owns freshness and durable interaction checks. */
24
+ export function classifyAgentActivity(activity) {
25
+ if (activity.root.state === 'awaitingHuman' || activity.aggregateSubagents === 'awaitingHuman')
26
+ return 'attention';
27
+ if (activity.root.state === 'disconnected' || activity.aggregateSubagents === 'disconnected')
28
+ return 'reconcile';
29
+ if (activity.root.state === 'working' ||
30
+ activity.aggregateSubagents === 'working' ||
31
+ activity.aggregateSubagents === 'awaitingAgent')
32
+ return 'active';
33
+ const rootSettled = activity.root.state === 'idle' ||
34
+ activity.root.state === 'blocked' ||
35
+ activity.root.state === 'awaitingAgent';
36
+ const subagentsSettled = activity.aggregateSubagents === 'idle' || activity.aggregateSubagents === 'blocked';
37
+ return rootSettled && subagentsSettled ? 'settled' : 'observe';
38
+ }
21
39
  /** A deliberately pure, exhaustive safety gate. Adapters may only enact this result. */
22
40
  export function decideAutopilot(input) {
23
41
  const { state, plan, activity, hasPendingInteraction, now, policy } = input;
42
+ if (input.hasActiveAttention || state.state === 'attentionRequired')
43
+ return {
44
+ kind: 'requestAttention',
45
+ reason: state.stopReason === 'noPlanProgress' ||
46
+ state.stopReason === 'reconcileFailed' ||
47
+ state.stopReason === 'startUnavailable' ||
48
+ state.stopReason === 'actionRateExceeded'
49
+ ? state.stopReason
50
+ : 'attentionRequired',
51
+ };
24
52
  if (!state.requestedEnabled)
25
53
  return { kind: 'disable', reason: 'manualDisabled' };
26
54
  if (!plan)
27
55
  return { kind: 'disable', reason: 'planRequired' };
28
56
  if (executionComplete(plan))
29
57
  return { kind: 'complete' };
30
- if (hasPendingInteraction || input.hasActiveAttention || state.state === 'attentionRequired')
58
+ if (hasPendingInteraction)
31
59
  return { kind: 'requestAttention', reason: 'attentionRequired' };
32
- if ((input.planIdentity && state.planIdentity && input.planIdentity !== state.planIdentity) ||
33
- (input.planFingerprint &&
34
- state.planFingerprint &&
35
- input.planFingerprint !== state.planFingerprint))
36
- return { kind: 'observe' };
37
60
  if (!activity || activity.confidence !== 'fresh')
38
61
  return { kind: 'reconcile' };
39
62
  if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
40
63
  return { kind: 'reconcile' };
41
- const rootSettled = activity.root.state === 'idle' || activity.root.state === 'blocked';
42
- const subagentsSettled = activity.aggregateSubagents === 'idle' || activity.aggregateSubagents === 'blocked';
43
- if (!rootSettled || !subagentsSettled)
44
- return activity.aggregateSubagents === 'disconnected'
45
- ? { kind: 'reconcile' }
46
- : { kind: 'observe' };
64
+ const disposition = classifyAgentActivity(activity);
65
+ if (disposition === 'attention')
66
+ return { kind: 'requestAttention', reason: 'attentionRequired' };
67
+ if (disposition === 'reconcile')
68
+ return { kind: 'reconcile' };
69
+ if (disposition !== 'settled')
70
+ return { kind: 'observe' };
71
+ if ((input.automaticActionCount ?? 0) >= policy.actionLimit)
72
+ return { kind: 'requestAttention', reason: 'actionRateExceeded' };
47
73
  // The outcome is intentionally interpreted only through durable lack of plan
48
74
  // progress: a failed or unknown automatic turn may retry within the same
49
75
  // bounded budget, while a completed turn with no fingerprint change does too.
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
6
  import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
7
- import { decideAutopilot, executionComplete } from './policy.js';
7
+ import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
8
8
  export class AutopilotCoordinator {
9
9
  deps;
10
10
  timers = new Map();
@@ -89,7 +89,6 @@ export class AutopilotCoordinator {
89
89
  const nextFingerprint = fingerprint(currentPlan.plan);
90
90
  if (prior.requestedEnabled &&
91
91
  prior.planIdentity === currentPlan.identity &&
92
- prior.planFingerprint === nextFingerprint &&
93
92
  prior.state !== 'attentionRequired')
94
93
  return this.snapshot(sessionId);
95
94
  const next = {
@@ -111,7 +110,8 @@ export class AutopilotCoordinator {
111
110
  disable(sessionId) {
112
111
  const now = this.deps.now();
113
112
  const prior = this.deps.store.find(sessionId) ?? disabledAutopilot(sessionId, now);
114
- if (!prior.requestedEnabled && prior.state === 'disabled')
113
+ const cancelled = this.cancelScheduledControl(prior, now);
114
+ if (!prior.requestedEnabled && prior.state === 'disabled' && !cancelled)
115
115
  return this.snapshot(sessionId);
116
116
  const next = {
117
117
  ...prior,
@@ -119,10 +119,11 @@ export class AutopilotCoordinator {
119
119
  requestedEnabled: false,
120
120
  generation: prior.generation + 1,
121
121
  nextEvaluationAt: null,
122
+ ...(cancelled ? { lastControlId: null } : {}),
122
123
  stopReason: 'manualDisabled',
123
124
  updatedAt: now,
124
125
  };
125
- this.persist(next);
126
+ this.persist(next, cancelled);
126
127
  this.cancelTimer(sessionId);
127
128
  return this.snapshot(sessionId);
128
129
  }
@@ -130,15 +131,18 @@ export class AutopilotCoordinator {
130
131
  const prior = this.deps.store.find(sessionId);
131
132
  if (!prior)
132
133
  return;
134
+ const now = this.deps.now();
135
+ const cancelled = this.cancelScheduledControl(prior, now);
133
136
  this.persist({
134
137
  ...prior,
135
138
  state: 'disabled',
136
139
  requestedEnabled: false,
137
140
  generation: prior.generation + 1,
138
141
  nextEvaluationAt: null,
142
+ ...(cancelled ? { lastControlId: null } : {}),
139
143
  stopReason: reason,
140
- updatedAt: this.deps.now(),
141
- });
144
+ updatedAt: now,
145
+ }, cancelled);
142
146
  this.cancelTimer(sessionId);
143
147
  }
144
148
  evaluate(sessionId) {
@@ -241,41 +245,71 @@ export class AutopilotCoordinator {
241
245
  decision.kind === 'complete' ||
242
246
  decision.kind === 'disable')
243
247
  this.cancelTimer(sessionId);
244
- if (next !== prior)
245
- this.persist(next);
248
+ if (next !== prior) {
249
+ const cancelled = decision.kind === 'requestAttention' ||
250
+ decision.kind === 'complete' ||
251
+ decision.kind === 'disable'
252
+ ? this.cancelScheduledControl(prior, now)
253
+ : undefined;
254
+ this.persist(cancelled ? { ...next, lastControlId: null } : next, cancelled);
255
+ }
246
256
  return this.snapshot(sessionId);
247
257
  }
248
- planUpdated(sessionId) {
258
+ turnCompleted(sessionId) {
259
+ this.activitySettled(sessionId);
260
+ }
261
+ /** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
262
+ planStatusChanged(sessionId) {
249
263
  const prior = this.deps.store.find(sessionId);
264
+ if (!prior?.requestedEnabled)
265
+ return;
250
266
  const plan = this.deps.plan(sessionId);
251
- if (!prior || !plan)
267
+ if (!plan) {
268
+ this.cancel(sessionId, 'planRemoved');
252
269
  return;
270
+ }
253
271
  if (prior.planIdentity && prior.planIdentity !== plan.identity) {
254
272
  this.cancel(sessionId, 'planReplaced');
255
273
  return;
256
274
  }
257
- const nextFingerprint = fingerprint(plan.plan);
258
- if (prior.planFingerprint !== nextFingerprint) {
275
+ if (executionComplete(plan.plan))
276
+ this.evaluate(sessionId);
277
+ }
278
+ /** Reacts only to fresh actor status; plan mutations are not scheduling signals. */
279
+ activityChanged(sessionId) {
280
+ const prior = this.deps.store.find(sessionId);
281
+ if (!prior?.requestedEnabled)
282
+ return;
283
+ const activity = this.deps.activity(sessionId);
284
+ if (!activity || activity.confidence !== 'fresh')
285
+ return;
286
+ const disposition = classifyAgentActivity(activity);
287
+ if (disposition === 'attention') {
288
+ this.evaluate(sessionId);
289
+ return;
290
+ }
291
+ if (disposition === 'active') {
292
+ this.cancelTimer(sessionId);
259
293
  const now = this.deps.now();
260
- this.persist({
261
- ...prior,
262
- planIdentity: plan.identity,
263
- planFingerprint: nextFingerprint,
264
- consecutiveNoProgress: 0,
265
- updatedAt: now,
266
- }, undefined, [
267
- {
268
- sessionId,
269
- type: 'autopilot.progress-reset',
270
- payload: { reason: 'planUpdated' },
271
- occurredAt: now,
272
- },
273
- ]);
294
+ const cancelled = this.cancelScheduledControl(prior, now);
295
+ const subagentsWorking = activity.aggregateSubagents === 'working' ||
296
+ activity.aggregateSubagents === 'awaitingAgent';
297
+ if (cancelled ||
298
+ prior.state !== 'monitoring' ||
299
+ prior.nextEvaluationAt ||
300
+ (subagentsWorking && prior.consecutiveNoProgress > 0))
301
+ this.persist({
302
+ ...prior,
303
+ state: 'monitoring',
304
+ ...(cancelled ? { generation: prior.generation + 1, lastControlId: null } : {}),
305
+ ...(subagentsWorking ? { consecutiveNoProgress: 0 } : {}),
306
+ nextEvaluationAt: null,
307
+ updatedAt: now,
308
+ }, cancelled);
309
+ return;
274
310
  }
275
- this.evaluate(sessionId);
276
- }
277
- turnCompleted(sessionId) {
278
- this.activitySettled(sessionId);
311
+ if (disposition === 'settled')
312
+ this.activitySettled(sessionId);
279
313
  }
280
314
  activitySettled(sessionId) {
281
315
  const prior = this.deps.store.find(sessionId);
@@ -295,14 +329,16 @@ export class AutopilotCoordinator {
295
329
  const prior = this.deps.store.find(sessionId);
296
330
  if (!prior || !prior.requestedEnabled)
297
331
  return;
332
+ const now = this.deps.now();
333
+ const cancelled = this.cancelScheduledControl(prior, now);
298
334
  this.persist({
299
335
  ...prior,
300
336
  state: 'monitoring',
301
337
  generation: prior.generation + 1,
302
338
  nextEvaluationAt: null,
303
339
  lastControlId: null,
304
- updatedAt: this.deps.now(),
305
- });
340
+ updatedAt: now,
341
+ }, cancelled);
306
342
  }
307
343
  recordControlIssued(sessionId, controlId) {
308
344
  const prior = this.deps.store.find(sessionId);
@@ -357,12 +393,15 @@ export class AutopilotCoordinator {
357
393
  if (session?.activeTurnId ||
358
394
  this.deps.pendingInteraction(sessionId) ||
359
395
  this.decision(sessionId, current).kind !== 'scheduleContinuation') {
396
+ const now = this.deps.now();
397
+ const cancelled = this.cancelScheduledControl(current, now);
360
398
  this.persist({
361
399
  ...current,
362
400
  state: 'monitoring',
401
+ ...(cancelled ? { generation: current.generation + 1, lastControlId: null } : {}),
363
402
  nextEvaluationAt: null,
364
- updatedAt: this.deps.now(),
365
- });
403
+ updatedAt: now,
404
+ }, cancelled);
366
405
  this.evaluate(sessionId);
367
406
  return;
368
407
  }
@@ -398,7 +437,7 @@ export class AutopilotCoordinator {
398
437
  requestedEnabled: false,
399
438
  generation: latest.generation + 1,
400
439
  nextEvaluationAt: null,
401
- stopReason: 'attentionRequired',
440
+ stopReason: 'startUnavailable',
402
441
  updatedAt: this.deps.now(),
403
442
  });
404
443
  return;
@@ -412,6 +451,14 @@ export class AutopilotCoordinator {
412
451
  this.completionTimers.get(sessionId)?.();
413
452
  this.completionTimers.delete(sessionId);
414
453
  }
454
+ cancelScheduledControl(state, updatedAt) {
455
+ if (!state.lastControlId)
456
+ return undefined;
457
+ const control = this.deps.store.findControl(state.sessionId, state.lastControlId);
458
+ return control?.status === 'scheduled'
459
+ ? { ...control, status: 'cancelled', updatedAt }
460
+ : undefined;
461
+ }
415
462
  updateControl(sessionId, controlId, status, failureCode, turnId = null, eventType) {
416
463
  const control = this.deps.store.findControl(sessionId, controlId);
417
464
  if (!control)
@@ -503,16 +550,16 @@ export class AutopilotCoordinator {
503
550
  }
504
551
  decision(sessionId, state) {
505
552
  const plan = this.deps.plan(sessionId);
553
+ const now = this.deps.now();
506
554
  return decideAutopilot({
507
555
  state,
508
556
  plan: plan?.plan ?? null,
509
- planIdentity: plan?.identity,
510
- planFingerprint: plan ? fingerprint(plan.plan) : null,
511
557
  activity: this.deps.activity(sessionId),
512
558
  hasPendingInteraction: this.deps.pendingInteraction(sessionId),
513
559
  hasActiveAttention: state.state === 'attentionRequired',
514
560
  lastTurnOutcome: this.lastTurnOutcome(sessionId, state.lastControlId),
515
- now: this.deps.now(),
561
+ automaticActionCount: this.deps.store.automaticActionsSince?.(sessionId, new Date(Date.parse(now) - this.deps.policy.actionWindowMs).toISOString()) ?? 0,
562
+ now,
516
563
  policy: this.deps.policy,
517
564
  });
518
565
  }
@@ -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 { autopilotAuditLabel } from '../../../../shared/contracts/autopilot-audit.js';
6
7
  import { toChatItems, toChatTurns } from './history-mapper.js';
7
8
  /**
8
9
  * ChatSnapshot is a lower-bound cut: baseSequence is sampled before the
@@ -63,42 +64,13 @@ export function registerGetHistory(app, deps) {
63
64
  function isBoundedAudit(source) {
64
65
  return !Array.isArray(source) && 'events' in source;
65
66
  }
66
- const auditLabels = {
67
- 'autopilot.continuation-scheduled': 'Autopilot scheduled a continuation',
68
- 'autopilot.control-issued': 'Autopilot issued an automatic continuation.',
69
- 'autopilot.turn-started': 'Autopilot continuation started',
70
- 'autopilot.turn-failed': 'Autopilot continuation failed',
71
- 'autopilot.progress-reset': 'Autopilot reset retry progress after the plan changed',
72
- 'org-plan.attention-required': 'Autopilot needs attention',
73
- 'org-plan.attention-resolved': 'Attention resolved',
74
- };
75
- function snapshotAuditLabel(payload) {
76
- if (!payload || typeof payload !== 'object')
77
- return null;
78
- const { state, reason } = payload;
79
- if (state === 'backoff')
80
- return 'Autopilot is backing off';
81
- if (state === 'attentionRequired')
82
- return 'Autopilot needs attention';
83
- if (state === 'completed')
84
- return 'Autopilot completed the plan';
85
- if (state === 'disabled' && reason === 'planRequired')
86
- return 'Autopilot requires an incomplete supervised plan';
87
- return null;
88
- }
89
67
  /** Maps journal vocabulary to a deliberately redacted, durable timeline projection. */
90
68
  function toAutopilotAudit(events) {
91
69
  return events.flatMap((event) => {
92
70
  const occurredAt = Date.parse(event.occurredAt);
93
71
  if (!Number.isFinite(occurredAt))
94
72
  return [];
95
- const label = event.type === 'org-plan.attention-resolved' &&
96
- event.payload &&
97
- typeof event.payload === 'object' &&
98
- event.payload.outcome === 'failed'
99
- ? 'Attention resolution failed'
100
- : (auditLabels[event.type] ??
101
- (event.type === 'autopilot.updated' ? snapshotAuditLabel(event.payload) : null));
73
+ const label = autopilotAuditLabel(event.type, event.payload);
102
74
  if (!label)
103
75
  return [];
104
76
  const controlId = event.payload &&
@@ -23,6 +23,8 @@ export class SqliteAutopilotStore {
23
23
  'attentionRequired',
24
24
  'noPlanProgress',
25
25
  'reconcileFailed',
26
+ 'startUnavailable',
27
+ 'actionRateExceeded',
26
28
  'planRemoved',
27
29
  'planReplaced',
28
30
  'sessionEnded',
@@ -57,7 +59,8 @@ export class SqliteAutopilotStore {
57
59
  const row = this.db
58
60
  .prepare('SELECT * FROM autopilot_controls WHERE session_id = ? AND control_id = ?')
59
61
  .get(sessionId, controlId);
60
- if (!row || !['scheduled', 'issued', 'started', 'failed'].includes(String(row.status)))
62
+ if (!row ||
63
+ !['scheduled', 'issued', 'started', 'failed', 'cancelled'].includes(String(row.status)))
61
64
  return null;
62
65
  const failureCode = row.failure_code === null ? null : String(row.failure_code);
63
66
  if (failureCode !== null && !['START_FAILED', 'START_UNAVAILABLE'].includes(failureCode))
@@ -74,7 +77,7 @@ export class SqliteAutopilotStore {
74
77
  }
75
78
  saveControl(control) {
76
79
  this.db
77
- .prepare("INSERT INTO autopilot_controls (session_id,control_id,status,created_at,updated_at,failure_code,turn_id) VALUES (?,?,?,?,?,?,?) ON CONFLICT(session_id,control_id) DO UPDATE SET status=excluded.status,updated_at=excluded.updated_at,failure_code=excluded.failure_code,turn_id=COALESCE(excluded.turn_id,autopilot_controls.turn_id) WHERE CASE autopilot_controls.status WHEN 'scheduled' THEN 0 WHEN 'issued' THEN 1 WHEN 'started' THEN 2 WHEN 'failed' THEN 2 END <= CASE excluded.status WHEN 'scheduled' THEN 0 WHEN 'issued' THEN 1 WHEN 'started' THEN 2 WHEN 'failed' THEN 2 END")
80
+ .prepare("INSERT INTO autopilot_controls (session_id,control_id,status,created_at,updated_at,failure_code,turn_id) VALUES (?,?,?,?,?,?,?) ON CONFLICT(session_id,control_id) DO UPDATE SET status=excluded.status,updated_at=excluded.updated_at,failure_code=excluded.failure_code,turn_id=COALESCE(excluded.turn_id,autopilot_controls.turn_id) WHERE autopilot_controls.status = excluded.status OR (autopilot_controls.status = 'scheduled' AND excluded.status IN ('issued','cancelled')) OR (autopilot_controls.status = 'issued' AND excluded.status IN ('started','failed'))")
78
81
  .run(control.sessionId, control.controlId, control.status, control.createdAt, control.updatedAt, control.failureCode, control.turnId ?? null);
79
82
  }
80
83
  commit(input) {
@@ -140,6 +143,12 @@ export class SqliteAutopilotStore {
140
143
  .prepare("SELECT turn_id,control_id FROM autopilot_controls WHERE session_id = ? AND status = 'started' AND turn_id IS NOT NULL")
141
144
  .all(sessionId).map((row) => [row.turn_id, row.control_id]));
142
145
  }
146
+ automaticActionsSince(sessionId, since) {
147
+ const row = this.db
148
+ .prepare("SELECT count(*) AS count FROM autopilot_controls WHERE session_id = ? AND status IN ('issued','started','failed') AND updated_at >= ?")
149
+ .get(sessionId, since);
150
+ return row.count;
151
+ }
143
152
  controlIds(sessionId) {
144
153
  return new Set(this.db
145
154
  .prepare('SELECT control_id FROM autopilot_controls WHERE session_id = ?')
@@ -4,11 +4,8 @@
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
6
  export const autopilotAuditEventTypes = [
7
- 'autopilot.continuation-scheduled',
8
- 'autopilot.control-issued',
9
7
  'autopilot.turn-started',
10
8
  'autopilot.turn-failed',
11
- 'autopilot.progress-reset',
12
9
  'autopilot.updated',
13
10
  'org-plan.attention-required',
14
11
  'org-plan.attention-resolved',
@@ -22,10 +19,10 @@ const renderableAutopilotAuditWhere = `
22
19
  type IN (${autopilotAuditEventTypes.map(() => '?').join(',')})
23
20
  AND (
24
21
  type <> 'autopilot.updated'
25
- OR json_extract(payload_json, '$.state') IN ('backoff', 'attentionRequired', 'completed')
22
+ OR json_extract(payload_json, '$.state') = 'completed'
26
23
  OR (
27
- json_extract(payload_json, '$.state') = 'disabled'
28
- AND json_extract(payload_json, '$.reason') = 'planRequired'
24
+ json_extract(payload_json, '$.state') = 'attentionRequired'
25
+ AND json_extract(payload_json, '$.reason') IN ('noPlanProgress', 'reconcileFailed', 'actionRateExceeded')
29
26
  )
30
27
  )`;
31
28
  export class SqliteEventJournal {
@@ -0,0 +1,38 @@
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
+ /** Maps durable events to the intentionally small user-visible Autopilot timeline. */
7
+ export function autopilotAuditLabel(type, payload) {
8
+ if (type === 'autopilot.turn-started')
9
+ return 'Continued execution automatically';
10
+ if (type === 'autopilot.turn-failed') {
11
+ const code = property(payload, 'code');
12
+ return code === 'START_UNAVAILABLE'
13
+ ? 'Automatic continuation could not start: session runtime unavailable'
14
+ : 'Automatic continuation failed';
15
+ }
16
+ if (type === 'org-plan.attention-required')
17
+ return 'Needs attention';
18
+ if (type === 'org-plan.attention-resolved')
19
+ return property(payload, 'outcome') === 'failed'
20
+ ? 'Attention resolution failed'
21
+ : 'Attention resolved';
22
+ if (type !== 'autopilot.updated')
23
+ return null;
24
+ const state = property(payload, 'state');
25
+ const reason = property(payload, 'reason');
26
+ if (state === 'completed')
27
+ return 'Completed the supervised plan';
28
+ if (state === 'attentionRequired' && reason === 'noPlanProgress')
29
+ return 'Automatic continuation stopped: no agent progress';
30
+ if (state === 'attentionRequired' && reason === 'reconcileFailed')
31
+ return 'Automatic continuation stopped: agent status unavailable';
32
+ if (state === 'attentionRequired' && reason === 'actionRateExceeded')
33
+ return 'Automatic continuation stopped: too many automatic actions';
34
+ return null;
35
+ }
36
+ function property(value, key) {
37
+ return value && typeof value === 'object' ? value[key] : undefined;
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.18.6",
3
+ "version": "0.18.7",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",