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.
- package/dist/client/assets/{index-DoUX4ONm.js → index-DgSHf9eV.js} +18 -19
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.js +4 -7
- package/dist/server/server/features/autopilot/application/policy.js +38 -12
- package/dist/server/server/features/autopilot/application/service.js +85 -38
- package/dist/server/server/features/sessions/get-history/endpoint.js +2 -30
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +11 -2
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +3 -6
- package/dist/server/shared/contracts/autopilot-audit.js +38 -0
- package/package.json +1 -1
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
258
|
-
|
|
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.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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
|
|
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')
|
|
22
|
+
OR json_extract(payload_json, '$.state') = 'completed'
|
|
26
23
|
OR (
|
|
27
|
-
json_extract(payload_json, '$.state') = '
|
|
28
|
-
AND json_extract(payload_json, '$.reason')
|
|
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
|
+
}
|