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.
@@ -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.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.';
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.output.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result })}\n`))
97
- .catch((error) => this.output.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { message: error instanceof Error ? error.message : 'REQUEST_FAILED' } })}\n`));
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);