gestalt-mobile 0.35.0 → 0.35.2
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-BTJImyK1.js → index-SZM7lpix.js} +25 -25
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +877 -3
- package/dist/server/server/composition.js +173 -9
- package/dist/server/server/features/autopilot/application/policy.js +3 -3
- package/dist/server/server/features/autopilot/application/service.js +944 -102
- package/dist/server/server/features/autopilot/domain/autopilot-session.js +42 -36
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +196 -1
- package/dist/server/server/features/autopilot/domain/supervision-protocol.js +98 -0
- package/dist/server/server/platform/codex/json-rpc-client.js +19 -2
- package/dist/server/server/platform/codex/session-runtime.js +87 -10
- package/dist/server/shared/contracts/autopilot-recovery.js +7 -0
- package/dist/server/shared/contracts/org-plan-checkpoint.js +9 -1
- package/dist/server/shared/org-plan-position.js +3 -3
- package/package.json +1 -1
|
@@ -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 { checkpointHandoffRecoveryMessage } from '../../../../shared/contracts/autopilot-recovery.js';
|
|
6
7
|
import { observableWakeConditions, } from './supervision-protocol.js';
|
|
7
8
|
export function disabledAutopilot(sessionId, now) {
|
|
8
9
|
return {
|
|
@@ -45,46 +46,51 @@ export function autopilotSnapshot(state, retryLimit, facts = {
|
|
|
45
46
|
// installed wait lease into a false idle signal.
|
|
46
47
|
const liveContinuation = parkedWait || scheduled || rootWorking || facts.executorActive || facts.reconciling;
|
|
47
48
|
const healthy = state.requestedEnabled &&
|
|
49
|
+
!state.checkpoints?.checkpointHandoffFailed &&
|
|
48
50
|
facts.planMatches &&
|
|
49
51
|
!['attentionRequired', 'safetyPaused'].includes(supervision) &&
|
|
50
52
|
!['attentionRequired', 'safetyPaused', 'completed', 'disabled'].includes(state.state) &&
|
|
51
53
|
liveContinuation;
|
|
52
|
-
const phase = state.
|
|
53
|
-
? '
|
|
54
|
-
: state.state === '
|
|
55
|
-
? '
|
|
56
|
-
: state.state === '
|
|
57
|
-
? '
|
|
58
|
-
: state.state === '
|
|
59
|
-
? '
|
|
60
|
-
:
|
|
61
|
-
? '
|
|
62
|
-
: healthy &&
|
|
63
|
-
? '
|
|
64
|
-
: healthy &&
|
|
65
|
-
? '
|
|
66
|
-
: healthy &&
|
|
67
|
-
? '
|
|
68
|
-
:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: phase === '
|
|
78
|
-
?
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
54
|
+
const phase = state.checkpoints?.checkpointHandoffFailed
|
|
55
|
+
? 'checkpointRecovery'
|
|
56
|
+
: state.state === 'disabled'
|
|
57
|
+
? 'off'
|
|
58
|
+
: state.state === 'completed'
|
|
59
|
+
? 'complete'
|
|
60
|
+
: state.state === 'attentionRequired'
|
|
61
|
+
? 'needsYou'
|
|
62
|
+
: state.state === 'safetyPaused' || supervision === 'safetyPaused'
|
|
63
|
+
? 'safetyPaused'
|
|
64
|
+
: healthy && (parkedWait || facts.executorActive)
|
|
65
|
+
? 'waitingForAgentEvent'
|
|
66
|
+
: healthy && scheduled
|
|
67
|
+
? 'continuationScheduled'
|
|
68
|
+
: healthy && (supervision === 'probeRequired' || facts.reconciling)
|
|
69
|
+
? 'checkingState'
|
|
70
|
+
: healthy && rootWorking
|
|
71
|
+
? 'rootWorking'
|
|
72
|
+
: 'degraded';
|
|
73
|
+
const nextExpectedAction = phase === 'checkpointRecovery'
|
|
74
|
+
? checkpointHandoffRecoveryMessage
|
|
75
|
+
: phase === 'rootWorking'
|
|
76
|
+
? 'Wait for the root turn to settle.'
|
|
77
|
+
: phase === 'continuationScheduled'
|
|
78
|
+
? 'Run the scheduled continuation.'
|
|
79
|
+
: phase === 'waitingForAgentEvent'
|
|
80
|
+
? parkedWait
|
|
81
|
+
? 'Wait for a subscribed agent or process event.'
|
|
82
|
+
: 'Wait for the active executor or owned process to settle.'
|
|
83
|
+
: phase === 'checkingState'
|
|
84
|
+
? 'Reconcile supervised execution state.'
|
|
85
|
+
: phase === 'needsYou'
|
|
86
|
+
? 'Respond to the pending attention request.'
|
|
87
|
+
: phase === 'complete'
|
|
88
|
+
? 'No further plan action is required.'
|
|
89
|
+
: phase === 'off'
|
|
90
|
+
? 'Enable Autopilot to supervise an incomplete plan.'
|
|
91
|
+
: phase === 'safetyPaused'
|
|
92
|
+
? 'Resume manually after reviewing the safety pause.'
|
|
93
|
+
: 'Restore a valid continuation before relying on Autopilot.';
|
|
88
94
|
return {
|
|
89
95
|
state: state.state,
|
|
90
96
|
enabled: state.requestedEnabled,
|
|
@@ -13,6 +13,44 @@ export const blockingResumeConditions = {
|
|
|
13
13
|
externalState: 'externalStateChanged',
|
|
14
14
|
materialAmbiguity: 'userGuidance',
|
|
15
15
|
};
|
|
16
|
+
export function checkpointTarget(kind, l1Id, l2Id) {
|
|
17
|
+
return kind === 'l2'
|
|
18
|
+
? JSON.stringify([kind, l1Id, l2Id])
|
|
19
|
+
: kind === 'l1'
|
|
20
|
+
? JSON.stringify([kind, l1Id])
|
|
21
|
+
: JSON.stringify([kind]);
|
|
22
|
+
}
|
|
23
|
+
function canonicalCheckpointTarget(value) {
|
|
24
|
+
if (typeof value !== 'string')
|
|
25
|
+
return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const target = JSON.parse(value);
|
|
28
|
+
if (!Array.isArray(target) ||
|
|
29
|
+
!['l1', 'l2', 'terminal'].includes(target[0]) ||
|
|
30
|
+
!target.every((part) => typeof part === 'string'))
|
|
31
|
+
return undefined;
|
|
32
|
+
if ((target[0] === 'l1' && target.length !== 2) ||
|
|
33
|
+
(target[0] === 'l2' && target.length !== 3) ||
|
|
34
|
+
(target[0] === 'terminal' && target.length !== 1))
|
|
35
|
+
return undefined;
|
|
36
|
+
if (!target.slice(1).every((part) => boundedText(part)))
|
|
37
|
+
return undefined;
|
|
38
|
+
return JSON.stringify(target);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const legacy = value.split(':');
|
|
42
|
+
return legacy[0] === 'l1' && legacy.length === 2 && boundedText(legacy[1])
|
|
43
|
+
? checkpointTarget('l1', legacy[1])
|
|
44
|
+
: legacy[0] === 'l2' &&
|
|
45
|
+
legacy.length === 3 &&
|
|
46
|
+
boundedText(legacy[1]) &&
|
|
47
|
+
boundedText(legacy[2])
|
|
48
|
+
? checkpointTarget('l2', legacy[1], legacy[2])
|
|
49
|
+
: value === 'terminal'
|
|
50
|
+
? checkpointTarget('terminal')
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
16
54
|
export function validStructuredBlock(value) {
|
|
17
55
|
return Boolean(value && blockingResumeConditions[value.reason] === value.resumeCondition);
|
|
18
56
|
}
|
|
@@ -112,6 +150,16 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
112
150
|
]);
|
|
113
151
|
const continuationGeneration = nonNegativeInteger(executorValue.continuationGeneration);
|
|
114
152
|
const continuationCount = nonNegativeInteger(executorValue.continuationCount);
|
|
153
|
+
const resumeFailures = executorValue.resumeFailures === undefined
|
|
154
|
+
? 0
|
|
155
|
+
: nonNegativeInteger(executorValue.resumeFailures);
|
|
156
|
+
const replacementValue = executorValue.replacement === undefined ? undefined : record(executorValue.replacement);
|
|
157
|
+
const replacementGeneration = replacementValue && nonNegativeInteger(replacementValue.generation);
|
|
158
|
+
const replacementTaskName = replacementValue && boundedText(replacementValue.taskName);
|
|
159
|
+
const replacementCanonicalTaskName = replacementValue && boundedText(replacementValue.canonicalTaskName);
|
|
160
|
+
const replacementCanonicalPosition = replacementValue && boundedText(replacementValue.canonicalPosition);
|
|
161
|
+
const replacementPlanIdentity = replacementValue && boundedText(replacementValue.planIdentity);
|
|
162
|
+
const replacementPlanFingerprint = replacementValue && boundedText(replacementValue.planFingerprint);
|
|
115
163
|
if (!canonicalPosition ||
|
|
116
164
|
!canonicalTaskName ||
|
|
117
165
|
!taskPath ||
|
|
@@ -120,7 +168,84 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
120
168
|
!lastActivityAt ||
|
|
121
169
|
!outcome ||
|
|
122
170
|
continuationGeneration === null ||
|
|
123
|
-
continuationCount === null
|
|
171
|
+
continuationCount === null ||
|
|
172
|
+
resumeFailures === null ||
|
|
173
|
+
(replacementValue &&
|
|
174
|
+
(!replacementGeneration ||
|
|
175
|
+
!replacementTaskName ||
|
|
176
|
+
!replacementCanonicalTaskName ||
|
|
177
|
+
!replacementCanonicalPosition ||
|
|
178
|
+
!replacementPlanIdentity ||
|
|
179
|
+
!replacementPlanFingerprint)))
|
|
180
|
+
return undefined;
|
|
181
|
+
const commandsValue = executorValue.commands;
|
|
182
|
+
if (commandsValue !== undefined && (!Array.isArray(commandsValue) || commandsValue.length > 32))
|
|
183
|
+
return undefined;
|
|
184
|
+
const commands = (commandsValue ?? []).flatMap((candidate) => {
|
|
185
|
+
const command = record(candidate);
|
|
186
|
+
const commandId = command && boundedText(command.commandId);
|
|
187
|
+
const status = command &&
|
|
188
|
+
stringValue(command.status, [
|
|
189
|
+
'scheduled',
|
|
190
|
+
'issued',
|
|
191
|
+
'accepted',
|
|
192
|
+
'failed',
|
|
193
|
+
'cancelled',
|
|
194
|
+
'superseded',
|
|
195
|
+
]);
|
|
196
|
+
const planIdentity = command && boundedText(command.planIdentity);
|
|
197
|
+
const planFingerprint = command && boundedText(command.planFingerprint);
|
|
198
|
+
const commandPosition = command && boundedText(command.canonicalPosition);
|
|
199
|
+
const commandTaskName = command && boundedText(command.canonicalTaskName);
|
|
200
|
+
const commandTaskPath = command && boundedText(command.taskPath);
|
|
201
|
+
const commandThreadId = command && boundedText(command.threadId);
|
|
202
|
+
const generation = command && nonNegativeInteger(command.generation);
|
|
203
|
+
const attempt = command?.attempt === undefined ? 0 : command && nonNegativeInteger(command.attempt);
|
|
204
|
+
const trigger = command && stringValue(command.trigger, ['partial', 'processExited', 'processResourceLimit']);
|
|
205
|
+
const processActionValue = command && record(command.processAction);
|
|
206
|
+
const processActionKind = processActionValue &&
|
|
207
|
+
stringValue(processActionValue.kind, ['transfer', 'consume', 'terminate']);
|
|
208
|
+
const processActionKey = processActionValue && boundedText(processActionValue.processKey);
|
|
209
|
+
const createdAt = command && boundedText(command.createdAt);
|
|
210
|
+
const updatedAt = command && boundedText(command.updatedAt);
|
|
211
|
+
return commandId &&
|
|
212
|
+
status &&
|
|
213
|
+
planIdentity &&
|
|
214
|
+
planFingerprint &&
|
|
215
|
+
commandPosition &&
|
|
216
|
+
commandTaskName &&
|
|
217
|
+
commandTaskPath &&
|
|
218
|
+
commandThreadId &&
|
|
219
|
+
typeof generation === 'number' &&
|
|
220
|
+
typeof attempt === 'number' &&
|
|
221
|
+
trigger &&
|
|
222
|
+
createdAt &&
|
|
223
|
+
updatedAt &&
|
|
224
|
+
(!processActionValue || (processActionKind && processActionKey))
|
|
225
|
+
? [
|
|
226
|
+
{
|
|
227
|
+
commandId,
|
|
228
|
+
status,
|
|
229
|
+
planIdentity,
|
|
230
|
+
planFingerprint,
|
|
231
|
+
canonicalPosition: commandPosition,
|
|
232
|
+
canonicalTaskName: commandTaskName,
|
|
233
|
+
taskPath: commandTaskPath,
|
|
234
|
+
threadId: commandThreadId,
|
|
235
|
+
generation,
|
|
236
|
+
...(attempt ? { attempt } : {}),
|
|
237
|
+
trigger,
|
|
238
|
+
...(processActionValue && processActionKind && processActionKey
|
|
239
|
+
? { processAction: { kind: processActionKind, processKey: processActionKey } }
|
|
240
|
+
: {}),
|
|
241
|
+
createdAt,
|
|
242
|
+
updatedAt,
|
|
243
|
+
},
|
|
244
|
+
]
|
|
245
|
+
: [];
|
|
246
|
+
});
|
|
247
|
+
if (commands.length !== (commandsValue ?? []).length ||
|
|
248
|
+
new Set(commands.map((command) => command.commandId)).size !== commands.length)
|
|
124
249
|
return undefined;
|
|
125
250
|
const executorBlockingValue = record(executorValue.blocking);
|
|
126
251
|
const executorBlocking = executorBlockingValue
|
|
@@ -142,6 +267,20 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
142
267
|
...(executorBlocking ? { blocking: executorBlocking } : {}),
|
|
143
268
|
continuationGeneration,
|
|
144
269
|
continuationCount,
|
|
270
|
+
...(resumeFailures ? { resumeFailures } : {}),
|
|
271
|
+
...(replacementValue
|
|
272
|
+
? {
|
|
273
|
+
replacement: {
|
|
274
|
+
generation: replacementGeneration,
|
|
275
|
+
taskName: replacementTaskName,
|
|
276
|
+
canonicalTaskName: replacementCanonicalTaskName,
|
|
277
|
+
canonicalPosition: replacementCanonicalPosition,
|
|
278
|
+
planIdentity: replacementPlanIdentity,
|
|
279
|
+
planFingerprint: replacementPlanFingerprint,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
: {}),
|
|
283
|
+
...(commands.length ? { commands } : {}),
|
|
145
284
|
},
|
|
146
285
|
...(blocking ? { blocking } : {}),
|
|
147
286
|
...(supervision ? { supervision } : {}),
|
|
@@ -152,6 +291,31 @@ function parseCheckpoints(value) {
|
|
|
152
291
|
const planIdentity = boundedText(value.planIdentity);
|
|
153
292
|
if (value.protocolVersion !== 1 || !planIdentity)
|
|
154
293
|
return undefined;
|
|
294
|
+
const rawCompletionEpochs = value.completionEpochs ?? [];
|
|
295
|
+
if (!Array.isArray(rawCompletionEpochs) || rawCompletionEpochs.length > 640)
|
|
296
|
+
return undefined;
|
|
297
|
+
const completionEpochs = rawCompletionEpochs.flatMap((candidate) => {
|
|
298
|
+
const epoch = record(candidate);
|
|
299
|
+
const target = epoch && canonicalCheckpointTarget(epoch.target);
|
|
300
|
+
return target &&
|
|
301
|
+
typeof epoch.epoch === 'number' &&
|
|
302
|
+
Number.isSafeInteger(epoch.epoch) &&
|
|
303
|
+
epoch.epoch >= 0 &&
|
|
304
|
+
typeof epoch.reopened === 'boolean' &&
|
|
305
|
+
(epoch.completed === undefined || typeof epoch.completed === 'boolean')
|
|
306
|
+
? [
|
|
307
|
+
{
|
|
308
|
+
target,
|
|
309
|
+
epoch: epoch.epoch,
|
|
310
|
+
reopened: epoch.reopened,
|
|
311
|
+
completed: epoch.completed ?? true,
|
|
312
|
+
},
|
|
313
|
+
]
|
|
314
|
+
: [];
|
|
315
|
+
});
|
|
316
|
+
if (completionEpochs.length !== rawCompletionEpochs.length ||
|
|
317
|
+
new Set(completionEpochs.map((epoch) => epoch.target)).size !== completionEpochs.length)
|
|
318
|
+
return undefined;
|
|
155
319
|
const rawReportedL2Ids = value.reportedL2Ids ?? [];
|
|
156
320
|
if (!Array.isArray(rawReportedL2Ids) || rawReportedL2Ids.length > 512)
|
|
157
321
|
return undefined;
|
|
@@ -178,6 +342,9 @@ function parseCheckpoints(value) {
|
|
|
178
342
|
const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
|
|
179
343
|
if (pendingTurnId === undefined)
|
|
180
344
|
return undefined;
|
|
345
|
+
if (value.checkpointHandoffFailed !== undefined &&
|
|
346
|
+
typeof value.checkpointHandoffFailed !== 'boolean')
|
|
347
|
+
return undefined;
|
|
181
348
|
if (typeof value.terminalReviewAccepted !== 'boolean')
|
|
182
349
|
return undefined;
|
|
183
350
|
const pendingKind = value.pendingKind === undefined
|
|
@@ -190,14 +357,42 @@ function parseCheckpoints(value) {
|
|
|
190
357
|
if (pendingKind !== null &&
|
|
191
358
|
!['l2Completed', 'l1Accepted', 'terminalReviewAccepted'].includes(String(pendingKind)))
|
|
192
359
|
return undefined;
|
|
360
|
+
const legacyTargets = [
|
|
361
|
+
...reportedL1Ids.map((id) => checkpointTarget('l1', id)),
|
|
362
|
+
...reportedL2Ids.flatMap((id) => {
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(id);
|
|
365
|
+
return Array.isArray(parsed) &&
|
|
366
|
+
parsed.length === 2 &&
|
|
367
|
+
parsed.every((part) => typeof part === 'string')
|
|
368
|
+
? [checkpointTarget('l2', parsed[0], parsed[1])]
|
|
369
|
+
: [];
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
}),
|
|
375
|
+
];
|
|
376
|
+
if (legacyTargets.length !== reportedL1Ids.length + reportedL2Ids.length)
|
|
377
|
+
return undefined;
|
|
378
|
+
const legacyEpochs = value.completionEpochs === undefined
|
|
379
|
+
? legacyTargets.map((target) => ({ target, epoch: 0, reopened: false, completed: true }))
|
|
380
|
+
: completionEpochs;
|
|
381
|
+
if (value.completionEpochs !== undefined &&
|
|
382
|
+
!legacyTargets.every((target) => completionEpochs.some((epoch) => epoch.target === target)))
|
|
383
|
+
return undefined;
|
|
193
384
|
return {
|
|
194
385
|
protocolVersion: 1,
|
|
195
386
|
planIdentity,
|
|
387
|
+
completionEpochs: legacyEpochs,
|
|
196
388
|
reportedL2Ids,
|
|
197
389
|
reportedL1Ids,
|
|
198
390
|
acceptedKeys,
|
|
199
391
|
pendingTurnId,
|
|
200
392
|
pendingKind: pendingKind,
|
|
393
|
+
...(value.checkpointHandoffFailed === undefined
|
|
394
|
+
? {}
|
|
395
|
+
: { checkpointHandoffFailed: value.checkpointHandoffFailed }),
|
|
201
396
|
terminalReviewAccepted: value.terminalReviewAccepted,
|
|
202
397
|
};
|
|
203
398
|
}
|
|
@@ -15,6 +15,59 @@ export const observableWakeConditions = [
|
|
|
15
15
|
'processLimitBreached',
|
|
16
16
|
'agentActivityChanged',
|
|
17
17
|
];
|
|
18
|
+
/**
|
|
19
|
+
* The durable protocol owns wait state, while the coordinator owns the live
|
|
20
|
+
* timer/subscription handles. Keeping this matrix here makes every lifecycle
|
|
21
|
+
* boundary explicit before either owner mutates its half of a continuation.
|
|
22
|
+
*/
|
|
23
|
+
export const lifecycleBoundaries = [
|
|
24
|
+
'checkpointPersisted',
|
|
25
|
+
'matchingRootFinal',
|
|
26
|
+
'proactiveWaitRegistered',
|
|
27
|
+
'waitDeadline',
|
|
28
|
+
'manualOff',
|
|
29
|
+
'manualInterruption',
|
|
30
|
+
'explicitRecovery',
|
|
31
|
+
'planReplacement',
|
|
32
|
+
'attention',
|
|
33
|
+
'terminal',
|
|
34
|
+
];
|
|
35
|
+
const cancelledAtEveryBoundary = new Set([
|
|
36
|
+
'checkpointPersisted',
|
|
37
|
+
'matchingRootFinal',
|
|
38
|
+
'proactiveWaitRegistered',
|
|
39
|
+
'waitDeadline',
|
|
40
|
+
'manualOff',
|
|
41
|
+
'manualInterruption',
|
|
42
|
+
'explicitRecovery',
|
|
43
|
+
'planReplacement',
|
|
44
|
+
'attention',
|
|
45
|
+
'terminal',
|
|
46
|
+
]);
|
|
47
|
+
/**
|
|
48
|
+
* Resolves ownership of work that existed immediately before a boundary.
|
|
49
|
+
* `arm` is intentionally limited to the root-final handoff: it never revives
|
|
50
|
+
* the timer which was cancelled at checkpoint persistence.
|
|
51
|
+
*/
|
|
52
|
+
export function lifecycleBoundaryDisposition(boundary, work) {
|
|
53
|
+
if (work === 'rootTimer')
|
|
54
|
+
return boundary === 'matchingRootFinal'
|
|
55
|
+
? 'arm'
|
|
56
|
+
: cancelledAtEveryBoundary.has(boundary)
|
|
57
|
+
? 'cancel'
|
|
58
|
+
: 'preserve';
|
|
59
|
+
if (work === 'executorTimer')
|
|
60
|
+
return cancelledAtEveryBoundary.has(boundary) ? 'cancel' : 'preserve';
|
|
61
|
+
if (work === 'waitLease')
|
|
62
|
+
return boundary === 'proactiveWaitRegistered' ? 'preserve' : 'consume';
|
|
63
|
+
if (work === 'pendingCheckpoint')
|
|
64
|
+
return boundary === 'checkpointPersisted' || boundary === 'matchingRootFinal'
|
|
65
|
+
? 'preserve'
|
|
66
|
+
: 'cancel';
|
|
67
|
+
return boundary === 'manualInterruption' || boundary === 'explicitRecovery'
|
|
68
|
+
? 'preserve'
|
|
69
|
+
: 'cancel';
|
|
70
|
+
}
|
|
18
71
|
/**
|
|
19
72
|
* A canonical, durable representation rather than a clock- or transcript-derived score.
|
|
20
73
|
* The string is intentionally opaque to callers; compare it only for equality.
|
|
@@ -31,9 +84,31 @@ export function semanticProgressKey(facts) {
|
|
|
31
84
|
ownedProcesses: [...facts.ownedProcesses]
|
|
32
85
|
.map(({ id, state, ownerGeneration }) => ({ id, state, ownerGeneration }))
|
|
33
86
|
.sort(compareBy('id')),
|
|
87
|
+
childActivity: canonicalChildActivity(facts.childActivity),
|
|
34
88
|
agentActivity: latestAgentActivity(facts.agentActivity),
|
|
35
89
|
});
|
|
36
90
|
}
|
|
91
|
+
function canonicalChildActivity(children) {
|
|
92
|
+
return [...children]
|
|
93
|
+
.map((child) => ({
|
|
94
|
+
id: child.id,
|
|
95
|
+
threadId: child.threadId,
|
|
96
|
+
taskName: child.taskName,
|
|
97
|
+
position: child.position,
|
|
98
|
+
generation: child.generation,
|
|
99
|
+
state: child.state,
|
|
100
|
+
outcome: child.outcome,
|
|
101
|
+
ownedProcesses: [...child.ownedProcesses]
|
|
102
|
+
.map(({ id, state, ownership }) => ({ id, state, ownership }))
|
|
103
|
+
.sort((left, right) => canonicalCompare(left, right))
|
|
104
|
+
.slice(0, 32),
|
|
105
|
+
}))
|
|
106
|
+
.sort((left, right) => canonicalCompare(left, right))
|
|
107
|
+
.slice(0, 64);
|
|
108
|
+
}
|
|
109
|
+
function canonicalCompare(left, right) {
|
|
110
|
+
return JSON.stringify(left).localeCompare(JSON.stringify(right));
|
|
111
|
+
}
|
|
37
112
|
function latestAgentActivity(observations) {
|
|
38
113
|
const latest = new Map();
|
|
39
114
|
for (const observation of observations) {
|
|
@@ -169,6 +244,29 @@ export function consumeWaitDeadline(state, leaseId, observedAt) {
|
|
|
169
244
|
lastReportId: state.lastReportId,
|
|
170
245
|
};
|
|
171
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* A persisted Org checkpoint supersedes every pre-boundary wait episode.
|
|
249
|
+
* Checkpoint delivery is authoritative progress even when the old lease was
|
|
250
|
+
* waiting for a different executor or process event.
|
|
251
|
+
*/
|
|
252
|
+
export function consumeCheckpointBoundary(state, progressKey) {
|
|
253
|
+
return consumeObsoleteWait(state, progressKey);
|
|
254
|
+
}
|
|
255
|
+
/** Consumes a stale wait durably, without allowing it to outlive its owner. */
|
|
256
|
+
export function consumeObsoleteWait(state, progressKey) {
|
|
257
|
+
if (state.outcome === 'attentionRequired' || state.outcome === 'safetyPaused')
|
|
258
|
+
return {
|
|
259
|
+
...state,
|
|
260
|
+
progressKey,
|
|
261
|
+
probeKey: null,
|
|
262
|
+
waitLease: null,
|
|
263
|
+
retryKey: null,
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
...startSupervisionProtocol(progressKey),
|
|
267
|
+
lastReportId: state.lastReportId,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
172
270
|
/** Manual recovery is the only transition out of durable safety pause. */
|
|
173
271
|
export function recoverSafetyPause(state, progressKey) {
|
|
174
272
|
return state.outcome === 'safetyPaused' ? startSupervisionProtocol(progressKey) : state;
|
|
@@ -52,6 +52,7 @@ export class JsonRpcClient {
|
|
|
52
52
|
pending = new Map();
|
|
53
53
|
notifications = new Set();
|
|
54
54
|
serverRequests = new Set();
|
|
55
|
+
serverResponseSettlements = new Set();
|
|
55
56
|
failure = null;
|
|
56
57
|
constructor(input, output) {
|
|
57
58
|
this.output = output;
|
|
@@ -80,6 +81,22 @@ export class JsonRpcClient {
|
|
|
80
81
|
this.serverRequests.add(listener);
|
|
81
82
|
return () => this.serverRequests.delete(listener);
|
|
82
83
|
}
|
|
84
|
+
onServerResponseSettled(listener) {
|
|
85
|
+
this.serverResponseSettlements.add(listener);
|
|
86
|
+
return () => this.serverResponseSettlements.delete(listener);
|
|
87
|
+
}
|
|
88
|
+
writeServerResponse(id, response, outcome) {
|
|
89
|
+
const line = `${JSON.stringify({ jsonrpc: '2.0', id, ...response })}\n`;
|
|
90
|
+
try {
|
|
91
|
+
this.output.write(line, (error) => {
|
|
92
|
+
const settled = error ? 'writeFailed' : outcome;
|
|
93
|
+
this.serverResponseSettlements.forEach((listener) => listener({ id, outcome: settled }));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
this.serverResponseSettlements.forEach((listener) => listener({ id, outcome: 'writeFailed' }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
83
100
|
receive(line) {
|
|
84
101
|
try {
|
|
85
102
|
const message = JSON.parse(line);
|
|
@@ -93,8 +110,8 @@ export class JsonRpcClient {
|
|
|
93
110
|
if (!listener)
|
|
94
111
|
return;
|
|
95
112
|
void Promise.resolve(listener({ id: message.id, method: message.method, params: message.params }))
|
|
96
|
-
.then((result) => this.
|
|
97
|
-
.catch((error) => this.
|
|
113
|
+
.then((result) => this.writeServerResponse(message.id, { result }, 'resultWritten'))
|
|
114
|
+
.catch((error) => this.writeServerResponse(message.id, { error: { message: error instanceof Error ? error.message : 'REQUEST_FAILED' } }, 'errorWritten'));
|
|
98
115
|
return;
|
|
99
116
|
}
|
|
100
117
|
const pending = this.pending.get(message.id);
|