gestalt-mobile 0.35.1 → 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-BkQMGFPJ.js → index-SZM7lpix.js} +23 -23
- 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/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/package.json +1 -1
|
@@ -35,6 +35,8 @@ class SessionResource {
|
|
|
35
35
|
spawnedAgentModels = new Map();
|
|
36
36
|
attemptedAgentModelRecovery = new Set();
|
|
37
37
|
ownedChildProcesses = new Map();
|
|
38
|
+
/** Retries from a durable Autopilot issued command reuse this opaque action id. */
|
|
39
|
+
completedProcessActions = new Map();
|
|
38
40
|
turnThreads = new Map();
|
|
39
41
|
childThreads = new Set();
|
|
40
42
|
constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
@@ -84,6 +86,7 @@ export class CodexSessionRuntime {
|
|
|
84
86
|
planMeasurementBaseUrl;
|
|
85
87
|
maxPendingRequests;
|
|
86
88
|
readerCwd;
|
|
89
|
+
serverResponseListeners = new Set();
|
|
87
90
|
constructor(launch,
|
|
88
91
|
// Kept as an ignored compatibility slot while callers migrate from the old
|
|
89
92
|
// correlated-map constructor shape. Live ownership is `sessions` only.
|
|
@@ -170,18 +173,40 @@ export class CodexSessionRuntime {
|
|
|
170
173
|
resolveServerRequest(sessionId, requestId, result) {
|
|
171
174
|
const resource = this.sessions.get(sessionId);
|
|
172
175
|
const pending = resource?.pendingRequests.get(requestId);
|
|
173
|
-
if (!resource || !pending)
|
|
176
|
+
if (!resource || !pending || pending.settling)
|
|
174
177
|
return false;
|
|
175
|
-
|
|
178
|
+
pending.settling = true;
|
|
176
179
|
pending.resolve(result);
|
|
177
180
|
return true;
|
|
178
181
|
}
|
|
182
|
+
rejectServerRequest(sessionId, requestId, error) {
|
|
183
|
+
const resource = this.sessions.get(sessionId);
|
|
184
|
+
const pending = resource?.pendingRequests.get(requestId);
|
|
185
|
+
if (!pending || pending.settling)
|
|
186
|
+
return false;
|
|
187
|
+
pending.settling = true;
|
|
188
|
+
pending.reject(error);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A response may already be queued in a blocked writable stream. Release
|
|
193
|
+
* the local holder without accepting any eventual late write as success.
|
|
194
|
+
*/
|
|
195
|
+
abandonServerRequest(sessionId, requestId) {
|
|
196
|
+
const resource = this.sessions.get(sessionId);
|
|
197
|
+
return resource?.pendingRequests.delete(requestId) === true;
|
|
198
|
+
}
|
|
199
|
+
onServerResponseSettled(listener) {
|
|
200
|
+
this.serverResponseListeners.add(listener);
|
|
201
|
+
return () => this.serverResponseListeners.delete(listener);
|
|
202
|
+
}
|
|
179
203
|
/** Distinguishes an offline relay writer from a live writer that cleared a request. */
|
|
180
204
|
attentionWriterState(sessionId, requestId) {
|
|
181
205
|
const resource = this.sessions.get(sessionId);
|
|
182
206
|
if (!resource || !resource.active)
|
|
183
207
|
return 'unavailable';
|
|
184
|
-
|
|
208
|
+
const pending = resource.pendingRequests.get(requestId);
|
|
209
|
+
return pending && !pending.settling ? 'available' : 'cleared';
|
|
185
210
|
}
|
|
186
211
|
async startTurn(session, text, clientUserMessageId, now) {
|
|
187
212
|
const resource = this.sessions.get(session.id);
|
|
@@ -193,18 +218,22 @@ export class CodexSessionRuntime {
|
|
|
193
218
|
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
194
219
|
...(session.model ? { model: session.model } : {}),
|
|
195
220
|
}));
|
|
221
|
+
resource.turnThreads.set(result, session.threadId);
|
|
196
222
|
return RelaySession.rehydrate(session).startTurn(result, now).snapshot;
|
|
197
223
|
}
|
|
198
224
|
async startExecutorTurn(session, childThreadId, text, clientUserMessageId) {
|
|
199
225
|
const resource = this.sessions.get(session.id);
|
|
200
226
|
if (!resource)
|
|
201
227
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
202
|
-
|
|
228
|
+
const result = decodeTurnStart(await resource.process.rpc.request('turn/start', {
|
|
203
229
|
threadId: childThreadId,
|
|
204
230
|
input: [{ type: 'text', text, text_elements: [] }],
|
|
205
231
|
clientUserMessageId,
|
|
206
232
|
...(session.model ? { model: session.model } : {}),
|
|
207
233
|
}));
|
|
234
|
+
if (resource.turnThreads.has(result) || resource.turnThreads.size < 256)
|
|
235
|
+
resource.turnThreads.set(result, childThreadId);
|
|
236
|
+
return result;
|
|
208
237
|
}
|
|
209
238
|
/** The sole authoritative in-process ownership probe. It never launches a child. */
|
|
210
239
|
ownsWriter(sessionId) {
|
|
@@ -241,6 +270,19 @@ export class CodexSessionRuntime {
|
|
|
241
270
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
242
271
|
await resource.process.rpc.request('turn/interrupt', { threadId: session.threadId, turnId });
|
|
243
272
|
}
|
|
273
|
+
async interruptExecutor(session, threadId) {
|
|
274
|
+
const resource = this.sessions.get(session.id);
|
|
275
|
+
if (!resource)
|
|
276
|
+
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
277
|
+
const history = await this.decodeHistory(resource.process, threadId, resource);
|
|
278
|
+
if (!history.activeTurnId)
|
|
279
|
+
return false;
|
|
280
|
+
await resource.process.rpc.request('turn/interrupt', {
|
|
281
|
+
threadId,
|
|
282
|
+
turnId: history.activeTurnId,
|
|
283
|
+
});
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
244
286
|
async queueTurnInput(session, turnId, text, clientUserMessageId) {
|
|
245
287
|
const resource = this.sessions.get(session.id);
|
|
246
288
|
if (!resource || !session.threadId)
|
|
@@ -419,15 +461,36 @@ export class CodexSessionRuntime {
|
|
|
419
461
|
process.state !== 'result-consumed' &&
|
|
420
462
|
process.state !== 'terminated-for-budget');
|
|
421
463
|
}
|
|
422
|
-
consumeChildProcessResult(sessionId, childThreadId, processId) {
|
|
423
|
-
this.sessions
|
|
424
|
-
|
|
425
|
-
|
|
464
|
+
consumeChildProcessResult(sessionId, childThreadId, processId, actionId) {
|
|
465
|
+
const owned = this.sessions.get(sessionId);
|
|
466
|
+
if (!owned || (actionId && owned.completedProcessActions.has(actionId)))
|
|
467
|
+
return;
|
|
468
|
+
owned.ownedChildProcesses.delete(childProcessKey(childThreadId, processId));
|
|
469
|
+
if (actionId)
|
|
470
|
+
owned.completedProcessActions.set(actionId, true);
|
|
426
471
|
}
|
|
427
|
-
async terminateChildProcess(session, childThreadId, processId) {
|
|
472
|
+
async terminateChildProcess(session, childThreadId, processId, actionId, expected) {
|
|
428
473
|
const owned = this.sessions.get(session.id);
|
|
429
474
|
if (!owned)
|
|
430
475
|
return false;
|
|
476
|
+
if (actionId && owned.completedProcessActions.has(actionId))
|
|
477
|
+
return owned.completedProcessActions.get(actionId);
|
|
478
|
+
// The action journal is deliberately runtime-local. After an app-server
|
|
479
|
+
// restart, reconcile the live terminal list before repeating termination:
|
|
480
|
+
// a process id (and OS pid) can already name a later command instance.
|
|
481
|
+
// Absence of this exact instance means the target has already converged;
|
|
482
|
+
// never send a second terminate RPC merely to rediscover that fact.
|
|
483
|
+
if (expected) {
|
|
484
|
+
const active = await this.listChildBackgroundTerminals(owned, { id: childThreadId });
|
|
485
|
+
const current = active.find((candidate) => candidate.processId === processId &&
|
|
486
|
+
candidate.itemId === expected.itemId &&
|
|
487
|
+
(expected.osPid === undefined || candidate.osPid === expected.osPid));
|
|
488
|
+
if (!current) {
|
|
489
|
+
if (actionId)
|
|
490
|
+
owned.completedProcessActions.set(actionId, true);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
431
494
|
const result = await owned.process.rpc.request('thread/backgroundTerminals/terminate', {
|
|
432
495
|
threadId: childThreadId,
|
|
433
496
|
processId,
|
|
@@ -439,6 +502,8 @@ export class CodexSessionRuntime {
|
|
|
439
502
|
if (process)
|
|
440
503
|
owned.ownedChildProcesses.set(key, { ...process, state: 'terminated-for-budget' });
|
|
441
504
|
}
|
|
505
|
+
if (actionId)
|
|
506
|
+
owned.completedProcessActions.set(actionId, terminated);
|
|
442
507
|
return terminated;
|
|
443
508
|
}
|
|
444
509
|
async listChildBackgroundTerminals(owned, child) {
|
|
@@ -739,13 +804,25 @@ export class CodexSessionRuntime {
|
|
|
739
804
|
this.onNotification?.(session.id, notification, this.resolveNotificationOrigin(resource, notification));
|
|
740
805
|
});
|
|
741
806
|
const requestUnsubscribe = process.rpc.onServerRequest((request) => this.holdServerRequest(resource, request));
|
|
807
|
+
const responseUnsubscribe = process.rpc.onServerResponseSettled?.((settlement) => {
|
|
808
|
+
const requestId = String(settlement.id);
|
|
809
|
+
if (!resource.pendingRequests.delete(requestId))
|
|
810
|
+
return;
|
|
811
|
+
this.serverResponseListeners.forEach((listener) => listener(resource.sessionId, requestId, settlement.outcome));
|
|
812
|
+
});
|
|
742
813
|
if (!resource.active) {
|
|
743
814
|
notificationUnsubscribe();
|
|
744
815
|
requestUnsubscribe();
|
|
816
|
+
responseUnsubscribe?.();
|
|
745
817
|
throw new Error('CODEX_SESSION_PROCESS_EXITED');
|
|
746
818
|
}
|
|
747
819
|
// The resource's private unsubscribe list is populated before it is published.
|
|
748
|
-
resource.attach([
|
|
820
|
+
resource.attach([
|
|
821
|
+
notificationUnsubscribe,
|
|
822
|
+
requestUnsubscribe,
|
|
823
|
+
...(responseUnsubscribe ? [responseUnsubscribe] : []),
|
|
824
|
+
exitUnsubscribe,
|
|
825
|
+
]);
|
|
749
826
|
return resource;
|
|
750
827
|
}
|
|
751
828
|
catch (error) {
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
/** Safe, shared UI copy for the bounded checkpoint transport recovery path. */
|
|
7
|
+
export const checkpointHandoffRecoveryMessage = 'Checkpoint handoff failed. Autopilot is reconnecting this session automatically. If it cannot recover, restore or reopen the session.';
|