gestalt-mobile 0.32.0 → 0.32.1
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/server/server/composition.contracts.js +81 -0
- package/dist/server/server/composition.js +41 -1
- package/dist/server/server/platform/codex/server-request.js +8 -0
- package/dist/server/server/platform/codex/session-runtime.js +8 -0
- package/dist/server/shared/contracts/agent-capacity-recovery.js +40 -0
- package/gestalt-supervision-capabilities.json +1 -0
- package/package.json +1 -1
|
@@ -1368,6 +1368,87 @@ describe('production composition', () => {
|
|
|
1368
1368
|
});
|
|
1369
1369
|
});
|
|
1370
1370
|
describeCompositionConcern('lifecycle', () => {
|
|
1371
|
+
it('acknowledges root-owned capacity recovery before recycling only its app-server', async () => {
|
|
1372
|
+
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
1373
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
1374
|
+
ownTemporaryPaths(root, dataDir);
|
|
1375
|
+
await mkdir(join(root, 'workspace'));
|
|
1376
|
+
const handles = [];
|
|
1377
|
+
const app = await composeAuthorizedApp({
|
|
1378
|
+
root,
|
|
1379
|
+
dataDir,
|
|
1380
|
+
relyingParty,
|
|
1381
|
+
installedCodexVersion: 'codex-cli 0.144.3',
|
|
1382
|
+
startAppServers: true,
|
|
1383
|
+
launchAppServer: liveAppServer(handles),
|
|
1384
|
+
profiles: {
|
|
1385
|
+
list: async () => [],
|
|
1386
|
+
require: async () => ({
|
|
1387
|
+
name: 'default',
|
|
1388
|
+
state: 'ok',
|
|
1389
|
+
status: 'ready',
|
|
1390
|
+
}),
|
|
1391
|
+
},
|
|
1392
|
+
});
|
|
1393
|
+
const sessionId = await createComposedSession(app);
|
|
1394
|
+
const first = handles.find((candidate) => candidate.calls.includes('thread/start'));
|
|
1395
|
+
expect(first?.request).toBeDefined();
|
|
1396
|
+
const started = await app.inject({
|
|
1397
|
+
method: 'POST',
|
|
1398
|
+
url: `/api/sessions/${sessionId}/turns`,
|
|
1399
|
+
payload: { text: 'continue supervised work' },
|
|
1400
|
+
});
|
|
1401
|
+
expect(started.statusCode).toBe(202);
|
|
1402
|
+
const turnId = started.json().activeTurnId;
|
|
1403
|
+
const threadId = (await app.inject(`/api/sessions/${sessionId}`)).json().threadId;
|
|
1404
|
+
const handlesBeforeRecovery = handles.length;
|
|
1405
|
+
first.notify?.({
|
|
1406
|
+
method: 'turn/started',
|
|
1407
|
+
params: { threadId, turn: { id: turnId } },
|
|
1408
|
+
});
|
|
1409
|
+
await expect(first.request({
|
|
1410
|
+
id: 90,
|
|
1411
|
+
method: 'item/tool/call',
|
|
1412
|
+
params: {
|
|
1413
|
+
threadId: 'child-thread',
|
|
1414
|
+
turnId,
|
|
1415
|
+
tool: 'gestalt_agent_capacity_recovery',
|
|
1416
|
+
arguments: { version: 1, reason: 'agentThreadLimit' },
|
|
1417
|
+
},
|
|
1418
|
+
})).rejects.toThrow('CODEX_SERVER_REQUEST_UNSUPPORTED');
|
|
1419
|
+
expect(handles).toHaveLength(handlesBeforeRecovery);
|
|
1420
|
+
await expect(first.request({
|
|
1421
|
+
id: 91,
|
|
1422
|
+
method: 'item/tool/call',
|
|
1423
|
+
params: {
|
|
1424
|
+
threadId,
|
|
1425
|
+
turnId,
|
|
1426
|
+
tool: 'gestalt_agent_capacity_recovery',
|
|
1427
|
+
arguments: { version: 1, reason: 'agentThreadLimit' },
|
|
1428
|
+
},
|
|
1429
|
+
})).resolves.toEqual({
|
|
1430
|
+
success: true,
|
|
1431
|
+
contentItems: [
|
|
1432
|
+
{ type: 'inputText', text: '{"accepted":true,"action":"sessionRuntimeRecycle"}' },
|
|
1433
|
+
],
|
|
1434
|
+
});
|
|
1435
|
+
await expect.poll(() => handles.length).toBeGreaterThan(handlesBeforeRecovery);
|
|
1436
|
+
const replacement = handles.find((candidate) => candidate !== first &&
|
|
1437
|
+
candidate.requests.some((request) => request.method === 'thread/resume'));
|
|
1438
|
+
expect(replacement?.requests).toContainEqual({
|
|
1439
|
+
method: 'thread/resume',
|
|
1440
|
+
params: expect.objectContaining({ threadId, cwd: join(root, 'workspace') }),
|
|
1441
|
+
});
|
|
1442
|
+
await expect
|
|
1443
|
+
.poll(async () => (await app.inject(`/api/sessions/${sessionId}`)).json().state)
|
|
1444
|
+
.toBe('ready');
|
|
1445
|
+
expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
1446
|
+
threadId,
|
|
1447
|
+
activeTurnId: null,
|
|
1448
|
+
desiredState: 'active',
|
|
1449
|
+
});
|
|
1450
|
+
await app.close();
|
|
1451
|
+
});
|
|
1371
1452
|
it('publishes an idle websocket status after saving a completed turn', async () => {
|
|
1372
1453
|
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
1373
1454
|
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
@@ -36,7 +36,7 @@ import { RelaySession, } from './features/sessions/model/relay-session.js';
|
|
|
36
36
|
import { AgentActivityRegistry } from './features/agent-activity/registry.js';
|
|
37
37
|
import { toAgentActivityDto } from './features/agent-activity/activity-dto.js';
|
|
38
38
|
import { decodeAgentActivityFacts } from './platform/codex/activity-facts.js';
|
|
39
|
-
import { isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
|
|
39
|
+
import { isAgentCapacityRecoveryCall, isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
|
|
40
40
|
import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
|
|
41
41
|
import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
|
|
42
42
|
import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
|
|
@@ -58,6 +58,7 @@ import { createRelyingPartyConfig } from './config.js';
|
|
|
58
58
|
import { parseOrgPlanAttention } from '../shared/contracts/org-plan-attention.js';
|
|
59
59
|
import { GestaltUpdateRestartScheduler } from './platform/process/gestalt-update-restart.js';
|
|
60
60
|
import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
|
|
61
|
+
import { agentCapacityRecoveryToolResponse } from '../shared/contracts/agent-capacity-recovery.js';
|
|
61
62
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
62
63
|
export async function composeRelayApp(options) {
|
|
63
64
|
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
@@ -346,6 +347,7 @@ export async function composeRelayApp(options) {
|
|
|
346
347
|
const gitFetches = new GitFetchCoordinator(fetchUpstream);
|
|
347
348
|
const gitSummaries = new GitSummaryCache(inspectGit);
|
|
348
349
|
let recoverExitedSession = () => { };
|
|
350
|
+
let scheduleAgentCapacityRecovery = () => false;
|
|
349
351
|
let planMeasurementRefresh;
|
|
350
352
|
const publishInteractionResolved = (sessionId, requestId, occurredAt, outcome) => {
|
|
351
353
|
const interaction = interactions
|
|
@@ -464,6 +466,11 @@ export async function composeRelayApp(options) {
|
|
|
464
466
|
const session = withPendingInteractions(sessions.find(sessionId));
|
|
465
467
|
if (!session)
|
|
466
468
|
return false;
|
|
469
|
+
if (!rawInteraction && isAgentCapacityRecoveryCall(request)) {
|
|
470
|
+
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
471
|
+
return (rootOwned &&
|
|
472
|
+
scheduleAgentCapacityRecovery(sessionId, () => runtime?.resolveServerRequest(sessionId, String(request.id), agentCapacityRecoveryToolResponse()) === true));
|
|
473
|
+
}
|
|
467
474
|
if (!rawInteraction && isAutopilotWaitLeaseCall(request)) {
|
|
468
475
|
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
469
476
|
if (rootOwned)
|
|
@@ -565,6 +572,39 @@ export async function composeRelayApp(options) {
|
|
|
565
572
|
autopilot.restore(session.id);
|
|
566
573
|
};
|
|
567
574
|
if (runtime) {
|
|
575
|
+
const capacityRecoveries = new Set();
|
|
576
|
+
scheduleAgentCapacityRecovery = (sessionId, acknowledge) => {
|
|
577
|
+
if (capacityRecoveries.has(sessionId) || !acknowledge())
|
|
578
|
+
return false;
|
|
579
|
+
capacityRecoveries.add(sessionId);
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
void (async () => {
|
|
582
|
+
try {
|
|
583
|
+
if (closing)
|
|
584
|
+
return;
|
|
585
|
+
const session = sessions.find(sessionId);
|
|
586
|
+
if (!session || session.desiredState !== 'active' || !session.threadId)
|
|
587
|
+
return;
|
|
588
|
+
const recovering = RelaySession.rehydrate(session).beginRecovery(new Date().toISOString()).snapshot;
|
|
589
|
+
saveSession(recovering);
|
|
590
|
+
try {
|
|
591
|
+
const restored = await runtime.recycle(recovering, new Date().toISOString());
|
|
592
|
+
saveSession(restored);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
const current = sessions.find(sessionId);
|
|
596
|
+
if (current)
|
|
597
|
+
saveSession(RelaySession.rehydrate(current).requireAttention(new Date().toISOString())
|
|
598
|
+
.snapshot);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
capacityRecoveries.delete(sessionId);
|
|
603
|
+
}
|
|
604
|
+
})();
|
|
605
|
+
}, 500);
|
|
606
|
+
return true;
|
|
607
|
+
};
|
|
568
608
|
const supervisor = new SessionSupervisor(async (sessionId) => {
|
|
569
609
|
const session = sessions.find(sessionId);
|
|
570
610
|
if (!session || session.desiredState !== 'active' || !session.threadId)
|
|
@@ -7,6 +7,7 @@ import { GESTALT_QUIZ_TOOL_NAME, parseQuiz } from '../../../shared/contracts/qui
|
|
|
7
7
|
import { GESTALT_ORG_PLAN_ATTENTION_TOOL_NAME, parseOrgPlanAttention, } from '../../../shared/contracts/org-plan-attention.js';
|
|
8
8
|
import { GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME, parseOrgPlanCheckpoint, } from '../../../shared/contracts/org-plan-checkpoint.js';
|
|
9
9
|
import { GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME, parseAutopilotWaitLease, } from '../../../shared/contracts/autopilot-wait-lease.js';
|
|
10
|
+
import { GESTALT_AGENT_CAPACITY_RECOVERY_TOOL_NAME, parseAgentCapacityRecovery, } from '../../../shared/contracts/agent-capacity-recovery.js';
|
|
10
11
|
export function toPendingInteraction(input) {
|
|
11
12
|
if (!Number.isSafeInteger(input.id) || !isRecord(input.params))
|
|
12
13
|
return null;
|
|
@@ -58,6 +59,13 @@ export function isAutopilotWaitLeaseCall(input) {
|
|
|
58
59
|
isRecord(input.params) &&
|
|
59
60
|
input.params.tool === GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME);
|
|
60
61
|
}
|
|
62
|
+
export function isAgentCapacityRecoveryCall(input) {
|
|
63
|
+
return (Number.isSafeInteger(input.id) &&
|
|
64
|
+
input.method === 'item/tool/call' &&
|
|
65
|
+
isRecord(input.params) &&
|
|
66
|
+
input.params.tool === GESTALT_AGENT_CAPACITY_RECOVERY_TOOL_NAME &&
|
|
67
|
+
parseAgentCapacityRecovery(input.params.arguments) !== null);
|
|
68
|
+
}
|
|
61
69
|
function isRecord(value) {
|
|
62
70
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
63
71
|
}
|
|
@@ -11,6 +11,7 @@ import { gestaltQuizDynamicTool } from '../../../shared/contracts/quiz.js';
|
|
|
11
11
|
import { gestaltOrgPlanAttentionDynamicTool } from '../../../shared/contracts/org-plan-attention.js';
|
|
12
12
|
import { gestaltOrgPlanCheckpointDynamicTool } from '../../../shared/contracts/org-plan-checkpoint.js';
|
|
13
13
|
import { gestaltAutopilotWaitLeaseDynamicTool } from '../../../shared/contracts/autopilot-wait-lease.js';
|
|
14
|
+
import { gestaltAgentCapacityRecoveryDynamicTool } from '../../../shared/contracts/agent-capacity-recovery.js';
|
|
14
15
|
import { countDiffLines } from '../../../shared/contracts/file-change.js';
|
|
15
16
|
import { threadPlanName } from './thread-plan-name.js';
|
|
16
17
|
import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
|
|
@@ -145,6 +146,11 @@ export class CodexSessionRuntime {
|
|
|
145
146
|
}
|
|
146
147
|
this.stop(sessionId);
|
|
147
148
|
}
|
|
149
|
+
/** Replaces only this session's app-server while retaining its durable root thread. */
|
|
150
|
+
async recycle(session, now) {
|
|
151
|
+
this.stop(session.id);
|
|
152
|
+
return this.restore(session, now);
|
|
153
|
+
}
|
|
148
154
|
/** Releases all relay-owned app-server children during graceful shutdown. */
|
|
149
155
|
stopAll() {
|
|
150
156
|
// Exit delivery can already be queued when shutdown begins. Declare the
|
|
@@ -574,6 +580,7 @@ export class CodexSessionRuntime {
|
|
|
574
580
|
gestaltOrgPlanAttentionDynamicTool,
|
|
575
581
|
gestaltOrgPlanCheckpointDynamicTool,
|
|
576
582
|
gestaltAutopilotWaitLeaseDynamicTool,
|
|
583
|
+
gestaltAgentCapacityRecoveryDynamicTool,
|
|
577
584
|
],
|
|
578
585
|
});
|
|
579
586
|
result = {
|
|
@@ -665,6 +672,7 @@ export class CodexSessionRuntime {
|
|
|
665
672
|
gestaltOrgPlanAttentionDynamicTool,
|
|
666
673
|
gestaltOrgPlanCheckpointDynamicTool,
|
|
667
674
|
gestaltAutopilotWaitLeaseDynamicTool,
|
|
675
|
+
gestaltAgentCapacityRecoveryDynamicTool,
|
|
668
676
|
],
|
|
669
677
|
...(session.model ? { model: session.model } : {}),
|
|
670
678
|
...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
/** Bounded recovery for a Codex collaboration registry whose resident slots leaked. */
|
|
7
|
+
export const GESTALT_AGENT_CAPACITY_RECOVERY_TOOL_NAME = 'gestalt_agent_capacity_recovery';
|
|
8
|
+
export const gestaltAgentCapacityRecoveryDynamicTool = {
|
|
9
|
+
type: 'function',
|
|
10
|
+
name: GESTALT_AGENT_CAPACITY_RECOVERY_TOOL_NAME,
|
|
11
|
+
description: 'Recover the current supervised session after Codex reports its agent thread limit and stale pending-init agents cannot be closed. This recycles only the current Codex runtime, preserves the durable root thread and Org Plan, and cannot request human authority.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
required: ['version', 'reason'],
|
|
16
|
+
properties: {
|
|
17
|
+
version: { const: 1 },
|
|
18
|
+
reason: { const: 'agentThreadLimit' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export function parseAgentCapacityRecovery(value) {
|
|
23
|
+
if (typeof value !== 'object' ||
|
|
24
|
+
value === null ||
|
|
25
|
+
Array.isArray(value) ||
|
|
26
|
+
Object.keys(value).length !== 2)
|
|
27
|
+
return null;
|
|
28
|
+
const candidate = value;
|
|
29
|
+
return candidate.version === 1 && candidate.reason === 'agentThreadLimit'
|
|
30
|
+
? { version: 1, reason: 'agentThreadLimit' }
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
export function agentCapacityRecoveryToolResponse() {
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
contentItems: [
|
|
37
|
+
{ type: 'inputText', text: '{"accepted":true,"action":"sessionRuntimeRecycle"}' },
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|