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.
@@ -13,6 +13,19 @@ export const blockingResumeConditions = {
13
13
  externalState: 'externalStateChanged',
14
14
  materialAmbiguity: 'userGuidance',
15
15
  };
16
+ export function executorAssignment(executor, state = 'resumable') {
17
+ const taskName = executor.taskPath.split('/').filter(Boolean).at(-1);
18
+ const identity = executorIdentity(executor.canonicalTaskName, executor.continuationGeneration);
19
+ return {
20
+ canonicalPosition: executor.canonicalPosition,
21
+ canonicalTaskName: executor.canonicalTaskName,
22
+ taskName: taskName || identity.taskName,
23
+ taskPath: executor.taskPath,
24
+ threadId: executor.threadId,
25
+ generation: identity.generation,
26
+ state,
27
+ };
28
+ }
16
29
  export function checkpointTarget(kind, l1Id, l2Id) {
17
30
  return kind === 'l2'
18
31
  ? JSON.stringify([kind, l1Id, l2Id])
@@ -153,6 +166,8 @@ export function parsePersistedSupervisedLifecycle(value) {
153
166
  const resumeFailures = executorValue.resumeFailures === undefined
154
167
  ? 0
155
168
  : nonNegativeInteger(executorValue.resumeFailures);
169
+ const assignmentValue = executorValue.assignment === undefined ? undefined : record(executorValue.assignment);
170
+ const assignment = assignmentValue ? parseExecutorAssignment(assignmentValue) : undefined;
156
171
  const replacementValue = executorValue.replacement === undefined ? undefined : record(executorValue.replacement);
157
172
  const replacementGeneration = replacementValue && nonNegativeInteger(replacementValue.generation);
158
173
  const replacementTaskName = replacementValue && boundedText(replacementValue.taskName);
@@ -160,6 +175,12 @@ export function parsePersistedSupervisedLifecycle(value) {
160
175
  const replacementCanonicalPosition = replacementValue && boundedText(replacementValue.canonicalPosition);
161
176
  const replacementPlanIdentity = replacementValue && boundedText(replacementValue.planIdentity);
162
177
  const replacementPlanFingerprint = replacementValue && boundedText(replacementValue.planFingerprint);
178
+ const replacementPreviousValue = replacementValue && record(replacementValue.previous);
179
+ const replacementPrevious = replacementPreviousValue
180
+ ? parseExecutorAssignment(replacementPreviousValue)
181
+ : undefined;
182
+ const replacementReason = replacementValue && stringValue(replacementValue.reason, ['explicitExecutorRejection']);
183
+ const replacementEvidence = replacementValue && boundedText(replacementValue.evidence);
163
184
  if (!canonicalPosition ||
164
185
  !canonicalTaskName ||
165
186
  !taskPath ||
@@ -170,14 +191,32 @@ export function parsePersistedSupervisedLifecycle(value) {
170
191
  continuationGeneration === null ||
171
192
  continuationCount === null ||
172
193
  resumeFailures === null ||
194
+ (executorValue.assignment !== undefined && !assignment) ||
173
195
  (replacementValue &&
174
196
  (!replacementGeneration ||
175
197
  !replacementTaskName ||
176
198
  !replacementCanonicalTaskName ||
177
199
  !replacementCanonicalPosition ||
178
200
  !replacementPlanIdentity ||
179
- !replacementPlanFingerprint)))
201
+ !replacementPlanFingerprint ||
202
+ (replacementValue.previous !== undefined && !replacementPrevious) ||
203
+ (replacementReason !== undefined && replacementReason !== 'explicitExecutorRejection') ||
204
+ (replacementValue.evidence !== undefined && !replacementEvidence))))
180
205
  return undefined;
206
+ if (replacementValue) {
207
+ try {
208
+ const identity = executorIdentity(replacementCanonicalTaskName, replacementGeneration);
209
+ if (identity.taskName !== replacementTaskName ||
210
+ identity.canonicalPosition !== replacementCanonicalPosition ||
211
+ (replacementPrevious &&
212
+ (replacementPrevious.canonicalTaskName !== canonicalTaskName ||
213
+ replacementPrevious.canonicalPosition !== canonicalPosition)))
214
+ return undefined;
215
+ }
216
+ catch {
217
+ return undefined;
218
+ }
219
+ }
181
220
  const commandsValue = executorValue.commands;
182
221
  if (commandsValue !== undefined && (!Array.isArray(commandsValue) || commandsValue.length > 32))
183
222
  return undefined;
@@ -268,6 +307,7 @@ export function parsePersistedSupervisedLifecycle(value) {
268
307
  continuationGeneration,
269
308
  continuationCount,
270
309
  ...(resumeFailures ? { resumeFailures } : {}),
310
+ ...(assignment ? { assignment } : {}),
271
311
  ...(replacementValue
272
312
  ? {
273
313
  replacement: {
@@ -277,6 +317,9 @@ export function parsePersistedSupervisedLifecycle(value) {
277
317
  canonicalPosition: replacementCanonicalPosition,
278
318
  planIdentity: replacementPlanIdentity,
279
319
  planFingerprint: replacementPlanFingerprint,
320
+ ...(replacementPrevious ? { previous: replacementPrevious } : {}),
321
+ ...(replacementReason ? { reason: replacementReason } : {}),
322
+ ...(replacementEvidence ? { evidence: replacementEvidence } : {}),
280
323
  },
281
324
  }
282
325
  : {}),
@@ -396,19 +439,50 @@ function parseCheckpoints(value) {
396
439
  terminalReviewAccepted: value.terminalReviewAccepted,
397
440
  };
398
441
  }
442
+ function parseExecutorAssignment(value) {
443
+ const canonicalPosition = boundedText(value.canonicalPosition);
444
+ const canonicalTaskName = boundedText(value.canonicalTaskName);
445
+ const taskName = boundedText(value.taskName);
446
+ const taskPath = boundedText(value.taskPath);
447
+ const threadId = boundedText(value.threadId);
448
+ const generation = nonNegativeInteger(value.generation);
449
+ const state = stringValue(value.state, ['resumable', 'closed', 'unavailable']);
450
+ if (!canonicalPosition ||
451
+ !canonicalTaskName ||
452
+ !taskName ||
453
+ !taskPath ||
454
+ !threadId ||
455
+ generation === null ||
456
+ generation < 1 ||
457
+ !state)
458
+ return undefined;
459
+ try {
460
+ const identity = executorIdentity(canonicalTaskName, generation);
461
+ return identity.canonicalPosition === canonicalPosition && identity.taskName === taskName
462
+ ? { canonicalPosition, canonicalTaskName, taskName, taskPath, threadId, generation, state }
463
+ : undefined;
464
+ }
465
+ catch {
466
+ return undefined;
467
+ }
468
+ }
399
469
  export function classifyExecutorOutcome(input) {
400
470
  if (input.objectiveComplete)
401
- return { outcome: 'objective_complete' };
402
- if (input.reportedOutcome === 'cancelled' || input.reportedOutcome === 'failed')
403
- return { outcome: input.reportedOutcome };
471
+ return { classification: 'done', outcome: 'objective_complete' };
404
472
  const blocking = input.blockingReason && input.resumeCondition
405
473
  ? { reason: input.blockingReason, resumeCondition: input.resumeCondition }
406
474
  : undefined;
407
475
  if (input.reportedOutcome === 'blocked' && validStructuredBlock(blocking))
408
- return { outcome: 'blocked', blocking };
476
+ return { classification: 'blocker', outcome: 'blocked', blocking };
477
+ if (input.active)
478
+ // Fresh activity outranks the previous turn's terminal transport status.
479
+ return { classification: 'ongoing', outcome: 'partial' };
480
+ if (input.reportedOutcome === 'cancelled' || input.reportedOutcome === 'failed')
481
+ return { classification: 'recoverableFailure', outcome: input.reportedOutcome };
409
482
  // A turn ending, a checkpoint, or free-form language about time/context is
410
- // not objective state. Incomplete Org state remains mechanically partial.
411
- return { outcome: 'partial' };
483
+ // not objective state. Incomplete, settled Org state is mechanically
484
+ // recoverable and must return to the same canonical executor.
485
+ return { classification: 'recoverableFailure', outcome: 'partial' };
412
486
  }
413
487
  export function decideSupervisedLifecycle(input) {
414
488
  if (input.explicitlyPausedOrCancelled || executionComplete(input.plan))
@@ -3,8 +3,13 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
+ /** Terminal roster entries remain visible and are not active implementation writers. */
7
+ export function hasActiveL1Writer(snapshot, position) {
8
+ return snapshot.subagents.some((child) => child.canonicalPosition === position &&
9
+ ['working', 'awaitingAgent', 'awaitingHuman'].includes(child.state));
10
+ }
6
11
  /** Expands a host-owned compact boundary signal from authoritative plan state. */
7
- export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
12
+ export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity, publicationReason = null) {
8
13
  if (kind === 'terminalReviewAccepted')
9
14
  return {
10
15
  version: 1,
@@ -12,8 +17,16 @@ export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
12
17
  planIdentity,
13
18
  verdict: 'ACCEPT',
14
19
  };
15
- const selected = plan.steps.find((step) => step.id === plan.currentStepId) ??
16
- [...plan.steps].reverse().find((step) => step.state === 'DONE' || step.state === 'WIP');
20
+ const reviewedId = publicationReason?.match(/^review:([^:]{1,128}):REVIEWED$/)?.[1];
21
+ const selected = kind === 'l1Accepted'
22
+ ? ((reviewedId
23
+ ? plan.steps.find((step) => step.id === reviewedId && step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
24
+ : undefined) ??
25
+ [...plan.steps]
26
+ .reverse()
27
+ .find((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED'))
28
+ : (plan.steps.find((step) => step.id === plan.currentStepId) ??
29
+ [...plan.steps].reverse().find((step) => step.state === 'DONE' || step.state === 'WIP'));
17
30
  if (!selected)
18
31
  return null;
19
32
  const l1Position = `L${plan.steps.indexOf(selected) + 1}`;
@@ -44,25 +57,80 @@ export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
44
57
  };
45
58
  }
46
59
  export function validOrgPlanCheckpoint(input) {
60
+ return validateOrgPlanCheckpoint(input).valid;
61
+ }
62
+ export function validateOrgPlanCheckpoint(input) {
47
63
  const { checkpoint, plan, planIdentity, rootOwned } = input;
48
- if (!rootOwned || checkpoint.planIdentity !== planIdentity)
49
- return false;
64
+ if (!rootOwned)
65
+ return { valid: false, reasonCode: 'rootNotOwner' };
66
+ if (checkpoint.planIdentity !== planIdentity)
67
+ return { valid: false, reasonCode: 'planIdentityMismatch' };
50
68
  if (checkpoint.kind === 'terminalReviewAccepted')
51
- return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED');
69
+ return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
70
+ ? { valid: true }
71
+ : { valid: false, reasonCode: 'terminalReviewIncomplete' };
52
72
  const l1 = plan.steps.find((step) => step.id === checkpoint.l1Id);
53
73
  if (!l1)
54
- return false;
74
+ return { valid: false, reasonCode: 'l1NotFound' };
55
75
  const position = `L${plan.steps.indexOf(l1) + 1}`;
56
76
  if (checkpoint.kind === 'l2Completed') {
57
77
  const l2 = l1.children.find((step) => step.id === checkpoint.l2Id);
58
78
  if (!l2)
59
- return false;
60
- return (checkpoint.position === `${position}.${l1.children.indexOf(l2) + 1}` &&
61
- l2.state === 'DONE' &&
62
- l1.reviewStatus === 'UNREVIEWED');
79
+ return { valid: false, reasonCode: 'l2NotFound' };
80
+ const expectedPosition = `${position}.${l1.children.indexOf(l2) + 1}`;
81
+ if (checkpoint.position !== expectedPosition)
82
+ return {
83
+ valid: false,
84
+ reasonCode: 'checkpointPositionMismatch',
85
+ expected: { position: expectedPosition },
86
+ observed: { position: checkpoint.position },
87
+ };
88
+ if (l2.state !== 'DONE')
89
+ return {
90
+ valid: false,
91
+ reasonCode: 'l2NotDone',
92
+ expected: { l2State: 'DONE' },
93
+ observed: { l2State: l2.state },
94
+ };
95
+ return l1.reviewStatus === 'UNREVIEWED'
96
+ ? { valid: true }
97
+ : { valid: false, reasonCode: 'l1AlreadyReviewed' };
63
98
  }
64
- return (checkpoint.position === position &&
65
- l1.state === 'DONE' &&
66
- l1.reviewStatus === 'REVIEWED' &&
67
- !input.hasActiveL1Writer(position));
99
+ if (checkpoint.position !== position)
100
+ return {
101
+ valid: false,
102
+ reasonCode: 'checkpointPositionMismatch',
103
+ expected: { position },
104
+ observed: { position: checkpoint.position },
105
+ };
106
+ if (l1.state !== 'DONE')
107
+ return {
108
+ valid: false,
109
+ reasonCode: 'l1NotDone',
110
+ expected: { l1State: 'DONE' },
111
+ observed: { l1State: l1.state },
112
+ };
113
+ if (l1.reviewStatus !== 'REVIEWED')
114
+ return {
115
+ valid: false,
116
+ reasonCode: 'l1NotReviewed',
117
+ expected: { reviewStatus: 'REVIEWED' },
118
+ observed: { reviewStatus: l1.reviewStatus ?? null },
119
+ };
120
+ const incompleteChild = l1.children.find((child) => child.state !== 'DONE');
121
+ if (incompleteChild)
122
+ return {
123
+ valid: false,
124
+ reasonCode: 'l1ChildrenIncomplete',
125
+ expected: { childrenDone: true },
126
+ observed: { childrenDone: false, childState: incompleteChild.state },
127
+ };
128
+ if (input.hasActiveL1Writer(position))
129
+ return {
130
+ valid: false,
131
+ reasonCode: 'executorStillActive',
132
+ expected: { executorTerminal: true },
133
+ observed: { executorTerminal: false },
134
+ };
135
+ return { valid: true };
68
136
  }
@@ -8,7 +8,11 @@ export class SupervisedPlanRegistry {
8
8
  plans = new Map();
9
9
  accept(sessionId, update) {
10
10
  if (update.kind === 'updated')
11
- this.plans.set(sessionId, { plan: update.plan, identity: update.identity });
11
+ this.plans.set(sessionId, {
12
+ plan: update.plan,
13
+ identity: update.identity,
14
+ publicationReason: update.reason,
15
+ });
12
16
  }
13
17
  find(sessionId) {
14
18
  return this.plans.get(sessionId)?.plan ?? null;
@@ -16,6 +20,9 @@ export class SupervisedPlanRegistry {
16
20
  identity(sessionId) {
17
21
  return this.plans.get(sessionId)?.identity ?? null;
18
22
  }
23
+ publicationReason(sessionId) {
24
+ return this.plans.get(sessionId)?.publicationReason ?? null;
25
+ }
19
26
  clear(sessionId) {
20
27
  this.plans.delete(sessionId);
21
28
  }
@@ -17,16 +17,22 @@ export class SqlitePendingInteractionStore {
17
17
  constructor(db) {
18
18
  this.db = db;
19
19
  }
20
- add(sessionId, interaction) {
20
+ add(sessionId, interaction, initialResolutionState = 'active') {
21
21
  this.db
22
- .prepare(`INSERT INTO pending_interactions (session_id,request_id,kind,payload_json,turn_id,requested_at)
23
- VALUES (?,?,?,?,?,?)
22
+ .prepare(`INSERT INTO pending_interactions (session_id,request_id,kind,payload_json,turn_id,requested_at,resolution_state)
23
+ VALUES (?,?,?,?,?,?,?)
24
24
  ON CONFLICT(session_id,request_id) DO UPDATE SET
25
25
  kind = excluded.kind, payload_json = excluded.payload_json, turn_id = excluded.turn_id,
26
26
  requested_at = excluded.requested_at, resolved_at = NULL, outcome = NULL, operation_key = NULL,
27
- resolution_state = 'active'
27
+ resolution_state = excluded.resolution_state
28
28
  WHERE pending_interactions.kind != 'orgPlanAttention'`)
29
- .run(sessionId, interaction.requestId, interaction.kind, JSON.stringify(interaction.payload), interaction.turnId ?? null, interaction.requestedAt ?? null);
29
+ .run(sessionId, interaction.requestId, interaction.kind, JSON.stringify(interaction.payload), interaction.turnId ?? null, interaction.requestedAt ?? null, initialResolutionState);
30
+ }
31
+ resolutionState(sessionId, requestId) {
32
+ const row = this.db
33
+ .prepare('SELECT resolution_state FROM pending_interactions WHERE session_id = ? AND request_id = ? AND resolved_at IS NULL')
34
+ .get(sessionId, requestId);
35
+ return row?.resolution_state ?? null;
30
36
  }
31
37
  resolve(sessionId, requestId, resolvedAt, outcome = 'answered') {
32
38
  return (this.db
@@ -56,7 +62,7 @@ export class SqlitePendingInteractionStore {
56
62
  /** Persist the in-flight delivery boundary before the app-server is notified. */
57
63
  beginDelivery(sessionId, requestId, operationKey) {
58
64
  return (this.db
59
- .prepare("UPDATE pending_interactions SET resolution_state = 'delivering' WHERE session_id = ? AND request_id = ? AND operation_key = ? AND resolved_at IS NULL AND resolution_state IN ('active','delivering')")
65
+ .prepare("UPDATE pending_interactions SET resolution_state = 'delivering' WHERE session_id = ? AND request_id = ? AND operation_key = ? AND resolved_at IS NULL AND resolution_state IN ('active','acknowledged','delivering')")
60
66
  .run(sessionId, requestId, operationKey).changes === 1);
61
67
  }
62
68
  /** A local writer outage is retryable only by the already-claimed key. */
@@ -93,13 +93,20 @@ export function parseOrgPlanAttentionResponse(value) {
93
93
  ...(typeof value.guidance === 'string' ? { guidance: value.guidance } : {}),
94
94
  };
95
95
  }
96
- /** The only result accepted by the held app-server dynamic-tool request. */
96
+ /** Legacy human resolution payload used only for pre-acknowledgement held requests. */
97
97
  export function toOrgPlanAttentionToolResponse(response) {
98
98
  return {
99
99
  success: true,
100
100
  contentItems: [{ type: 'inputText', text: JSON.stringify(response) }],
101
101
  };
102
102
  }
103
+ /** Immediate persist-first acknowledgement returned to the dynamic-tool caller. */
104
+ export function toOrgPlanAttentionAcknowledgement(acknowledgement = { accepted: true }) {
105
+ return {
106
+ success: acknowledgement.accepted,
107
+ contentItems: [{ type: 'inputText', text: JSON.stringify(acknowledgement) }],
108
+ };
109
+ }
103
110
  export function isOrgPlanAttentionToolResponse(value) {
104
111
  if (!isRecord(value) || value.success !== true || !Array.isArray(value.contentItems))
105
112
  return false;
@@ -176,13 +176,37 @@ export function parseOrgPlanCheckpoint(value) {
176
176
  };
177
177
  }
178
178
  /** Acknowledgement deliberately contains no plan path, findings, or model text. */
179
- export function toOrgPlanCheckpointToolResponse() {
179
+ export function toOrgPlanCheckpointToolResponse(status, failure) {
180
+ const result = status === 'recorded'
181
+ ? {
182
+ status,
183
+ durable: true,
184
+ next: 'emitBoundaryFinalAndEndTurn',
185
+ allowFurtherTools: false,
186
+ }
187
+ : status === 'alreadyRecorded'
188
+ ? {
189
+ status,
190
+ durable: true,
191
+ next: 'continueFromDurableCheckpoint',
192
+ allowBoundaryFinal: false,
193
+ }
194
+ : {
195
+ status,
196
+ reasonCode: failure?.reasonCode ?? 'checkpointPersistenceFailed',
197
+ durable: false,
198
+ ...(failure?.expected ? { expected: failure.expected } : {}),
199
+ ...(failure?.observed ? { observed: failure.observed } : {}),
200
+ ...(failure?.correlationId ? { correlationId: failure.correlationId } : {}),
201
+ next: 'reconcileCheckpointState',
202
+ allowBoundaryFinal: false,
203
+ };
180
204
  return {
181
- success: true,
205
+ success: status !== 'failed',
182
206
  contentItems: [
183
207
  {
184
208
  type: 'inputText',
185
- text: '{"accepted":true,"next":"emitBoundaryFinalAndEndTurn","allowFurtherTools":false}',
209
+ text: JSON.stringify(result),
186
210
  },
187
211
  ],
188
212
  };
@@ -13,5 +13,7 @@ export const planSignalReasons = [
13
13
  'resync',
14
14
  ];
15
15
  export function isPlanSignalReason(value) {
16
- return typeof value === 'string' && planSignalReasons.includes(value);
16
+ return (typeof value === 'string' &&
17
+ (planSignalReasons.includes(value) ||
18
+ /^review:[A-Za-z0-9._-]{1,128}:REVIEWED$/.test(value)));
17
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",