gestalt-mobile 0.41.1 → 0.41.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-G2PznC5F.js → index-BRly-q56.js} +23 -23
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +192 -50
- package/dist/server/server/composition.js +175 -33
- package/dist/server/server/features/agent-activity/model.js +8 -2
- package/dist/server/server/features/autopilot/application/policy.js +4 -4
- package/dist/server/server/features/autopilot/application/service.js +103 -15
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +9 -4
- package/dist/server/server/features/org-plan-attention/resolve/endpoint.js +5 -3
- package/dist/server/server/features/org-plan-checkpoint/application/validate.js +83 -15
- package/dist/server/server/features/plans/application/supervised-plan-registry.js +8 -1
- package/dist/server/server/features/skills/list-available/endpoint.js +2 -1
- package/dist/server/server/features/skills/model/skill-profile.js +8 -4
- package/dist/server/server/platform/catalog/profile-command.js +2 -12
- package/dist/server/server/platform/codex/codex-process-launcher.js +1 -1
- package/dist/server/server/platform/codex/session-runtime.js +16 -8
- package/dist/server/server/platform/persistence/sqlite-pending-interaction-store.js +12 -6
- package/dist/server/shared/contracts/org-plan-attention.js +43 -7
- package/dist/server/shared/contracts/org-plan-checkpoint.js +5 -1
- package/dist/server/shared/contracts/plan-signal.js +3 -1
- package/package.json +1 -1
package/dist/client/index.html
CHANGED
|
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
16
16
|
<link rel="icon" href="/icons/gestalt-mobile-192.png" />
|
|
17
17
|
<link rel="apple-touch-icon" href="/icons/gestalt-mobile-180.png" />
|
|
18
18
|
<title>Gestalt Mobile</title>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BRly-q56.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-D57mr3B1.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
@@ -12,14 +12,14 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { describe, expect, it, onTestFinished, vi } from 'vitest';
|
|
13
13
|
import WebSocket from 'ws';
|
|
14
14
|
import { composeRelayApp } from './composition.js';
|
|
15
|
-
import {
|
|
15
|
+
import { autopilotExecutorLaunchPrompt } from './features/autopilot/application/policy.js';
|
|
16
16
|
import { CodexJsonRpcError } from './platform/codex/json-rpc-client.js';
|
|
17
17
|
import { SqliteAuthorizationStore } from './platform/auth/sqlite-authorization-store.js';
|
|
18
18
|
import { authorizationSessionId, authorizedDeviceId, localOwnerId, webAuthnCredentialId, } from './features/auth/domain/identifiers.js';
|
|
19
19
|
import { deviceNickname } from './features/auth/domain/device-nickname.js';
|
|
20
20
|
import { workspaceId } from './platform/catalog/workspace-id.js';
|
|
21
21
|
import { planStatusDirectoryPath, planStatusFilePath, } from './platform/plans/filesystem-plan-status-source.js';
|
|
22
|
-
import { toOrgPlanAttentionToolResponse } from '../shared/contracts/org-plan-attention.js';
|
|
22
|
+
import { toOrgPlanAttentionAcknowledgement, toOrgPlanAttentionToolResponse, } from '../shared/contracts/org-plan-attention.js';
|
|
23
23
|
import { toOrgPlanCheckpointToolResponse } from '../shared/contracts/org-plan-checkpoint.js';
|
|
24
24
|
import { applyProjectionEvent, createChatProjection, } from '../client/features/chat/chat-projection.js';
|
|
25
25
|
function fakeAppServer(calls) {
|
|
@@ -153,6 +153,25 @@ const autopilotPlanText = (childState = 'TODO') => `#+TITLE: Autopilot fixture
|
|
|
153
153
|
const completedAutopilotPlanText = () => autopilotPlanText('DONE')
|
|
154
154
|
.replace('* WIP [#A] Parent', '* DONE [#A] Parent')
|
|
155
155
|
.replace(':REVIEW_STATUS: UNREVIEWED', ':REVIEW_STATUS: REVIEWED');
|
|
156
|
+
const acceptedL1WithNextTodoPlanText = () => `${completedAutopilotPlanText()}
|
|
157
|
+
* TODO [#B] Next parent
|
|
158
|
+
:PROPERTIES:
|
|
159
|
+
:ID: next-parent
|
|
160
|
+
:SKILLS: $gestalt:org-plan
|
|
161
|
+
:REVIEW_STATUS: UNREVIEWED
|
|
162
|
+
:END:
|
|
163
|
+
- Effort :: Small
|
|
164
|
+
- Goal :: Continue on the next root turn.
|
|
165
|
+
- Notes :: Must remain pending at the accepted-L1 boundary.
|
|
166
|
+
** TODO [#B] Next child
|
|
167
|
+
:PROPERTIES:
|
|
168
|
+
:ID: next-child
|
|
169
|
+
:END:
|
|
170
|
+
- Why :: Verify boundary separation.
|
|
171
|
+
- Change :: Start only after durable handoff.
|
|
172
|
+
- Tests :: Exercise the next continuation.
|
|
173
|
+
- Done when :: The next L1 starts exactly once.
|
|
174
|
+
`;
|
|
156
175
|
async function installAutopilotPlan(app, sessionId, workspacePath, name, childState = 'TODO', reason = 'supervision-start') {
|
|
157
176
|
const planPath = join(workspacePath, `${name}.org`);
|
|
158
177
|
await writeFile(planPath, autopilotPlanText(childState));
|
|
@@ -478,7 +497,12 @@ describe('production composition', () => {
|
|
|
478
497
|
input: [
|
|
479
498
|
{
|
|
480
499
|
type: 'text',
|
|
481
|
-
text:
|
|
500
|
+
text: autopilotExecutorLaunchPrompt({
|
|
501
|
+
canonicalTaskName: 'l1',
|
|
502
|
+
canonicalPosition: 'L1',
|
|
503
|
+
generation: 1,
|
|
504
|
+
taskName: 'l1',
|
|
505
|
+
}),
|
|
482
506
|
text_elements: [],
|
|
483
507
|
},
|
|
484
508
|
],
|
|
@@ -958,6 +982,83 @@ describe('production composition', () => {
|
|
|
958
982
|
expect(checkpoint.count).toBe(1);
|
|
959
983
|
await fixture.app.close();
|
|
960
984
|
});
|
|
985
|
+
it('records and idempotently replays accepted L1 after REVIEWED advances to the next TODO L1', async () => {
|
|
986
|
+
const timers = [];
|
|
987
|
+
const fixture = await createProductionAutopilotFixture({
|
|
988
|
+
autopilotSchedule: (callback) => {
|
|
989
|
+
const timer = { callback, cancelled: false, fired: false };
|
|
990
|
+
timers.push(timer);
|
|
991
|
+
return () => {
|
|
992
|
+
timer.cancelled = true;
|
|
993
|
+
};
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
const initial = timers.find((timer) => !timer.cancelled && !timer.fired);
|
|
997
|
+
initial.fired = true;
|
|
998
|
+
initial.callback();
|
|
999
|
+
await vi.waitFor(() => expect(fixture.handles.flatMap((candidate) => candidate.requests.filter((request) => request.method === 'turn/start'))).toHaveLength(1));
|
|
1000
|
+
const handle = fixture.handles.find((candidate) => candidate.request);
|
|
1001
|
+
const session = (await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json();
|
|
1002
|
+
const threadId = session.threadId;
|
|
1003
|
+
const turnId = session.activeTurnId;
|
|
1004
|
+
await writeFile(join(fixture.workspacePath, 'autopilot.org'), acceptedL1WithNextTodoPlanText());
|
|
1005
|
+
await expect
|
|
1006
|
+
.poll(async () => (await fixture.app.inject(`/api/sessions/${fixture.sessionId}/plan`)).json()
|
|
1007
|
+
.currentStepId)
|
|
1008
|
+
.toBe('next-parent');
|
|
1009
|
+
handle.deferResponseSettlement = true;
|
|
1010
|
+
const checkpointCall = (id) => ({
|
|
1011
|
+
id,
|
|
1012
|
+
method: 'item/tool/call',
|
|
1013
|
+
params: {
|
|
1014
|
+
threadId,
|
|
1015
|
+
turnId,
|
|
1016
|
+
tool: 'gestalt_org_plan_checkpoint',
|
|
1017
|
+
arguments: { kind: 'l1Accepted' },
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
const startsBefore = fixture.handles
|
|
1021
|
+
.flatMap((candidate) => candidate.requests)
|
|
1022
|
+
.filter((request) => request.method === 'turn/start').length;
|
|
1023
|
+
const first = handle.request(checkpointCall(813));
|
|
1024
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(813)).toBe(true));
|
|
1025
|
+
handle.responseSettled({ id: 813, outcome: 'resultWritten' });
|
|
1026
|
+
await expect(first).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
1027
|
+
const replay = handle.request(checkpointCall(814));
|
|
1028
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(814)).toBe(true));
|
|
1029
|
+
handle.responseSettled({ id: 814, outcome: 'resultWritten' });
|
|
1030
|
+
await expect(replay).resolves.toEqual(toOrgPlanCheckpointToolResponse('alreadyRecorded'));
|
|
1031
|
+
const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
|
|
1032
|
+
const acceptedEvents = database
|
|
1033
|
+
.prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.milestone-checkpointed'")
|
|
1034
|
+
.get(fixture.sessionId);
|
|
1035
|
+
const pendingInteractions = database
|
|
1036
|
+
.prepare('SELECT count(*) AS count FROM pending_interactions WHERE session_id = ? AND resolved_at IS NULL')
|
|
1037
|
+
.get(fixture.sessionId);
|
|
1038
|
+
database.close();
|
|
1039
|
+
expect(acceptedEvents.count).toBe(1);
|
|
1040
|
+
expect(pendingInteractions.count).toBe(0);
|
|
1041
|
+
expect(fixture.handles
|
|
1042
|
+
.flatMap((candidate) => candidate.requests)
|
|
1043
|
+
.filter((request) => request.method === 'turn/start')).toHaveLength(startsBefore);
|
|
1044
|
+
handle.notify({ method: 'turn/completed', params: { threadId, turn: { id: turnId } } });
|
|
1045
|
+
await expect
|
|
1046
|
+
.poll(async () => (await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json().activeTurnId)
|
|
1047
|
+
.toBeNull();
|
|
1048
|
+
const startedTurns = () => fixture.handles
|
|
1049
|
+
.flatMap((candidate) => candidate.requests)
|
|
1050
|
+
.filter((request) => request.method === 'turn/start').length;
|
|
1051
|
+
for (let attempts = 0; attempts < 3 && startedTurns() === startsBefore; attempts += 1) {
|
|
1052
|
+
await vi.waitFor(() => expect(timers.find((timer) => !timer.cancelled && !timer.fired)).toBeDefined());
|
|
1053
|
+
const continuation = timers.find((timer) => !timer.cancelled && !timer.fired);
|
|
1054
|
+
continuation.fired = true;
|
|
1055
|
+
continuation.callback();
|
|
1056
|
+
await vi.waitFor(() => expect(startedTurns() > startsBefore ||
|
|
1057
|
+
timers.some((timer) => !timer.cancelled && !timer.fired)).toBe(true));
|
|
1058
|
+
}
|
|
1059
|
+
await vi.waitFor(() => expect(startedTurns()).toBe(startsBefore + 1));
|
|
1060
|
+
await fixture.app.close();
|
|
1061
|
+
});
|
|
961
1062
|
it('returns a typed failed checkpoint result when persistence never occurs', async () => {
|
|
962
1063
|
let coordinator;
|
|
963
1064
|
const timers = [];
|
|
@@ -980,7 +1081,13 @@ describe('production composition', () => {
|
|
|
980
1081
|
const { handle, response, startsBefore, writersBefore } = await holdCheckpointRequest(fixture, timers, 995, false);
|
|
981
1082
|
await vi.waitFor(() => expect(handle.pendingResponses.has(995)).toBe(true));
|
|
982
1083
|
handle.responseSettled({ id: 995, outcome: 'resultWritten' });
|
|
983
|
-
await
|
|
1084
|
+
const failure = (await response);
|
|
1085
|
+
expect(failure.success).toBe(false);
|
|
1086
|
+
expect(JSON.parse(failure.contentItems[0].text)).toMatchObject({
|
|
1087
|
+
status: 'failed',
|
|
1088
|
+
reasonCode: 'checkpointPersistenceFailed',
|
|
1089
|
+
correlationId: expect.any(String),
|
|
1090
|
+
});
|
|
984
1091
|
expect(coordinator.checkpointAccepted).not.toBe(original);
|
|
985
1092
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
986
1093
|
expect(fixture.handles
|
|
@@ -1340,14 +1447,20 @@ describe('production composition', () => {
|
|
|
1340
1447
|
const planIdentity = createHash('sha256')
|
|
1341
1448
|
.update(join(fixture.workspacePath, 'autopilot.org'))
|
|
1342
1449
|
.digest('hex');
|
|
1343
|
-
await
|
|
1450
|
+
const rejected = (await handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity)));
|
|
1451
|
+
expect(rejected.success).toBe(false);
|
|
1452
|
+
expect(JSON.parse(rejected.contentItems[0].text)).toMatchObject({
|
|
1453
|
+
status: 'failed',
|
|
1454
|
+
reasonCode: 'rootNotOwner',
|
|
1455
|
+
correlationId: expect.any(String),
|
|
1456
|
+
});
|
|
1344
1457
|
expect(timers).toHaveLength(timersAfterFinal);
|
|
1345
1458
|
expect(fixture.handles
|
|
1346
1459
|
.flatMap((candidate) => candidate.requests)
|
|
1347
1460
|
.filter((request) => request.method === 'turn/start')).toHaveLength(startsAfterFinal);
|
|
1348
1461
|
await fixture.app.close();
|
|
1349
1462
|
});
|
|
1350
|
-
it('production attention
|
|
1463
|
+
it('production attention approval resumes Autopilot and a complete plan transitions to completed', async () => {
|
|
1351
1464
|
const fixture = await createProductionAutopilotFixture();
|
|
1352
1465
|
const handle = fixture.handles.find((candidate) => candidate.request);
|
|
1353
1466
|
expect((await fixture.app.inject({
|
|
@@ -1356,6 +1469,7 @@ describe('production composition', () => {
|
|
|
1356
1469
|
payload: { enabled: true },
|
|
1357
1470
|
})).statusCode).toBe(200);
|
|
1358
1471
|
const attention = handle.request(attentionCall(810));
|
|
1472
|
+
await expect(attention).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
1359
1473
|
await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1360
1474
|
autopilot: { state: 'attentionRequired', enabled: false, reason: 'attentionRequired' },
|
|
1361
1475
|
}));
|
|
@@ -1365,16 +1479,6 @@ describe('production composition', () => {
|
|
|
1365
1479
|
url: `/api/sessions/${fixture.sessionId}/attention/810/resolve`,
|
|
1366
1480
|
payload: { operationKey: 'resume-attention', action: 'resume' },
|
|
1367
1481
|
})).statusCode).toBe(202);
|
|
1368
|
-
await expect(attention).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
1369
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1370
|
-
expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1371
|
-
autopilot: { state: 'attentionRequired', enabled: false },
|
|
1372
|
-
});
|
|
1373
|
-
expect((await fixture.app.inject({
|
|
1374
|
-
method: 'PUT',
|
|
1375
|
-
url: `/api/sessions/${fixture.sessionId}/autopilot`,
|
|
1376
|
-
payload: { enabled: true },
|
|
1377
|
-
})).statusCode).toBe(200);
|
|
1378
1482
|
await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1379
1483
|
autopilot: { enabled: true, state: expect.stringMatching(/monitoring|backoff/) },
|
|
1380
1484
|
}));
|
|
@@ -1662,7 +1766,7 @@ describe('production composition', () => {
|
|
|
1662
1766
|
// rejected child continuation; it must never target the dead child.
|
|
1663
1767
|
expect((starts[3]?.params).threadId).not.toBe('child-1');
|
|
1664
1768
|
expect((starts[3]?.params).input?.[0]?.text).toContain('Launch task_name l1_g2 for canonical L1');
|
|
1665
|
-
expect((starts[3]?.params).input?.[0]?.text).toContain('
|
|
1769
|
+
expect((starts[3]?.params).input?.[0]?.text).toContain('agent_type org-plan-executor and reasoning_effort high');
|
|
1666
1770
|
// Capacity recovery is allowed only for the active root control. It
|
|
1667
1771
|
// recycles that writer but must retain the one durable g2 handoff.
|
|
1668
1772
|
const rootHandle = fixture.handles.find((candidate) => candidate.requests.some((request) => request.method === 'turn/start' &&
|
|
@@ -1959,6 +2063,56 @@ describe('production composition', () => {
|
|
|
1959
2063
|
});
|
|
1960
2064
|
});
|
|
1961
2065
|
describeCompositionConcern('attention', () => {
|
|
2066
|
+
it('bounds a wedged attention acknowledgement writer without losing durable attention', async () => {
|
|
2067
|
+
const fixture = await createProductionAutopilotFixture({
|
|
2068
|
+
attentionAcknowledgementDeadlineMs: 100,
|
|
2069
|
+
});
|
|
2070
|
+
const handle = fixture.handles.find((candidate) => candidate.request);
|
|
2071
|
+
handle.deferResponseSettlement = true;
|
|
2072
|
+
const response = handle.request(attentionCall(699));
|
|
2073
|
+
const rejected = expect(response).rejects.toThrow('CODEX_SERVER_REQUEST_CANCELLED');
|
|
2074
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(699)).toBe(true));
|
|
2075
|
+
await rejected;
|
|
2076
|
+
expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}/attention`)).json()).toMatchObject({ requestId: '699', attention: { reason: 'hardBlock' } });
|
|
2077
|
+
await fixture.app.close();
|
|
2078
|
+
});
|
|
2079
|
+
it('returns a bounded failure when durable attention persistence fails', async () => {
|
|
2080
|
+
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
2081
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
2082
|
+
ownTemporaryPaths(root, dataDir);
|
|
2083
|
+
await mkdir(join(root, 'workspace'));
|
|
2084
|
+
const handles = [];
|
|
2085
|
+
const app = await composeAuthorizedApp({
|
|
2086
|
+
root,
|
|
2087
|
+
dataDir,
|
|
2088
|
+
relyingParty,
|
|
2089
|
+
installedCodexVersion: 'codex-cli 0.144.3',
|
|
2090
|
+
startAppServers: true,
|
|
2091
|
+
launchAppServer: liveAppServer(handles),
|
|
2092
|
+
profiles: {
|
|
2093
|
+
list: async () => [],
|
|
2094
|
+
require: async () => ({
|
|
2095
|
+
name: 'default',
|
|
2096
|
+
state: 'ok',
|
|
2097
|
+
status: 'ready',
|
|
2098
|
+
}),
|
|
2099
|
+
},
|
|
2100
|
+
});
|
|
2101
|
+
await createComposedSession(app);
|
|
2102
|
+
const handle = await vi.waitFor(() => {
|
|
2103
|
+
const candidate = handles.find((item) => item.request);
|
|
2104
|
+
expect(candidate).toBeDefined();
|
|
2105
|
+
return candidate;
|
|
2106
|
+
});
|
|
2107
|
+
const database = new DatabaseSync(join(dataDir, 'relay.sqlite'));
|
|
2108
|
+
database.exec(`CREATE TRIGGER reject_attention_insert
|
|
2109
|
+
BEFORE INSERT ON pending_interactions
|
|
2110
|
+
WHEN NEW.kind = 'orgPlanAttention'
|
|
2111
|
+
BEGIN SELECT RAISE(ABORT, 'injected attention persistence failure'); END`);
|
|
2112
|
+
database.close();
|
|
2113
|
+
await expect(handle.request(attentionCall(700))).resolves.toEqual(toOrgPlanAttentionAcknowledgement({ accepted: false, reason: 'persistenceFailed' }));
|
|
2114
|
+
await app.close();
|
|
2115
|
+
});
|
|
1962
2116
|
it('publishes isolated typed required, resolved, and failed attention transitions through the feature-only seam', async () => {
|
|
1963
2117
|
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
1964
2118
|
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
@@ -2004,6 +2158,7 @@ describe('production composition', () => {
|
|
|
2004
2158
|
const unsubscribeA = transitions.subscribe(sessionA, (event) => receivedA.push(event));
|
|
2005
2159
|
const unsubscribeB = transitions.subscribe(sessionB, (event) => receivedB.push(event));
|
|
2006
2160
|
const resolving = handleA.request(attentionCall(701));
|
|
2161
|
+
await expect(resolving).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2007
2162
|
await vi.waitFor(() => expect(receivedA).toEqual([
|
|
2008
2163
|
expect.objectContaining({ kind: 'required', requestId: '701' }),
|
|
2009
2164
|
]));
|
|
@@ -2013,12 +2168,12 @@ describe('production composition', () => {
|
|
|
2013
2168
|
url: `/api/sessions/${sessionA}/attention/701/resolve`,
|
|
2014
2169
|
payload: { operationKey: 'resolve-a', action: 'resume' },
|
|
2015
2170
|
})).statusCode).toBe(202);
|
|
2016
|
-
await expect(resolving).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
2017
2171
|
await vi.waitFor(() => expect(receivedA).toEqual([
|
|
2018
2172
|
expect.objectContaining({ kind: 'required', requestId: '701' }),
|
|
2019
2173
|
expect.objectContaining({ kind: 'resolved', requestId: '701' }),
|
|
2020
2174
|
]));
|
|
2021
2175
|
const failing = handleB.request(attentionCall(702, 'permissionRequired'));
|
|
2176
|
+
await expect(failing).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2022
2177
|
await vi.waitFor(() => expect(receivedB).toEqual([
|
|
2023
2178
|
expect.objectContaining({ kind: 'required', requestId: '702' }),
|
|
2024
2179
|
]));
|
|
@@ -2026,13 +2181,10 @@ describe('production composition', () => {
|
|
|
2026
2181
|
method: 'serverRequest/resolved',
|
|
2027
2182
|
params: { threadId: 'thread-ignored', requestId: 702 },
|
|
2028
2183
|
});
|
|
2029
|
-
|
|
2030
|
-
await vi.waitFor(() => expect(receivedB).toEqual([
|
|
2031
|
-
expect.objectContaining({ kind: 'required', requestId: '702' }),
|
|
2032
|
-
expect.objectContaining({ kind: 'failed', requestId: '702' }),
|
|
2033
|
-
]));
|
|
2184
|
+
expect(receivedB).toEqual([expect.objectContaining({ kind: 'required', requestId: '702' })]);
|
|
2034
2185
|
unsubscribeA();
|
|
2035
2186
|
const afterUnsubscribe = handleA.request(attentionCall(703));
|
|
2187
|
+
await expect(afterUnsubscribe).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2036
2188
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionA}/attention`)).json()).toMatchObject({
|
|
2037
2189
|
requestId: '703',
|
|
2038
2190
|
}));
|
|
@@ -2041,9 +2193,6 @@ describe('production composition', () => {
|
|
|
2041
2193
|
method: 'serverRequest/resolved',
|
|
2042
2194
|
params: { threadId: 'thread-ignored', requestId: 703 },
|
|
2043
2195
|
});
|
|
2044
|
-
await expect(afterUnsubscribe).rejects.toMatchObject({
|
|
2045
|
-
message: 'CODEX_SERVER_REQUEST_CLEARED',
|
|
2046
|
-
});
|
|
2047
2196
|
expect(receivedA).toHaveLength(2);
|
|
2048
2197
|
unsubscribeB();
|
|
2049
2198
|
await app.close();
|
|
@@ -2074,6 +2223,7 @@ describe('production composition', () => {
|
|
|
2074
2223
|
await vi.waitFor(() => expect(handles.find((handle) => handle.request)?.request).toBeDefined());
|
|
2075
2224
|
const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
|
|
2076
2225
|
const attentionFirst = handle.request(attentionCall(711, 'permissionRequired'));
|
|
2226
|
+
await expect(attentionFirst).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2077
2227
|
const inputFirst = handle.request({
|
|
2078
2228
|
id: 712,
|
|
2079
2229
|
method: 'item/tool/requestUserInput',
|
|
@@ -2096,11 +2246,11 @@ describe('production composition', () => {
|
|
|
2096
2246
|
url: `/api/sessions/${sessionId}/attention/711/resolve`,
|
|
2097
2247
|
payload: { operationKey: 'attention-first', action: 'resume' },
|
|
2098
2248
|
});
|
|
2099
|
-
await expect(attentionFirst).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
2100
2249
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
2101
2250
|
agentActivity: { root: { state: 'working' } },
|
|
2102
2251
|
}));
|
|
2103
2252
|
const attentionSecond = handle.request(attentionCall(713));
|
|
2253
|
+
await expect(attentionSecond).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2104
2254
|
const inputSecond = handle.request({
|
|
2105
2255
|
id: 714,
|
|
2106
2256
|
method: 'item/tool/requestUserInput',
|
|
@@ -2111,7 +2261,6 @@ describe('production composition', () => {
|
|
|
2111
2261
|
url: `/api/sessions/${sessionId}/attention/713/resolve`,
|
|
2112
2262
|
payload: { operationKey: 'attention-second', action: 'resume' },
|
|
2113
2263
|
});
|
|
2114
|
-
await expect(attentionSecond).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
2115
2264
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
2116
2265
|
agentActivity: { root: { state: 'awaitingHuman', reason: 'pendingInteraction' } },
|
|
2117
2266
|
}));
|
|
@@ -2150,10 +2299,8 @@ describe('production composition', () => {
|
|
|
2150
2299
|
const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
|
|
2151
2300
|
const active = handle.request(attentionCall(801));
|
|
2152
2301
|
const terminal = handle.request(attentionCall(802));
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
guidance: 'Sensitive terminal guidance.',
|
|
2156
|
-
});
|
|
2302
|
+
await expect(active).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2303
|
+
await expect(terminal).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2157
2304
|
const accepted = await first.inject({
|
|
2158
2305
|
method: 'POST',
|
|
2159
2306
|
url: `/api/sessions/${sessionId}/attention/802/resolve`,
|
|
@@ -2164,9 +2311,7 @@ describe('production composition', () => {
|
|
|
2164
2311
|
},
|
|
2165
2312
|
});
|
|
2166
2313
|
const resolvedAt = accepted.json().resolvedAt;
|
|
2167
|
-
await expect(terminal).resolves.toEqual(terminalResponse);
|
|
2168
2314
|
await first.close();
|
|
2169
|
-
await expect(active).rejects.toMatchObject({ message: 'CODEX_SERVER_REQUEST_CANCELLED' });
|
|
2170
2315
|
const reopened = await composeAuthorizedApp({
|
|
2171
2316
|
root,
|
|
2172
2317
|
dataDir,
|
|
@@ -2205,7 +2350,7 @@ describe('production composition', () => {
|
|
|
2205
2350
|
})).json()).toEqual({ code: 'ATTENTION_OPERATION_STALE' });
|
|
2206
2351
|
await reopened.close();
|
|
2207
2352
|
});
|
|
2208
|
-
it('
|
|
2353
|
+
it('resolves acknowledged attention offline while retaining legacy writer diagnostics', async () => {
|
|
2209
2354
|
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
2210
2355
|
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
2211
2356
|
ownTemporaryPaths(root, dataDir);
|
|
@@ -2230,18 +2375,17 @@ describe('production composition', () => {
|
|
|
2230
2375
|
const started = handles.filter((handle) => handle.calls.includes('thread/start'));
|
|
2231
2376
|
const supportedRequest = started[0].request(attentionCall(901));
|
|
2232
2377
|
const legacyRequest = started[1].request(attentionCall(902));
|
|
2378
|
+
await expect(supportedRequest).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2379
|
+
await expect(legacyRequest).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2233
2380
|
await vi.waitFor(async () => expect((await first.inject(`/api/sessions/${legacySession}/attention`)).statusCode).toBe(200));
|
|
2234
2381
|
await first.close();
|
|
2235
|
-
await expect(supportedRequest).rejects.toMatchObject({
|
|
2236
|
-
message: 'CODEX_SERVER_REQUEST_CANCELLED',
|
|
2237
|
-
});
|
|
2238
|
-
await expect(legacyRequest).rejects.toMatchObject({
|
|
2239
|
-
message: 'CODEX_SERVER_REQUEST_CANCELLED',
|
|
2240
|
-
});
|
|
2241
2382
|
const database = new DatabaseSync(join(dataDir, 'relay.sqlite'));
|
|
2242
2383
|
database
|
|
2243
2384
|
.prepare('UPDATE relay_sessions SET attention_tool_capability = NULL WHERE id = ?')
|
|
2244
2385
|
.run(legacySession);
|
|
2386
|
+
database
|
|
2387
|
+
.prepare("UPDATE pending_interactions SET resolution_state = 'active' WHERE session_id = ? AND request_id = '902'")
|
|
2388
|
+
.run(legacySession);
|
|
2245
2389
|
database.close();
|
|
2246
2390
|
const offline = await composeAuthorizedApp({
|
|
2247
2391
|
root,
|
|
@@ -2254,7 +2398,7 @@ describe('production composition', () => {
|
|
|
2254
2398
|
method: 'POST',
|
|
2255
2399
|
url: `/api/sessions/${supportedSession}/attention/901/resolve`,
|
|
2256
2400
|
payload: { operationKey: 'offline-supported', action: 'resume' },
|
|
2257
|
-
})).
|
|
2401
|
+
})).statusCode).toBe(202);
|
|
2258
2402
|
expect((await offline.inject({
|
|
2259
2403
|
method: 'POST',
|
|
2260
2404
|
url: `/api/sessions/${legacySession}/attention/902/resolve`,
|
|
@@ -3703,6 +3847,7 @@ describe('production composition', () => {
|
|
|
3703
3847
|
},
|
|
3704
3848
|
},
|
|
3705
3849
|
});
|
|
3850
|
+
await expect(attention).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
3706
3851
|
await vi.waitFor(async () => {
|
|
3707
3852
|
expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
3708
3853
|
pendingInteractions: [
|
|
@@ -3747,7 +3892,6 @@ describe('production composition', () => {
|
|
|
3747
3892
|
guidance: 'The release permission is now granted.',
|
|
3748
3893
|
},
|
|
3749
3894
|
})).toMatchObject({ statusCode: 202 });
|
|
3750
|
-
expect(await attention).toEqual(attentionResponse);
|
|
3751
3895
|
expect((await app.inject({
|
|
3752
3896
|
method: 'POST',
|
|
3753
3897
|
url: `/api/sessions/${sessionId}/attention/8/resolve`,
|
|
@@ -3773,8 +3917,8 @@ describe('production composition', () => {
|
|
|
3773
3917
|
}),
|
|
3774
3918
|
]));
|
|
3775
3919
|
expect(JSON.stringify(attentionHistory)).not.toContain('The release permission is now granted.');
|
|
3776
|
-
//
|
|
3777
|
-
//
|
|
3920
|
+
// Clearing the already-acknowledged dynamic request must not erase the
|
|
3921
|
+
// durable attention record or wedge the caller.
|
|
3778
3922
|
const clearedAttention = handle.request({
|
|
3779
3923
|
id: 9,
|
|
3780
3924
|
method: 'item/tool/call',
|
|
@@ -3788,6 +3932,7 @@ describe('production composition', () => {
|
|
|
3788
3932
|
},
|
|
3789
3933
|
},
|
|
3790
3934
|
});
|
|
3935
|
+
await expect(clearedAttention).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
3791
3936
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
3792
3937
|
agentActivity: { root: { state: 'awaitingHuman' } },
|
|
3793
3938
|
}));
|
|
@@ -3795,14 +3940,11 @@ describe('production composition', () => {
|
|
|
3795
3940
|
method: 'serverRequest/resolved',
|
|
3796
3941
|
params: { threadId: 'thread-1', requestId: 9 },
|
|
3797
3942
|
});
|
|
3798
|
-
await expect(clearedAttention).rejects.toMatchObject({
|
|
3799
|
-
message: 'CODEX_SERVER_REQUEST_CLEARED',
|
|
3800
|
-
});
|
|
3801
3943
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
3802
|
-
agentActivity: { root: { state: '
|
|
3944
|
+
agentActivity: { root: { state: 'awaitingHuman' } },
|
|
3803
3945
|
}));
|
|
3804
3946
|
expect((await app.inject(`/api/sessions/${sessionId}/history`)).json().interactions).toEqual(expect.arrayContaining([
|
|
3805
|
-
expect.objectContaining({ requestId: '9', kind: 'orgPlanAttention',
|
|
3947
|
+
expect.objectContaining({ requestId: '9', kind: 'orgPlanAttention', resolvedAt: null }),
|
|
3806
3948
|
]));
|
|
3807
3949
|
const quiz = handle.request({
|
|
3808
3950
|
id: 71,
|