gestalt-mobile 0.18.4 → 0.18.5

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.
@@ -90,6 +90,7 @@ export async function composeRelayApp(options) {
90
90
  const planMeasurementHelperPath = options.planMeasurementHelperPath ?? process.env.GESTALT_MOBILE_ORG_PLAN_HELPER;
91
91
  const withPendingInteractions = (session) => (session ? { ...session, pendingInteractions: interactions.list(session.id) } : null);
92
92
  const events = new SessionEventBus();
93
+ let notifyAutopilotSettled = () => undefined;
93
94
  const attentionTransitions = {
94
95
  subscribe: (sessionId, listener) => events.subscribe(sessionId, (event) => {
95
96
  if (event.type !== 'org-plan.attention-required' &&
@@ -110,7 +111,13 @@ export async function composeRelayApp(options) {
110
111
  }),
111
112
  };
112
113
  options.onAttentionTransitions?.(attentionTransitions);
113
- const activity = new AgentActivityRegistry((snapshot, occurredAt) => events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', snapshot, occurredAt)), {
114
+ const activity = new AgentActivityRegistry((snapshot, occurredAt) => {
115
+ events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', snapshot, occurredAt));
116
+ if (snapshot.confidence === 'fresh' &&
117
+ ['idle', 'blocked'].includes(snapshot.root.state) &&
118
+ ['idle', 'blocked'].includes(snapshot.aggregateSubagents))
119
+ notifyAutopilotSettled(snapshot.sessionId);
120
+ }, {
114
121
  // Evidence arms one bounded reconciliation; healthy sessions are never polled.
115
122
  schedule: options.activitySchedule ??
116
123
  ((callback, delayMs) => {
@@ -209,6 +216,7 @@ export async function composeRelayApp(options) {
209
216
  events.publish(journal.append(sessionId, type, payload, occurredAt, outboxId));
210
217
  },
211
218
  });
219
+ notifyAutopilotSettled = (sessionId) => autopilot.activitySettled(sessionId);
212
220
  options.onAutopilotCoordinator?.(autopilot);
213
221
  const workspaces = new FilesystemWorkspaceCatalog(root);
214
222
  const models = new CodexModelCatalog(root, options.launchAppServer ?? launchCodexAppServer);
@@ -38,7 +38,9 @@ export function decideAutopilot(input) {
38
38
  return { kind: 'reconcile' };
39
39
  if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
40
40
  return { kind: 'reconcile' };
41
- if (activity.root.state !== 'idle' || activity.aggregateSubagents !== 'idle')
41
+ const rootSettled = activity.root.state === 'idle' || activity.root.state === 'blocked';
42
+ const subagentsSettled = activity.aggregateSubagents === 'idle' || activity.aggregateSubagents === 'blocked';
43
+ if (!rootSettled || !subagentsSettled)
42
44
  return activity.aggregateSubagents === 'disconnected'
43
45
  ? { kind: 'reconcile' }
44
46
  : { kind: 'observe' };
@@ -145,20 +145,8 @@ export class AutopilotCoordinator {
145
145
  const prior = this.deps.store.find(sessionId);
146
146
  if (!prior)
147
147
  return this.snapshot(sessionId);
148
- const plan = this.deps.plan(sessionId);
149
148
  const session = this.deps.session(sessionId);
150
- const decision = decideAutopilot({
151
- state: prior,
152
- plan: plan?.plan ?? null,
153
- planIdentity: plan?.identity,
154
- planFingerprint: plan ? fingerprint(plan.plan) : null,
155
- activity: this.deps.activity(sessionId),
156
- hasPendingInteraction: this.deps.pendingInteraction(sessionId),
157
- hasActiveAttention: prior.state === 'attentionRequired',
158
- lastTurnOutcome: this.lastTurnOutcome(sessionId, prior.lastControlId),
159
- now: this.deps.now(),
160
- policy: this.deps.policy,
161
- });
149
+ const decision = this.decision(sessionId, prior);
162
150
  const now = this.deps.now();
163
151
  this.deps.diagnostic?.(sessionId, decision.kind);
164
152
  let next = prior;
@@ -172,6 +160,21 @@ export class AutopilotCoordinator {
172
160
  case 'scheduleContinuation':
173
161
  if (session?.activeTurnId || this.deps.pendingInteraction(sessionId))
174
162
  break;
163
+ const existingControl = prior.lastControlId
164
+ ? this.deps.store.findControl(sessionId, prior.lastControlId)
165
+ : null;
166
+ if (existingControl?.status === 'scheduled') {
167
+ const evaluationAt = prior.nextEvaluationAt ?? decision.at;
168
+ if (prior.state !== 'backoff' || prior.nextEvaluationAt !== evaluationAt)
169
+ this.persist({
170
+ ...prior,
171
+ state: 'backoff',
172
+ nextEvaluationAt: evaluationAt,
173
+ updatedAt: now,
174
+ });
175
+ this.arm(sessionId, prior.generation, evaluationAt);
176
+ break;
177
+ }
175
178
  const scheduledControlId = this.deps.nextControlId(sessionId, prior.generation);
176
179
  next = {
177
180
  ...prior,
@@ -272,9 +275,18 @@ export class AutopilotCoordinator {
272
275
  this.evaluate(sessionId);
273
276
  }
274
277
  turnCompleted(sessionId) {
278
+ this.activitySettled(sessionId);
279
+ }
280
+ activitySettled(sessionId) {
281
+ const prior = this.deps.store.find(sessionId);
282
+ if (!prior?.requestedEnabled || prior.state !== 'monitoring')
283
+ return;
275
284
  this.completionTimers.get(sessionId)?.();
276
285
  this.completionTimers.set(sessionId, this.deps.schedule(() => {
277
286
  this.completionTimers.delete(sessionId);
287
+ const current = this.deps.store.find(sessionId);
288
+ if (!current?.requestedEnabled || current.state !== 'monitoring')
289
+ return;
278
290
  this.evaluate(sessionId);
279
291
  }, this.deps.policy.quiescenceMs));
280
292
  }
@@ -339,13 +351,21 @@ export class AutopilotCoordinator {
339
351
  async fire(sessionId, generation) {
340
352
  this.timers.delete(sessionId);
341
353
  const current = this.deps.store.find(sessionId);
354
+ if (!current || current.generation !== generation || !current.requestedEnabled)
355
+ return;
342
356
  const session = this.deps.session(sessionId);
343
- if (!current ||
344
- current.generation !== generation ||
345
- !current.requestedEnabled ||
346
- session?.activeTurnId ||
347
- this.deps.pendingInteraction(sessionId))
357
+ if (session?.activeTurnId ||
358
+ this.deps.pendingInteraction(sessionId) ||
359
+ this.decision(sessionId, current).kind !== 'scheduleContinuation') {
360
+ this.persist({
361
+ ...current,
362
+ state: 'monitoring',
363
+ nextEvaluationAt: null,
364
+ updatedAt: this.deps.now(),
365
+ });
366
+ this.evaluate(sessionId);
348
367
  return;
368
+ }
349
369
  const id = current.lastControlId;
350
370
  if (!id)
351
371
  return;
@@ -481,6 +501,21 @@ export class AutopilotCoordinator {
481
501
  return 'completed';
482
502
  return 'unknown';
483
503
  }
504
+ decision(sessionId, state) {
505
+ const plan = this.deps.plan(sessionId);
506
+ return decideAutopilot({
507
+ state,
508
+ plan: plan?.plan ?? null,
509
+ planIdentity: plan?.identity,
510
+ planFingerprint: plan ? fingerprint(plan.plan) : null,
511
+ activity: this.deps.activity(sessionId),
512
+ hasPendingInteraction: this.deps.pendingInteraction(sessionId),
513
+ hasActiveAttention: state.state === 'attentionRequired',
514
+ lastTurnOutcome: this.lastTurnOutcome(sessionId, state.lastControlId),
515
+ now: this.deps.now(),
516
+ policy: this.deps.policy,
517
+ });
518
+ }
484
519
  async reconcile(sessionId, generation) {
485
520
  try {
486
521
  const result = await this.deps.reconcile(sessionId);
@@ -20,6 +20,14 @@ export const orgPlanAttentionResumeConditions = [
20
20
  'permissionGranted',
21
21
  'externalStateChanged',
22
22
  ];
23
+ export const orgPlanAttentionResumeConditionByReason = {
24
+ planChange: 'planRevision',
25
+ hardBlock: 'externalStateChanged',
26
+ missingDependency: 'dependencyInstalled',
27
+ permissionRequired: 'permissionGranted',
28
+ externalState: 'externalStateChanged',
29
+ materialAmbiguity: 'userGuidance',
30
+ };
23
31
  export const gestaltOrgPlanAttentionDynamicTool = {
24
32
  type: 'function',
25
33
  name: GESTALT_ORG_PLAN_ATTENTION_TOOL_NAME,
@@ -34,6 +42,13 @@ export const gestaltOrgPlanAttentionDynamicTool = {
34
42
  requestedAction: { type: 'string', minLength: 1, maxLength: 600 },
35
43
  resumeCondition: { type: 'string', enum: orgPlanAttentionResumeConditions },
36
44
  },
45
+ oneOf: orgPlanAttentionReasons.map((reason) => ({
46
+ properties: {
47
+ reason: { const: reason },
48
+ resumeCondition: { const: orgPlanAttentionResumeConditionByReason[reason] },
49
+ },
50
+ required: ['reason', 'resumeCondition'],
51
+ })),
37
52
  },
38
53
  };
39
54
  export function parseOrgPlanAttention(value) {
@@ -42,7 +57,8 @@ export function parseOrgPlanAttention(value) {
42
57
  if (!isReason(value.reason) ||
43
58
  !isBoundedText(value.summary, 600) ||
44
59
  !isBoundedText(value.requestedAction, 600) ||
45
- !isResumeCondition(value.resumeCondition))
60
+ !isResumeCondition(value.resumeCondition) ||
61
+ orgPlanAttentionResumeConditionByReason[value.reason] !== value.resumeCondition)
46
62
  return null;
47
63
  return {
48
64
  reason: value.reason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",