gestalt-mobile 0.41.0 → 0.41.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-G2PznC5F.js → index-9uZwieND.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +200 -44
- package/dist/server/server/composition.js +155 -37
- package/dist/server/server/features/agent-activity/model.js +8 -2
- package/dist/server/server/features/autopilot/application/policy.js +2 -2
- package/dist/server/server/features/autopilot/application/service.js +93 -29
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +81 -7
- 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/platform/persistence/sqlite-pending-interaction-store.js +12 -6
- package/dist/server/shared/contracts/org-plan-attention.js +8 -1
- package/dist/server/shared/contracts/org-plan-checkpoint.js +27 -3
- package/dist/server/shared/contracts/plan-signal.js +3 -1
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ import { authorizationSessionId, authorizedDeviceId, localOwnerId, webAuthnCrede
|
|
|
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));
|
|
@@ -789,13 +808,21 @@ describe('production composition', () => {
|
|
|
789
808
|
beforeWriteDatabase.close();
|
|
790
809
|
expect(beforeWrite.resolved_at).toBeNull();
|
|
791
810
|
handle.responseSettled({ id: 811, outcome: 'resultWritten' });
|
|
792
|
-
await expect(checkpointResponse).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
811
|
+
await expect(checkpointResponse).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
812
|
+
const replayResponse = handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity));
|
|
813
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(812)).toBe(true));
|
|
814
|
+
handle.responseSettled({ id: 812, outcome: 'resultWritten' });
|
|
815
|
+
await expect(replayResponse).resolves.toEqual(toOrgPlanCheckpointToolResponse('alreadyRecorded'));
|
|
793
816
|
const afterWriteDatabase = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
|
|
794
817
|
const afterWrite = afterWriteDatabase
|
|
795
818
|
.prepare('SELECT resolved_at FROM pending_interactions WHERE session_id = ? AND request_id = ?')
|
|
796
819
|
.get(fixture.sessionId, '811');
|
|
820
|
+
const checkpointEvents = afterWriteDatabase
|
|
821
|
+
.prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.step-checkpointed'")
|
|
822
|
+
.get(fixture.sessionId);
|
|
797
823
|
afterWriteDatabase.close();
|
|
798
824
|
expect(afterWrite.resolved_at).toEqual(expect.any(String));
|
|
825
|
+
expect(checkpointEvents.count).toBe(1);
|
|
799
826
|
expect(timers).toHaveLength(schedulesBeforeCheckpoint);
|
|
800
827
|
expect(coordinator.snapshot(fixture.sessionId).health).not.toMatchObject({
|
|
801
828
|
phase: 'waitingForAgentEvent',
|
|
@@ -941,7 +968,7 @@ describe('production composition', () => {
|
|
|
941
968
|
});
|
|
942
969
|
const { handle, response } = await holdCheckpointRequest(fixture, timers, 812, true, true);
|
|
943
970
|
handle.responseSettled({ id: 812, outcome: 'resultWritten' });
|
|
944
|
-
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
971
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
945
972
|
const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
|
|
946
973
|
const checkpoint = database
|
|
947
974
|
.prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.step-checkpointed'")
|
|
@@ -950,7 +977,84 @@ describe('production composition', () => {
|
|
|
950
977
|
expect(checkpoint.count).toBe(1);
|
|
951
978
|
await fixture.app.close();
|
|
952
979
|
});
|
|
953
|
-
it('
|
|
980
|
+
it('records and idempotently replays accepted L1 after REVIEWED advances to the next TODO L1', async () => {
|
|
981
|
+
const timers = [];
|
|
982
|
+
const fixture = await createProductionAutopilotFixture({
|
|
983
|
+
autopilotSchedule: (callback) => {
|
|
984
|
+
const timer = { callback, cancelled: false, fired: false };
|
|
985
|
+
timers.push(timer);
|
|
986
|
+
return () => {
|
|
987
|
+
timer.cancelled = true;
|
|
988
|
+
};
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
const initial = timers.find((timer) => !timer.cancelled && !timer.fired);
|
|
992
|
+
initial.fired = true;
|
|
993
|
+
initial.callback();
|
|
994
|
+
await vi.waitFor(() => expect(fixture.handles.flatMap((candidate) => candidate.requests.filter((request) => request.method === 'turn/start'))).toHaveLength(1));
|
|
995
|
+
const handle = fixture.handles.find((candidate) => candidate.request);
|
|
996
|
+
const session = (await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json();
|
|
997
|
+
const threadId = session.threadId;
|
|
998
|
+
const turnId = session.activeTurnId;
|
|
999
|
+
await writeFile(join(fixture.workspacePath, 'autopilot.org'), acceptedL1WithNextTodoPlanText());
|
|
1000
|
+
await expect
|
|
1001
|
+
.poll(async () => (await fixture.app.inject(`/api/sessions/${fixture.sessionId}/plan`)).json()
|
|
1002
|
+
.currentStepId)
|
|
1003
|
+
.toBe('next-parent');
|
|
1004
|
+
handle.deferResponseSettlement = true;
|
|
1005
|
+
const checkpointCall = (id) => ({
|
|
1006
|
+
id,
|
|
1007
|
+
method: 'item/tool/call',
|
|
1008
|
+
params: {
|
|
1009
|
+
threadId,
|
|
1010
|
+
turnId,
|
|
1011
|
+
tool: 'gestalt_org_plan_checkpoint',
|
|
1012
|
+
arguments: { kind: 'l1Accepted' },
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
const startsBefore = fixture.handles
|
|
1016
|
+
.flatMap((candidate) => candidate.requests)
|
|
1017
|
+
.filter((request) => request.method === 'turn/start').length;
|
|
1018
|
+
const first = handle.request(checkpointCall(813));
|
|
1019
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(813)).toBe(true));
|
|
1020
|
+
handle.responseSettled({ id: 813, outcome: 'resultWritten' });
|
|
1021
|
+
await expect(first).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
1022
|
+
const replay = handle.request(checkpointCall(814));
|
|
1023
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(814)).toBe(true));
|
|
1024
|
+
handle.responseSettled({ id: 814, outcome: 'resultWritten' });
|
|
1025
|
+
await expect(replay).resolves.toEqual(toOrgPlanCheckpointToolResponse('alreadyRecorded'));
|
|
1026
|
+
const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
|
|
1027
|
+
const acceptedEvents = database
|
|
1028
|
+
.prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.milestone-checkpointed'")
|
|
1029
|
+
.get(fixture.sessionId);
|
|
1030
|
+
const pendingInteractions = database
|
|
1031
|
+
.prepare('SELECT count(*) AS count FROM pending_interactions WHERE session_id = ? AND resolved_at IS NULL')
|
|
1032
|
+
.get(fixture.sessionId);
|
|
1033
|
+
database.close();
|
|
1034
|
+
expect(acceptedEvents.count).toBe(1);
|
|
1035
|
+
expect(pendingInteractions.count).toBe(0);
|
|
1036
|
+
expect(fixture.handles
|
|
1037
|
+
.flatMap((candidate) => candidate.requests)
|
|
1038
|
+
.filter((request) => request.method === 'turn/start')).toHaveLength(startsBefore);
|
|
1039
|
+
handle.notify({ method: 'turn/completed', params: { threadId, turn: { id: turnId } } });
|
|
1040
|
+
await expect
|
|
1041
|
+
.poll(async () => (await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json().activeTurnId)
|
|
1042
|
+
.toBeNull();
|
|
1043
|
+
const startedTurns = () => fixture.handles
|
|
1044
|
+
.flatMap((candidate) => candidate.requests)
|
|
1045
|
+
.filter((request) => request.method === 'turn/start').length;
|
|
1046
|
+
for (let attempts = 0; attempts < 3 && startedTurns() === startsBefore; attempts += 1) {
|
|
1047
|
+
await vi.waitFor(() => expect(timers.find((timer) => !timer.cancelled && !timer.fired)).toBeDefined());
|
|
1048
|
+
const continuation = timers.find((timer) => !timer.cancelled && !timer.fired);
|
|
1049
|
+
continuation.fired = true;
|
|
1050
|
+
continuation.callback();
|
|
1051
|
+
await vi.waitFor(() => expect(startedTurns() > startsBefore ||
|
|
1052
|
+
timers.some((timer) => !timer.cancelled && !timer.fired)).toBe(true));
|
|
1053
|
+
}
|
|
1054
|
+
await vi.waitFor(() => expect(startedTurns()).toBe(startsBefore + 1));
|
|
1055
|
+
await fixture.app.close();
|
|
1056
|
+
});
|
|
1057
|
+
it('returns a typed failed checkpoint result when persistence never occurs', async () => {
|
|
954
1058
|
let coordinator;
|
|
955
1059
|
const timers = [];
|
|
956
1060
|
const fixture = await createProductionAutopilotFixture({
|
|
@@ -969,8 +1073,16 @@ describe('production composition', () => {
|
|
|
969
1073
|
vi.spyOn(coordinator, 'checkpointAccepted').mockImplementation(() => {
|
|
970
1074
|
throw new Error('injected-before-checkpoint-persistence');
|
|
971
1075
|
});
|
|
972
|
-
const { response, startsBefore, writersBefore } = await holdCheckpointRequest(fixture, timers, 995, false);
|
|
973
|
-
await expect(
|
|
1076
|
+
const { handle, response, startsBefore, writersBefore } = await holdCheckpointRequest(fixture, timers, 995, false);
|
|
1077
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(995)).toBe(true));
|
|
1078
|
+
handle.responseSettled({ id: 995, outcome: 'resultWritten' });
|
|
1079
|
+
const failure = (await response);
|
|
1080
|
+
expect(failure.success).toBe(false);
|
|
1081
|
+
expect(JSON.parse(failure.contentItems[0].text)).toMatchObject({
|
|
1082
|
+
status: 'failed',
|
|
1083
|
+
reasonCode: 'checkpointPersistenceFailed',
|
|
1084
|
+
correlationId: expect.any(String),
|
|
1085
|
+
});
|
|
974
1086
|
expect(coordinator.checkpointAccepted).not.toBe(original);
|
|
975
1087
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
976
1088
|
expect(fixture.handles
|
|
@@ -1139,7 +1251,7 @@ describe('production composition', () => {
|
|
|
1139
1251
|
};
|
|
1140
1252
|
if (scenario === 'resultWritten-before-timeout') {
|
|
1141
1253
|
handle.responseSettled({ id: 992, outcome: 'resultWritten' });
|
|
1142
|
-
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
1254
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
1143
1255
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1144
1256
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
1145
1257
|
expect(failures()).toBe(0);
|
|
@@ -1147,7 +1259,7 @@ describe('production composition', () => {
|
|
|
1147
1259
|
else if (scenario === 'duplicate-settlement') {
|
|
1148
1260
|
handle.responseSettled({ id: 992, outcome: 'resultWritten' });
|
|
1149
1261
|
handle.responseSettled({ id: 992, outcome: 'resultWritten' });
|
|
1150
|
-
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
1262
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
1151
1263
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1152
1264
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
1153
1265
|
expect(failures()).toBe(0);
|
|
@@ -1292,7 +1404,7 @@ describe('production composition', () => {
|
|
|
1292
1404
|
expect(approvalTurn.statusCode).toBe(202);
|
|
1293
1405
|
await approvalFixture.app.close();
|
|
1294
1406
|
});
|
|
1295
|
-
it('
|
|
1407
|
+
it('returns failed for a checkpoint delivered after its owning root final has closed', async () => {
|
|
1296
1408
|
const timers = [];
|
|
1297
1409
|
const fixture = await createProductionAutopilotFixture({
|
|
1298
1410
|
autopilotSchedule: (callback) => {
|
|
@@ -1330,7 +1442,13 @@ describe('production composition', () => {
|
|
|
1330
1442
|
const planIdentity = createHash('sha256')
|
|
1331
1443
|
.update(join(fixture.workspacePath, 'autopilot.org'))
|
|
1332
1444
|
.digest('hex');
|
|
1333
|
-
await
|
|
1445
|
+
const rejected = (await handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity)));
|
|
1446
|
+
expect(rejected.success).toBe(false);
|
|
1447
|
+
expect(JSON.parse(rejected.contentItems[0].text)).toMatchObject({
|
|
1448
|
+
status: 'failed',
|
|
1449
|
+
reasonCode: 'rootNotOwner',
|
|
1450
|
+
correlationId: expect.any(String),
|
|
1451
|
+
});
|
|
1334
1452
|
expect(timers).toHaveLength(timersAfterFinal);
|
|
1335
1453
|
expect(fixture.handles
|
|
1336
1454
|
.flatMap((candidate) => candidate.requests)
|
|
@@ -1346,6 +1464,7 @@ describe('production composition', () => {
|
|
|
1346
1464
|
payload: { enabled: true },
|
|
1347
1465
|
})).statusCode).toBe(200);
|
|
1348
1466
|
const attention = handle.request(attentionCall(810));
|
|
1467
|
+
await expect(attention).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
1349
1468
|
await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1350
1469
|
autopilot: { state: 'attentionRequired', enabled: false, reason: 'attentionRequired' },
|
|
1351
1470
|
}));
|
|
@@ -1355,7 +1474,6 @@ describe('production composition', () => {
|
|
|
1355
1474
|
url: `/api/sessions/${fixture.sessionId}/attention/810/resolve`,
|
|
1356
1475
|
payload: { operationKey: 'resume-attention', action: 'resume' },
|
|
1357
1476
|
})).statusCode).toBe(202);
|
|
1358
|
-
await expect(attention).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
1359
1477
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1360
1478
|
expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1361
1479
|
autopilot: { state: 'attentionRequired', enabled: false },
|
|
@@ -1949,6 +2067,56 @@ describe('production composition', () => {
|
|
|
1949
2067
|
});
|
|
1950
2068
|
});
|
|
1951
2069
|
describeCompositionConcern('attention', () => {
|
|
2070
|
+
it('bounds a wedged attention acknowledgement writer without losing durable attention', async () => {
|
|
2071
|
+
const fixture = await createProductionAutopilotFixture({
|
|
2072
|
+
attentionAcknowledgementDeadlineMs: 100,
|
|
2073
|
+
});
|
|
2074
|
+
const handle = fixture.handles.find((candidate) => candidate.request);
|
|
2075
|
+
handle.deferResponseSettlement = true;
|
|
2076
|
+
const response = handle.request(attentionCall(699));
|
|
2077
|
+
const rejected = expect(response).rejects.toThrow('CODEX_SERVER_REQUEST_CANCELLED');
|
|
2078
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(699)).toBe(true));
|
|
2079
|
+
await rejected;
|
|
2080
|
+
expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}/attention`)).json()).toMatchObject({ requestId: '699', attention: { reason: 'hardBlock' } });
|
|
2081
|
+
await fixture.app.close();
|
|
2082
|
+
});
|
|
2083
|
+
it('returns a bounded failure when durable attention persistence fails', async () => {
|
|
2084
|
+
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
2085
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
2086
|
+
ownTemporaryPaths(root, dataDir);
|
|
2087
|
+
await mkdir(join(root, 'workspace'));
|
|
2088
|
+
const handles = [];
|
|
2089
|
+
const app = await composeAuthorizedApp({
|
|
2090
|
+
root,
|
|
2091
|
+
dataDir,
|
|
2092
|
+
relyingParty,
|
|
2093
|
+
installedCodexVersion: 'codex-cli 0.144.3',
|
|
2094
|
+
startAppServers: true,
|
|
2095
|
+
launchAppServer: liveAppServer(handles),
|
|
2096
|
+
profiles: {
|
|
2097
|
+
list: async () => [],
|
|
2098
|
+
require: async () => ({
|
|
2099
|
+
name: 'default',
|
|
2100
|
+
state: 'ok',
|
|
2101
|
+
status: 'ready',
|
|
2102
|
+
}),
|
|
2103
|
+
},
|
|
2104
|
+
});
|
|
2105
|
+
await createComposedSession(app);
|
|
2106
|
+
const handle = await vi.waitFor(() => {
|
|
2107
|
+
const candidate = handles.find((item) => item.request);
|
|
2108
|
+
expect(candidate).toBeDefined();
|
|
2109
|
+
return candidate;
|
|
2110
|
+
});
|
|
2111
|
+
const database = new DatabaseSync(join(dataDir, 'relay.sqlite'));
|
|
2112
|
+
database.exec(`CREATE TRIGGER reject_attention_insert
|
|
2113
|
+
BEFORE INSERT ON pending_interactions
|
|
2114
|
+
WHEN NEW.kind = 'orgPlanAttention'
|
|
2115
|
+
BEGIN SELECT RAISE(ABORT, 'injected attention persistence failure'); END`);
|
|
2116
|
+
database.close();
|
|
2117
|
+
await expect(handle.request(attentionCall(700))).resolves.toEqual(toOrgPlanAttentionAcknowledgement({ accepted: false, reason: 'persistenceFailed' }));
|
|
2118
|
+
await app.close();
|
|
2119
|
+
});
|
|
1952
2120
|
it('publishes isolated typed required, resolved, and failed attention transitions through the feature-only seam', async () => {
|
|
1953
2121
|
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
1954
2122
|
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
@@ -1994,6 +2162,7 @@ describe('production composition', () => {
|
|
|
1994
2162
|
const unsubscribeA = transitions.subscribe(sessionA, (event) => receivedA.push(event));
|
|
1995
2163
|
const unsubscribeB = transitions.subscribe(sessionB, (event) => receivedB.push(event));
|
|
1996
2164
|
const resolving = handleA.request(attentionCall(701));
|
|
2165
|
+
await expect(resolving).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
1997
2166
|
await vi.waitFor(() => expect(receivedA).toEqual([
|
|
1998
2167
|
expect.objectContaining({ kind: 'required', requestId: '701' }),
|
|
1999
2168
|
]));
|
|
@@ -2003,12 +2172,12 @@ describe('production composition', () => {
|
|
|
2003
2172
|
url: `/api/sessions/${sessionA}/attention/701/resolve`,
|
|
2004
2173
|
payload: { operationKey: 'resolve-a', action: 'resume' },
|
|
2005
2174
|
})).statusCode).toBe(202);
|
|
2006
|
-
await expect(resolving).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
2007
2175
|
await vi.waitFor(() => expect(receivedA).toEqual([
|
|
2008
2176
|
expect.objectContaining({ kind: 'required', requestId: '701' }),
|
|
2009
2177
|
expect.objectContaining({ kind: 'resolved', requestId: '701' }),
|
|
2010
2178
|
]));
|
|
2011
2179
|
const failing = handleB.request(attentionCall(702, 'permissionRequired'));
|
|
2180
|
+
await expect(failing).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2012
2181
|
await vi.waitFor(() => expect(receivedB).toEqual([
|
|
2013
2182
|
expect.objectContaining({ kind: 'required', requestId: '702' }),
|
|
2014
2183
|
]));
|
|
@@ -2016,13 +2185,10 @@ describe('production composition', () => {
|
|
|
2016
2185
|
method: 'serverRequest/resolved',
|
|
2017
2186
|
params: { threadId: 'thread-ignored', requestId: 702 },
|
|
2018
2187
|
});
|
|
2019
|
-
|
|
2020
|
-
await vi.waitFor(() => expect(receivedB).toEqual([
|
|
2021
|
-
expect.objectContaining({ kind: 'required', requestId: '702' }),
|
|
2022
|
-
expect.objectContaining({ kind: 'failed', requestId: '702' }),
|
|
2023
|
-
]));
|
|
2188
|
+
expect(receivedB).toEqual([expect.objectContaining({ kind: 'required', requestId: '702' })]);
|
|
2024
2189
|
unsubscribeA();
|
|
2025
2190
|
const afterUnsubscribe = handleA.request(attentionCall(703));
|
|
2191
|
+
await expect(afterUnsubscribe).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2026
2192
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionA}/attention`)).json()).toMatchObject({
|
|
2027
2193
|
requestId: '703',
|
|
2028
2194
|
}));
|
|
@@ -2031,9 +2197,6 @@ describe('production composition', () => {
|
|
|
2031
2197
|
method: 'serverRequest/resolved',
|
|
2032
2198
|
params: { threadId: 'thread-ignored', requestId: 703 },
|
|
2033
2199
|
});
|
|
2034
|
-
await expect(afterUnsubscribe).rejects.toMatchObject({
|
|
2035
|
-
message: 'CODEX_SERVER_REQUEST_CLEARED',
|
|
2036
|
-
});
|
|
2037
2200
|
expect(receivedA).toHaveLength(2);
|
|
2038
2201
|
unsubscribeB();
|
|
2039
2202
|
await app.close();
|
|
@@ -2064,6 +2227,7 @@ describe('production composition', () => {
|
|
|
2064
2227
|
await vi.waitFor(() => expect(handles.find((handle) => handle.request)?.request).toBeDefined());
|
|
2065
2228
|
const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
|
|
2066
2229
|
const attentionFirst = handle.request(attentionCall(711, 'permissionRequired'));
|
|
2230
|
+
await expect(attentionFirst).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2067
2231
|
const inputFirst = handle.request({
|
|
2068
2232
|
id: 712,
|
|
2069
2233
|
method: 'item/tool/requestUserInput',
|
|
@@ -2086,11 +2250,11 @@ describe('production composition', () => {
|
|
|
2086
2250
|
url: `/api/sessions/${sessionId}/attention/711/resolve`,
|
|
2087
2251
|
payload: { operationKey: 'attention-first', action: 'resume' },
|
|
2088
2252
|
});
|
|
2089
|
-
await expect(attentionFirst).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
2090
2253
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
2091
2254
|
agentActivity: { root: { state: 'working' } },
|
|
2092
2255
|
}));
|
|
2093
2256
|
const attentionSecond = handle.request(attentionCall(713));
|
|
2257
|
+
await expect(attentionSecond).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2094
2258
|
const inputSecond = handle.request({
|
|
2095
2259
|
id: 714,
|
|
2096
2260
|
method: 'item/tool/requestUserInput',
|
|
@@ -2101,7 +2265,6 @@ describe('production composition', () => {
|
|
|
2101
2265
|
url: `/api/sessions/${sessionId}/attention/713/resolve`,
|
|
2102
2266
|
payload: { operationKey: 'attention-second', action: 'resume' },
|
|
2103
2267
|
});
|
|
2104
|
-
await expect(attentionSecond).resolves.toEqual(toOrgPlanAttentionToolResponse({ action: 'resume' }));
|
|
2105
2268
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
2106
2269
|
agentActivity: { root: { state: 'awaitingHuman', reason: 'pendingInteraction' } },
|
|
2107
2270
|
}));
|
|
@@ -2140,10 +2303,8 @@ describe('production composition', () => {
|
|
|
2140
2303
|
const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
|
|
2141
2304
|
const active = handle.request(attentionCall(801));
|
|
2142
2305
|
const terminal = handle.request(attentionCall(802));
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
guidance: 'Sensitive terminal guidance.',
|
|
2146
|
-
});
|
|
2306
|
+
await expect(active).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2307
|
+
await expect(terminal).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2147
2308
|
const accepted = await first.inject({
|
|
2148
2309
|
method: 'POST',
|
|
2149
2310
|
url: `/api/sessions/${sessionId}/attention/802/resolve`,
|
|
@@ -2154,9 +2315,7 @@ describe('production composition', () => {
|
|
|
2154
2315
|
},
|
|
2155
2316
|
});
|
|
2156
2317
|
const resolvedAt = accepted.json().resolvedAt;
|
|
2157
|
-
await expect(terminal).resolves.toEqual(terminalResponse);
|
|
2158
2318
|
await first.close();
|
|
2159
|
-
await expect(active).rejects.toMatchObject({ message: 'CODEX_SERVER_REQUEST_CANCELLED' });
|
|
2160
2319
|
const reopened = await composeAuthorizedApp({
|
|
2161
2320
|
root,
|
|
2162
2321
|
dataDir,
|
|
@@ -2195,7 +2354,7 @@ describe('production composition', () => {
|
|
|
2195
2354
|
})).json()).toEqual({ code: 'ATTENTION_OPERATION_STALE' });
|
|
2196
2355
|
await reopened.close();
|
|
2197
2356
|
});
|
|
2198
|
-
it('
|
|
2357
|
+
it('resolves acknowledged attention offline while retaining legacy writer diagnostics', async () => {
|
|
2199
2358
|
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
2200
2359
|
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
2201
2360
|
ownTemporaryPaths(root, dataDir);
|
|
@@ -2220,18 +2379,17 @@ describe('production composition', () => {
|
|
|
2220
2379
|
const started = handles.filter((handle) => handle.calls.includes('thread/start'));
|
|
2221
2380
|
const supportedRequest = started[0].request(attentionCall(901));
|
|
2222
2381
|
const legacyRequest = started[1].request(attentionCall(902));
|
|
2382
|
+
await expect(supportedRequest).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2383
|
+
await expect(legacyRequest).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
2223
2384
|
await vi.waitFor(async () => expect((await first.inject(`/api/sessions/${legacySession}/attention`)).statusCode).toBe(200));
|
|
2224
2385
|
await first.close();
|
|
2225
|
-
await expect(supportedRequest).rejects.toMatchObject({
|
|
2226
|
-
message: 'CODEX_SERVER_REQUEST_CANCELLED',
|
|
2227
|
-
});
|
|
2228
|
-
await expect(legacyRequest).rejects.toMatchObject({
|
|
2229
|
-
message: 'CODEX_SERVER_REQUEST_CANCELLED',
|
|
2230
|
-
});
|
|
2231
2386
|
const database = new DatabaseSync(join(dataDir, 'relay.sqlite'));
|
|
2232
2387
|
database
|
|
2233
2388
|
.prepare('UPDATE relay_sessions SET attention_tool_capability = NULL WHERE id = ?')
|
|
2234
2389
|
.run(legacySession);
|
|
2390
|
+
database
|
|
2391
|
+
.prepare("UPDATE pending_interactions SET resolution_state = 'active' WHERE session_id = ? AND request_id = '902'")
|
|
2392
|
+
.run(legacySession);
|
|
2235
2393
|
database.close();
|
|
2236
2394
|
const offline = await composeAuthorizedApp({
|
|
2237
2395
|
root,
|
|
@@ -2244,7 +2402,7 @@ describe('production composition', () => {
|
|
|
2244
2402
|
method: 'POST',
|
|
2245
2403
|
url: `/api/sessions/${supportedSession}/attention/901/resolve`,
|
|
2246
2404
|
payload: { operationKey: 'offline-supported', action: 'resume' },
|
|
2247
|
-
})).
|
|
2405
|
+
})).statusCode).toBe(202);
|
|
2248
2406
|
expect((await offline.inject({
|
|
2249
2407
|
method: 'POST',
|
|
2250
2408
|
url: `/api/sessions/${legacySession}/attention/902/resolve`,
|
|
@@ -3693,6 +3851,7 @@ describe('production composition', () => {
|
|
|
3693
3851
|
},
|
|
3694
3852
|
},
|
|
3695
3853
|
});
|
|
3854
|
+
await expect(attention).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
3696
3855
|
await vi.waitFor(async () => {
|
|
3697
3856
|
expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
3698
3857
|
pendingInteractions: [
|
|
@@ -3737,7 +3896,6 @@ describe('production composition', () => {
|
|
|
3737
3896
|
guidance: 'The release permission is now granted.',
|
|
3738
3897
|
},
|
|
3739
3898
|
})).toMatchObject({ statusCode: 202 });
|
|
3740
|
-
expect(await attention).toEqual(attentionResponse);
|
|
3741
3899
|
expect((await app.inject({
|
|
3742
3900
|
method: 'POST',
|
|
3743
3901
|
url: `/api/sessions/${sessionId}/attention/8/resolve`,
|
|
@@ -3763,8 +3921,8 @@ describe('production composition', () => {
|
|
|
3763
3921
|
}),
|
|
3764
3922
|
]));
|
|
3765
3923
|
expect(JSON.stringify(attentionHistory)).not.toContain('The release permission is now granted.');
|
|
3766
|
-
//
|
|
3767
|
-
//
|
|
3924
|
+
// Clearing the already-acknowledged dynamic request must not erase the
|
|
3925
|
+
// durable attention record or wedge the caller.
|
|
3768
3926
|
const clearedAttention = handle.request({
|
|
3769
3927
|
id: 9,
|
|
3770
3928
|
method: 'item/tool/call',
|
|
@@ -3778,6 +3936,7 @@ describe('production composition', () => {
|
|
|
3778
3936
|
},
|
|
3779
3937
|
},
|
|
3780
3938
|
});
|
|
3939
|
+
await expect(clearedAttention).resolves.toEqual(toOrgPlanAttentionAcknowledgement());
|
|
3781
3940
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
3782
3941
|
agentActivity: { root: { state: 'awaitingHuman' } },
|
|
3783
3942
|
}));
|
|
@@ -3785,14 +3944,11 @@ describe('production composition', () => {
|
|
|
3785
3944
|
method: 'serverRequest/resolved',
|
|
3786
3945
|
params: { threadId: 'thread-1', requestId: 9 },
|
|
3787
3946
|
});
|
|
3788
|
-
await expect(clearedAttention).rejects.toMatchObject({
|
|
3789
|
-
message: 'CODEX_SERVER_REQUEST_CLEARED',
|
|
3790
|
-
});
|
|
3791
3947
|
await vi.waitFor(async () => expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
|
|
3792
|
-
agentActivity: { root: { state: '
|
|
3948
|
+
agentActivity: { root: { state: 'awaitingHuman' } },
|
|
3793
3949
|
}));
|
|
3794
3950
|
expect((await app.inject(`/api/sessions/${sessionId}/history`)).json().interactions).toEqual(expect.arrayContaining([
|
|
3795
|
-
expect.objectContaining({ requestId: '9', kind: 'orgPlanAttention',
|
|
3951
|
+
expect.objectContaining({ requestId: '9', kind: 'orgPlanAttention', resolvedAt: null }),
|
|
3796
3952
|
]));
|
|
3797
3953
|
const quiz = handle.request({
|
|
3798
3954
|
id: 71,
|