gestalt-mobile 0.36.0 → 0.36.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.
@@ -196,7 +196,7 @@ async function createProductionAutopilotFixture(overrides = {}) {
196
196
  await installAutopilotPlan(app, sessionId, workspacePath, 'autopilot');
197
197
  return { app, dataDir, handles, root, sessionId, workspacePath };
198
198
  }
199
- async function holdCheckpointRequest(fixture, timers, requestId, expectHeld = true) {
199
+ async function holdCheckpointRequest(fixture, timers, requestId, expectHeld = true, compact = false) {
200
200
  timers.find((timer) => !timer.cancelled).callback();
201
201
  await vi.waitFor(() => expect(fixture.handles.flatMap((candidate) => candidate.requests.filter((request) => request.method === 'turn/start'))).toHaveLength(1));
202
202
  const handle = fixture.handles.find((candidate) => candidate.requests.some((request) => request.method === 'turn/start'));
@@ -218,7 +218,18 @@ async function holdCheckpointRequest(fixture, timers, requestId, expectHeld = tr
218
218
  const startsBefore = fixture.handles
219
219
  .flatMap((candidate) => candidate.requests)
220
220
  .filter((request) => request.method === 'turn/start').length;
221
- const response = handle.request(l2CheckpointCall(requestId, threadId, turnId, planIdentity));
221
+ const response = handle.request(compact
222
+ ? {
223
+ id: requestId,
224
+ method: 'item/tool/call',
225
+ params: {
226
+ threadId,
227
+ turnId,
228
+ tool: 'gestalt_org_plan_checkpoint',
229
+ arguments: { kind: 'l2Completed' },
230
+ },
231
+ }
232
+ : l2CheckpointCall(requestId, threadId, turnId, planIdentity));
222
233
  // The caller can be synchronously rejected by a coordinator exception.
223
234
  // Mark it observed immediately; callers still assert its exact outcome.
224
235
  void response.catch(() => { });
@@ -915,6 +926,28 @@ describe('production composition', () => {
915
926
  .filter((request) => request.method === 'turn/start')).toHaveLength(2));
916
927
  await fixture.app.close();
917
928
  });
929
+ it('expands and acknowledges a compact checkpoint signal from the active root', async () => {
930
+ const timers = [];
931
+ const fixture = await createProductionAutopilotFixture({
932
+ autopilotSchedule: (callback) => {
933
+ const timer = { callback, cancelled: false };
934
+ timers.push(timer);
935
+ return () => {
936
+ timer.cancelled = true;
937
+ };
938
+ },
939
+ });
940
+ const { handle, response } = await holdCheckpointRequest(fixture, timers, 812, true, true);
941
+ handle.responseSettled({ id: 812, outcome: 'resultWritten' });
942
+ await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
943
+ const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
944
+ const checkpoint = database
945
+ .prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.step-checkpointed'")
946
+ .get(fixture.sessionId);
947
+ database.close();
948
+ expect(checkpoint.count).toBe(1);
949
+ await fixture.app.close();
950
+ });
918
951
  it('leaves an unsupported checkpoint exception before persistence inert', async () => {
919
952
  let coordinator;
920
953
  const timers = [];
@@ -3652,8 +3685,6 @@ describe('production composition', () => {
3652
3685
  tool: 'gestalt_org_plan_attention',
3653
3686
  arguments: {
3654
3687
  reason: 'permissionRequired',
3655
- summary: 'A protected release needs approval.',
3656
- requestedAction: 'Grant the release permission.',
3657
3688
  resumeCondition: 'permissionGranted',
3658
3689
  },
3659
3690
  },
@@ -46,9 +46,9 @@ import { RelaySession, } from './features/sessions/model/relay-session.js';
46
46
  import { AgentActivityRegistry } from './features/agent-activity/registry.js';
47
47
  import { toAgentActivityDto } from './features/agent-activity/activity-dto.js';
48
48
  import { decodeAgentActivityFacts } from './platform/codex/activity-facts.js';
49
- import { completedCommandId, isAgentCapacityRecoveryCall, isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
49
+ import { completedCommandId, compactOrgPlanCheckpointKind, isAgentCapacityRecoveryCall, isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
50
50
  import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
51
- import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
51
+ import { resolveOrgPlanCheckpointSignal, validOrgPlanCheckpoint, } from './features/org-plan-checkpoint/application/validate.js';
52
52
  import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
53
53
  import { promoteRecentThread } from './features/sessions/promote-recent-thread/use-case.js';
54
54
  import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
@@ -676,10 +676,24 @@ export async function composeRelayApp(options) {
676
676
  if (completedSession)
677
677
  events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
678
678
  }, (sessionId, request, origin) => {
679
- const rawInteraction = toPendingInteraction(request);
679
+ let rawInteraction = toPendingInteraction(request);
680
680
  const session = withPendingInteractions(sessions.find(sessionId));
681
681
  if (!session)
682
682
  return false;
683
+ const compactCheckpointKind = compactOrgPlanCheckpointKind(request);
684
+ if (!rawInteraction && compactCheckpointKind) {
685
+ const retained = supervisedPlans.find(sessionId);
686
+ const identity = supervisedPlans.identity(sessionId);
687
+ const checkpoint = retained && identity
688
+ ? resolveOrgPlanCheckpointSignal(compactCheckpointKind, retained, identity)
689
+ : null;
690
+ if (checkpoint)
691
+ rawInteraction = {
692
+ requestId: String(request.id),
693
+ kind: 'orgPlanCheckpoint',
694
+ payload: checkpoint,
695
+ };
696
+ }
683
697
  if (!rawInteraction && isAgentCapacityRecoveryCall(request)) {
684
698
  const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
685
699
  return (rootOwned &&
@@ -3,6 +3,46 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
+ /** Expands a host-owned compact boundary signal from authoritative plan state. */
7
+ export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
8
+ if (kind === 'terminalReviewAccepted')
9
+ return {
10
+ version: 1,
11
+ kind,
12
+ planIdentity,
13
+ verdict: 'ACCEPT',
14
+ };
15
+ const selected = plan.steps.find((step) => step.id === plan.currentStepId) ??
16
+ [...plan.steps].reverse().find((step) => step.state === 'DONE' || step.state === 'WIP');
17
+ if (!selected)
18
+ return null;
19
+ const l1Position = `L${plan.steps.indexOf(selected) + 1}`;
20
+ if (kind === 'l1Accepted')
21
+ return {
22
+ version: 1,
23
+ kind,
24
+ planIdentity,
25
+ l1Id: selected.id,
26
+ position: l1Position,
27
+ verdict: 'ACCEPT',
28
+ commit: { kind: 'notRequired' },
29
+ };
30
+ const child = [...selected.children].reverse().find((step) => step.state === 'DONE');
31
+ if (!child)
32
+ return null;
33
+ return {
34
+ version: 1,
35
+ kind,
36
+ planIdentity,
37
+ l1Id: selected.id,
38
+ l2Id: child.id,
39
+ position: `${l1Position}.${selected.children.indexOf(child) + 1}`,
40
+ status: 'DONE',
41
+ changes: 'Validated from the authoritative Org Plan boundary.',
42
+ files: 'Recorded by the supervised executor.',
43
+ tests: 'Focused verification recorded before the boundary.',
44
+ };
45
+ }
6
46
  export function validOrgPlanCheckpoint(input) {
7
47
  const { checkpoint, plan, planIdentity, rootOwned } = input;
8
48
  if (!rootOwned || checkpoint.planIdentity !== planIdentity)
@@ -70,6 +70,20 @@ export function isAutopilotWaitLeaseCall(input) {
70
70
  isRecord(input.params) &&
71
71
  input.params.tool === GESTALT_AUTOPILOT_WAIT_LEASE_TOOL_NAME);
72
72
  }
73
+ /** Compatibility signal used by newer hosts that let Mobile derive boundary details. */
74
+ export function compactOrgPlanCheckpointKind(input) {
75
+ if (!Number.isSafeInteger(input.id) ||
76
+ input.method !== 'item/tool/call' ||
77
+ !isRecord(input.params) ||
78
+ input.params.tool !== GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME ||
79
+ !isRecord(input.params.arguments) ||
80
+ Object.keys(input.params.arguments).length !== 1)
81
+ return null;
82
+ const kind = input.params.arguments.kind;
83
+ return kind === 'l2Completed' || kind === 'l1Accepted' || kind === 'terminalReviewAccepted'
84
+ ? kind
85
+ : null;
86
+ }
73
87
  export function isAgentCapacityRecoveryCall(input) {
74
88
  return (Number.isSafeInteger(input.id) &&
75
89
  input.method === 'item/tool/call' &&
@@ -951,7 +951,11 @@ function asRecord(value) {
951
951
  return value && typeof value === 'object' ? value : undefined;
952
952
  }
953
953
  function decodeAgentTaskPath(value) {
954
- const subagent = asRecord(asRecord(value)?.subagent);
954
+ const source = asRecord(value);
955
+ // Codex 0.153 renamed this source discriminator from `subagent` to
956
+ // `subAgent`. Accept both spellings because the task path is the durable
957
+ // correlation between a physical child thread and its Org Plan executor.
958
+ const subagent = asRecord(source?.subAgent) ?? asRecord(source?.subagent);
955
959
  const spawn = asRecord(subagent?.thread_spawn);
956
960
  const path = boundedString(spawn?.agent_path, 256);
957
961
  return path?.startsWith('/') && !path.includes('..') ? path : undefined;
@@ -52,7 +52,21 @@ export const gestaltOrgPlanAttentionDynamicTool = {
52
52
  },
53
53
  };
54
54
  export function parseOrgPlanAttention(value) {
55
- if (!isRecord(value) || Object.keys(value).length !== 4)
55
+ if (!isRecord(value))
56
+ return null;
57
+ const keys = Object.keys(value);
58
+ if (keys.length === 2 &&
59
+ keys.every((key) => key === 'reason' || key === 'resumeCondition') &&
60
+ isReason(value.reason) &&
61
+ isResumeCondition(value.resumeCondition) &&
62
+ orgPlanAttentionResumeConditionByReason[value.reason] === value.resumeCondition)
63
+ return {
64
+ reason: value.reason,
65
+ summary: `Supervised execution requires human attention (${value.reason}).`,
66
+ requestedAction: `Satisfy the ${value.resumeCondition} resume condition, then resume or disable Autopilot.`,
67
+ resumeCondition: value.resumeCondition,
68
+ };
69
+ if (keys.length !== 4)
56
70
  return null;
57
71
  if (!isReason(value.reason) ||
58
72
  !isBoundedText(value.summary, 600) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",