gestalt-mobile 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/index-C79AKZvE.css +1 -0
- package/dist/client/assets/index-mKzhyiTl.js +33 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/composition.js +76 -7
- package/dist/server/server/features/autopilot/application/policy.js +21 -3
- package/dist/server/server/features/autopilot/application/service.js +299 -13
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +50 -1
- package/dist/server/server/features/autopilot/domain/supervision-protocol.js +203 -0
- package/dist/server/server/features/org-plan-checkpoint/application/validate.js +20 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +1 -0
- package/dist/server/server/features/sessions/interaction/kind.js +2 -0
- package/dist/server/server/features/sessions/interaction/response-validator.js +4 -1
- package/dist/server/server/platform/codex/normalizer.js +22 -17
- package/dist/server/server/platform/codex/server-request.js +20 -0
- package/dist/server/server/platform/codex/session-runtime.js +61 -4
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +16 -3
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +35 -0
- package/dist/server/shared/contracts/autopilot-wait-lease.js +62 -0
- package/dist/server/shared/contracts/org-plan-checkpoint.js +131 -0
- package/package.json +1 -1
- package/dist/client/assets/index-B7x1bL59.css +0 -1
- package/dist/client/assets/index-Bz23puhw.js +0 -33
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-mKzhyiTl.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C79AKZvE.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="app"></div>
|
|
@@ -35,7 +35,9 @@ import { mapWithConcurrency } from './platform/runtime/concurrency.js';
|
|
|
35
35
|
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
|
-
import { resolvedServerRequestId, toPendingInteraction } from './platform/codex/server-request.js';
|
|
38
|
+
import { isAutopilotWaitLeaseCall, 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';
|
|
@@ -49,9 +51,10 @@ import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-com
|
|
|
49
51
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
50
52
|
import { SqliteAutopilotStore } from './platform/persistence/sqlite-autopilot-store.js';
|
|
51
53
|
import { AutopilotCoordinator } from './features/autopilot/application/service.js';
|
|
52
|
-
import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
|
|
54
|
+
import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, autopilotExecutorLaunchPrompt, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
|
|
53
55
|
import { createRelyingPartyConfig } from './config.js';
|
|
54
56
|
import { parseOrgPlanAttention } from '../shared/contracts/org-plan-attention.js';
|
|
57
|
+
import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
|
|
55
58
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
56
59
|
export async function composeRelayApp(options) {
|
|
57
60
|
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
@@ -217,7 +220,7 @@ export async function composeRelayApp(options) {
|
|
|
217
220
|
throw new Error('AUTOPILOT_START_UNAVAILABLE');
|
|
218
221
|
await options.autopilotBeforeTurnAccepted?.({ sessionId, controlId });
|
|
219
222
|
const continuationPrompt = launchIdentity
|
|
220
|
-
?
|
|
223
|
+
? autopilotExecutorLaunchPrompt(launchIdentity)
|
|
221
224
|
: AUTOPILOT_CONTINUATION_PROMPT;
|
|
222
225
|
const started = await runtime.startTurn(writer.session, continuationPrompt, controlId, new Date().toISOString());
|
|
223
226
|
if (!started.activeTurnId)
|
|
@@ -333,6 +336,7 @@ export async function composeRelayApp(options) {
|
|
|
333
336
|
events.publish(journal.append(sessionId, 'interaction.resolved', { requestId, turnId: interaction?.turnId ?? null, resolvedAt: occurredAt, outcome }, occurredAt));
|
|
334
337
|
if (interaction?.kind === 'orgPlanAttention')
|
|
335
338
|
events.publish(journal.append(sessionId, 'org-plan.attention-resolved', { requestId, turnId: interaction.turnId ?? null, resolvedAt: occurredAt, outcome }, occurredAt));
|
|
339
|
+
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
336
340
|
};
|
|
337
341
|
const publishAttentionSettlement = (sessionId, requestId, occurredAt, outcome) => {
|
|
338
342
|
const remaining = interactions.list(sessionId);
|
|
@@ -379,7 +383,7 @@ export async function composeRelayApp(options) {
|
|
|
379
383
|
autopilot.planStatusChanged(sessionId);
|
|
380
384
|
};
|
|
381
385
|
runtime = options.startAppServers
|
|
382
|
-
? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification) => {
|
|
386
|
+
? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification, origin) => {
|
|
383
387
|
const occurredAt = new Date().toISOString();
|
|
384
388
|
for (const activityFact of decodeAgentActivityFacts(sessionId, occurredAt, notification))
|
|
385
389
|
activity.observe(activityFact);
|
|
@@ -395,7 +399,18 @@ export async function composeRelayApp(options) {
|
|
|
395
399
|
}
|
|
396
400
|
}
|
|
397
401
|
const currentSession = sessions.find(sessionId);
|
|
398
|
-
const
|
|
402
|
+
const completionTurnId = notification.method === 'turn/completed' &&
|
|
403
|
+
notification.params &&
|
|
404
|
+
typeof notification.params === 'object' &&
|
|
405
|
+
typeof notification.params.turn?.id === 'string'
|
|
406
|
+
? notification.params.turn.id
|
|
407
|
+
: undefined;
|
|
408
|
+
// A completion with no thread identity is only root-owned when it exactly
|
|
409
|
+
// matches the durable active root turn; unknown child turns never fall back.
|
|
410
|
+
const resolvedOrigin = origin.kind === 'unknown' && completionTurnId === currentSession?.activeTurnId
|
|
411
|
+
? { ...origin, kind: 'root' }
|
|
412
|
+
: origin;
|
|
413
|
+
const normalized = normalizeCodexNotification(sessionId, 0, occurredAt, notification, currentSession?.workspacePath, currentSession?.activeTurnId, resolvedOrigin);
|
|
399
414
|
if (!normalized)
|
|
400
415
|
return;
|
|
401
416
|
let completedSession;
|
|
@@ -412,11 +427,60 @@ export async function composeRelayApp(options) {
|
|
|
412
427
|
events.publish(journal.append(sessionId, normalized.type, normalized.payload, normalized.occurredAt));
|
|
413
428
|
if (completedSession)
|
|
414
429
|
events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
|
|
415
|
-
}, (sessionId, request) => {
|
|
430
|
+
}, (sessionId, request, origin) => {
|
|
416
431
|
const rawInteraction = toPendingInteraction(request);
|
|
417
432
|
const session = withPendingInteractions(sessions.find(sessionId));
|
|
418
|
-
if (!
|
|
433
|
+
if (!session)
|
|
434
|
+
return false;
|
|
435
|
+
if (!rawInteraction && isAutopilotWaitLeaseCall(request)) {
|
|
436
|
+
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
437
|
+
if (rootOwned)
|
|
438
|
+
autopilot.rejectProbe(sessionId);
|
|
439
|
+
return (runtime?.resolveServerRequest(sessionId, String(request.id), autopilotWaitLeaseToolResponse()) === true);
|
|
440
|
+
}
|
|
441
|
+
if (!rawInteraction)
|
|
419
442
|
return false;
|
|
443
|
+
if (rawInteraction.kind === 'autopilotWaitLease') {
|
|
444
|
+
const lease = rawInteraction.payload;
|
|
445
|
+
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
446
|
+
const accepted = rootOwned &&
|
|
447
|
+
autopilot.reportProbe(sessionId, {
|
|
448
|
+
id: lease.reportId,
|
|
449
|
+
kind: 'wait',
|
|
450
|
+
leaseId: lease.leaseId,
|
|
451
|
+
wakeConditions: lease.wakeConditions,
|
|
452
|
+
});
|
|
453
|
+
if (!accepted) {
|
|
454
|
+
if (rootOwned)
|
|
455
|
+
autopilot.rejectProbe(sessionId);
|
|
456
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse()) === true);
|
|
457
|
+
}
|
|
458
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse()) === true);
|
|
459
|
+
}
|
|
460
|
+
if (rawInteraction.kind === 'orgPlanCheckpoint') {
|
|
461
|
+
const checkpoint = parseOrgPlanCheckpoint(rawInteraction.payload);
|
|
462
|
+
const retained = supervisedPlans.find(sessionId);
|
|
463
|
+
const identity = supervisedPlans.identity(sessionId);
|
|
464
|
+
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
465
|
+
if (!checkpoint ||
|
|
466
|
+
!retained ||
|
|
467
|
+
!identity ||
|
|
468
|
+
checkpoint.planIdentity !== identity ||
|
|
469
|
+
!rootOwned)
|
|
470
|
+
return false;
|
|
471
|
+
const valid = validOrgPlanCheckpoint({
|
|
472
|
+
checkpoint,
|
|
473
|
+
plan: retained,
|
|
474
|
+
planIdentity: identity,
|
|
475
|
+
rootOwned,
|
|
476
|
+
hasActiveL1Writer: (position) => hasActiveL1Writer(activity.snapshot(sessionId, new Date().toISOString()), position),
|
|
477
|
+
});
|
|
478
|
+
if (!valid)
|
|
479
|
+
return false;
|
|
480
|
+
if (!autopilot.checkpointAccepted(sessionId, checkpoint, session.activeTurnId, new Date().toISOString()))
|
|
481
|
+
return false;
|
|
482
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse()) === true);
|
|
483
|
+
}
|
|
420
484
|
const interaction = {
|
|
421
485
|
...rawInteraction,
|
|
422
486
|
turnId: session.activeTurnId,
|
|
@@ -431,6 +495,7 @@ export async function composeRelayApp(options) {
|
|
|
431
495
|
? { attentionReason: interaction.payload.reason }
|
|
432
496
|
: {}),
|
|
433
497
|
});
|
|
498
|
+
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
434
499
|
const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
|
|
435
500
|
sessions.save(updated);
|
|
436
501
|
events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
|
|
@@ -959,3 +1024,7 @@ function resolveStateDatabasePath(root, stateHome) {
|
|
|
959
1024
|
const legacyPath = legacyRelayStatePath(root, stateHome);
|
|
960
1025
|
return existsSync(legacyPath) ? legacyPath : currentPath;
|
|
961
1026
|
}
|
|
1027
|
+
function hasActiveL1Writer(snapshot, position) {
|
|
1028
|
+
return snapshot.subagents.some((child) => child.canonicalPosition === position &&
|
|
1029
|
+
['working', 'awaitingAgent', 'awaitingHuman'].includes(child.state));
|
|
1030
|
+
}
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
export const AUTOPILOT_PROMPT_VERSION = '
|
|
7
|
-
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. For a subagent dedicated to that position, use the collaboration-safe task_name l<a> or l<a>_<b> and refer to it by its canonical L label. Invoke gestalt_org_plan_attention only for a decision-table blocker
|
|
6
|
+
export const AUTOPILOT_PROMPT_VERSION = 'v5';
|
|
7
|
+
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. For a subagent dedicated to that position, use the collaboration-safe task_name l<a> or l<a>_<b> and refer to it by its canonical L label. Invoke gestalt_org_plan_attention only for a decision-table blocker. When the Autopilot probe is active, register gestalt_autopilot_wait_lease with an observable wake condition, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
|
|
8
8
|
export const AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT = 'Continue the same assigned Org L1 from its durable state. A prior turn ending did not complete the objective. Consume any supplied process result, take the next legal L2 action, and report only at the L1 review boundary or through structured attention.';
|
|
9
|
+
/** Builds a physical launch instruction while keeping durable L1 identity canonical. */
|
|
10
|
+
export function autopilotExecutorLaunchPrompt(identity) {
|
|
11
|
+
const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition}; retain the canonical label in status and review output.`;
|
|
12
|
+
if (identity.generation === 1)
|
|
13
|
+
return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch}`;
|
|
14
|
+
return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch} This is replacement generation ${identity.generation}: the durable ${identity.canonicalTaskName} slot may remain reserved. Do not reuse that task_name or attempt to change an existing agent's model in place. Interrupt the previous physical executor if it is still running, then spawn ${identity.taskName} with agent_type worker and an explicit model selected by the supervisor. Transfer sole ${identity.canonicalPosition} ownership and continue from its durable Org Plan and worktree state.`;
|
|
15
|
+
}
|
|
9
16
|
export const defaultAutopilotPolicy = Object.freeze({
|
|
10
17
|
quiescenceMs: 1_000,
|
|
11
18
|
staleAfterMs: 30_000,
|
|
@@ -59,12 +66,23 @@ export function decideAutopilot(input) {
|
|
|
59
66
|
return { kind: 'disable', reason: 'manualDisabled' };
|
|
60
67
|
if (!plan)
|
|
61
68
|
return { kind: 'disable', reason: 'planRequired' };
|
|
62
|
-
if (executionComplete(plan))
|
|
69
|
+
if (executionComplete(plan)) {
|
|
70
|
+
// Checkpoint-aware sessions deliberately reserve a later root turn for
|
|
71
|
+
// terminal whole-branch review. The Org projection remains authoritative:
|
|
72
|
+
// this only controls whether a completed L1 may end the current report turn.
|
|
73
|
+
if (state.checkpoints?.protocolVersion === 1 && !state.checkpoints.terminalReviewAccepted)
|
|
74
|
+
return {
|
|
75
|
+
kind: 'scheduleContinuation',
|
|
76
|
+
at: new Date(Date.parse(now) + policy.backoffMs(state.consecutiveNoProgress)).toISOString(),
|
|
77
|
+
};
|
|
63
78
|
return { kind: 'complete' };
|
|
79
|
+
}
|
|
64
80
|
// Quiz, approval, and other held requests are ordinary session work. Only a
|
|
65
81
|
// validated Org attention record may turn an incomplete plan into a human stop.
|
|
66
82
|
if (hasPendingInteraction)
|
|
67
83
|
return { kind: 'observe' };
|
|
84
|
+
if ((input.automaticActionCount ?? 0) > policy.actionLimit)
|
|
85
|
+
return { kind: 'safetyPause', reason: 'actionRateExceeded' };
|
|
68
86
|
if (!activity || activity.confidence !== 'fresh')
|
|
69
87
|
return { kind: 'reconcile' };
|
|
70
88
|
if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
|
|
@@ -3,15 +3,19 @@
|
|
|
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';
|
|
10
|
+
import { consumeObservableWake, recordAutomaticContinuation, recoverSafetyPause, reportProbe, semanticProgressKey, startSupervisionProtocol, } from '../domain/supervision-protocol.js';
|
|
9
11
|
export class AutopilotCoordinator {
|
|
10
12
|
deps;
|
|
11
13
|
timers = new Map();
|
|
12
14
|
completionTimers = new Map();
|
|
13
15
|
executorTimers = new Map();
|
|
14
16
|
publishedSnapshots = new Map();
|
|
17
|
+
planEventKeys = new Map();
|
|
18
|
+
activityEventKeys = new Map();
|
|
15
19
|
/** Serializes asynchronous watchdog and timer work per relay session. */
|
|
16
20
|
operations = new Map();
|
|
17
21
|
constructor(deps) {
|
|
@@ -35,7 +39,7 @@ export class AutopilotCoordinator {
|
|
|
35
39
|
!session ||
|
|
36
40
|
!session.threadId ||
|
|
37
41
|
!state.requestedEnabled ||
|
|
38
|
-
['disabled', 'attentionRequired', 'completed'].includes(state.state))
|
|
42
|
+
['disabled', 'attentionRequired', 'completed', 'safetyPaused'].includes(state.state))
|
|
39
43
|
return;
|
|
40
44
|
// A relay restart deliberately drops its writer before rehydrating the
|
|
41
45
|
// coordinator. The plan-status watcher is authoritative and asynchronous,
|
|
@@ -77,6 +81,88 @@ export class AutopilotCoordinator {
|
|
|
77
81
|
}
|
|
78
82
|
dispose(sessionId) {
|
|
79
83
|
this.cancelTimer(sessionId);
|
|
84
|
+
this.planEventKeys.delete(sessionId);
|
|
85
|
+
this.activityEventKeys.delete(sessionId);
|
|
86
|
+
}
|
|
87
|
+
/** Accepts a session-owned structured probe response; no transcript text is inspected. */
|
|
88
|
+
reportProbe(sessionId, report) {
|
|
89
|
+
const state = this.deps.store.find(sessionId);
|
|
90
|
+
if (!state?.requestedEnabled)
|
|
91
|
+
return false;
|
|
92
|
+
const nextProtocol = reportProbe(state.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), report);
|
|
93
|
+
if (nextProtocol === state.supervision)
|
|
94
|
+
return false;
|
|
95
|
+
const now = this.deps.now();
|
|
96
|
+
this.persist({
|
|
97
|
+
...state,
|
|
98
|
+
supervision: nextProtocol,
|
|
99
|
+
...(nextProtocol.outcome === 'attentionRequired'
|
|
100
|
+
? {
|
|
101
|
+
state: 'attentionRequired',
|
|
102
|
+
requestedEnabled: false,
|
|
103
|
+
stopReason: 'attentionRequired',
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
updatedAt: now,
|
|
107
|
+
});
|
|
108
|
+
if (nextProtocol.outcome === 'parked' || nextProtocol.outcome === 'attentionRequired')
|
|
109
|
+
this.cancelTimer(sessionId);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/** Fails closed only for the currently active bounded probe. */
|
|
113
|
+
rejectProbe(sessionId) {
|
|
114
|
+
const state = this.deps.store.find(sessionId);
|
|
115
|
+
if (!state?.requestedEnabled || state.supervision?.outcome !== 'probeRequired')
|
|
116
|
+
return false;
|
|
117
|
+
const now = this.deps.now();
|
|
118
|
+
const cancelled = this.cancelScheduledControl(state, now);
|
|
119
|
+
this.cancelTimer(sessionId);
|
|
120
|
+
this.persist({
|
|
121
|
+
...state,
|
|
122
|
+
state: 'safetyPaused',
|
|
123
|
+
requestedEnabled: false,
|
|
124
|
+
generation: state.generation + 1,
|
|
125
|
+
nextEvaluationAt: null,
|
|
126
|
+
...(cancelled ? { lastControlId: null } : {}),
|
|
127
|
+
stopReason: 'safetyPaused',
|
|
128
|
+
supervision: {
|
|
129
|
+
...state.supervision,
|
|
130
|
+
outcome: 'safetyPaused',
|
|
131
|
+
safetyPauseReason: 'invalidProbeReport',
|
|
132
|
+
},
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
}, cancelled, [
|
|
135
|
+
{
|
|
136
|
+
sessionId,
|
|
137
|
+
type: 'autopilot.safety-paused',
|
|
138
|
+
payload: { reason: 'invalidProbeReport' },
|
|
139
|
+
occurredAt: now,
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/** A durable matching wake grants the one retry and schedules no work by itself. */
|
|
145
|
+
observableWake(sessionId, wake) {
|
|
146
|
+
const state = this.deps.store.find(sessionId);
|
|
147
|
+
if (!state?.requestedEnabled || !state.supervision)
|
|
148
|
+
return false;
|
|
149
|
+
const nextProtocol = consumeObservableWake(state.supervision, wake);
|
|
150
|
+
if (nextProtocol === state.supervision)
|
|
151
|
+
return false;
|
|
152
|
+
this.persist({ ...state, supervision: nextProtocol, updatedAt: this.deps.now() });
|
|
153
|
+
this.evaluate(sessionId);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/** Wakes a parked lease only when this semantic event was explicitly requested. */
|
|
157
|
+
semanticEvent(sessionId, condition) {
|
|
158
|
+
const lease = this.deps.store.find(sessionId)?.supervision?.waitLease;
|
|
159
|
+
if (!lease || !lease.wakeConditions.includes(condition))
|
|
160
|
+
return false;
|
|
161
|
+
return this.observableWake(sessionId, {
|
|
162
|
+
leaseId: lease.id,
|
|
163
|
+
condition,
|
|
164
|
+
progressKey: this.progressKey(sessionId),
|
|
165
|
+
});
|
|
80
166
|
}
|
|
81
167
|
enable(sessionId) {
|
|
82
168
|
const session = this.deps.session(sessionId);
|
|
@@ -106,6 +192,7 @@ export class AutopilotCoordinator {
|
|
|
106
192
|
stopReason: null,
|
|
107
193
|
executor: undefined,
|
|
108
194
|
blocking: undefined,
|
|
195
|
+
supervision: recoverSafetyPause(prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId)),
|
|
109
196
|
updatedAt: now,
|
|
110
197
|
};
|
|
111
198
|
this.persist(next);
|
|
@@ -136,6 +223,7 @@ export class AutopilotCoordinator {
|
|
|
136
223
|
...(cancelled ? { lastControlId: null } : {}),
|
|
137
224
|
stopReason: 'manualDisabled',
|
|
138
225
|
blocking: undefined,
|
|
226
|
+
supervision: recoverSafetyPause(prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId)),
|
|
139
227
|
updatedAt: now,
|
|
140
228
|
};
|
|
141
229
|
this.persist(next, cancelled);
|
|
@@ -259,6 +347,17 @@ export class AutopilotCoordinator {
|
|
|
259
347
|
updatedAt: now,
|
|
260
348
|
};
|
|
261
349
|
break;
|
|
350
|
+
case 'safetyPause':
|
|
351
|
+
next = {
|
|
352
|
+
...prior,
|
|
353
|
+
state: 'safetyPaused',
|
|
354
|
+
requestedEnabled: false,
|
|
355
|
+
generation: prior.generation + 1,
|
|
356
|
+
nextEvaluationAt: null,
|
|
357
|
+
stopReason: 'safetyPaused',
|
|
358
|
+
updatedAt: now,
|
|
359
|
+
};
|
|
360
|
+
break;
|
|
262
361
|
case 'disable':
|
|
263
362
|
next = {
|
|
264
363
|
...prior,
|
|
@@ -274,12 +373,14 @@ export class AutopilotCoordinator {
|
|
|
274
373
|
}
|
|
275
374
|
if (decision.kind === 'requestAttention' ||
|
|
276
375
|
decision.kind === 'complete' ||
|
|
277
|
-
decision.kind === 'disable'
|
|
376
|
+
decision.kind === 'disable' ||
|
|
377
|
+
decision.kind === 'safetyPause')
|
|
278
378
|
this.cancelTimer(sessionId);
|
|
279
379
|
if (next !== prior) {
|
|
280
380
|
const cancelled = decision.kind === 'requestAttention' ||
|
|
281
381
|
decision.kind === 'complete' ||
|
|
282
|
-
decision.kind === 'disable'
|
|
382
|
+
decision.kind === 'disable' ||
|
|
383
|
+
decision.kind === 'safetyPause'
|
|
283
384
|
? this.cancelScheduledControl(prior, now)
|
|
284
385
|
: undefined;
|
|
285
386
|
this.persist(cancelled ? { ...next, lastControlId: null } : next, cancelled);
|
|
@@ -294,11 +395,31 @@ export class AutopilotCoordinator {
|
|
|
294
395
|
state?.stopReason === 'planRemoved' ||
|
|
295
396
|
state?.stopReason === 'planReplaced' ||
|
|
296
397
|
state?.stopReason === 'sessionEnded';
|
|
398
|
+
const checkpointBoundary = Boolean(state?.checkpoints?.pendingTurnId &&
|
|
399
|
+
state.checkpoints.planIdentity === this.deps.plan(sessionId)?.identity);
|
|
297
400
|
const finalAllowed = !state ||
|
|
298
401
|
!plan ||
|
|
299
402
|
explicitlyStopped ||
|
|
300
|
-
|
|
403
|
+
checkpointBoundary ||
|
|
404
|
+
(!state.checkpoints && executionComplete(plan)) ||
|
|
301
405
|
validStructuredBlock(state.blocking);
|
|
406
|
+
if (checkpointBoundary && state?.checkpoints) {
|
|
407
|
+
const occurredAt = this.deps.now();
|
|
408
|
+
this.persist({
|
|
409
|
+
...state,
|
|
410
|
+
checkpoints: { ...state.checkpoints, pendingTurnId: null },
|
|
411
|
+
updatedAt: occurredAt,
|
|
412
|
+
}, undefined, [
|
|
413
|
+
{
|
|
414
|
+
sessionId,
|
|
415
|
+
type: state.checkpoints.terminalReviewAccepted
|
|
416
|
+
? 'org-plan.terminal-review-reported'
|
|
417
|
+
: 'org-plan.milestone-reported',
|
|
418
|
+
payload: { turnId: state.checkpoints.pendingTurnId },
|
|
419
|
+
occurredAt,
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
302
423
|
if (!finalAllowed) {
|
|
303
424
|
const occurredAt = this.deps.now();
|
|
304
425
|
this.commit({
|
|
@@ -316,6 +437,47 @@ export class AutopilotCoordinator {
|
|
|
316
437
|
this.activitySettled(sessionId, 'rootFinalAttempt');
|
|
317
438
|
return finalAllowed;
|
|
318
439
|
}
|
|
440
|
+
/** Records an already validated root-only checkpoint; it never changes Org state. */
|
|
441
|
+
checkpointAccepted(sessionId, checkpoint, turnId, occurredAt) {
|
|
442
|
+
const prior = this.deps.store.find(sessionId);
|
|
443
|
+
const retained = this.deps.plan(sessionId);
|
|
444
|
+
if (!prior || !retained || !turnId || checkpoint.planIdentity !== retained.identity)
|
|
445
|
+
return false;
|
|
446
|
+
const previous = prior.checkpoints;
|
|
447
|
+
if (previous && previous.planIdentity !== retained.identity)
|
|
448
|
+
return false;
|
|
449
|
+
const reported = previous?.reportedL1Ids ?? [];
|
|
450
|
+
const key = createHash('sha256').update(JSON.stringify(checkpoint)).digest('hex');
|
|
451
|
+
const acceptedKeys = previous?.acceptedKeys ?? [];
|
|
452
|
+
if (acceptedKeys.includes(key))
|
|
453
|
+
return true;
|
|
454
|
+
if (checkpoint.kind === 'l1Accepted' && reported.includes(checkpoint.l1Id))
|
|
455
|
+
return false;
|
|
456
|
+
if (checkpoint.kind === 'terminalReviewAccepted' && previous?.terminalReviewAccepted)
|
|
457
|
+
return false;
|
|
458
|
+
const checkpoints = {
|
|
459
|
+
protocolVersion: 1,
|
|
460
|
+
planIdentity: retained.identity,
|
|
461
|
+
reportedL1Ids: checkpoint.kind === 'l1Accepted' ? [...reported, checkpoint.l1Id] : reported,
|
|
462
|
+
acceptedKeys: [...acceptedKeys, key],
|
|
463
|
+
pendingTurnId: turnId,
|
|
464
|
+
terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
|
|
465
|
+
};
|
|
466
|
+
this.persist({ ...prior, checkpoints, updatedAt: occurredAt }, undefined, [
|
|
467
|
+
{
|
|
468
|
+
sessionId,
|
|
469
|
+
type: checkpoint.kind === 'l1Accepted'
|
|
470
|
+
? 'org-plan.milestone-checkpointed'
|
|
471
|
+
: 'org-plan.terminal-review-checkpointed',
|
|
472
|
+
payload: checkpoint.kind === 'l1Accepted'
|
|
473
|
+
? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
|
|
474
|
+
: { turnId },
|
|
475
|
+
occurredAt,
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
this.semanticEvent(sessionId, 'checkpointChanged');
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
319
481
|
/** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
|
|
320
482
|
planStatusChanged(sessionId) {
|
|
321
483
|
const prior = this.deps.store.find(sessionId);
|
|
@@ -330,8 +492,16 @@ export class AutopilotCoordinator {
|
|
|
330
492
|
this.cancel(sessionId, 'planReplaced');
|
|
331
493
|
return;
|
|
332
494
|
}
|
|
333
|
-
if (
|
|
495
|
+
if (this.semanticEvent(sessionId, 'planChanged') ||
|
|
496
|
+
this.semanticEvent(sessionId, 'reviewChanged'))
|
|
497
|
+
return;
|
|
498
|
+
if (executionComplete(plan.plan)) {
|
|
499
|
+
const eventKey = `plan:${fingerprint(plan.plan)}`;
|
|
500
|
+
if (this.planEventKeys.get(sessionId) === eventKey)
|
|
501
|
+
return;
|
|
502
|
+
this.planEventKeys.set(sessionId, eventKey);
|
|
334
503
|
this.evaluate(sessionId);
|
|
504
|
+
}
|
|
335
505
|
}
|
|
336
506
|
/**
|
|
337
507
|
* Records a validated, session-private supervision request. It intentionally
|
|
@@ -386,6 +556,25 @@ export class AutopilotCoordinator {
|
|
|
386
556
|
const activity = this.deps.activity(sessionId);
|
|
387
557
|
if (!activity || activity.confidence !== 'fresh')
|
|
388
558
|
return;
|
|
559
|
+
const eventKey = JSON.stringify({
|
|
560
|
+
root: [activity.root.state, activity.root.reason],
|
|
561
|
+
children: activity.subagents.map((child) => [
|
|
562
|
+
child.id,
|
|
563
|
+
child.state,
|
|
564
|
+
child.reason,
|
|
565
|
+
child.continuationGeneration ?? 0,
|
|
566
|
+
(child.ownedProcesses ?? []).map((process) => [
|
|
567
|
+
process.processId,
|
|
568
|
+
process.state,
|
|
569
|
+
process.ownership,
|
|
570
|
+
]),
|
|
571
|
+
]),
|
|
572
|
+
});
|
|
573
|
+
if (this.activityEventKeys.get(sessionId) === eventKey)
|
|
574
|
+
return;
|
|
575
|
+
this.activityEventKeys.set(sessionId, eventKey);
|
|
576
|
+
if (this.semanticEvent(sessionId, 'agentActivityChanged'))
|
|
577
|
+
return;
|
|
389
578
|
const disposition = classifyAgentActivity(activity);
|
|
390
579
|
if (disposition === 'attention') {
|
|
391
580
|
this.evaluate(sessionId);
|
|
@@ -528,6 +717,39 @@ export class AutopilotCoordinator {
|
|
|
528
717
|
await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
|
|
529
718
|
return;
|
|
530
719
|
}
|
|
720
|
+
const protocol = recordAutomaticContinuation(current.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId));
|
|
721
|
+
if (protocol.outcome === 'probeRequired') {
|
|
722
|
+
this.persist({ ...current, supervision: protocol, updatedAt: this.deps.now() }, undefined, [
|
|
723
|
+
{
|
|
724
|
+
sessionId,
|
|
725
|
+
type: 'autopilot.probe-required',
|
|
726
|
+
payload: { progressKey: protocol.progressKey },
|
|
727
|
+
occurredAt: this.deps.now(),
|
|
728
|
+
},
|
|
729
|
+
]);
|
|
730
|
+
}
|
|
731
|
+
if (protocol.outcome === 'safetyPaused') {
|
|
732
|
+
const now = this.deps.now();
|
|
733
|
+
const cancelled = this.cancelScheduledControl(current, now);
|
|
734
|
+
this.persist({
|
|
735
|
+
...current,
|
|
736
|
+
state: 'safetyPaused',
|
|
737
|
+
requestedEnabled: false,
|
|
738
|
+
stopReason: 'safetyPaused',
|
|
739
|
+
...(cancelled ? { generation: current.generation + 1, lastControlId: null } : {}),
|
|
740
|
+
nextEvaluationAt: null,
|
|
741
|
+
supervision: protocol,
|
|
742
|
+
updatedAt: now,
|
|
743
|
+
}, cancelled, [
|
|
744
|
+
{
|
|
745
|
+
sessionId,
|
|
746
|
+
type: 'autopilot.safety-paused',
|
|
747
|
+
payload: { progressKey: protocol.progressKey },
|
|
748
|
+
occurredAt: now,
|
|
749
|
+
},
|
|
750
|
+
]);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
531
753
|
const id = current.lastControlId;
|
|
532
754
|
if (!id)
|
|
533
755
|
return;
|
|
@@ -585,8 +807,9 @@ export class AutopilotCoordinator {
|
|
|
585
807
|
this.deps.pendingInteraction(sessionId))
|
|
586
808
|
return false;
|
|
587
809
|
const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor);
|
|
588
|
-
|
|
589
|
-
|
|
810
|
+
const executorChanged = executor ? this.persistExecutor(sessionId, executor) : false;
|
|
811
|
+
if (executorChanged && this.wakeForExecutorChange(sessionId, executor))
|
|
812
|
+
return true;
|
|
590
813
|
const decision = decideSupervisedLifecycle({
|
|
591
814
|
plan: retained.plan,
|
|
592
815
|
event,
|
|
@@ -611,7 +834,11 @@ export class AutopilotCoordinator {
|
|
|
611
834
|
return true;
|
|
612
835
|
case 'monitorProcess': {
|
|
613
836
|
const processId = decision.action.process.processId;
|
|
614
|
-
|
|
837
|
+
const alreadyMonitoring = executor?.ownedProcesses.some((process) => process.processId === processId &&
|
|
838
|
+
process.ownership === 'supervisor' &&
|
|
839
|
+
process.state === 'detached-active');
|
|
840
|
+
if (!alreadyMonitoring)
|
|
841
|
+
controller.transferProcess(sessionId, decision.action.process.ownerThreadId, processId);
|
|
615
842
|
if (executor)
|
|
616
843
|
this.persistExecutor(sessionId, {
|
|
617
844
|
...executor,
|
|
@@ -619,10 +846,11 @@ export class AutopilotCoordinator {
|
|
|
619
846
|
? { ...process, ownership: 'supervisor', state: 'detached-active' }
|
|
620
847
|
: process),
|
|
621
848
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
849
|
+
if (!alreadyMonitoring)
|
|
850
|
+
this.audit(sessionId, 'autopilot.process-monitoring', {
|
|
851
|
+
threadId: decision.action.process.ownerThreadId,
|
|
852
|
+
processId,
|
|
853
|
+
});
|
|
626
854
|
this.armExecutorRefresh(sessionId, decision.action.pollAfterMs);
|
|
627
855
|
return true;
|
|
628
856
|
}
|
|
@@ -724,8 +952,22 @@ export class AutopilotCoordinator {
|
|
|
724
952
|
persistExecutor(sessionId, executor) {
|
|
725
953
|
const current = this.deps.store.find(sessionId);
|
|
726
954
|
if (!current || JSON.stringify(current.executor) === JSON.stringify(executor))
|
|
727
|
-
return;
|
|
955
|
+
return false;
|
|
728
956
|
this.persist({ ...current, executor, updatedAt: this.deps.now() });
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
wakeForExecutorChange(sessionId, executor) {
|
|
960
|
+
const processes = executor.ownedProcesses;
|
|
961
|
+
const conditions = [
|
|
962
|
+
...(processes.some((process) => process.state === 'exited-awaiting-result')
|
|
963
|
+
? ['processExited', 'processResultAvailable']
|
|
964
|
+
: []),
|
|
965
|
+
...(processes.some((process) => process.state === 'terminated-for-budget')
|
|
966
|
+
? ['processLimitBreached']
|
|
967
|
+
: []),
|
|
968
|
+
'executorChanged',
|
|
969
|
+
];
|
|
970
|
+
return conditions.some((condition) => this.semanticEvent(sessionId, condition));
|
|
729
971
|
}
|
|
730
972
|
freshExecutorIdentity(sessionId) {
|
|
731
973
|
const plan = this.deps.plan(sessionId)?.plan;
|
|
@@ -900,6 +1142,8 @@ export class AutopilotCoordinator {
|
|
|
900
1142
|
return 'unknown';
|
|
901
1143
|
}
|
|
902
1144
|
decision(sessionId, state) {
|
|
1145
|
+
if (state.supervision?.outcome === 'parked')
|
|
1146
|
+
return { kind: 'observe' };
|
|
903
1147
|
const plan = this.deps.plan(sessionId);
|
|
904
1148
|
const now = this.deps.now();
|
|
905
1149
|
return decideAutopilot({
|
|
@@ -914,6 +1158,48 @@ export class AutopilotCoordinator {
|
|
|
914
1158
|
policy: this.deps.policy,
|
|
915
1159
|
});
|
|
916
1160
|
}
|
|
1161
|
+
progressKey(sessionId) {
|
|
1162
|
+
const retained = this.deps.plan(sessionId);
|
|
1163
|
+
const activity = this.deps.activity(sessionId);
|
|
1164
|
+
const interactions = this.deps.pendingInteraction(sessionId)
|
|
1165
|
+
? [{ id: 'pending', kind: 'session', state: 'pending' }]
|
|
1166
|
+
: [];
|
|
1167
|
+
return semanticProgressKey({
|
|
1168
|
+
plan: {
|
|
1169
|
+
identity: retained?.identity ?? 'none',
|
|
1170
|
+
fingerprint: retained ? fingerprint(retained.plan) : 'none',
|
|
1171
|
+
currentPosition: retained?.plan.currentStepId ?? null,
|
|
1172
|
+
},
|
|
1173
|
+
review: { status: null },
|
|
1174
|
+
checkpoint: {
|
|
1175
|
+
pendingTurnId: this.deps.store.find(sessionId)?.checkpoints?.pendingTurnId ?? null,
|
|
1176
|
+
terminalReviewAccepted: this.deps.store.find(sessionId)?.checkpoints?.terminalReviewAccepted ?? false,
|
|
1177
|
+
},
|
|
1178
|
+
pendingInteractions: interactions,
|
|
1179
|
+
executor: {
|
|
1180
|
+
generation: this.deps.store.find(sessionId)?.executor?.continuationGeneration ?? 0,
|
|
1181
|
+
state: this.deps.store.find(sessionId)?.executor?.outcome ?? null,
|
|
1182
|
+
},
|
|
1183
|
+
ownedProcesses: (this.deps.store.find(sessionId)?.executor?.ownedProcesses ?? []).map((process) => ({
|
|
1184
|
+
id: process.processId,
|
|
1185
|
+
state: process.state,
|
|
1186
|
+
ownerGeneration: this.deps.store.find(sessionId)?.executor?.continuationGeneration ?? 0,
|
|
1187
|
+
})),
|
|
1188
|
+
agentActivity: [
|
|
1189
|
+
...(activity
|
|
1190
|
+
? [
|
|
1191
|
+
{
|
|
1192
|
+
agentId: 'root',
|
|
1193
|
+
// Activity time is liveness-only; semantic progress uses the
|
|
1194
|
+
// sequenced state projection, never the observation clock.
|
|
1195
|
+
sequence: 0,
|
|
1196
|
+
state: activity.root.state,
|
|
1197
|
+
},
|
|
1198
|
+
]
|
|
1199
|
+
: []),
|
|
1200
|
+
],
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
917
1203
|
async reconcile(sessionId, generation) {
|
|
918
1204
|
try {
|
|
919
1205
|
const result = await this.deps.reconcile(sessionId);
|