gestalt-mobile 0.30.3 → 0.31.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-DHqGG2Sf.css → index-DDwVphGb.css} +1 -1
- package/dist/client/assets/{index-BK3qsn4l.js → index-DKzUE8x1.js} +24 -23
- package/dist/client/index.html +2 -2
- package/dist/server/server/composition.contracts.js +62 -1
- package/dist/server/server/composition.js +30 -4
- package/dist/server/server/features/agent-activity/activity-dto.js +35 -0
- package/dist/server/server/features/agent-activity/model.js +21 -4
- package/dist/server/server/features/autopilot/application/service.js +172 -24
- package/dist/server/server/features/autopilot/domain/autopilot-session.js +100 -1
- package/dist/server/server/features/sessions/get-session/endpoint.js +23 -9
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +14 -2
- package/dist/server/server/features/sessions/register-routes.js +1 -1
- package/dist/server/server/features/sessions/session-status.js +55 -0
- package/dist/server/server/platform/codex/session-runtime.js +15 -4
- package/gestalt-supervision-capabilities.json +15 -0
- package/package.json +3 -2
|
@@ -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 { observableWakeConditions, } from './supervision-protocol.js';
|
|
6
7
|
export function disabledAutopilot(sessionId, now) {
|
|
7
8
|
return {
|
|
8
9
|
sessionId,
|
|
@@ -18,10 +19,99 @@ export function disabledAutopilot(sessionId, now) {
|
|
|
18
19
|
updatedAt: now,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
export function autopilotSnapshot(state, retryLimit
|
|
22
|
+
export function autopilotSnapshot(state, retryLimit, facts = {
|
|
23
|
+
activeTurn: false,
|
|
24
|
+
executorActive: false,
|
|
25
|
+
control: 'none',
|
|
26
|
+
timerArmed: false,
|
|
27
|
+
reconciling: false,
|
|
28
|
+
planMatches: false,
|
|
29
|
+
parkedSubscriptionActive: false,
|
|
30
|
+
transitionFresh: false,
|
|
31
|
+
observedAt: state.updatedAt,
|
|
32
|
+
}) {
|
|
33
|
+
const supervision = state.supervision?.outcome ?? 'none';
|
|
34
|
+
const waitCategories = supportedWakeCategories(state.supervision?.waitLease?.wakeConditions ?? []);
|
|
35
|
+
const parkedWait = supervision === 'parked' && waitCategories.length > 0 && facts.parkedSubscriptionActive;
|
|
36
|
+
const evaluationAt = state.nextEvaluationAt ? Date.parse(state.nextEvaluationAt) : Number.NaN;
|
|
37
|
+
const scheduled = state.state === 'backoff' &&
|
|
38
|
+
facts.control === 'scheduled' &&
|
|
39
|
+
facts.timerArmed &&
|
|
40
|
+
Number.isFinite(evaluationAt) &&
|
|
41
|
+
evaluationAt > Date.parse(facts.observedAt);
|
|
42
|
+
const rootWorking = facts.activeTurn;
|
|
43
|
+
// Runtime observations are current at observedAt. A slow or old durable
|
|
44
|
+
// transition must not turn an actually active root/timer/reconciliation or
|
|
45
|
+
// installed wait lease into a false idle signal.
|
|
46
|
+
const liveContinuation = parkedWait || scheduled || rootWorking || facts.executorActive || facts.reconciling;
|
|
47
|
+
const healthy = state.requestedEnabled &&
|
|
48
|
+
facts.planMatches &&
|
|
49
|
+
!['attentionRequired', 'safetyPaused'].includes(supervision) &&
|
|
50
|
+
!['attentionRequired', 'safetyPaused', 'completed', 'disabled'].includes(state.state) &&
|
|
51
|
+
liveContinuation;
|
|
52
|
+
const phase = state.state === 'disabled'
|
|
53
|
+
? 'off'
|
|
54
|
+
: state.state === 'completed'
|
|
55
|
+
? 'complete'
|
|
56
|
+
: state.state === 'attentionRequired'
|
|
57
|
+
? 'needsYou'
|
|
58
|
+
: state.state === 'safetyPaused' || supervision === 'safetyPaused'
|
|
59
|
+
? 'safetyPaused'
|
|
60
|
+
: healthy && (parkedWait || facts.executorActive)
|
|
61
|
+
? 'waitingForAgentEvent'
|
|
62
|
+
: healthy && scheduled
|
|
63
|
+
? 'continuationScheduled'
|
|
64
|
+
: healthy && (supervision === 'probeRequired' || facts.reconciling)
|
|
65
|
+
? 'checkingState'
|
|
66
|
+
: healthy && rootWorking
|
|
67
|
+
? 'rootWorking'
|
|
68
|
+
: 'degraded';
|
|
69
|
+
const nextExpectedAction = phase === 'rootWorking'
|
|
70
|
+
? 'Wait for the root turn to settle.'
|
|
71
|
+
: phase === 'continuationScheduled'
|
|
72
|
+
? 'Run the scheduled continuation.'
|
|
73
|
+
: phase === 'waitingForAgentEvent'
|
|
74
|
+
? parkedWait
|
|
75
|
+
? 'Wait for a subscribed agent or process event.'
|
|
76
|
+
: 'Wait for the active executor or owned process to settle.'
|
|
77
|
+
: phase === 'checkingState'
|
|
78
|
+
? 'Reconcile supervised execution state.'
|
|
79
|
+
: phase === 'needsYou'
|
|
80
|
+
? 'Respond to the pending attention request.'
|
|
81
|
+
: phase === 'complete'
|
|
82
|
+
? 'No further plan action is required.'
|
|
83
|
+
: phase === 'off'
|
|
84
|
+
? 'Enable Autopilot to supervise an incomplete plan.'
|
|
85
|
+
: phase === 'safetyPaused'
|
|
86
|
+
? 'Resume manually after reviewing the safety pause.'
|
|
87
|
+
: 'Restore a valid continuation before relying on Autopilot.';
|
|
22
88
|
return {
|
|
23
89
|
state: state.state,
|
|
24
90
|
enabled: state.requestedEnabled,
|
|
91
|
+
health: {
|
|
92
|
+
healthy,
|
|
93
|
+
phase,
|
|
94
|
+
supervision,
|
|
95
|
+
wait: {
|
|
96
|
+
present: Boolean(state.supervision?.waitLease),
|
|
97
|
+
wakeCategories: [...waitCategories].slice(0, 9),
|
|
98
|
+
},
|
|
99
|
+
observedAt: facts.observedAt,
|
|
100
|
+
lastTransitionAt: state.updatedAt,
|
|
101
|
+
nextExpectedAction,
|
|
102
|
+
...(!healthy && state.requestedEnabled
|
|
103
|
+
? {
|
|
104
|
+
degradationReason: !facts.planMatches
|
|
105
|
+
? 'planMismatch'
|
|
106
|
+
: !facts.transitionFresh ||
|
|
107
|
+
(Number.isFinite(evaluationAt) && evaluationAt <= Date.parse(facts.observedAt))
|
|
108
|
+
? 'staleTransition'
|
|
109
|
+
: state.supervision?.waitLease && !parkedWait
|
|
110
|
+
? 'invalidWaitLease'
|
|
111
|
+
: 'missingContinuation',
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
},
|
|
25
115
|
...(state.stopReason ? { reason: state.stopReason } : {}),
|
|
26
116
|
retry: { position: state.consecutiveNoProgress, limit: retryLimit },
|
|
27
117
|
...(state.lastControlId
|
|
@@ -40,3 +130,12 @@ export function autopilotSnapshot(state, retryLimit) {
|
|
|
40
130
|
updatedAt: state.updatedAt,
|
|
41
131
|
};
|
|
42
132
|
}
|
|
133
|
+
function supportedWakeCategories(conditions) {
|
|
134
|
+
if (conditions.length > observableWakeConditions.length)
|
|
135
|
+
return [];
|
|
136
|
+
const categories = conditions.filter((condition) => typeof condition === 'string' &&
|
|
137
|
+
observableWakeConditions.includes(condition));
|
|
138
|
+
return categories.length === conditions.length && new Set(categories).size === categories.length
|
|
139
|
+
? categories
|
|
140
|
+
: [];
|
|
141
|
+
}
|
|
@@ -4,16 +4,30 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { buildResumeCommand } from '../application/resume-command.js';
|
|
7
|
-
|
|
7
|
+
import { toAgentActivityDto } from '../../agent-activity/activity-dto.js';
|
|
8
|
+
import { deriveSessionStatus } from '../session-status.js';
|
|
9
|
+
export function registerGetSession(app, find, activity, autopilot, plan) {
|
|
8
10
|
app.get('/api/sessions/:id', async (request, reply) => {
|
|
9
11
|
const session = find(request.params.id);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
:
|
|
12
|
+
if (!session)
|
|
13
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
14
|
+
const activitySnapshot = activity?.(session.id) ?? null;
|
|
15
|
+
const autopilotSnapshot = autopilot?.(session.id) ?? null;
|
|
16
|
+
const retainedPlan = plan?.(session.id) ?? null;
|
|
17
|
+
return reply.send({
|
|
18
|
+
...session,
|
|
19
|
+
...(activitySnapshot ? { agentActivity: toAgentActivityDto(activitySnapshot) } : {}),
|
|
20
|
+
...(autopilotSnapshot ? { autopilot: autopilotSnapshot } : {}),
|
|
21
|
+
...(retainedPlan ? { plan: retainedPlan } : {}),
|
|
22
|
+
sessionStatus: deriveSessionStatus({
|
|
23
|
+
session,
|
|
24
|
+
plan: retainedPlan,
|
|
25
|
+
activity: activitySnapshot,
|
|
26
|
+
autopilot: autopilotSnapshot,
|
|
27
|
+
pendingAttention: Boolean(session.pendingInteractions?.length),
|
|
28
|
+
observedAt: new Date().toISOString(),
|
|
29
|
+
}),
|
|
30
|
+
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
31
|
+
});
|
|
18
32
|
});
|
|
19
33
|
}
|
|
@@ -4,14 +4,26 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { buildResumeCommand } from '../application/resume-command.js';
|
|
7
|
+
import { toAgentActivityDto } from '../../agent-activity/activity-dto.js';
|
|
8
|
+
import { deriveSessionStatus } from '../session-status.js';
|
|
7
9
|
export function registerListSessions(app, deps) {
|
|
8
10
|
app.get('/api/sessions', async () => deps.list().map((session) => {
|
|
9
11
|
const plan = deps.plan?.(session.id) ?? null;
|
|
12
|
+
const activity = deps.activity?.(session.id) ?? null;
|
|
13
|
+
const autopilot = deps.autopilot?.(session.id) ?? null;
|
|
10
14
|
return {
|
|
11
15
|
...session,
|
|
12
|
-
...(
|
|
13
|
-
...(
|
|
16
|
+
...(activity ? { agentActivity: toAgentActivityDto(activity) } : {}),
|
|
17
|
+
...(autopilot ? { autopilot } : {}),
|
|
14
18
|
...(plan ? { plan } : {}),
|
|
19
|
+
sessionStatus: deriveSessionStatus({
|
|
20
|
+
session,
|
|
21
|
+
plan,
|
|
22
|
+
activity,
|
|
23
|
+
autopilot,
|
|
24
|
+
pendingAttention: Boolean(session.pendingInteractions?.length),
|
|
25
|
+
observedAt: new Date().toISOString(),
|
|
26
|
+
}),
|
|
15
27
|
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
16
28
|
};
|
|
17
29
|
}));
|
|
@@ -48,7 +48,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
48
48
|
...sessions,
|
|
49
49
|
reportFailure: (operation, error) => deps.logger.error(`${operation} failed: ${safeErrorLabel(error)}`),
|
|
50
50
|
});
|
|
51
|
-
registerGetSession(app, sessions.find, sessions.agentActivity, sessions.autopilotSnapshot);
|
|
51
|
+
registerGetSession(app, sessions.find, sessions.agentActivity, sessions.autopilotSnapshot, sessions.plan);
|
|
52
52
|
if (sessions.refreshActivity)
|
|
53
53
|
registerRefreshActivity(app, {
|
|
54
54
|
exists: (id) => sessions.find(id) !== null,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A conservative, server-owned verdict. It deliberately uses only durable
|
|
8
|
+
* state and bounded activity facts: neither model prose nor opaque identity is
|
|
9
|
+
* accepted as evidence of progress.
|
|
10
|
+
*/
|
|
11
|
+
export function deriveSessionStatus(input) {
|
|
12
|
+
const confidence = input.activity?.confidence ?? 'stale';
|
|
13
|
+
const base = (state, reason, nextExpectedAction) => ({
|
|
14
|
+
state,
|
|
15
|
+
reason,
|
|
16
|
+
confidence,
|
|
17
|
+
observedAt: input.observedAt,
|
|
18
|
+
nextExpectedAction,
|
|
19
|
+
});
|
|
20
|
+
if (input.pendingAttention || input.autopilot?.state === 'attentionRequired')
|
|
21
|
+
return base('idle', 'needsYou', 'Respond to the pending request.');
|
|
22
|
+
const activeProcess = input.activity?.subagents.some((agent) => agent.ownedProcesses?.some((process) => process.state === 'running' || process.state === 'detached-active'));
|
|
23
|
+
const rootWorking = Boolean(input.session.activeTurnId) ||
|
|
24
|
+
(input.activity?.confidence === 'fresh' && input.activity.root.state === 'working');
|
|
25
|
+
const observedChildWorking = input.activity?.subagents.some((agent) => agent.state === 'working' || agent.state === 'awaitingAgent');
|
|
26
|
+
// A completed plan is authoritative. Old child/process rows are historical
|
|
27
|
+
// unless their activity observation is fresh; an active root turn remains
|
|
28
|
+
// direct session evidence regardless of roster freshness.
|
|
29
|
+
const childWorking = input.activity?.confidence === 'fresh' ? observedChildWorking : false;
|
|
30
|
+
const currentProcess = input.activity?.confidence === 'fresh' ? activeProcess : false;
|
|
31
|
+
if (input.plan?.executionComplete || input.plan?.allDone) {
|
|
32
|
+
if (rootWorking)
|
|
33
|
+
return base('working', 'rootTurn', 'Wait for the active turn to settle.');
|
|
34
|
+
if (childWorking)
|
|
35
|
+
return base('working', 'agent', 'Wait for the active agent to settle.');
|
|
36
|
+
if (currentProcess)
|
|
37
|
+
return base('working', 'process', 'Wait for the owned process result.');
|
|
38
|
+
return base('idle', 'complete', 'No further plan action is required.');
|
|
39
|
+
}
|
|
40
|
+
if (rootWorking)
|
|
41
|
+
return base('working', 'rootTurn', 'Wait for the supervisor turn to settle.');
|
|
42
|
+
if (childWorking)
|
|
43
|
+
return base('working', 'agent', 'Wait for the active agent to settle.');
|
|
44
|
+
if (currentProcess)
|
|
45
|
+
return base('working', 'process', 'Wait for the owned process result.');
|
|
46
|
+
if (input.plan && input.autopilot?.health?.healthy)
|
|
47
|
+
return base('working', 'autopilot', input.autopilot.health.nextExpectedAction);
|
|
48
|
+
if (input.plan)
|
|
49
|
+
return base('idle', 'incompleteWithoutContinuation', 'Resume supervision or enable a healthy Autopilot continuation.');
|
|
50
|
+
if (input.activity?.root.state === 'disconnected')
|
|
51
|
+
return base('idle', 'disconnected', 'Restore or reopen the session.');
|
|
52
|
+
if (input.session.state === 'stopped' || input.session.state === 'released')
|
|
53
|
+
return base('idle', 'stopped', 'Open the session to continue.');
|
|
54
|
+
return base('idle', 'unknown', 'Refresh session status.');
|
|
55
|
+
}
|
|
@@ -294,9 +294,10 @@ export class CodexSessionRuntime {
|
|
|
294
294
|
if (!owned)
|
|
295
295
|
return [];
|
|
296
296
|
const children = [];
|
|
297
|
+
const childIds = new Set();
|
|
297
298
|
const cursors = new Set();
|
|
298
299
|
let cursor;
|
|
299
|
-
for (let page = 0; page < 4 &&
|
|
300
|
+
for (let page = 0; page < 4 && childIds.size < 64; page += 1) {
|
|
300
301
|
const result = await owned.process.rpc.request('thread/list', {
|
|
301
302
|
parentThreadId: session.threadId,
|
|
302
303
|
...(cursor ? { cursor } : {}),
|
|
@@ -320,6 +321,7 @@ export class CodexSessionRuntime {
|
|
|
320
321
|
rawStatus === 'systemError'
|
|
321
322
|
? rawStatus
|
|
322
323
|
: undefined;
|
|
324
|
+
childIds.add(value.id);
|
|
323
325
|
return [
|
|
324
326
|
{
|
|
325
327
|
id: value.id,
|
|
@@ -339,19 +341,21 @@ export class CodexSessionRuntime {
|
|
|
339
341
|
},
|
|
340
342
|
];
|
|
341
343
|
}));
|
|
344
|
+
if (childIds.size > 64)
|
|
345
|
+
throw new Error('CODEX_CHILD_LIST_UNSUPPORTED');
|
|
342
346
|
const next = typeof response?.nextCursor === 'string' && response.nextCursor.length <= 256
|
|
343
347
|
? response.nextCursor
|
|
344
348
|
: undefined;
|
|
345
349
|
if (!next)
|
|
346
|
-
return this.withResolvedChildModels(owned, children);
|
|
347
|
-
if (cursors.has(next) ||
|
|
350
|
+
return this.withResolvedChildModels(owned, uniqueDirectChildren(children));
|
|
351
|
+
if (cursors.has(next) || childIds.size >= 64)
|
|
348
352
|
throw new Error('CODEX_CHILD_LIST_UNSUPPORTED');
|
|
349
353
|
cursors.add(next);
|
|
350
354
|
cursor = next;
|
|
351
355
|
}
|
|
352
356
|
if (cursor)
|
|
353
357
|
throw new Error('CODEX_CHILD_LIST_UNSUPPORTED');
|
|
354
|
-
return this.withResolvedChildModels(owned, children);
|
|
358
|
+
return this.withResolvedChildModels(owned, uniqueDirectChildren(children));
|
|
355
359
|
}
|
|
356
360
|
async withResolvedChildModels(resource, children) {
|
|
357
361
|
for (const child of children) {
|
|
@@ -784,6 +788,13 @@ export class CodexSessionRuntime {
|
|
|
784
788
|
return origin;
|
|
785
789
|
}
|
|
786
790
|
}
|
|
791
|
+
/** `thread/list` pages can replay an id while a child transitions. Keep its newest row. */
|
|
792
|
+
function uniqueDirectChildren(children) {
|
|
793
|
+
const result = new Map();
|
|
794
|
+
for (const child of children)
|
|
795
|
+
result.set(child.id, child);
|
|
796
|
+
return [...result.values()].slice(0, 64);
|
|
797
|
+
}
|
|
787
798
|
function isMethodNotFound(error) {
|
|
788
799
|
return Boolean(error && typeof error === 'object' && error.code === -32601);
|
|
789
800
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"component": "mobile",
|
|
4
|
+
"supervisionContract": 1,
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"supervision-start",
|
|
7
|
+
"wait-lease-tool",
|
|
8
|
+
"checkpoint-tool",
|
|
9
|
+
"controller-status",
|
|
10
|
+
"canonical-agent-identity",
|
|
11
|
+
"session-verdict",
|
|
12
|
+
"acknowledgement-safe-composer",
|
|
13
|
+
"org-plan-contract"
|
|
14
|
+
]
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gestalt-mobile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Mobile-first web relay for durable Codex development sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"dist/server",
|
|
27
27
|
"dist/client",
|
|
28
28
|
"README.md",
|
|
29
|
-
"LICENSE"
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"gestalt-supervision-capabilities.json"
|
|
30
31
|
],
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=24.0.0"
|