gestalt-mobile 0.29.0 → 0.30.1
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/README.md +3 -4
- package/dist/client/assets/index-CKhqx71S.css +1 -0
- package/dist/client/assets/index-CedbthUQ.js +34 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/cli.js +27 -2
- package/dist/server/server/composition.contracts.js +1 -1
- package/dist/server/server/composition.js +32 -4
- package/dist/server/server/features/autopilot/application/policy.js +17 -2
- package/dist/server/server/features/autopilot/application/service.js +239 -12
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +11 -1
- package/dist/server/server/features/autopilot/domain/supervision-protocol.js +203 -0
- package/dist/server/server/features/sessions/application/resume-command.js +1 -3
- package/dist/server/server/features/sessions/interaction/kind.js +1 -0
- package/dist/server/server/features/sessions/interaction/response-validator.js +4 -1
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +2 -2
- package/dist/server/server/features/sessions/model/relay-session.js +2 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +11 -6
- package/dist/server/server/features/skills/model/skill-profile.js +25 -8
- package/dist/server/server/platform/catalog/launcher-profile-catalog.js +21 -0
- package/dist/server/server/platform/catalog/profile-command.js +2 -13
- package/dist/server/server/platform/codex/codex-process-launcher.js +1 -1
- package/dist/server/server/platform/codex/server-request.js +13 -0
- package/dist/server/server/platform/codex/session-runtime.js +3 -0
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +11 -2
- 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/package.json +2 -2
- package/dist/client/assets/index-BtlbAtm9.css +0 -1
- package/dist/client/assets/index-CRULnRRB.js +0 -33
- package/dist/server/server/platform/catalog/codex-profile-catalog.js +0 -42
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-CedbthUQ.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CKhqx71S.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="app"></div>
|
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { promisify } from 'node:util';
|
|
13
13
|
import { composeRelayApp } from './composition.js';
|
|
14
14
|
import { CliUsageError, parseConfig } from './config.js';
|
|
15
|
-
import {
|
|
15
|
+
import { LauncherProfileCatalog } from './platform/catalog/launcher-profile-catalog.js';
|
|
16
16
|
import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
|
|
17
17
|
const runFile = promisify(execFile);
|
|
18
18
|
export const usage = `Usage: gestalt-mobile [options]
|
|
@@ -28,6 +28,26 @@ Options:
|
|
|
28
28
|
--skills list List saved global skill profiles without starting the server
|
|
29
29
|
--help Show this help
|
|
30
30
|
--version Show the installed version`;
|
|
31
|
+
export async function runStartupDoctor(cwd) {
|
|
32
|
+
if (process.env.GESTALT_STARTUP_DIAGNOSTICS_DONE === '1')
|
|
33
|
+
return { ok: true, report: '' };
|
|
34
|
+
try {
|
|
35
|
+
const result = await runFile('gestalt', ['doctor'], {
|
|
36
|
+
cwd,
|
|
37
|
+
env: process.env,
|
|
38
|
+
timeout: 60_000,
|
|
39
|
+
maxBuffer: 1024 * 1024,
|
|
40
|
+
});
|
|
41
|
+
return { ok: true, report: `${result.stdout}${result.stderr}` };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const failure = error;
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
report: `${failure.stdout ?? ''}${failure.stderr ?? ''}${failure.message ? `${failure.message}\n` : ''}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
31
51
|
export function packageRoot(moduleUrl) {
|
|
32
52
|
let directory = dirname(fileURLToPath(moduleUrl));
|
|
33
53
|
while (true) {
|
|
@@ -155,13 +175,18 @@ export async function runCli(dependencies = {}) {
|
|
|
155
175
|
stderr.write(`Unknown skill profile: ${config.skillsProfile}\n\n${usage}\n`);
|
|
156
176
|
return 2;
|
|
157
177
|
}
|
|
178
|
+
const diagnosis = await (dependencies.runStartupDoctor ?? runStartupDoctor)(cwd);
|
|
179
|
+
if (diagnosis.report)
|
|
180
|
+
stderr.write(diagnosis.report);
|
|
181
|
+
if (!diagnosis.ok)
|
|
182
|
+
stderr.write('WARNING: Startup diagnostics reported a problem; Gestalt Mobile will continue so it can show recovery controls.\n');
|
|
158
183
|
const app = await (dependencies.compose ?? composeRelayApp)({
|
|
159
184
|
root: config.root,
|
|
160
185
|
dataDir: config.dataDir,
|
|
161
186
|
relyingParty: config.relyingParty,
|
|
162
187
|
passkeyAuthEnabled: config.passkeyAuthEnabled,
|
|
163
188
|
staticDir: packagedClientDir(moduleUrl),
|
|
164
|
-
profiles: new
|
|
189
|
+
profiles: new LauncherProfileCatalog(),
|
|
165
190
|
installedCodexVersion: await (dependencies.probeCodexVersion ?? probeCodexVersion)(),
|
|
166
191
|
startAppServers: true,
|
|
167
192
|
homeDirectory: dependencies.homeDirectory,
|
|
@@ -210,7 +210,7 @@ async function composeAuthorizedApp(options) {
|
|
|
210
210
|
if (!store.sessionDevice(authorizationSessionId('test-session'), '2026-08-02T00:00:00.000Z'))
|
|
211
211
|
store.saveSession(authorizationSessionId('test-session'), {
|
|
212
212
|
deviceId: device.id,
|
|
213
|
-
expiresAt: '
|
|
213
|
+
expiresAt: '2099-09-01T00:00:00.000Z',
|
|
214
214
|
});
|
|
215
215
|
store.close();
|
|
216
216
|
const app = await composeRelayApp({ ...options, homeDirectory });
|
|
@@ -35,7 +35,7 @@ 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
39
|
import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
|
|
40
40
|
import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
|
|
41
41
|
import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
|
|
@@ -51,9 +51,10 @@ import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-com
|
|
|
51
51
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
52
52
|
import { SqliteAutopilotStore } from './platform/persistence/sqlite-autopilot-store.js';
|
|
53
53
|
import { AutopilotCoordinator } from './features/autopilot/application/service.js';
|
|
54
|
-
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';
|
|
55
55
|
import { createRelyingPartyConfig } from './config.js';
|
|
56
56
|
import { parseOrgPlanAttention } from '../shared/contracts/org-plan-attention.js';
|
|
57
|
+
import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
|
|
57
58
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
58
59
|
export async function composeRelayApp(options) {
|
|
59
60
|
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
@@ -219,7 +220,7 @@ export async function composeRelayApp(options) {
|
|
|
219
220
|
throw new Error('AUTOPILOT_START_UNAVAILABLE');
|
|
220
221
|
await options.autopilotBeforeTurnAccepted?.({ sessionId, controlId });
|
|
221
222
|
const continuationPrompt = launchIdentity
|
|
222
|
-
?
|
|
223
|
+
? autopilotExecutorLaunchPrompt(launchIdentity)
|
|
223
224
|
: AUTOPILOT_CONTINUATION_PROMPT;
|
|
224
225
|
const started = await runtime.startTurn(writer.session, continuationPrompt, controlId, new Date().toISOString());
|
|
225
226
|
if (!started.activeTurnId)
|
|
@@ -335,6 +336,7 @@ export async function composeRelayApp(options) {
|
|
|
335
336
|
events.publish(journal.append(sessionId, 'interaction.resolved', { requestId, turnId: interaction?.turnId ?? null, resolvedAt: occurredAt, outcome }, occurredAt));
|
|
336
337
|
if (interaction?.kind === 'orgPlanAttention')
|
|
337
338
|
events.publish(journal.append(sessionId, 'org-plan.attention-resolved', { requestId, turnId: interaction.turnId ?? null, resolvedAt: occurredAt, outcome }, occurredAt));
|
|
339
|
+
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
338
340
|
};
|
|
339
341
|
const publishAttentionSettlement = (sessionId, requestId, occurredAt, outcome) => {
|
|
340
342
|
const remaining = interactions.list(sessionId);
|
|
@@ -428,8 +430,33 @@ export async function composeRelayApp(options) {
|
|
|
428
430
|
}, (sessionId, request, origin) => {
|
|
429
431
|
const rawInteraction = toPendingInteraction(request);
|
|
430
432
|
const session = withPendingInteractions(sessions.find(sessionId));
|
|
431
|
-
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)
|
|
432
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
|
+
}
|
|
433
460
|
if (rawInteraction.kind === 'orgPlanCheckpoint') {
|
|
434
461
|
const checkpoint = parseOrgPlanCheckpoint(rawInteraction.payload);
|
|
435
462
|
const retained = supervisedPlans.find(sessionId);
|
|
@@ -468,6 +495,7 @@ export async function composeRelayApp(options) {
|
|
|
468
495
|
? { attentionReason: interaction.payload.reason }
|
|
469
496
|
: {}),
|
|
470
497
|
});
|
|
498
|
+
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
471
499
|
const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
|
|
472
500
|
sessions.save(updated);
|
|
473
501
|
events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
|
|
@@ -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 = 'v6';
|
|
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. If the active L1 is still WIP and its executor stopped or is idle after a partial report or L2 checkpoint, call followup_task on that same executor before sending any response. Treat a status question as an interruption: answer briefly, then perform that continuation in the same turn. 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,
|
|
@@ -74,6 +81,14 @@ export function decideAutopilot(input) {
|
|
|
74
81
|
// validated Org attention record may turn an incomplete plan into a human stop.
|
|
75
82
|
if (hasPendingInteraction)
|
|
76
83
|
return { kind: 'observe' };
|
|
84
|
+
const semanticProgressObserved = Boolean(input.semanticProgressKey &&
|
|
85
|
+
state.supervision &&
|
|
86
|
+
input.semanticProgressKey !== state.supervision.progressKey);
|
|
87
|
+
const semanticLoopConfirmed = (state.supervision?.unchangedContinuations ?? 3) >= 3;
|
|
88
|
+
if ((input.automaticActionCount ?? 0) > policy.actionLimit &&
|
|
89
|
+
!semanticProgressObserved &&
|
|
90
|
+
semanticLoopConfirmed)
|
|
91
|
+
return { kind: 'safetyPause', reason: 'actionRateExceeded' };
|
|
77
92
|
if (!activity || activity.confidence !== 'fresh')
|
|
78
93
|
return { kind: 'reconcile' };
|
|
79
94
|
if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
|
|
@@ -7,12 +7,15 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
|
|
8
8
|
import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, } from '../domain/supervised-lifecycle.js';
|
|
9
9
|
import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
|
|
10
|
+
import { consumeObservableWake, recordAutomaticContinuation, recoverSafetyPause, reportProbe, semanticProgressKey, startSupervisionProtocol, } from '../domain/supervision-protocol.js';
|
|
10
11
|
export class AutopilotCoordinator {
|
|
11
12
|
deps;
|
|
12
13
|
timers = new Map();
|
|
13
14
|
completionTimers = new Map();
|
|
14
15
|
executorTimers = new Map();
|
|
15
16
|
publishedSnapshots = new Map();
|
|
17
|
+
planEventKeys = new Map();
|
|
18
|
+
activityEventKeys = new Map();
|
|
16
19
|
/** Serializes asynchronous watchdog and timer work per relay session. */
|
|
17
20
|
operations = new Map();
|
|
18
21
|
constructor(deps) {
|
|
@@ -36,7 +39,7 @@ export class AutopilotCoordinator {
|
|
|
36
39
|
!session ||
|
|
37
40
|
!session.threadId ||
|
|
38
41
|
!state.requestedEnabled ||
|
|
39
|
-
['disabled', 'attentionRequired', 'completed'].includes(state.state))
|
|
42
|
+
['disabled', 'attentionRequired', 'completed', 'safetyPaused'].includes(state.state))
|
|
40
43
|
return;
|
|
41
44
|
// A relay restart deliberately drops its writer before rehydrating the
|
|
42
45
|
// coordinator. The plan-status watcher is authoritative and asynchronous,
|
|
@@ -78,6 +81,88 @@ export class AutopilotCoordinator {
|
|
|
78
81
|
}
|
|
79
82
|
dispose(sessionId) {
|
|
80
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
|
+
});
|
|
81
166
|
}
|
|
82
167
|
enable(sessionId) {
|
|
83
168
|
const session = this.deps.session(sessionId);
|
|
@@ -107,6 +192,7 @@ export class AutopilotCoordinator {
|
|
|
107
192
|
stopReason: null,
|
|
108
193
|
executor: undefined,
|
|
109
194
|
blocking: undefined,
|
|
195
|
+
supervision: recoverSafetyPause(prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId)),
|
|
110
196
|
updatedAt: now,
|
|
111
197
|
};
|
|
112
198
|
this.persist(next);
|
|
@@ -137,6 +223,7 @@ export class AutopilotCoordinator {
|
|
|
137
223
|
...(cancelled ? { lastControlId: null } : {}),
|
|
138
224
|
stopReason: 'manualDisabled',
|
|
139
225
|
blocking: undefined,
|
|
226
|
+
supervision: recoverSafetyPause(prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId)),
|
|
140
227
|
updatedAt: now,
|
|
141
228
|
};
|
|
142
229
|
this.persist(next, cancelled);
|
|
@@ -260,6 +347,17 @@ export class AutopilotCoordinator {
|
|
|
260
347
|
updatedAt: now,
|
|
261
348
|
};
|
|
262
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;
|
|
263
361
|
case 'disable':
|
|
264
362
|
next = {
|
|
265
363
|
...prior,
|
|
@@ -275,12 +373,14 @@ export class AutopilotCoordinator {
|
|
|
275
373
|
}
|
|
276
374
|
if (decision.kind === 'requestAttention' ||
|
|
277
375
|
decision.kind === 'complete' ||
|
|
278
|
-
decision.kind === 'disable'
|
|
376
|
+
decision.kind === 'disable' ||
|
|
377
|
+
decision.kind === 'safetyPause')
|
|
279
378
|
this.cancelTimer(sessionId);
|
|
280
379
|
if (next !== prior) {
|
|
281
380
|
const cancelled = decision.kind === 'requestAttention' ||
|
|
282
381
|
decision.kind === 'complete' ||
|
|
283
|
-
decision.kind === 'disable'
|
|
382
|
+
decision.kind === 'disable' ||
|
|
383
|
+
decision.kind === 'safetyPause'
|
|
284
384
|
? this.cancelScheduledControl(prior, now)
|
|
285
385
|
: undefined;
|
|
286
386
|
this.persist(cancelled ? { ...next, lastControlId: null } : next, cancelled);
|
|
@@ -375,6 +475,7 @@ export class AutopilotCoordinator {
|
|
|
375
475
|
occurredAt,
|
|
376
476
|
},
|
|
377
477
|
]);
|
|
478
|
+
this.semanticEvent(sessionId, 'checkpointChanged');
|
|
378
479
|
return true;
|
|
379
480
|
}
|
|
380
481
|
/** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
|
|
@@ -391,8 +492,16 @@ export class AutopilotCoordinator {
|
|
|
391
492
|
this.cancel(sessionId, 'planReplaced');
|
|
392
493
|
return;
|
|
393
494
|
}
|
|
394
|
-
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);
|
|
395
503
|
this.evaluate(sessionId);
|
|
504
|
+
}
|
|
396
505
|
}
|
|
397
506
|
/**
|
|
398
507
|
* Records a validated, session-private supervision request. It intentionally
|
|
@@ -447,6 +556,25 @@ export class AutopilotCoordinator {
|
|
|
447
556
|
const activity = this.deps.activity(sessionId);
|
|
448
557
|
if (!activity || activity.confidence !== 'fresh')
|
|
449
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;
|
|
450
578
|
const disposition = classifyAgentActivity(activity);
|
|
451
579
|
if (disposition === 'attention') {
|
|
452
580
|
this.evaluate(sessionId);
|
|
@@ -589,6 +717,39 @@ export class AutopilotCoordinator {
|
|
|
589
717
|
await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
|
|
590
718
|
return;
|
|
591
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
|
+
}
|
|
592
753
|
const id = current.lastControlId;
|
|
593
754
|
if (!id)
|
|
594
755
|
return;
|
|
@@ -646,8 +807,9 @@ export class AutopilotCoordinator {
|
|
|
646
807
|
this.deps.pendingInteraction(sessionId))
|
|
647
808
|
return false;
|
|
648
809
|
const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor);
|
|
649
|
-
|
|
650
|
-
|
|
810
|
+
const executorChanged = executor ? this.persistExecutor(sessionId, executor) : false;
|
|
811
|
+
if (executorChanged && this.wakeForExecutorChange(sessionId, executor))
|
|
812
|
+
return true;
|
|
651
813
|
const decision = decideSupervisedLifecycle({
|
|
652
814
|
plan: retained.plan,
|
|
653
815
|
event,
|
|
@@ -672,7 +834,11 @@ export class AutopilotCoordinator {
|
|
|
672
834
|
return true;
|
|
673
835
|
case 'monitorProcess': {
|
|
674
836
|
const processId = decision.action.process.processId;
|
|
675
|
-
|
|
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);
|
|
676
842
|
if (executor)
|
|
677
843
|
this.persistExecutor(sessionId, {
|
|
678
844
|
...executor,
|
|
@@ -680,10 +846,11 @@ export class AutopilotCoordinator {
|
|
|
680
846
|
? { ...process, ownership: 'supervisor', state: 'detached-active' }
|
|
681
847
|
: process),
|
|
682
848
|
});
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
849
|
+
if (!alreadyMonitoring)
|
|
850
|
+
this.audit(sessionId, 'autopilot.process-monitoring', {
|
|
851
|
+
threadId: decision.action.process.ownerThreadId,
|
|
852
|
+
processId,
|
|
853
|
+
});
|
|
687
854
|
this.armExecutorRefresh(sessionId, decision.action.pollAfterMs);
|
|
688
855
|
return true;
|
|
689
856
|
}
|
|
@@ -785,8 +952,22 @@ export class AutopilotCoordinator {
|
|
|
785
952
|
persistExecutor(sessionId, executor) {
|
|
786
953
|
const current = this.deps.store.find(sessionId);
|
|
787
954
|
if (!current || JSON.stringify(current.executor) === JSON.stringify(executor))
|
|
788
|
-
return;
|
|
955
|
+
return false;
|
|
789
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));
|
|
790
971
|
}
|
|
791
972
|
freshExecutorIdentity(sessionId) {
|
|
792
973
|
const plan = this.deps.plan(sessionId)?.plan;
|
|
@@ -961,8 +1142,11 @@ export class AutopilotCoordinator {
|
|
|
961
1142
|
return 'unknown';
|
|
962
1143
|
}
|
|
963
1144
|
decision(sessionId, state) {
|
|
1145
|
+
if (state.supervision?.outcome === 'parked')
|
|
1146
|
+
return { kind: 'observe' };
|
|
964
1147
|
const plan = this.deps.plan(sessionId);
|
|
965
1148
|
const now = this.deps.now();
|
|
1149
|
+
const currentProgressKey = this.progressKey(sessionId);
|
|
966
1150
|
return decideAutopilot({
|
|
967
1151
|
state,
|
|
968
1152
|
plan: plan?.plan ?? null,
|
|
@@ -971,10 +1155,53 @@ export class AutopilotCoordinator {
|
|
|
971
1155
|
hasActiveAttention: validStructuredBlock(this.deps.attention?.(sessionId) ?? state.blocking),
|
|
972
1156
|
lastTurnOutcome: this.lastTurnOutcome(sessionId, state.lastControlId),
|
|
973
1157
|
automaticActionCount: this.deps.store.automaticActionsSince?.(sessionId, new Date(Date.parse(now) - this.deps.policy.actionWindowMs).toISOString()) ?? 0,
|
|
1158
|
+
semanticProgressKey: currentProgressKey,
|
|
974
1159
|
now,
|
|
975
1160
|
policy: this.deps.policy,
|
|
976
1161
|
});
|
|
977
1162
|
}
|
|
1163
|
+
progressKey(sessionId) {
|
|
1164
|
+
const retained = this.deps.plan(sessionId);
|
|
1165
|
+
const activity = this.deps.activity(sessionId);
|
|
1166
|
+
const interactions = this.deps.pendingInteraction(sessionId)
|
|
1167
|
+
? [{ id: 'pending', kind: 'session', state: 'pending' }]
|
|
1168
|
+
: [];
|
|
1169
|
+
return semanticProgressKey({
|
|
1170
|
+
plan: {
|
|
1171
|
+
identity: retained?.identity ?? 'none',
|
|
1172
|
+
fingerprint: retained ? fingerprint(retained.plan) : 'none',
|
|
1173
|
+
currentPosition: retained?.plan.currentStepId ?? null,
|
|
1174
|
+
},
|
|
1175
|
+
review: { status: null },
|
|
1176
|
+
checkpoint: {
|
|
1177
|
+
pendingTurnId: this.deps.store.find(sessionId)?.checkpoints?.pendingTurnId ?? null,
|
|
1178
|
+
terminalReviewAccepted: this.deps.store.find(sessionId)?.checkpoints?.terminalReviewAccepted ?? false,
|
|
1179
|
+
},
|
|
1180
|
+
pendingInteractions: interactions,
|
|
1181
|
+
executor: {
|
|
1182
|
+
generation: this.deps.store.find(sessionId)?.executor?.continuationGeneration ?? 0,
|
|
1183
|
+
state: this.deps.store.find(sessionId)?.executor?.outcome ?? null,
|
|
1184
|
+
},
|
|
1185
|
+
ownedProcesses: (this.deps.store.find(sessionId)?.executor?.ownedProcesses ?? []).map((process) => ({
|
|
1186
|
+
id: process.processId,
|
|
1187
|
+
state: process.state,
|
|
1188
|
+
ownerGeneration: this.deps.store.find(sessionId)?.executor?.continuationGeneration ?? 0,
|
|
1189
|
+
})),
|
|
1190
|
+
agentActivity: [
|
|
1191
|
+
...(activity
|
|
1192
|
+
? [
|
|
1193
|
+
{
|
|
1194
|
+
agentId: 'root',
|
|
1195
|
+
// Activity time is liveness-only; semantic progress uses the
|
|
1196
|
+
// sequenced state projection, never the observation clock.
|
|
1197
|
+
sequence: 0,
|
|
1198
|
+
state: activity.root.state,
|
|
1199
|
+
},
|
|
1200
|
+
]
|
|
1201
|
+
: []),
|
|
1202
|
+
],
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
978
1205
|
async reconcile(sessionId, generation) {
|
|
979
1206
|
try {
|
|
980
1207
|
const result = await this.deps.reconcile(sessionId);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { orgPlanAgentDisplayName } from '../../../../shared/org-plan-position.js';
|
|
7
|
+
import { parseSupervisionProtocolState, } from './supervision-protocol.js';
|
|
7
8
|
export const blockingResumeConditions = {
|
|
8
9
|
planChange: 'planRevision',
|
|
9
10
|
hardBlock: 'externalStateChanged',
|
|
@@ -25,11 +26,19 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
25
26
|
return undefined;
|
|
26
27
|
const executorValue = record(root.executor);
|
|
27
28
|
const checkpointsValue = record(root.checkpoints);
|
|
29
|
+
const supervisionValue = root.supervision;
|
|
30
|
+
const supervision = supervisionValue === undefined ? undefined : parseSupervisionProtocolState(supervisionValue);
|
|
31
|
+
if (supervisionValue !== undefined && !supervision)
|
|
32
|
+
return undefined;
|
|
28
33
|
const checkpoints = checkpointsValue ? parseCheckpoints(checkpointsValue) : undefined;
|
|
29
34
|
if (checkpointsValue && !checkpoints)
|
|
30
35
|
return undefined;
|
|
31
36
|
if (!executorValue)
|
|
32
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
...(blocking ? { blocking } : {}),
|
|
39
|
+
...(supervision ? { supervision } : {}),
|
|
40
|
+
...(checkpoints ? { checkpoints } : {}),
|
|
41
|
+
};
|
|
33
42
|
const processesValue = executorValue.ownedProcesses;
|
|
34
43
|
if (!Array.isArray(processesValue) || processesValue.length > 64)
|
|
35
44
|
return undefined;
|
|
@@ -135,6 +144,7 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
135
144
|
continuationCount,
|
|
136
145
|
},
|
|
137
146
|
...(blocking ? { blocking } : {}),
|
|
147
|
+
...(supervision ? { supervision } : {}),
|
|
138
148
|
...(checkpoints ? { checkpoints } : {}),
|
|
139
149
|
};
|
|
140
150
|
}
|