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
|
@@ -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',
|
|
@@ -24,8 +25,20 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
24
25
|
if (blockingValue && !blocking)
|
|
25
26
|
return undefined;
|
|
26
27
|
const executorValue = record(root.executor);
|
|
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;
|
|
33
|
+
const checkpoints = checkpointsValue ? parseCheckpoints(checkpointsValue) : undefined;
|
|
34
|
+
if (checkpointsValue && !checkpoints)
|
|
35
|
+
return undefined;
|
|
27
36
|
if (!executorValue)
|
|
28
|
-
return
|
|
37
|
+
return {
|
|
38
|
+
...(blocking ? { blocking } : {}),
|
|
39
|
+
...(supervision ? { supervision } : {}),
|
|
40
|
+
...(checkpoints ? { checkpoints } : {}),
|
|
41
|
+
};
|
|
29
42
|
const processesValue = executorValue.ownedProcesses;
|
|
30
43
|
if (!Array.isArray(processesValue) || processesValue.length > 64)
|
|
31
44
|
return undefined;
|
|
@@ -131,6 +144,42 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
131
144
|
continuationCount,
|
|
132
145
|
},
|
|
133
146
|
...(blocking ? { blocking } : {}),
|
|
147
|
+
...(supervision ? { supervision } : {}),
|
|
148
|
+
...(checkpoints ? { checkpoints } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function parseCheckpoints(value) {
|
|
152
|
+
const planIdentity = boundedText(value.planIdentity);
|
|
153
|
+
if (value.protocolVersion !== 1 || !planIdentity)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (!Array.isArray(value.reportedL1Ids) || value.reportedL1Ids.length > 128)
|
|
156
|
+
return undefined;
|
|
157
|
+
const reportedL1Ids = value.reportedL1Ids
|
|
158
|
+
.map((id) => boundedText(id))
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
if (reportedL1Ids.length !== value.reportedL1Ids.length ||
|
|
161
|
+
new Set(reportedL1Ids).size !== reportedL1Ids.length)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (!Array.isArray(value.acceptedKeys) || value.acceptedKeys.length > 128)
|
|
164
|
+
return undefined;
|
|
165
|
+
const acceptedKeys = value.acceptedKeys
|
|
166
|
+
.map((key) => boundedText(key))
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
if (acceptedKeys.length !== value.acceptedKeys.length ||
|
|
169
|
+
new Set(acceptedKeys).size !== acceptedKeys.length)
|
|
170
|
+
return undefined;
|
|
171
|
+
const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
|
|
172
|
+
if (pendingTurnId === undefined)
|
|
173
|
+
return undefined;
|
|
174
|
+
if (typeof value.terminalReviewAccepted !== 'boolean')
|
|
175
|
+
return undefined;
|
|
176
|
+
return {
|
|
177
|
+
protocolVersion: 1,
|
|
178
|
+
planIdentity,
|
|
179
|
+
reportedL1Ids,
|
|
180
|
+
acceptedKeys,
|
|
181
|
+
pendingTurnId,
|
|
182
|
+
terminalReviewAccepted: value.terminalReviewAccepted,
|
|
134
183
|
};
|
|
135
184
|
}
|
|
136
185
|
export function classifyExecutorOutcome(input) {
|
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
/** Facts which may wake a parked supervisor without inspecting conversation text. */
|
|
7
|
+
export const observableWakeConditions = [
|
|
8
|
+
'planChanged',
|
|
9
|
+
'reviewChanged',
|
|
10
|
+
'checkpointChanged',
|
|
11
|
+
'interactionChanged',
|
|
12
|
+
'executorChanged',
|
|
13
|
+
'processExited',
|
|
14
|
+
'processResultAvailable',
|
|
15
|
+
'processLimitBreached',
|
|
16
|
+
'agentActivityChanged',
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* A canonical, durable representation rather than a clock- or transcript-derived score.
|
|
20
|
+
* The string is intentionally opaque to callers; compare it only for equality.
|
|
21
|
+
*/
|
|
22
|
+
export function semanticProgressKey(facts) {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
plan: facts.plan,
|
|
25
|
+
review: facts.review,
|
|
26
|
+
checkpoint: facts.checkpoint,
|
|
27
|
+
pendingInteractions: [...facts.pendingInteractions]
|
|
28
|
+
.map(({ id, kind, state }) => ({ id, kind, state }))
|
|
29
|
+
.sort(compareBy('id')),
|
|
30
|
+
executor: facts.executor,
|
|
31
|
+
ownedProcesses: [...facts.ownedProcesses]
|
|
32
|
+
.map(({ id, state, ownerGeneration }) => ({ id, state, ownerGeneration }))
|
|
33
|
+
.sort(compareBy('id')),
|
|
34
|
+
agentActivity: latestAgentActivity(facts.agentActivity),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function latestAgentActivity(observations) {
|
|
38
|
+
const latest = new Map();
|
|
39
|
+
for (const observation of observations) {
|
|
40
|
+
const current = latest.get(observation.agentId);
|
|
41
|
+
if (!current ||
|
|
42
|
+
observation.sequence > current.sequence ||
|
|
43
|
+
(observation.sequence === current.sequence &&
|
|
44
|
+
observation.state.localeCompare(current.state) < 0))
|
|
45
|
+
latest.set(observation.agentId, observation);
|
|
46
|
+
}
|
|
47
|
+
return [...latest.values()]
|
|
48
|
+
.map(({ agentId, sequence, state }) => ({ agentId, sequence, state }))
|
|
49
|
+
.sort(compareBy('agentId'));
|
|
50
|
+
}
|
|
51
|
+
function compareBy(key) {
|
|
52
|
+
return (left, right) => left[key].localeCompare(right[key]);
|
|
53
|
+
}
|
|
54
|
+
export function startSupervisionProtocol(progressKey) {
|
|
55
|
+
return {
|
|
56
|
+
outcome: 'active',
|
|
57
|
+
progressKey,
|
|
58
|
+
unchangedContinuations: 0,
|
|
59
|
+
probeKey: null,
|
|
60
|
+
lastReportId: null,
|
|
61
|
+
waitLease: null,
|
|
62
|
+
retryKey: null,
|
|
63
|
+
safetyPauseReason: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Records an automatic continuation result; prose and timestamps are not inputs. */
|
|
67
|
+
export function recordAutomaticContinuation(state, progressKey) {
|
|
68
|
+
if (state.outcome === 'parked' ||
|
|
69
|
+
state.outcome === 'attentionRequired' ||
|
|
70
|
+
state.outcome === 'safetyPaused')
|
|
71
|
+
return state;
|
|
72
|
+
// The probe itself is the one continuation allowed while this key is
|
|
73
|
+
// unchanged. Reaching the scheduling boundary again without a structured
|
|
74
|
+
// report or semantic progress means that probe failed closed.
|
|
75
|
+
if (state.outcome === 'probeRequired' && progressKey === state.probeKey)
|
|
76
|
+
return { ...state, outcome: 'safetyPaused', safetyPauseReason: 'invalidProbeReport' };
|
|
77
|
+
if (state.outcome === 'retrying' && progressKey === state.retryKey)
|
|
78
|
+
return state.unchangedContinuations === 0
|
|
79
|
+
? { ...state, unchangedContinuations: 1 }
|
|
80
|
+
: { ...state, outcome: 'safetyPaused', safetyPauseReason: 'retryRecurrence' };
|
|
81
|
+
if (progressKey !== state.progressKey)
|
|
82
|
+
return { ...startSupervisionProtocol(progressKey), lastReportId: state.lastReportId };
|
|
83
|
+
const unchangedContinuations = Math.min(3, state.unchangedContinuations + 1);
|
|
84
|
+
if (unchangedContinuations === 3 && state.probeKey !== progressKey)
|
|
85
|
+
return { ...state, unchangedContinuations, outcome: 'probeRequired', probeKey: progressKey };
|
|
86
|
+
return { ...state, unchangedContinuations };
|
|
87
|
+
}
|
|
88
|
+
/** Accepts only structured, bounded probe reports. Unsupported waits fail closed. */
|
|
89
|
+
export function reportProbe(state, report) {
|
|
90
|
+
if (state.lastReportId === report.id)
|
|
91
|
+
return state;
|
|
92
|
+
if (state.outcome !== 'probeRequired' || !state.probeKey)
|
|
93
|
+
return state;
|
|
94
|
+
if (report.kind === 'attention')
|
|
95
|
+
return { ...state, outcome: 'attentionRequired', lastReportId: report.id };
|
|
96
|
+
if (report.kind === 'actionable')
|
|
97
|
+
return {
|
|
98
|
+
...startSupervisionProtocol(state.progressKey),
|
|
99
|
+
lastReportId: report.id,
|
|
100
|
+
};
|
|
101
|
+
const wakeConditions = uniqueSupportedWakeConditions(report.wakeConditions);
|
|
102
|
+
if (!report.leaseId || wakeConditions.length === 0)
|
|
103
|
+
return state;
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
outcome: 'parked',
|
|
107
|
+
lastReportId: report.id,
|
|
108
|
+
waitLease: { id: report.leaseId, probeKey: state.probeKey, wakeConditions },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** A wake is useful only when it matches the durable lease and changed semantic state. */
|
|
112
|
+
export function consumeObservableWake(state, wake) {
|
|
113
|
+
const lease = state.waitLease;
|
|
114
|
+
if (state.outcome !== 'parked' ||
|
|
115
|
+
!lease ||
|
|
116
|
+
wake.leaseId !== lease.id ||
|
|
117
|
+
!lease.wakeConditions.includes(wake.condition) ||
|
|
118
|
+
wake.progressKey === lease.probeKey)
|
|
119
|
+
return state;
|
|
120
|
+
return {
|
|
121
|
+
...state,
|
|
122
|
+
outcome: 'retrying',
|
|
123
|
+
progressKey: wake.progressKey,
|
|
124
|
+
unchangedContinuations: 0,
|
|
125
|
+
probeKey: null,
|
|
126
|
+
waitLease: null,
|
|
127
|
+
retryKey: wake.progressKey,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Manual recovery is the only transition out of durable safety pause. */
|
|
131
|
+
export function recoverSafetyPause(state, progressKey) {
|
|
132
|
+
return state.outcome === 'safetyPaused' ? startSupervisionProtocol(progressKey) : state;
|
|
133
|
+
}
|
|
134
|
+
/** Reject malformed persisted values instead of silently resuming automatic work. */
|
|
135
|
+
export function parseSupervisionProtocolState(value) {
|
|
136
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
137
|
+
return undefined;
|
|
138
|
+
const candidate = value;
|
|
139
|
+
const base = startSupervisionProtocol(typeof candidate.progressKey === 'string' ? candidate.progressKey : '');
|
|
140
|
+
if (!base.progressKey || !isProtocolOutcome(candidate.outcome))
|
|
141
|
+
return undefined;
|
|
142
|
+
const unchangedContinuations = candidate.unchangedContinuations;
|
|
143
|
+
if (unchangedContinuations !== 0 &&
|
|
144
|
+
unchangedContinuations !== 1 &&
|
|
145
|
+
unchangedContinuations !== 2 &&
|
|
146
|
+
unchangedContinuations !== 3)
|
|
147
|
+
return undefined;
|
|
148
|
+
const probeKey = nullableText(candidate.probeKey);
|
|
149
|
+
const lastReportId = nullableText(candidate.lastReportId);
|
|
150
|
+
const retryKey = nullableText(candidate.retryKey);
|
|
151
|
+
if (probeKey === undefined || lastReportId === undefined || retryKey === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
const waitLease = parseWaitLease(candidate.waitLease);
|
|
154
|
+
if (waitLease === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
if (candidate.safetyPauseReason !== null &&
|
|
157
|
+
candidate.safetyPauseReason !== 'retryRecurrence' &&
|
|
158
|
+
candidate.safetyPauseReason !== 'invalidProbeReport')
|
|
159
|
+
return undefined;
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
outcome: candidate.outcome,
|
|
163
|
+
unchangedContinuations,
|
|
164
|
+
probeKey,
|
|
165
|
+
lastReportId,
|
|
166
|
+
waitLease,
|
|
167
|
+
retryKey,
|
|
168
|
+
safetyPauseReason: candidate.safetyPauseReason,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function uniqueSupportedWakeConditions(conditions) {
|
|
172
|
+
return [...new Set(conditions)].filter((condition) => observableWakeConditions.includes(condition));
|
|
173
|
+
}
|
|
174
|
+
function isProtocolOutcome(value) {
|
|
175
|
+
return [
|
|
176
|
+
'active',
|
|
177
|
+
'probeRequired',
|
|
178
|
+
'parked',
|
|
179
|
+
'retrying',
|
|
180
|
+
'attentionRequired',
|
|
181
|
+
'safetyPaused',
|
|
182
|
+
].includes(value);
|
|
183
|
+
}
|
|
184
|
+
function nullableText(value) {
|
|
185
|
+
return value === null ? null : typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
186
|
+
}
|
|
187
|
+
function parseWaitLease(value) {
|
|
188
|
+
if (value === null)
|
|
189
|
+
return null;
|
|
190
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
191
|
+
return undefined;
|
|
192
|
+
const candidate = value;
|
|
193
|
+
if (typeof candidate.id !== 'string' ||
|
|
194
|
+
!candidate.id ||
|
|
195
|
+
typeof candidate.probeKey !== 'string' ||
|
|
196
|
+
!candidate.probeKey ||
|
|
197
|
+
!Array.isArray(candidate.wakeConditions))
|
|
198
|
+
return undefined;
|
|
199
|
+
const wakeConditions = uniqueSupportedWakeConditions(candidate.wakeConditions);
|
|
200
|
+
return wakeConditions.length === candidate.wakeConditions.length
|
|
201
|
+
? { id: candidate.id, probeKey: candidate.probeKey, wakeConditions }
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export function validOrgPlanCheckpoint(input) {
|
|
7
|
+
const { checkpoint, plan, planIdentity, rootOwned } = input;
|
|
8
|
+
if (!rootOwned || checkpoint.planIdentity !== planIdentity)
|
|
9
|
+
return false;
|
|
10
|
+
if (checkpoint.kind === 'terminalReviewAccepted')
|
|
11
|
+
return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED');
|
|
12
|
+
const l1 = plan.steps.find((step) => step.id === checkpoint.l1Id);
|
|
13
|
+
if (!l1)
|
|
14
|
+
return false;
|
|
15
|
+
const position = `L${plan.steps.indexOf(l1) + 1}`;
|
|
16
|
+
return (checkpoint.position === position &&
|
|
17
|
+
l1.state === 'DONE' &&
|
|
18
|
+
l1.reviewStatus === 'REVIEWED' &&
|
|
19
|
+
!input.hasActiveL1Writer(position));
|
|
20
|
+
}
|
|
@@ -83,6 +83,7 @@ function toActivityHistory(events, throughSequence) {
|
|
|
83
83
|
label: payload.label,
|
|
84
84
|
detail: payload.detail,
|
|
85
85
|
...(typeof payload.turnId === 'string' ? { turnId: payload.turnId } : {}),
|
|
86
|
+
...(typeof payload.actorTurnId === 'string' ? { actorTurnId: payload.actorTurnId } : {}),
|
|
86
87
|
...(Number.isFinite(occurredAt) ? { occurredAt } : {}),
|
|
87
88
|
...(changes.length ? { changes } : {}),
|
|
88
89
|
});
|
|
@@ -11,7 +11,10 @@ export function isValidInteractionResponse(kind, value) {
|
|
|
11
11
|
return false;
|
|
12
12
|
if (kind === 'userInput')
|
|
13
13
|
return isValidUserInputResponse(value);
|
|
14
|
-
if (kind === 'quiz' ||
|
|
14
|
+
if (kind === 'quiz' ||
|
|
15
|
+
kind === 'orgPlanAttention' ||
|
|
16
|
+
kind === 'orgPlanCheckpoint' ||
|
|
17
|
+
kind === 'autopilotWaitLease')
|
|
15
18
|
return false;
|
|
16
19
|
if (kind === 'permissionsApproval')
|
|
17
20
|
return isRecord(value.permissions) && (value.scope === 'turn' || value.scope === 'session');
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { countDiffLines } from '../../../shared/contracts/file-change.js';
|
|
7
7
|
import { relative } from 'node:path';
|
|
8
|
-
export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath, activeTurnId) {
|
|
8
|
+
export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath, activeTurnId, origin = { kind: 'root' }) {
|
|
9
9
|
const decoded = decodeNotification(notification);
|
|
10
10
|
if (!decoded)
|
|
11
11
|
return null;
|
|
12
|
-
if (decoded.method === 'item/agentMessage/delta')
|
|
12
|
+
if (decoded.method === 'item/agentMessage/delta') {
|
|
13
|
+
if (origin.kind !== 'root')
|
|
14
|
+
return null;
|
|
13
15
|
return {
|
|
14
16
|
sessionId,
|
|
15
17
|
sequence,
|
|
@@ -18,12 +20,11 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
|
|
|
18
20
|
payload: {
|
|
19
21
|
text: decoded.params.delta,
|
|
20
22
|
...(decoded.params.itemId ? { itemId: decoded.params.itemId } : {}),
|
|
21
|
-
...(
|
|
22
|
-
? { turnId: decoded.params.turnId ?? activeTurnId }
|
|
23
|
-
: {}),
|
|
23
|
+
...(activeTurnId ? { turnId: activeTurnId } : {}),
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
}
|
|
27
|
+
if (decoded.method === 'turn/completed' && origin.kind === 'root')
|
|
27
28
|
return {
|
|
28
29
|
sessionId,
|
|
29
30
|
sequence,
|
|
@@ -37,13 +38,15 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
|
|
|
37
38
|
type: 'fileChange',
|
|
38
39
|
status: 'in_progress',
|
|
39
40
|
changes: decoded.params.changes,
|
|
40
|
-
}, workspacePath, activeTurnId, decoded.params.turnId);
|
|
41
|
+
}, workspacePath, activeTurnId, decoded.params.turnId, origin);
|
|
41
42
|
return activity
|
|
42
43
|
? { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity }
|
|
43
44
|
: null;
|
|
44
45
|
}
|
|
45
46
|
if (decoded.method === 'item/started' || decoded.method === 'item/completed') {
|
|
46
|
-
const message =
|
|
47
|
+
const message = origin.kind === 'root'
|
|
48
|
+
? safeAgentMessage(decoded.params.item, activeTurnId, decoded.params.turnId)
|
|
49
|
+
: null;
|
|
47
50
|
if (message)
|
|
48
51
|
return {
|
|
49
52
|
sessionId,
|
|
@@ -52,7 +55,7 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
|
|
|
52
55
|
type: decoded.method === 'item/started' ? 'agentMessageStarted' : 'agentMessageCompleted',
|
|
53
56
|
payload: message,
|
|
54
57
|
};
|
|
55
|
-
const activity = safeActivity(decoded.params.item, workspacePath, activeTurnId, decoded.params.turnId);
|
|
58
|
+
const activity = safeActivity(decoded.params.item, workspacePath, activeTurnId, decoded.params.turnId, origin);
|
|
56
59
|
if (activity)
|
|
57
60
|
return { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity };
|
|
58
61
|
}
|
|
@@ -120,19 +123,21 @@ export function decodeNotification(input) {
|
|
|
120
123
|
}
|
|
121
124
|
return null;
|
|
122
125
|
}
|
|
123
|
-
function safeActivity(item, workspacePath, activeTurnId, notificationTurnId) {
|
|
126
|
+
function safeActivity(item, workspacePath, activeTurnId, notificationTurnId, origin = { kind: 'root' }) {
|
|
124
127
|
if (!item || typeof item !== 'object')
|
|
125
128
|
return null;
|
|
126
129
|
const value = item;
|
|
127
130
|
if (typeof value.id !== 'string' || typeof value.type !== 'string')
|
|
128
131
|
return null;
|
|
129
|
-
const owner =
|
|
130
|
-
? { turnId:
|
|
131
|
-
:
|
|
132
|
-
? { turnId:
|
|
133
|
-
:
|
|
134
|
-
? { turnId:
|
|
135
|
-
:
|
|
132
|
+
const owner = origin.kind === 'child' && activeTurnId
|
|
133
|
+
? { turnId: activeTurnId, actorTurnId: origin.physicalTurnId }
|
|
134
|
+
: notificationTurnId && origin.kind === 'root'
|
|
135
|
+
? { turnId: notificationTurnId }
|
|
136
|
+
: safeId(value.turnId)
|
|
137
|
+
? { turnId: value.turnId }
|
|
138
|
+
: activeTurnId && origin.kind === 'root'
|
|
139
|
+
? { turnId: activeTurnId }
|
|
140
|
+
: {};
|
|
136
141
|
const status = typeof value.status === 'string' ? ` · ${value.status}` : '';
|
|
137
142
|
if (value.type === 'commandExecution' && typeof value.command === 'string')
|
|
138
143
|
return { id: value.id, label: `Command${status}`, detail: value.command, ...owner };
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { GESTALT_QUIZ_TOOL_NAME, parseQuiz } from '../../../shared/contracts/quiz.js';
|
|
7
7
|
import { GESTALT_ORG_PLAN_ATTENTION_TOOL_NAME, parseOrgPlanAttention, } from '../../../shared/contracts/org-plan-attention.js';
|
|
8
|
+
import { GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME, parseOrgPlanCheckpoint, } from '../../../shared/contracts/org-plan-checkpoint.js';
|
|
9
|
+
import { GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME, parseAutopilotWaitLease, } from '../../../shared/contracts/autopilot-wait-lease.js';
|
|
8
10
|
export function toPendingInteraction(input) {
|
|
9
11
|
if (!Number.isSafeInteger(input.id) || !isRecord(input.params))
|
|
10
12
|
return null;
|
|
@@ -19,6 +21,18 @@ export function toPendingInteraction(input) {
|
|
|
19
21
|
? { requestId: String(input.id), kind: 'orgPlanAttention', payload: attention }
|
|
20
22
|
: null;
|
|
21
23
|
}
|
|
24
|
+
if (input.params.tool === GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME) {
|
|
25
|
+
const checkpoint = parseOrgPlanCheckpoint(input.params.arguments);
|
|
26
|
+
return checkpoint
|
|
27
|
+
? { requestId: String(input.id), kind: 'orgPlanCheckpoint', payload: checkpoint }
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
if (input.params.tool === GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME) {
|
|
31
|
+
const lease = parseAutopilotWaitLease(input.params.arguments);
|
|
32
|
+
return lease
|
|
33
|
+
? { requestId: String(input.id), kind: 'autopilotWaitLease', payload: lease }
|
|
34
|
+
: null;
|
|
35
|
+
}
|
|
22
36
|
return null;
|
|
23
37
|
}
|
|
24
38
|
const kind = {
|
|
@@ -38,6 +52,12 @@ export function resolvedServerRequestId(input) {
|
|
|
38
52
|
return null;
|
|
39
53
|
return String(requestId);
|
|
40
54
|
}
|
|
55
|
+
export function isAutopilotWaitLeaseCall(input) {
|
|
56
|
+
return (Number.isSafeInteger(input.id) &&
|
|
57
|
+
input.method === 'item/tool/call' &&
|
|
58
|
+
isRecord(input.params) &&
|
|
59
|
+
input.params.tool === GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME);
|
|
60
|
+
}
|
|
41
61
|
function isRecord(value) {
|
|
42
62
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
43
63
|
}
|
|
@@ -9,6 +9,8 @@ import { createPlanMeasurementSnapshot, } from '../../features/plans/application
|
|
|
9
9
|
import { canRebindMissingRollout, rebindMissingRollout, } from '../../features/sessions/restore-session/use-case.js';
|
|
10
10
|
import { gestaltQuizDynamicTool } from '../../../shared/contracts/quiz.js';
|
|
11
11
|
import { gestaltOrgPlanAttentionDynamicTool } from '../../../shared/contracts/org-plan-attention.js';
|
|
12
|
+
import { gestaltOrgPlanCheckpointDynamicTool } from '../../../shared/contracts/org-plan-checkpoint.js';
|
|
13
|
+
import { gestaltAutopilotWaitLeaseDynamicTool } from '../../../shared/contracts/autopilot-wait-lease.js';
|
|
12
14
|
import { countDiffLines } from '../../../shared/contracts/file-change.js';
|
|
13
15
|
import { threadPlanName } from './thread-plan-name.js';
|
|
14
16
|
import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
|
|
@@ -32,6 +34,8 @@ class SessionResource {
|
|
|
32
34
|
spawnedAgentModels = new Map();
|
|
33
35
|
attemptedAgentModelRecovery = new Set();
|
|
34
36
|
ownedChildProcesses = new Map();
|
|
37
|
+
turnThreads = new Map();
|
|
38
|
+
childThreads = new Set();
|
|
35
39
|
constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
36
40
|
this.sessionId = sessionId;
|
|
37
41
|
this.process = process;
|
|
@@ -561,7 +565,12 @@ export class CodexSessionRuntime {
|
|
|
561
565
|
? { approvalPolicy: session.executionPolicy.approvalPolicy }
|
|
562
566
|
: {}),
|
|
563
567
|
...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
|
|
564
|
-
dynamicTools: [
|
|
568
|
+
dynamicTools: [
|
|
569
|
+
gestaltQuizDynamicTool,
|
|
570
|
+
gestaltOrgPlanAttentionDynamicTool,
|
|
571
|
+
gestaltOrgPlanCheckpointDynamicTool,
|
|
572
|
+
gestaltAutopilotWaitLeaseDynamicTool,
|
|
573
|
+
],
|
|
565
574
|
});
|
|
566
575
|
result = {
|
|
567
576
|
session: RelaySession.rehydrate(session).restore(now).supportsAttentionTool(now).snapshot,
|
|
@@ -629,7 +638,8 @@ export class CodexSessionRuntime {
|
|
|
629
638
|
resource.pendingRequests.set(requestId, { resolve, reject });
|
|
630
639
|
let accepted = false;
|
|
631
640
|
try {
|
|
632
|
-
accepted =
|
|
641
|
+
accepted =
|
|
642
|
+
this.onServerRequest?.(resource.sessionId, request, this.resolveNotificationOrigin(resource, request)) === true;
|
|
633
643
|
}
|
|
634
644
|
catch {
|
|
635
645
|
// Publication failures are protocol failures, not permission to retain
|
|
@@ -646,7 +656,12 @@ export class CodexSessionRuntime {
|
|
|
646
656
|
return {
|
|
647
657
|
cwd: session.workspacePath,
|
|
648
658
|
approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
|
|
649
|
-
dynamicTools: [
|
|
659
|
+
dynamicTools: [
|
|
660
|
+
gestaltQuizDynamicTool,
|
|
661
|
+
gestaltOrgPlanAttentionDynamicTool,
|
|
662
|
+
gestaltOrgPlanCheckpointDynamicTool,
|
|
663
|
+
gestaltAutopilotWaitLeaseDynamicTool,
|
|
664
|
+
],
|
|
650
665
|
...(session.model ? { model: session.model } : {}),
|
|
651
666
|
...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
|
|
652
667
|
};
|
|
@@ -709,7 +724,7 @@ export class CodexSessionRuntime {
|
|
|
709
724
|
// The notification callback reconciles the durable interaction record.
|
|
710
725
|
pending?.reject(new Error('CODEX_SERVER_REQUEST_CLEARED'));
|
|
711
726
|
}
|
|
712
|
-
this.onNotification?.(session.id, notification);
|
|
727
|
+
this.onNotification?.(session.id, notification, this.resolveNotificationOrigin(resource, notification));
|
|
713
728
|
});
|
|
714
729
|
const requestUnsubscribe = process.rpc.onServerRequest((request) => this.holdServerRequest(resource, request));
|
|
715
730
|
if (!resource.active) {
|
|
@@ -726,6 +741,48 @@ export class CodexSessionRuntime {
|
|
|
726
741
|
throw error;
|
|
727
742
|
}
|
|
728
743
|
}
|
|
744
|
+
resolveNotificationOrigin(resource, notification) {
|
|
745
|
+
const params = asRecord(notification.params);
|
|
746
|
+
if (!params)
|
|
747
|
+
return { kind: 'unknown' };
|
|
748
|
+
const item = asRecord(params.item);
|
|
749
|
+
if (item?.type === 'collabAgentToolCall' || item?.type === 'collabToolCall') {
|
|
750
|
+
for (const value of [item.receiverThreadId, item.newThreadId])
|
|
751
|
+
if (boundedString(value, 256) && resource.childThreads.size < 64)
|
|
752
|
+
resource.childThreads.add(boundedString(value, 256));
|
|
753
|
+
if (Array.isArray(item.receiverThreadIds))
|
|
754
|
+
for (const value of item.receiverThreadIds)
|
|
755
|
+
if (boundedString(value, 256) && resource.childThreads.size < 64)
|
|
756
|
+
resource.childThreads.add(boundedString(value, 256));
|
|
757
|
+
const states = asRecord(item.agentsStates);
|
|
758
|
+
if (states)
|
|
759
|
+
for (const childId of Object.keys(states))
|
|
760
|
+
if (boundedString(childId, 256) && resource.childThreads.size < 64)
|
|
761
|
+
resource.childThreads.add(childId);
|
|
762
|
+
}
|
|
763
|
+
const turn = asRecord(params.turn);
|
|
764
|
+
const turnId = boundedString(params.turnId, 256) ??
|
|
765
|
+
boundedString(turn?.id, 256) ??
|
|
766
|
+
boundedString(item?.turnId, 256);
|
|
767
|
+
if (notification.method === 'turn/started' && turnId && boundedString(params.threadId, 256)) {
|
|
768
|
+
if (resource.turnThreads.has(turnId) || resource.turnThreads.size < 256)
|
|
769
|
+
resource.turnThreads.set(turnId, boundedString(params.threadId, 256));
|
|
770
|
+
}
|
|
771
|
+
const resolvedThreadId = boundedString(params.threadId, 256) ??
|
|
772
|
+
(turnId ? resource.turnThreads.get(turnId) : undefined);
|
|
773
|
+
const origin = {
|
|
774
|
+
kind: resolvedThreadId && resolvedThreadId === resource.threadId
|
|
775
|
+
? 'root'
|
|
776
|
+
: resolvedThreadId && resource.childThreads.has(resolvedThreadId)
|
|
777
|
+
? 'child'
|
|
778
|
+
: 'unknown',
|
|
779
|
+
...(turnId ? { physicalTurnId: turnId } : {}),
|
|
780
|
+
...(resolvedThreadId ? { physicalThreadId: resolvedThreadId } : {}),
|
|
781
|
+
};
|
|
782
|
+
if (notification.method === 'turn/completed' && turnId)
|
|
783
|
+
resource.turnThreads.delete(turnId);
|
|
784
|
+
return origin;
|
|
785
|
+
}
|
|
729
786
|
}
|
|
730
787
|
function isMethodNotFound(error) {
|
|
731
788
|
return Boolean(error && typeof error === 'object' && error.code === -32601);
|
|
@@ -16,7 +16,14 @@ export class SqliteAutopilotStore {
|
|
|
16
16
|
if (!row)
|
|
17
17
|
return null;
|
|
18
18
|
const lifecycle = parseLifecycle(row.lifecycle_json);
|
|
19
|
-
if (![
|
|
19
|
+
if (![
|
|
20
|
+
'disabled',
|
|
21
|
+
'monitoring',
|
|
22
|
+
'backoff',
|
|
23
|
+
'attentionRequired',
|
|
24
|
+
'completed',
|
|
25
|
+
'safetyPaused',
|
|
26
|
+
].includes(String(row.state)) ||
|
|
20
27
|
![
|
|
21
28
|
'manualDisabled',
|
|
22
29
|
'planRequired',
|
|
@@ -30,6 +37,7 @@ export class SqliteAutopilotStore {
|
|
|
30
37
|
'planRemoved',
|
|
31
38
|
'planReplaced',
|
|
32
39
|
'sessionEnded',
|
|
40
|
+
'safetyPaused',
|
|
33
41
|
'null',
|
|
34
42
|
].includes(String(row.stop_reason)) ||
|
|
35
43
|
!Number.isSafeInteger(Number(row.generation)) ||
|
|
@@ -57,8 +65,13 @@ export class SqliteAutopilotStore {
|
|
|
57
65
|
save(state) {
|
|
58
66
|
this.db
|
|
59
67
|
.prepare('INSERT INTO autopilot_sessions (session_id,state,requested_enabled,plan_identity,plan_fingerprint,generation,no_progress_count,next_evaluation_at,last_control_id,stop_reason,lifecycle_json,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET state=excluded.state,requested_enabled=excluded.requested_enabled,plan_identity=excluded.plan_identity,plan_fingerprint=excluded.plan_fingerprint,generation=excluded.generation,no_progress_count=excluded.no_progress_count,next_evaluation_at=excluded.next_evaluation_at,last_control_id=excluded.last_control_id,stop_reason=excluded.stop_reason,lifecycle_json=excluded.lifecycle_json,updated_at=excluded.updated_at')
|
|
60
|
-
.run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking
|
|
61
|
-
? JSON.stringify({
|
|
68
|
+
.run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking || state.supervision || state.checkpoints
|
|
69
|
+
? JSON.stringify({
|
|
70
|
+
executor: state.executor,
|
|
71
|
+
blocking: state.blocking,
|
|
72
|
+
supervision: state.supervision,
|
|
73
|
+
checkpoints: state.checkpoints,
|
|
74
|
+
})
|
|
62
75
|
: null, state.updatedAt);
|
|
63
76
|
}
|
|
64
77
|
findControl(sessionId, controlId) {
|