gestalt-mobile 0.37.0 → 0.37.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.
@@ -149,18 +149,18 @@ export async function composeRelayApp(options) {
149
149
  reconcile: async (sessionId) => {
150
150
  const session = sessions.find(sessionId);
151
151
  if (!session)
152
- return;
152
+ return false;
153
153
  // Kimi sessions own no child-process projection; one detached history
154
154
  // read supplies the whole reconciliation, mirroring the codex tail.
155
155
  if (session.provider === 'kimi') {
156
156
  if (!kimiRuntime)
157
- return;
157
+ return false;
158
158
  const history = await kimiRuntime.readHistory(session);
159
159
  // Forget may commit while the detached history reader is still in
160
160
  // flight. Revalidate ownership before handing its result to the
161
161
  // registry, whose publisher intentionally keeps the journal strict.
162
162
  if (!sessions.find(sessionId))
163
- return;
163
+ return false;
164
164
  const occurredAt = new Date().toISOString();
165
165
  activity.observe({
166
166
  sessionId,
@@ -169,16 +169,22 @@ export async function composeRelayApp(options) {
169
169
  ...(session.threadId ? { threadId: session.threadId } : {}),
170
170
  ...(history.activeTurnId ? { turnId: history.activeTurnId } : {}),
171
171
  });
172
- return;
172
+ return true;
173
173
  }
174
174
  if (!runtime)
175
- return;
175
+ return false;
176
176
  const history = await runtime.readHistory(session);
177
- // Forget may commit while the detached history reader is still in
178
- // flight. Revalidate ownership before handing its result to the
179
- // registry, whose publisher intentionally keeps the journal strict.
177
+ const children = await runtime.listDirectChildren(session);
178
+ const childProcesses = new Map();
179
+ await mapWithConcurrency([...children], 4, async (child) => {
180
+ childProcesses.set(child.id, await runtime.inspectChildProcesses(session, child));
181
+ });
182
+ // The writer read is also asynchronous; it cannot publish after the
183
+ // durable owner has gone away either.
180
184
  if (!sessions.find(sessionId))
181
- return;
185
+ return false;
186
+ // Publish only after the entire authoritative read succeeds. A fresh
187
+ // root with stale child topology is not a valid supervision snapshot.
182
188
  const occurredAt = new Date().toISOString();
183
189
  activity.observe({
184
190
  sessionId,
@@ -187,19 +193,11 @@ export async function composeRelayApp(options) {
187
193
  ...(session.threadId ? { threadId: session.threadId } : {}),
188
194
  ...(history.activeTurnId ? { turnId: history.activeTurnId } : {}),
189
195
  });
190
- const children = await runtime.listDirectChildren(session);
191
- const childProcesses = new Map();
192
- await mapWithConcurrency([...children], 4, async (child) => {
193
- childProcesses.set(child.id, await runtime.inspectChildProcesses(session, child));
194
- });
195
- // The writer read is also asynchronous; it cannot publish after the
196
- // durable owner has gone away either.
197
- if (!sessions.find(sessionId))
198
- return;
199
196
  activity.childrenReconciled(sessionId, occurredAt, children.map((child) => ({
200
197
  ...child,
201
198
  processes: childProcesses.get(child.id) ?? [],
202
199
  })));
200
+ return true;
203
201
  },
204
202
  });
205
203
  const autopilot = new AutopilotCoordinator({
@@ -157,7 +157,9 @@ export class AgentActivityRegistry {
157
157
  const now = this.options.now?.() ?? new Date().toISOString();
158
158
  this.reconciling(sessionId, now);
159
159
  try {
160
- await this.options.reconcile?.(sessionId);
160
+ const observed = await this.options.reconcile?.(sessionId);
161
+ if (observed !== true)
162
+ throw new Error('AGENT_ACTIVITY_RECONCILE_NO_EVIDENCE');
161
163
  if (this.#generation.get(sessionId) === generation)
162
164
  this.reconciled(sessionId, this.options.now?.() ?? now);
163
165
  }
@@ -91,7 +91,7 @@ export function decideAutopilot(input) {
91
91
  return { kind: 'safetyPause', reason: 'actionRateExceeded' };
92
92
  if (!activity || activity.confidence !== 'fresh')
93
93
  return { kind: 'reconcile' };
94
- if (Date.parse(now) - Date.parse(activity.root.lastActivityAt) > policy.staleAfterMs)
94
+ if (Date.parse(now) - Date.parse(activity.root.observedAt) > policy.staleAfterMs)
95
95
  return { kind: 'reconcile' };
96
96
  const disposition = classifyAgentActivity(activity);
97
97
  if (disposition === 'attention')
@@ -417,9 +417,11 @@ export class AutopilotCoordinator {
417
417
  case 'scheduleContinuation':
418
418
  if (session?.activeTurnId || this.deps.pendingInteraction(sessionId))
419
419
  break;
420
+ const plan = this.deps.plan(sessionId)?.plan;
420
421
  if (this.deps.executorController &&
421
- this.deps.plan(sessionId)?.plan &&
422
- this.currentExecutor(sessionId, this.deps.plan(sessionId).plan, prior.consecutiveNoProgress, prior.executor)) {
422
+ plan &&
423
+ !this.requiresRootBoundary(prior, plan) &&
424
+ this.currentExecutor(sessionId, plan, prior.consecutiveNoProgress, prior.executor)) {
423
425
  this.enqueue(sessionId, async () => {
424
426
  await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
425
427
  });
@@ -1012,6 +1014,7 @@ export class AutopilotCoordinator {
1012
1014
  if (this.deps.executorController &&
1013
1015
  retained &&
1014
1016
  !current.executor?.replacement &&
1017
+ !this.requiresRootBoundary(current, retained.plan) &&
1015
1018
  this.currentExecutor(sessionId, retained.plan, current.consecutiveNoProgress, current.executor)) {
1016
1019
  const now = this.deps.now();
1017
1020
  const cancelled = this.cancelScheduledControl(current, now);
@@ -1159,6 +1162,10 @@ export class AutopilotCoordinator {
1159
1162
  !state?.requestedEnabled ||
1160
1163
  this.deps.pendingInteraction(sessionId))
1161
1164
  return false;
1165
+ // Checkpoints are root-owned report boundaries. Once an executor has made
1166
+ // one eligible, never mechanically resume it past the unreported result.
1167
+ if (this.requiresRootBoundary(state, retained.plan))
1168
+ return false;
1162
1169
  const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor);
1163
1170
  const executorChanged = executor ? this.persistExecutor(sessionId, executor) : false;
1164
1171
  if (executor && (await this.reconcileSplitBrain(sessionId, executor)))
@@ -1167,6 +1174,18 @@ export class AutopilotCoordinator {
1167
1174
  this.supersedeExecutorCommands(sessionId, retained.identity, retained.plan, executor);
1168
1175
  if (executorChanged && this.wakeForExecutorChange(sessionId, executor))
1169
1176
  return true;
1177
+ if (executor && this.executorTurnActive(executor, this.deps.activity(sessionId))) {
1178
+ // A malformed root yield must not send a duplicate continuation into a
1179
+ // still-running executor. A valid parked lease already owns its wake;
1180
+ // otherwise retain one bounded reconciliation watchdog as the fallback.
1181
+ const parkedLease = state.supervision?.waitLease;
1182
+ const parkedWakeLive = state.supervision?.outcome === 'parked' &&
1183
+ parkedLease &&
1184
+ this.parkedSubscriptions.get(sessionId) === parkedLease.id;
1185
+ if (!parkedWakeLive)
1186
+ this.armExecutorRefresh(sessionId, this.deps.policy.executorContinuationMaxMs);
1187
+ return true;
1188
+ }
1170
1189
  const decision = decideSupervisedLifecycle({
1171
1190
  plan: retained.plan,
1172
1191
  event,
@@ -1249,6 +1268,21 @@ export class AutopilotCoordinator {
1249
1268
  }
1250
1269
  }
1251
1270
  }
1271
+ requiresRootBoundary(state, plan) {
1272
+ const reportedL2 = new Set(state.checkpoints?.reportedL2Ids ?? []);
1273
+ const reportedL1 = new Set(state.checkpoints?.reportedL1Ids ?? []);
1274
+ const epochs = new Map(state.checkpoints?.completionEpochs?.map((entry) => [entry.target, entry]) ?? []);
1275
+ const epochNeedsReport = (target) => {
1276
+ const epoch = epochs.get(target);
1277
+ return Boolean(epoch?.reopened || epoch?.completed === false);
1278
+ };
1279
+ return plan.steps.some((l1) => l1.children.some((l2) => l2.state === 'DONE' &&
1280
+ (!reportedL2.has(JSON.stringify([l1.id, l2.id])) ||
1281
+ epochNeedsReport(checkpointTarget('l2', l1.id, l2.id)))) ||
1282
+ (l1.state === 'DONE' &&
1283
+ l1.reviewStatus === 'REVIEWED' &&
1284
+ (!reportedL1.has(l1.id) || epochNeedsReport(checkpointTarget('l1', l1.id)))));
1285
+ }
1252
1286
  currentExecutor(sessionId, plan, continuationCount, persisted) {
1253
1287
  const stepIndex = plan.steps.findIndex((step) => step.id === plan.currentStepId || step.state === 'WIP');
1254
1288
  const index = stepIndex >= 0 ? stepIndex : plan.steps.findIndex((step) => step.state !== 'DONE');
@@ -1987,6 +2021,14 @@ export class AutopilotCoordinator {
1987
2021
  child.state === 'awaitingAgent' ||
1988
2022
  child.ownedProcesses?.some((process) => process.state === 'running' || process.state === 'detached-active')));
1989
2023
  }
2024
+ executorTurnActive(executor, activity) {
2025
+ if (activity?.confidence !== 'fresh')
2026
+ return false;
2027
+ const child = activity.subagents.find((candidate) => (candidate.threadId ?? candidate.id) === executor.threadId &&
2028
+ candidate.taskPath === executor.taskPath &&
2029
+ (candidate.continuationGeneration ?? 1) === executor.continuationGeneration);
2030
+ return child?.state === 'working' || child?.state === 'awaitingAgent';
2031
+ }
1990
2032
  /** Builds both GET and pre-commit event payloads from the same prospective facts. */
1991
2033
  snapshotFor(next, prospectiveControl) {
1992
2034
  const session = this.deps.session(next.sessionId);
@@ -2180,7 +2222,7 @@ export class AutopilotCoordinator {
2180
2222
  // yield to. A later activity/watchdog event will evaluate it again.
2181
2223
  const activity = this.deps.activity(sessionId);
2182
2224
  if (activity?.confidence === 'fresh' &&
2183
- Date.parse(this.deps.now()) - Date.parse(activity.root.lastActivityAt) <=
2225
+ Date.parse(this.deps.now()) - Date.parse(activity.root.observedAt) <=
2184
2226
  this.deps.policy.staleAfterMs) {
2185
2227
  this.evaluate(sessionId);
2186
2228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.37.0",
3
+ "version": "0.37.1",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",