gestalt-mobile 0.35.1 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{index-BkQMGFPJ.js → index-SZM7lpix.js} +23 -23
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +936 -3
- package/dist/server/server/composition.js +175 -11
- 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/autopilot-wait-lease.js +13 -3
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import { CodexModelCatalog } from './platform/codex/codex-model-catalog.js';
|
|
|
16
16
|
import { createRecentThreadLister } from './platform/codex/recent-thread-lister.js';
|
|
17
17
|
import { CodexSessionRuntime } from './platform/codex/session-runtime.js';
|
|
18
18
|
import { normalizeCodexNotification } from './platform/codex/normalizer.js';
|
|
19
|
+
import { isMissingCodexThreadRollout } from './platform/codex/json-rpc-client.js';
|
|
19
20
|
import { migrate } from './platform/persistence/migrate.js';
|
|
20
21
|
import { openRelayDatabase } from './platform/persistence/sqlite.js';
|
|
21
22
|
import { SqliteAuthorizationStore } from './platform/auth/sqlite-authorization-store.js';
|
|
@@ -93,6 +94,8 @@ export async function composeRelayApp(options) {
|
|
|
93
94
|
const idempotency = new SqliteIdempotencyStore(database);
|
|
94
95
|
const autopilotStore = new SqliteAutopilotStore(database);
|
|
95
96
|
const attentionResolutionOperations = new Map();
|
|
97
|
+
const checkpointHandoffTimers = new Map();
|
|
98
|
+
const checkpointHandoffRecoveries = new Set();
|
|
96
99
|
const supervisedPlans = new SupervisedPlanRegistry();
|
|
97
100
|
const planStatusSource = new FilesystemPlanStatusSource(join(dirname(databasePath), 'plans'));
|
|
98
101
|
const planMeasurementHelperPath = options.planMeasurementHelperPath ?? process.env.GESTALT_MOBILE_ORG_PLAN_HELPER;
|
|
@@ -263,7 +266,17 @@ export async function composeRelayApp(options) {
|
|
|
263
266
|
.update(`${sessionId}:${threadId}:${generation}:${randomUUID()}`)
|
|
264
267
|
.digest('hex')
|
|
265
268
|
.slice(0, 16)}`;
|
|
266
|
-
|
|
269
|
+
let turnId;
|
|
270
|
+
try {
|
|
271
|
+
turnId = await runtime.startExecutorTurn(writer.session, threadId, `${AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT}${context}`, clientId);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
// A confirmed app-server missing-rollout response is a rejection;
|
|
275
|
+
// transport loss remains ambiguous and must retain the issued fence.
|
|
276
|
+
if (isMissingCodexThreadRollout(error))
|
|
277
|
+
throw new Error('AUTOPILOT_EXECUTOR_REJECTED');
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
267
280
|
activity.observe({
|
|
268
281
|
sessionId,
|
|
269
282
|
occurredAt: new Date().toISOString(),
|
|
@@ -273,16 +286,21 @@ export async function composeRelayApp(options) {
|
|
|
273
286
|
});
|
|
274
287
|
},
|
|
275
288
|
refresh: (sessionId) => activity.refresh(sessionId),
|
|
276
|
-
|
|
289
|
+
interrupt: async (sessionId, threadId) => {
|
|
290
|
+
const session = sessions.find(sessionId);
|
|
291
|
+
return session && runtime ? runtime.interruptExecutor(session, threadId) : false;
|
|
292
|
+
},
|
|
293
|
+
transferProcess: (sessionId, threadId, processId, actionId) => {
|
|
294
|
+
void actionId;
|
|
277
295
|
activity.transferProcessOwnership(sessionId, threadId, processId, new Date().toISOString());
|
|
278
296
|
},
|
|
279
|
-
consumeProcess: (sessionId, threadId, processId) => {
|
|
280
|
-
runtime?.consumeChildProcessResult(sessionId, threadId, processId);
|
|
297
|
+
consumeProcess: (sessionId, threadId, processId, _actionId) => {
|
|
298
|
+
runtime?.consumeChildProcessResult(sessionId, threadId, processId, _actionId);
|
|
281
299
|
},
|
|
282
|
-
terminateProcess: async (sessionId, threadId, processId) => {
|
|
300
|
+
terminateProcess: async (sessionId, threadId, processId, actionId, processInstance) => {
|
|
283
301
|
const session = sessions.find(sessionId);
|
|
284
302
|
return session && runtime
|
|
285
|
-
? runtime.terminateChildProcess(session, threadId, processId)
|
|
303
|
+
? runtime.terminateChildProcess(session, threadId, processId, actionId, processInstance)
|
|
286
304
|
: false;
|
|
287
305
|
},
|
|
288
306
|
},
|
|
@@ -381,6 +399,91 @@ export async function composeRelayApp(options) {
|
|
|
381
399
|
publishInteractionResolved(sessionId, interaction.requestId, occurredAt, outcome);
|
|
382
400
|
}
|
|
383
401
|
};
|
|
402
|
+
const checkpointTimerKey = (sessionId, requestId) => `${sessionId}:${requestId}`;
|
|
403
|
+
const cancelCheckpointHandoffTimer = (sessionId, requestId) => {
|
|
404
|
+
const key = checkpointTimerKey(sessionId, requestId);
|
|
405
|
+
const timer = checkpointHandoffTimers.get(key);
|
|
406
|
+
if (!timer)
|
|
407
|
+
return;
|
|
408
|
+
clearTimeout(timer.handle);
|
|
409
|
+
checkpointHandoffTimers.delete(key);
|
|
410
|
+
};
|
|
411
|
+
/** Fail a held checkpoint exactly once and retain its durable root boundary. */
|
|
412
|
+
const failCheckpointHandoff = (sessionId, requestId, turnId) => {
|
|
413
|
+
cancelCheckpointHandoffTimer(sessionId, requestId);
|
|
414
|
+
const occurredAt = new Date().toISOString();
|
|
415
|
+
if (!interactions.resolve(sessionId, requestId, occurredAt, 'failed'))
|
|
416
|
+
return false;
|
|
417
|
+
autopilot.checkpointHandoffFailed(sessionId, turnId);
|
|
418
|
+
events.publish(journal.append(sessionId, 'org-plan.checkpoint-handoff-failed', { requestId, reason: 'checkpointHandoffFailed' }, occurredAt));
|
|
419
|
+
publishAttentionSettlement(sessionId, requestId, occurredAt, 'failed');
|
|
420
|
+
// Neither this timeout nor a failed transport may schedule continuation.
|
|
421
|
+
// If a successful response is already blocked in the writable stream, drop
|
|
422
|
+
// its local ownership so any late write cannot be mistaken for an ack.
|
|
423
|
+
const rejected = runtime?.rejectServerRequest(sessionId, requestId, new Error('CHECKPOINT_HANDOFF_FAILED'));
|
|
424
|
+
if (rejected === false) {
|
|
425
|
+
const recoveryKey = checkpointTimerKey(sessionId, requestId);
|
|
426
|
+
const ownedRuntime = runtime;
|
|
427
|
+
const session = sessions.find(sessionId);
|
|
428
|
+
if (ownedRuntime &&
|
|
429
|
+
session?.activeTurnId === turnId &&
|
|
430
|
+
!checkpointHandoffRecoveries.has(recoveryKey)) {
|
|
431
|
+
checkpointHandoffRecoveries.add(recoveryKey);
|
|
432
|
+
void (async () => {
|
|
433
|
+
try {
|
|
434
|
+
// Recycle only the session-local writer. Its close cancels the
|
|
435
|
+
// blocked output, and only successful reconstruction releases the
|
|
436
|
+
// durable checkpoint through the runtime-recovery boundary.
|
|
437
|
+
if (runtime !== ownedRuntime || sessions.find(sessionId)?.activeTurnId !== turnId)
|
|
438
|
+
return;
|
|
439
|
+
const rebuilt = await ownedRuntime.recycle(session, new Date().toISOString());
|
|
440
|
+
if (runtime !== ownedRuntime)
|
|
441
|
+
return;
|
|
442
|
+
const recovered = turnId && rebuilt.activeTurnId === turnId
|
|
443
|
+
? RelaySession.rehydrate(rebuilt).completeTurn(turnId, new Date().toISOString())
|
|
444
|
+
.snapshot
|
|
445
|
+
: rebuilt;
|
|
446
|
+
saveSession(recovered);
|
|
447
|
+
activity.observe({
|
|
448
|
+
sessionId,
|
|
449
|
+
occurredAt: new Date().toISOString(),
|
|
450
|
+
kind: 'turnCompleted',
|
|
451
|
+
});
|
|
452
|
+
autopilot.recoverCheckpointHandoff(sessionId);
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// The durable failure remains visible; a later normal recovery can retry.
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
checkpointHandoffRecoveries.delete(recoveryKey);
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
};
|
|
465
|
+
const armCheckpointHandoffDeadline = (sessionId, requestId, turnId) => {
|
|
466
|
+
cancelCheckpointHandoffTimer(sessionId, requestId);
|
|
467
|
+
const key = checkpointTimerKey(sessionId, requestId);
|
|
468
|
+
const token = Symbol(key);
|
|
469
|
+
const handle = setTimeout(() => {
|
|
470
|
+
if (checkpointHandoffTimers.get(key)?.token !== token)
|
|
471
|
+
return;
|
|
472
|
+
checkpointHandoffTimers.delete(key);
|
|
473
|
+
failCheckpointHandoff(sessionId, requestId, turnId);
|
|
474
|
+
}, options.checkpointHandoffDeadlineMs ?? 5_000);
|
|
475
|
+
handle.unref?.();
|
|
476
|
+
checkpointHandoffTimers.set(key, { handle, token });
|
|
477
|
+
};
|
|
478
|
+
const failPendingCheckpointHandoffs = (sessionId, recoverBoundary) => {
|
|
479
|
+
for (const interaction of interactions.list(sessionId)) {
|
|
480
|
+
if (interaction.kind !== 'orgPlanCheckpoint')
|
|
481
|
+
continue;
|
|
482
|
+
failCheckpointHandoff(sessionId, interaction.requestId, interaction.turnId ?? null);
|
|
483
|
+
}
|
|
484
|
+
if (recoverBoundary)
|
|
485
|
+
autopilot.recoverCheckpointHandoff(sessionId);
|
|
486
|
+
};
|
|
384
487
|
let closing = false;
|
|
385
488
|
let runtime = null;
|
|
386
489
|
const acceptPlanUpdate = (sessionId, update) => {
|
|
@@ -478,7 +581,7 @@ export async function composeRelayApp(options) {
|
|
|
478
581
|
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
479
582
|
if (rootOwned)
|
|
480
583
|
autopilot.rejectProbe(sessionId);
|
|
481
|
-
return (runtime?.resolveServerRequest(sessionId, String(request.id), autopilotWaitLeaseToolResponse()) === true);
|
|
584
|
+
return (runtime?.resolveServerRequest(sessionId, String(request.id), autopilotWaitLeaseToolResponse(false)) === true);
|
|
482
585
|
}
|
|
483
586
|
if (!rawInteraction)
|
|
484
587
|
return false;
|
|
@@ -502,7 +605,7 @@ export async function composeRelayApp(options) {
|
|
|
502
605
|
if (!accepted) {
|
|
503
606
|
if (rootOwned)
|
|
504
607
|
autopilot.rejectProbe(sessionId);
|
|
505
|
-
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse()) === true);
|
|
608
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse(false)) === true);
|
|
506
609
|
}
|
|
507
610
|
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, autopilotWaitLeaseToolResponse()) === true);
|
|
508
611
|
}
|
|
@@ -526,9 +629,42 @@ export async function composeRelayApp(options) {
|
|
|
526
629
|
});
|
|
527
630
|
if (!valid)
|
|
528
631
|
return false;
|
|
529
|
-
if (!
|
|
632
|
+
if (!session.activeTurnId)
|
|
633
|
+
return false;
|
|
634
|
+
let accepted = false;
|
|
635
|
+
try {
|
|
636
|
+
accepted = autopilot.checkpointAccepted(sessionId, checkpoint, session.activeTurnId, new Date().toISOString());
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
if (!autopilot.checkpointHandoffFailed(sessionId, session.activeTurnId))
|
|
640
|
+
return false;
|
|
641
|
+
const interaction = {
|
|
642
|
+
...rawInteraction,
|
|
643
|
+
turnId: session.activeTurnId,
|
|
644
|
+
requestedAt: new Date().toISOString(),
|
|
645
|
+
};
|
|
646
|
+
interactions.add(sessionId, interaction);
|
|
647
|
+
failCheckpointHandoff(sessionId, rawInteraction.requestId, session.activeTurnId);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
if (!accepted)
|
|
530
651
|
return false;
|
|
531
|
-
|
|
652
|
+
const interaction = {
|
|
653
|
+
...rawInteraction,
|
|
654
|
+
turnId: session.activeTurnId,
|
|
655
|
+
requestedAt: new Date().toISOString(),
|
|
656
|
+
};
|
|
657
|
+
interactions.add(sessionId, interaction);
|
|
658
|
+
armCheckpointHandoffDeadline(sessionId, rawInteraction.requestId, session.activeTurnId);
|
|
659
|
+
try {
|
|
660
|
+
runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse());
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// The accepted hold remains owned by the bounded deadline.
|
|
664
|
+
}
|
|
665
|
+
// `true` retains the checkpoint-specific held request. Its timer
|
|
666
|
+
// is cancelled solely by the JSON-RPC write-settlement callback.
|
|
667
|
+
return true;
|
|
532
668
|
}
|
|
533
669
|
const interaction = {
|
|
534
670
|
...rawInteraction,
|
|
@@ -554,10 +690,24 @@ export async function composeRelayApp(options) {
|
|
|
554
690
|
return true;
|
|
555
691
|
}, (sessionId) => {
|
|
556
692
|
activity.disconnected(sessionId, new Date().toISOString());
|
|
693
|
+
failPendingCheckpointHandoffs(sessionId, false);
|
|
557
694
|
dismissPendingInteractions(sessionId, new Date().toISOString(), 'failed');
|
|
558
695
|
recoverExitedSession(sessionId);
|
|
559
696
|
}, resolveSkills, planStatusSource, acceptPlanUpdate, options.planMeasurementBaseUrl, 30_000, 64, root)
|
|
560
697
|
: null;
|
|
698
|
+
runtime?.onServerResponseSettled((sessionId, requestId, outcome) => {
|
|
699
|
+
const interaction = interactions.find(sessionId, requestId);
|
|
700
|
+
if (interaction?.kind !== 'orgPlanCheckpoint')
|
|
701
|
+
return;
|
|
702
|
+
if (outcome !== 'resultWritten') {
|
|
703
|
+
failCheckpointHandoff(sessionId, requestId, interaction.turnId ?? null);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
cancelCheckpointHandoffTimer(sessionId, requestId);
|
|
707
|
+
const occurredAt = new Date().toISOString();
|
|
708
|
+
if (interactions.resolve(sessionId, requestId, occurredAt, 'answered'))
|
|
709
|
+
publishAttentionSettlement(sessionId, requestId, occurredAt, 'answered');
|
|
710
|
+
});
|
|
561
711
|
if (runtime && planMeasurementHelperPath) {
|
|
562
712
|
planMeasurementRefresh = new PlanMeasurementRefresh(async (sessionId) => {
|
|
563
713
|
const session = sessions.find(sessionId);
|
|
@@ -1069,6 +1219,10 @@ export async function composeRelayApp(options) {
|
|
|
1069
1219
|
}
|
|
1070
1220
|
await runtime?.watchPlanStatus(session);
|
|
1071
1221
|
autopilot.restore(session.id);
|
|
1222
|
+
// A prior process cannot resolve its original JSON-RPC request. Turn
|
|
1223
|
+
// that bounded handoff into one durable failure and release the
|
|
1224
|
+
// checkpoint only through the explicit runtime-recovery boundary.
|
|
1225
|
+
failPendingCheckpointHandoffs(session.id, true);
|
|
1072
1226
|
});
|
|
1073
1227
|
};
|
|
1074
1228
|
app.addHook('onListen', async () => {
|
|
@@ -1083,11 +1237,21 @@ export async function composeRelayApp(options) {
|
|
|
1083
1237
|
for (const session of sessions.list()) {
|
|
1084
1238
|
autopilot.dispose(session.id);
|
|
1085
1239
|
activity.dispose(session.id);
|
|
1240
|
+
for (const key of [...checkpointHandoffTimers.keys()]) {
|
|
1241
|
+
if (key.startsWith(`${session.id}:`)) {
|
|
1242
|
+
const timer = checkpointHandoffTimers.get(key);
|
|
1243
|
+
if (timer)
|
|
1244
|
+
clearTimeout(timer.handle);
|
|
1245
|
+
checkpointHandoffTimers.delete(key);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1086
1248
|
// Relay shutdown only releases this process's writer. A typed attention
|
|
1087
1249
|
// request remains a durable human-visible blocker for the next relay
|
|
1088
1250
|
// instance; only an app-server-cleared request is a failed audit outcome.
|
|
1089
1251
|
for (const interaction of interactions.list(session.id)) {
|
|
1090
|
-
|
|
1252
|
+
// Checkpoints are recovered by the next relay: their original root
|
|
1253
|
+
// boundary is durable and must not be replaced by a shutdown dismissal.
|
|
1254
|
+
if (interaction.kind === 'orgPlanAttention' || interaction.kind === 'orgPlanCheckpoint')
|
|
1091
1255
|
continue;
|
|
1092
1256
|
const occurredAt = new Date().toISOString();
|
|
1093
1257
|
if (interactions.resolve(session.id, interaction.requestId, occurredAt, 'dismissed'))
|