gestalt-mobile 0.25.2 → 0.25.3
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-BkpRh3eb.js → index-CFw8G97O.js} +18 -18
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +52 -62
- package/dist/server/server/composition.js +63 -4
- package/dist/server/server/features/agent-activity/model.js +27 -0
- package/dist/server/server/features/agent-activity/registry.js +24 -0
- package/dist/server/server/features/autopilot/application/policy.js +14 -12
- package/dist/server/server/features/autopilot/application/service.js +339 -17
- package/dist/server/server/features/autopilot/domain/autopilot-session.js +2 -0
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +255 -0
- package/dist/server/server/platform/codex/session-runtime.js +163 -0
- package/dist/server/server/platform/persistence/migrate.js +7 -2
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +21 -3
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +5 -0
- package/dist/server/shared/contracts/autopilot-audit.js +12 -0
- package/dist/server/shared/org-plan-position.js +12 -2
- 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-CFw8G97O.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-D3wXcWGp.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
@@ -49,6 +49,9 @@ function liveAppServer(handles) {
|
|
|
49
49
|
request: async (method, params) => {
|
|
50
50
|
handle.calls.push(method);
|
|
51
51
|
handle.requests.push({ method, params });
|
|
52
|
+
const overridden = await handle.respond?.(method, params);
|
|
53
|
+
if (overridden !== undefined)
|
|
54
|
+
return overridden;
|
|
52
55
|
if (method === 'thread/start')
|
|
53
56
|
return { thread: { id: `thread-${handles.length}` } };
|
|
54
57
|
if (method === 'turn/start')
|
|
@@ -363,7 +366,7 @@ describe('production composition', () => {
|
|
|
363
366
|
expect(messages.filter((message) => message.event.type === 'autopilot.updated').at(-1)?.event
|
|
364
367
|
.payload).toMatchObject({ enabled: true });
|
|
365
368
|
// This is production composition, not a coordinator fake: the scheduler reaches the
|
|
366
|
-
// real runtime adapter and can pass only
|
|
369
|
+
// real runtime adapter and can pass only policy-owned lifecycle context and an opaque ID.
|
|
367
370
|
await vi.waitFor(() => expect(handles.at(-1)?.calls.filter((call) => call === 'turn/start')).toHaveLength(1), { timeout: 2_500 });
|
|
368
371
|
const automaticStart = handles.at(-1)?.requests.find((call) => call.method === 'turn/start');
|
|
369
372
|
expect(automaticStart?.params).toEqual({
|
|
@@ -371,7 +374,7 @@ describe('production composition', () => {
|
|
|
371
374
|
input: [
|
|
372
375
|
{
|
|
373
376
|
type: 'text',
|
|
374
|
-
text: AUTOPILOT_CONTINUATION_PROMPT
|
|
377
|
+
text: `${AUTOPILOT_CONTINUATION_PROMPT} Launch task_name l1 for canonical L1; retain the canonical label in status and review output.`,
|
|
375
378
|
text_elements: [],
|
|
376
379
|
},
|
|
377
380
|
],
|
|
@@ -556,7 +559,7 @@ describe('production composition', () => {
|
|
|
556
559
|
expect(events.count).toBe(1);
|
|
557
560
|
await fixture.app.close();
|
|
558
561
|
});
|
|
559
|
-
it('production restart unexplained issued
|
|
562
|
+
it('production restart keeps unexplained issued state supervised while persisted activeTurn records one started audit without replay', async () => {
|
|
560
563
|
let coordinator;
|
|
561
564
|
const fixture = await createProductionAutopilotFixture({
|
|
562
565
|
onAutopilotCoordinator: (value) => {
|
|
@@ -577,11 +580,11 @@ describe('production composition', () => {
|
|
|
577
580
|
database.close();
|
|
578
581
|
coordinator.restore(fixture.sessionId);
|
|
579
582
|
await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
580
|
-
autopilot: { enabled:
|
|
583
|
+
autopilot: { enabled: true, state: 'monitoring', reason: 'reconcileFailed' },
|
|
581
584
|
}));
|
|
582
585
|
expect(fixture.handles.flatMap((handle) => handle.calls)).not.toContain('turn/start');
|
|
583
|
-
//
|
|
584
|
-
//
|
|
586
|
+
// Re-enabling is idempotent because runtime ambiguity remains under
|
|
587
|
+
// condition-based supervision rather than becoming synthetic attention.
|
|
585
588
|
expect((await fixture.app.inject({
|
|
586
589
|
method: 'PUT',
|
|
587
590
|
url: `/api/sessions/${fixture.sessionId}/autopilot`,
|
|
@@ -804,6 +807,37 @@ describe('production composition', () => {
|
|
|
804
807
|
it('production child-idle transition wakes an idle supervised autopilot', async () => {
|
|
805
808
|
const timers = [];
|
|
806
809
|
const fixture = await createProductionAutopilotFixture({
|
|
810
|
+
autopilotActivity: (sessionId) => {
|
|
811
|
+
const observedAt = new Date().toISOString();
|
|
812
|
+
return {
|
|
813
|
+
sessionId,
|
|
814
|
+
rootThreadId: 'thread-1',
|
|
815
|
+
root: {
|
|
816
|
+
state: 'idle',
|
|
817
|
+
reason: 'turnCompleted',
|
|
818
|
+
observedAt,
|
|
819
|
+
lastActivityAt: observedAt,
|
|
820
|
+
},
|
|
821
|
+
subagents: [
|
|
822
|
+
{
|
|
823
|
+
id: 'child-1',
|
|
824
|
+
threadId: 'child-1',
|
|
825
|
+
taskPath: '/root/l1',
|
|
826
|
+
canonicalTaskName: 'l1',
|
|
827
|
+
canonicalPosition: 'L1',
|
|
828
|
+
continuationGeneration: 1,
|
|
829
|
+
outcome: 'partial',
|
|
830
|
+
ownedProcesses: [],
|
|
831
|
+
state: 'idle',
|
|
832
|
+
reason: 'turnCompleted',
|
|
833
|
+
observedAt,
|
|
834
|
+
lastActivityAt: observedAt,
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
aggregateSubagents: 'idle',
|
|
838
|
+
confidence: 'fresh',
|
|
839
|
+
};
|
|
840
|
+
},
|
|
807
841
|
autopilotSchedule: (callback) => {
|
|
808
842
|
const timer = { callback, cancelled: false, fired: false };
|
|
809
843
|
timers.push(timer);
|
|
@@ -818,57 +852,17 @@ describe('production composition', () => {
|
|
|
818
852
|
payload: { enabled: false },
|
|
819
853
|
})).statusCode).toBe(200);
|
|
820
854
|
timers.length = 0;
|
|
821
|
-
const handle = fixture.handles.find((candidate) => candidate.notify);
|
|
822
|
-
handle.notify({
|
|
823
|
-
method: 'thread/started',
|
|
824
|
-
params: { thread: { id: 'thread-1', status: { type: 'active' } } },
|
|
825
|
-
});
|
|
826
|
-
handle.notify({
|
|
827
|
-
method: 'item/started',
|
|
828
|
-
params: {
|
|
829
|
-
item: {
|
|
830
|
-
type: 'collabToolCall',
|
|
831
|
-
tool: 'spawn_agent',
|
|
832
|
-
status: 'inProgress',
|
|
833
|
-
senderThreadId: 'thread-1',
|
|
834
|
-
receiverThreadId: 'child-1',
|
|
835
|
-
agentStatus: 'working',
|
|
836
|
-
},
|
|
837
|
-
},
|
|
838
|
-
});
|
|
839
|
-
handle.notify({
|
|
840
|
-
method: 'thread/status/changed',
|
|
841
|
-
params: { threadId: 'thread-1', status: { type: 'idle' } },
|
|
842
|
-
});
|
|
843
855
|
expect((await fixture.app.inject({
|
|
844
856
|
method: 'PUT',
|
|
845
857
|
url: `/api/sessions/${fixture.sessionId}/autopilot`,
|
|
846
858
|
payload: { enabled: true },
|
|
847
859
|
})).statusCode).toBe(200);
|
|
848
|
-
expect(timers).toHaveLength(0);
|
|
849
|
-
handle.notify({
|
|
850
|
-
method: 'item/completed',
|
|
851
|
-
params: {
|
|
852
|
-
item: {
|
|
853
|
-
type: 'collabToolCall',
|
|
854
|
-
tool: 'wait',
|
|
855
|
-
status: 'completed',
|
|
856
|
-
senderThreadId: 'thread-1',
|
|
857
|
-
receiverThreadId: 'child-1',
|
|
858
|
-
agentStatus: 'idle',
|
|
859
|
-
},
|
|
860
|
-
},
|
|
861
|
-
});
|
|
862
860
|
expect(timers.filter((timer) => !timer.cancelled && !timer.fired)).toHaveLength(1);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
await vi.waitFor(() => expect(fixture.handles
|
|
870
|
-
.flatMap((candidate) => candidate.calls)
|
|
871
|
-
.filter((call) => call === 'turn/start')).toHaveLength(1));
|
|
861
|
+
const timer = timers.find((candidate) => !candidate.cancelled && !candidate.fired);
|
|
862
|
+
timer.fired = true;
|
|
863
|
+
timer.callback();
|
|
864
|
+
await vi.waitFor(() => expect(fixture.handles.flatMap((candidate) => candidate.requests.filter((call) => call.method === 'turn/start'))).toHaveLength(1));
|
|
865
|
+
expect(fixture.handles.flatMap((candidate) => candidate.requests.filter((call) => call.method === 'turn/start'))[0]?.params).toMatchObject({ threadId: 'child-1' });
|
|
872
866
|
await fixture.app.close();
|
|
873
867
|
});
|
|
874
868
|
it('production blocked child wakes the idle supervisor for bounded recovery', async () => {
|
|
@@ -929,8 +923,8 @@ describe('production composition', () => {
|
|
|
929
923
|
expect(quietTimer).toBeDefined();
|
|
930
924
|
quietTimer.fired = true;
|
|
931
925
|
quietTimer.callback();
|
|
926
|
+
await vi.waitFor(() => expect(timers.find((timer) => !timer.cancelled && !timer.fired)).toBeDefined());
|
|
932
927
|
const continuationTimer = timers.find((timer) => !timer.cancelled && !timer.fired);
|
|
933
|
-
expect(continuationTimer).toBeDefined();
|
|
934
928
|
continuationTimer.fired = true;
|
|
935
929
|
continuationTimer.callback();
|
|
936
930
|
await vi.waitFor(() => expect(fixture.handles
|
|
@@ -938,10 +932,9 @@ describe('production composition', () => {
|
|
|
938
932
|
.filter((call) => call === 'turn/start')).toHaveLength(1));
|
|
939
933
|
await fixture.app.close();
|
|
940
934
|
});
|
|
941
|
-
it('production incompatible
|
|
935
|
+
it('production incompatible reconcile remains supervised and schedules reinspection', async () => {
|
|
942
936
|
let reconciliations = 0;
|
|
943
937
|
const timers = [];
|
|
944
|
-
let coordinator;
|
|
945
938
|
const fixture = await createProductionAutopilotFixture({
|
|
946
939
|
autopilotActivity: () => null,
|
|
947
940
|
autopilotReconcile: async () => {
|
|
@@ -952,9 +945,6 @@ describe('production composition', () => {
|
|
|
952
945
|
timers.push(callback);
|
|
953
946
|
return () => undefined;
|
|
954
947
|
},
|
|
955
|
-
onAutopilotCoordinator: (value) => {
|
|
956
|
-
coordinator = value;
|
|
957
|
-
},
|
|
958
948
|
});
|
|
959
949
|
expect((await fixture.app.inject({
|
|
960
950
|
method: 'PUT',
|
|
@@ -962,14 +952,14 @@ describe('production composition', () => {
|
|
|
962
952
|
payload: { enabled: true },
|
|
963
953
|
})).statusCode).toBe(200);
|
|
964
954
|
await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
965
|
-
autopilot: { state: '
|
|
955
|
+
autopilot: { state: 'monitoring', enabled: true, reason: 'reconcileFailed' },
|
|
966
956
|
}));
|
|
967
957
|
const stableReconciliations = reconciliations;
|
|
968
958
|
const stableTimers = timers.length;
|
|
969
|
-
|
|
970
|
-
await
|
|
971
|
-
expect(reconciliations).
|
|
972
|
-
expect(timers).
|
|
959
|
+
expect(stableTimers).toBeGreaterThan(0);
|
|
960
|
+
await timers.at(-1)?.();
|
|
961
|
+
await vi.waitFor(() => expect(reconciliations).toBeGreaterThan(stableReconciliations));
|
|
962
|
+
expect(timers.length).toBeGreaterThanOrEqual(stableTimers);
|
|
973
963
|
await fixture.app.close();
|
|
974
964
|
});
|
|
975
965
|
it('production post-acceptance fault recovers one accepted control without a second turn or audit', async () => {
|
|
@@ -49,7 +49,7 @@ import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-com
|
|
|
49
49
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
50
50
|
import { SqliteAutopilotStore } from './platform/persistence/sqlite-autopilot-store.js';
|
|
51
51
|
import { AutopilotCoordinator } from './features/autopilot/application/service.js';
|
|
52
|
-
import { AUTOPILOT_CONTINUATION_PROMPT, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
|
|
52
|
+
import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
|
|
53
53
|
import { createRelyingPartyConfig } from './config.js';
|
|
54
54
|
import { parseOrgPlanAttention } from '../shared/contracts/org-plan-attention.js';
|
|
55
55
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
@@ -144,11 +144,18 @@ export async function composeRelayApp(options) {
|
|
|
144
144
|
...(history.activeTurnId ? { turnId: history.activeTurnId } : {}),
|
|
145
145
|
});
|
|
146
146
|
const children = await runtime.listDirectChildren(session);
|
|
147
|
+
const childProcesses = new Map();
|
|
148
|
+
await mapWithConcurrency([...children], 4, async (child) => {
|
|
149
|
+
childProcesses.set(child.id, await runtime.inspectChildProcesses(session, child));
|
|
150
|
+
});
|
|
147
151
|
// The writer read is also asynchronous; it cannot publish after the
|
|
148
152
|
// durable owner has gone away either.
|
|
149
153
|
if (!sessions.find(sessionId))
|
|
150
154
|
return;
|
|
151
|
-
activity.childrenReconciled(sessionId, occurredAt, children)
|
|
155
|
+
activity.childrenReconciled(sessionId, occurredAt, children.map((child) => ({
|
|
156
|
+
...child,
|
|
157
|
+
processes: childProcesses.get(child.id) ?? [],
|
|
158
|
+
})));
|
|
152
159
|
},
|
|
153
160
|
});
|
|
154
161
|
const autopilot = new AutopilotCoordinator({
|
|
@@ -165,6 +172,15 @@ export async function composeRelayApp(options) {
|
|
|
165
172
|
? options.autopilotActivity(sessionId)
|
|
166
173
|
: activity.snapshot(sessionId, new Date().toISOString()),
|
|
167
174
|
pendingInteraction: (sessionId) => interactions.list(sessionId).length > 0,
|
|
175
|
+
attention: (sessionId) => {
|
|
176
|
+
const interaction = interactions
|
|
177
|
+
.list(sessionId)
|
|
178
|
+
.find((candidate) => candidate.kind === 'orgPlanAttention');
|
|
179
|
+
const attention = interaction ? parseOrgPlanAttention(interaction.payload) : null;
|
|
180
|
+
return attention
|
|
181
|
+
? { reason: attention.reason, resumeCondition: attention.resumeCondition }
|
|
182
|
+
: null;
|
|
183
|
+
},
|
|
168
184
|
reconcile: async (sessionId) => {
|
|
169
185
|
if (options.autopilotReconcile)
|
|
170
186
|
return options.autopilotReconcile(sessionId);
|
|
@@ -180,7 +196,7 @@ export async function composeRelayApp(options) {
|
|
|
180
196
|
}),
|
|
181
197
|
nextControlId: (sessionId, generation) => `autopilot-${generation}-${createHash('sha256').update(`${sessionId}:${randomUUID()}`).digest('hex').slice(0, 16)}`,
|
|
182
198
|
turnStarter: {
|
|
183
|
-
start: async (sessionId, controlId, generation) => {
|
|
199
|
+
start: async (sessionId, controlId, generation, launchIdentity) => {
|
|
184
200
|
const current = () => {
|
|
185
201
|
const state = autopilotStore.find(sessionId);
|
|
186
202
|
return Boolean(state &&
|
|
@@ -200,7 +216,10 @@ export async function composeRelayApp(options) {
|
|
|
200
216
|
if (!current() || writer.session.activeTurnId || interactions.list(sessionId).length)
|
|
201
217
|
throw new Error('AUTOPILOT_START_UNAVAILABLE');
|
|
202
218
|
await options.autopilotBeforeTurnAccepted?.({ sessionId, controlId });
|
|
203
|
-
const
|
|
219
|
+
const continuationPrompt = launchIdentity
|
|
220
|
+
? `${AUTOPILOT_CONTINUATION_PROMPT} Launch task_name ${launchIdentity.taskName} for canonical ${launchIdentity.canonicalPosition}; retain the canonical label in status and review output.`
|
|
221
|
+
: AUTOPILOT_CONTINUATION_PROMPT;
|
|
222
|
+
const started = await runtime.startTurn(writer.session, continuationPrompt, controlId, new Date().toISOString());
|
|
204
223
|
if (!started.activeTurnId)
|
|
205
224
|
throw new Error('AUTOPILOT_START_UNAVAILABLE');
|
|
206
225
|
sessions.save(started);
|
|
@@ -218,6 +237,46 @@ export async function composeRelayApp(options) {
|
|
|
218
237
|
});
|
|
219
238
|
},
|
|
220
239
|
},
|
|
240
|
+
executorController: {
|
|
241
|
+
resume: async (sessionId, threadId, generation, trigger) => {
|
|
242
|
+
const session = sessions.find(sessionId);
|
|
243
|
+
if (!session || !runtime || interactions.list(sessionId).length)
|
|
244
|
+
throw new Error('AUTOPILOT_EXECUTOR_UNAVAILABLE');
|
|
245
|
+
const writer = await runtime.ensureWriter(session, new Date().toISOString());
|
|
246
|
+
if (interactions.list(sessionId).length)
|
|
247
|
+
throw new Error('AUTOPILOT_EXECUTOR_UNAVAILABLE');
|
|
248
|
+
const context = trigger.kind === 'processExited'
|
|
249
|
+
? ` Process result ${trigger.resultArtifact} exited and is ready in this executor history.`
|
|
250
|
+
: trigger.kind === 'processResourceLimit'
|
|
251
|
+
? ` Process ${trigger.processId} exceeded its explicit resource budget and was terminated; diagnose before retrying.`
|
|
252
|
+
: '';
|
|
253
|
+
const clientId = `autopilot-executor-${generation}-${createHash('sha256')
|
|
254
|
+
.update(`${sessionId}:${threadId}:${generation}:${randomUUID()}`)
|
|
255
|
+
.digest('hex')
|
|
256
|
+
.slice(0, 16)}`;
|
|
257
|
+
const turnId = await runtime.startExecutorTurn(writer.session, threadId, `${AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT}${context}`, clientId);
|
|
258
|
+
activity.observe({
|
|
259
|
+
sessionId,
|
|
260
|
+
occurredAt: new Date().toISOString(),
|
|
261
|
+
kind: 'turnStarted',
|
|
262
|
+
threadId,
|
|
263
|
+
turnId,
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
refresh: (sessionId) => activity.refresh(sessionId),
|
|
267
|
+
transferProcess: (sessionId, threadId, processId) => {
|
|
268
|
+
activity.transferProcessOwnership(sessionId, threadId, processId, new Date().toISOString());
|
|
269
|
+
},
|
|
270
|
+
consumeProcess: (sessionId, threadId, processId) => {
|
|
271
|
+
runtime?.consumeChildProcessResult(sessionId, threadId, processId);
|
|
272
|
+
},
|
|
273
|
+
terminateProcess: async (sessionId, threadId, processId) => {
|
|
274
|
+
const session = sessions.find(sessionId);
|
|
275
|
+
return session && runtime
|
|
276
|
+
? runtime.terminateChildProcess(session, threadId, processId)
|
|
277
|
+
: false;
|
|
278
|
+
},
|
|
279
|
+
},
|
|
221
280
|
publish: (sessionId, type, payload, occurredAt, outboxId) => {
|
|
222
281
|
if (!sessions.find(sessionId))
|
|
223
282
|
return;
|
|
@@ -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 { parseOrgPlanAgentIdentity } from '../../../shared/org-plan-position.js';
|
|
6
7
|
const states = [
|
|
7
8
|
'blocked',
|
|
8
9
|
'awaitingHuman',
|
|
@@ -81,6 +82,9 @@ export function projectAgentActivity(current, fact) {
|
|
|
81
82
|
root.reason = 'missingCollaborationMetadata';
|
|
82
83
|
else {
|
|
83
84
|
const before = children.get(fact.childId);
|
|
85
|
+
const taskPath = fact.childTaskPath ?? before?.taskPath;
|
|
86
|
+
const identity = taskPath ? parseOrgPlanAgentIdentity(taskPath) : null;
|
|
87
|
+
const outcome = childOutcome(fact.childStatus, fact.collaborationAction, before?.outcome);
|
|
84
88
|
const child = Object.freeze({
|
|
85
89
|
id: fact.childId,
|
|
86
90
|
...(fact.childThreadId
|
|
@@ -99,6 +103,20 @@ export function projectAgentActivity(current, fact) {
|
|
|
99
103
|
: before?.model
|
|
100
104
|
? { model: before.model }
|
|
101
105
|
: {}),
|
|
106
|
+
...(taskPath ? { taskPath } : {}),
|
|
107
|
+
...(identity
|
|
108
|
+
? {
|
|
109
|
+
canonicalTaskName: identity.canonicalTaskName,
|
|
110
|
+
canonicalPosition: identity.canonicalPosition,
|
|
111
|
+
continuationGeneration: identity.generation,
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
...(outcome ? { outcome } : {}),
|
|
115
|
+
...(fact.childOwnedProcesses
|
|
116
|
+
? { ownedProcesses: Object.freeze([...fact.childOwnedProcesses]) }
|
|
117
|
+
: before?.ownedProcesses
|
|
118
|
+
? { ownedProcesses: before.ownedProcesses }
|
|
119
|
+
: {}),
|
|
102
120
|
state: childState(fact.childStatus, fact.collaborationAction, before?.state),
|
|
103
121
|
reason: childReason(fact.childStatus, fact.collaborationAction),
|
|
104
122
|
observedAt: fact.occurredAt,
|
|
@@ -145,6 +163,15 @@ function applyStatus(root, status) {
|
|
|
145
163
|
else if (status === 'active' || status === 'working')
|
|
146
164
|
Object.assign(root, { state: 'working', reason: 'turnActive' });
|
|
147
165
|
}
|
|
166
|
+
function childOutcome(status, action, previous) {
|
|
167
|
+
if (status === 'error' || status === 'failed' || status === 'systemError' || status === 'errored')
|
|
168
|
+
return 'failed';
|
|
169
|
+
if (status === 'interrupted' || status === 'shutdown' || action === 'close_agent')
|
|
170
|
+
return 'cancelled';
|
|
171
|
+
if (status === 'completed' || status === 'idle')
|
|
172
|
+
return 'partial';
|
|
173
|
+
return previous;
|
|
174
|
+
}
|
|
148
175
|
function childState(status, action, previous = 'working') {
|
|
149
176
|
if (status === 'error' || status === 'failed' || status === 'systemError' || status === 'errored')
|
|
150
177
|
return 'blocked';
|
|
@@ -91,6 +91,8 @@ export class AgentActivityRegistry {
|
|
|
91
91
|
...(child.nickname ? { childNickname: child.nickname } : {}),
|
|
92
92
|
...(child.role ? { childRole: child.role } : {}),
|
|
93
93
|
...(child.model ? { childModel: child.model } : {}),
|
|
94
|
+
...(child.taskPath ? { childTaskPath: child.taskPath } : {}),
|
|
95
|
+
...(child.processes ? { childOwnedProcesses: child.processes } : {}),
|
|
94
96
|
});
|
|
95
97
|
for (const child of next.subagents)
|
|
96
98
|
if (!seen.has(child.id))
|
|
@@ -112,6 +114,28 @@ export class AgentActivityRegistry {
|
|
|
112
114
|
}
|
|
113
115
|
return next;
|
|
114
116
|
}
|
|
117
|
+
transferProcessOwnership(sessionId, childThreadId, processId, occurredAt) {
|
|
118
|
+
const current = this.snapshot(sessionId, occurredAt);
|
|
119
|
+
const child = current.subagents.find((candidate) => candidate.id === childThreadId);
|
|
120
|
+
if (!child?.ownedProcesses?.some((process) => process.processId === processId))
|
|
121
|
+
return current;
|
|
122
|
+
const next = withActivityConfidence(projectAgentActivity(current, {
|
|
123
|
+
sessionId,
|
|
124
|
+
occurredAt,
|
|
125
|
+
kind: 'collaboration',
|
|
126
|
+
childId: child.id,
|
|
127
|
+
childThreadId: child.threadId,
|
|
128
|
+
childTaskPath: child.taskPath,
|
|
129
|
+
childOwnedProcesses: child.ownedProcesses.map((process) => process.processId === processId
|
|
130
|
+
? { ...process, ownership: 'supervisor', state: 'detached-active' }
|
|
131
|
+
: process),
|
|
132
|
+
}), 'fresh');
|
|
133
|
+
if (next !== current) {
|
|
134
|
+
this.#snapshots.set(sessionId, next);
|
|
135
|
+
this.publish(next, occurredAt);
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
115
139
|
dispose(sessionId) {
|
|
116
140
|
this.#disposed.add(sessionId);
|
|
117
141
|
this.#snapshots.delete(sessionId);
|
|
@@ -3,14 +3,20 @@
|
|
|
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 = '
|
|
6
|
+
export const AUTOPILOT_PROMPT_VERSION = 'v3';
|
|
7
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; otherwise immediately perform the next legal lifecycle action. Do not send a status-only response.';
|
|
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.';
|
|
8
9
|
export const defaultAutopilotPolicy = Object.freeze({
|
|
9
10
|
quiescenceMs: 1_000,
|
|
10
11
|
staleAfterMs: 30_000,
|
|
11
12
|
retryLimit: 3,
|
|
12
13
|
actionLimit: 12,
|
|
13
14
|
actionWindowMs: 10 * 60_000,
|
|
15
|
+
executorContinuationBaseMs: 1_000,
|
|
16
|
+
executorContinuationMaxMs: 60_000,
|
|
17
|
+
processPollMs: 1_000,
|
|
18
|
+
processMaxElapsedMs: 2 * 60 * 60_000,
|
|
19
|
+
processMaxRssBytes: 12 * 1024 * 1024 * 1024,
|
|
14
20
|
backoffMs: (attempt) => Math.min(60_000, 1_000 * 2 ** Math.max(0, attempt)),
|
|
15
21
|
promptVersion: AUTOPILOT_PROMPT_VERSION,
|
|
16
22
|
});
|
|
@@ -39,7 +45,7 @@ export function classifyAgentActivity(activity) {
|
|
|
39
45
|
/** A deliberately pure, exhaustive safety gate. Adapters may only enact this result. */
|
|
40
46
|
export function decideAutopilot(input) {
|
|
41
47
|
const { state, plan, activity, hasPendingInteraction, now, policy } = input;
|
|
42
|
-
if (input.hasActiveAttention
|
|
48
|
+
if (input.hasActiveAttention)
|
|
43
49
|
return {
|
|
44
50
|
kind: 'requestAttention',
|
|
45
51
|
reason: state.stopReason === 'noPlanProgress' ||
|
|
@@ -55,27 +61,23 @@ export function decideAutopilot(input) {
|
|
|
55
61
|
return { kind: 'disable', reason: 'planRequired' };
|
|
56
62
|
if (executionComplete(plan))
|
|
57
63
|
return { kind: 'complete' };
|
|
64
|
+
// Quiz, approval, and other held requests are ordinary session work. Only a
|
|
65
|
+
// validated Org attention record may turn an incomplete plan into a human stop.
|
|
58
66
|
if (hasPendingInteraction)
|
|
59
|
-
return { kind: '
|
|
67
|
+
return { kind: 'observe' };
|
|
60
68
|
if (!activity || activity.confidence !== 'fresh')
|
|
61
69
|
return { kind: 'reconcile' };
|
|
62
70
|
if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
|
|
63
71
|
return { kind: 'reconcile' };
|
|
64
72
|
const disposition = classifyAgentActivity(activity);
|
|
65
73
|
if (disposition === 'attention')
|
|
66
|
-
return { kind: '
|
|
74
|
+
return { kind: 'observe' };
|
|
67
75
|
if (disposition === 'reconcile')
|
|
68
76
|
return { kind: 'reconcile' };
|
|
69
77
|
if (disposition !== 'settled')
|
|
70
78
|
return { kind: 'observe' };
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// The outcome is intentionally interpreted only through durable lack of plan
|
|
74
|
-
// progress: a failed or unknown automatic turn may retry within the same
|
|
75
|
-
// bounded budget, while a completed turn with no fingerprint change does too.
|
|
76
|
-
if (state.consecutiveNoProgress >= policy.retryLimit &&
|
|
77
|
-
['completed', 'failed', 'unknown', undefined].includes(input.lastTurnOutcome))
|
|
78
|
-
return { kind: 'requestAttention', reason: 'noPlanProgress' };
|
|
79
|
+
// Automatic action counts and partial generations pace continuation, but
|
|
80
|
+
// cannot manufacture a human blocker while durable Org state remains WIP.
|
|
79
81
|
return {
|
|
80
82
|
kind: 'scheduleContinuation',
|
|
81
83
|
at: new Date(Date.parse(now) + policy.backoffMs(state.consecutiveNoProgress)).toISOString(),
|