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.
@@ -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('leaves an unsupported checkpoint exception before persistence inert', async () => {
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(response).rejects.toThrow('CODEX_SERVER_REQUEST_UNSUPPORTED');
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('rejects a checkpoint delivered after its owning root final has closed', async () => {
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 expect(handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity))).rejects.toThrow('CODEX_SERVER_REQUEST_UNSUPPORTED');
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
- await expect(failing).rejects.toMatchObject({ message: 'CODEX_SERVER_REQUEST_CLEARED' });
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
- const terminalResponse = toOrgPlanAttentionToolResponse({
2144
- action: 'resume',
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('reports supported offline writers separately from legacy sessions without the attention capability', async () => {
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
- })).json()).toEqual({ code: 'ATTENTION_WRITER_UNAVAILABLE' });
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
- // A server-cleared attention request is a durable failed audit and must
3767
- // re-project the root rather than leaving the GUI awaiting a vanished alert.
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: 'working' } },
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', outcome: 'failed' }),
3951
+ expect.objectContaining({ requestId: '9', kind: 'orgPlanAttention', resolvedAt: null }),
3796
3952
  ]));
3797
3953
  const quiz = handle.request({
3798
3954
  id: 71,