gestalt-mobile 0.41.3 → 0.42.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.
package/README.md CHANGED
@@ -343,6 +343,19 @@ durable history, deletes only the exact smoke-created Codex threads through the
343
343
  app-server afterwards, and always removes its temporary state. It reports
344
344
  `SKIP` when the isolated profile is unavailable.
345
345
 
346
+ ## Control-plane traces
347
+
348
+ Export the durable Org Plan, Autopilot, and agent-projection timeline for one relay session without starting the server:
349
+
350
+ ```sh
351
+ gestalt-mobile trace <session-id>
352
+ gestalt-mobile trace <session-id> --json
353
+ ```
354
+
355
+ Use `--cwd <workspace>` to select another workspace state database, or `--data-dir <directory>` when the server uses an explicit data directory. The export excludes chat messages, prompts, model output, and environment values. New checkpoint handoffs share a stable `traceId` across checkpoint persistence, continuation scheduling, control dispatch, turn outcome, and agent projection events. The human format includes bounded diagnoses for missing transitions; JSON is suitable for issue attachments and automated analysis.
356
+
357
+ Browser receipt is intentionally not inferred from server persistence. A trace that contains `agent.activity.updated` proves the backend projection was journaled, while its diagnostic notes that the browser event cursor is still needed to prove delivery.
358
+
346
359
  ## Run from source
347
360
 
348
361
  ```sh
@@ -15,8 +15,11 @@ import { CliUsageError, parseConfig } from './config.js';
15
15
  import { LauncherProfileCatalog } from './platform/catalog/launcher-profile-catalog.js';
16
16
  import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
17
17
  import { loadPwaIcon } from './platform/pwa/load-pwa-icon.js';
18
+ import { exportControlPlaneTrace, formatControlPlaneTrace, } from './features/control-plane-trace/export-trace.js';
19
+ import { relayStatePath } from './platform/persistence/state-path.js';
18
20
  const runFile = promisify(execFile);
19
21
  export const usage = `Usage: gestalt-mobile [options]
22
+ gestalt-mobile trace <session-id> [--json] [--cwd <path>] [--data-dir <path>]
20
23
 
21
24
  Options:
22
25
  --cwd <path> Workspace root (default: current directory)
@@ -125,6 +128,33 @@ export function installShutdownHandlers(app, signalSource = process) {
125
128
  return shutdown;
126
129
  }
127
130
  function parseInvocation(args, cwd) {
131
+ if (args[0] === 'trace') {
132
+ const sessionId = args[1];
133
+ if (!sessionId || sessionId.startsWith('--'))
134
+ throw new CliUsageError('trace requires a session ID');
135
+ let json = false;
136
+ let root = cwd;
137
+ let dataDir;
138
+ for (let index = 2; index < args.length; index += 1) {
139
+ const option = args[index];
140
+ if (option === '--json') {
141
+ if (json)
142
+ throw new CliUsageError('Duplicate option: --json');
143
+ json = true;
144
+ continue;
145
+ }
146
+ if (option !== '--cwd' && option !== '--data-dir')
147
+ throw new CliUsageError(`Unknown trace option: ${option}`);
148
+ const value = args[++index];
149
+ if (!value || value.startsWith('--'))
150
+ throw new CliUsageError(`Missing value for ${option}`);
151
+ if (option === '--cwd')
152
+ root = resolve(cwd, value);
153
+ else
154
+ dataDir = resolve(cwd, value);
155
+ }
156
+ return { command: 'trace', sessionId, json, root, ...(dataDir ? { dataDir } : {}) };
157
+ }
128
158
  if (args.includes('--help')) {
129
159
  if (args.length !== 1)
130
160
  throw new CliUsageError('--help cannot be combined with other arguments');
@@ -187,6 +217,23 @@ export async function runCli(dependencies = {}) {
187
217
  stdout.write(`${await packageVersion(moduleUrl)}\n`);
188
218
  return 0;
189
219
  }
220
+ if (invocation.command === 'trace') {
221
+ const stateHome = dependencies.environment?.XDG_STATE_HOME ??
222
+ process.env.XDG_STATE_HOME ??
223
+ resolve(dependencies.homeDirectory ?? homedir(), '.local/state');
224
+ const databasePath = invocation.dataDir
225
+ ? resolve(invocation.dataDir, 'relay.sqlite')
226
+ : relayStatePath(invocation.root, stateHome);
227
+ try {
228
+ const trace = (dependencies.exportTrace ?? exportControlPlaneTrace)(databasePath, invocation.sessionId);
229
+ stdout.write(invocation.json ? `${JSON.stringify(trace, null, 2)}\n` : formatControlPlaneTrace(trace));
230
+ return 0;
231
+ }
232
+ catch (error) {
233
+ stderr.write(`Unable to export control-plane trace: ${error instanceof Error ? error.message : 'unknown error'}\n`);
234
+ return 1;
235
+ }
236
+ }
190
237
  const skillProfiles = new FilesystemSkillProfileStore(dependencies.homeDirectory ?? homedir());
191
238
  if (invocation.command === 'list')
192
239
  return listSkillProfiles(skillProfiles, stdout, stderr);
@@ -1045,6 +1045,16 @@ describe('production composition', () => {
1045
1045
  await expect
1046
1046
  .poll(async () => (await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json().activeTurnId)
1047
1047
  .toBeNull();
1048
+ await expect
1049
+ .poll(() => {
1050
+ const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
1051
+ const scheduled = database
1052
+ .prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'autopilot.continuation-scheduled'")
1053
+ .get(fixture.sessionId);
1054
+ database.close();
1055
+ return scheduled.count;
1056
+ })
1057
+ .toBeGreaterThan(0);
1048
1058
  const startedTurns = () => fixture.handles
1049
1059
  .flatMap((candidate) => candidate.requests)
1050
1060
  .filter((request) => request.method === 'turn/start').length;
@@ -1611,8 +1621,14 @@ describe('production composition', () => {
1611
1621
  handle.notify(completion);
1612
1622
  handle.notify(completion);
1613
1623
  await vi.waitFor(() => expect(timers.filter((timer) => !timer.cancelled && !timer.fired)).toHaveLength(1));
1614
- await runNextTimer();
1615
- await runNextTimer();
1624
+ for (let attempts = 0; attempts < 3; attempts += 1) {
1625
+ const pending = timers.find((timer) => !timer.cancelled && !timer.fired);
1626
+ if (!pending)
1627
+ break;
1628
+ pending.fired = true;
1629
+ pending.callback();
1630
+ await Promise.resolve();
1631
+ }
1616
1632
  await vi.waitFor(() => expect(fixture.handles
1617
1633
  .flatMap((candidate) => candidate.calls)
1618
1634
  .filter((call) => call === 'turn/start')).toHaveLength(2), { timeout: 3_500 });
@@ -137,7 +137,14 @@ export async function composeRelayApp(options) {
137
137
  };
138
138
  options.onAttentionTransitions?.(attentionTransitions);
139
139
  const activity = new AgentActivityRegistry((snapshot, occurredAt) => {
140
- events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', toAgentActivityDto(snapshot), occurredAt));
140
+ events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', {
141
+ ...toAgentActivityDto(snapshot),
142
+ ...(autopilotStore.find(snapshot.sessionId)?.checkpoints?.activeHandoffId
143
+ ? {
144
+ traceId: autopilotStore.find(snapshot.sessionId).checkpoints.activeHandoffId,
145
+ }
146
+ : {}),
147
+ }, occurredAt));
141
148
  publishSessionStatus(snapshot.sessionId, occurredAt);
142
149
  notifyAutopilotActivity(snapshot.sessionId);
143
150
  }, {
@@ -496,7 +503,13 @@ export async function composeRelayApp(options) {
496
503
  if (!interactions.resolve(sessionId, requestId, occurredAt, 'failed'))
497
504
  return false;
498
505
  autopilot.checkpointHandoffFailed(sessionId, turnId);
499
- events.publish(journal.append(sessionId, 'org-plan.checkpoint-handoff-failed', { requestId, reason: 'checkpointHandoffFailed' }, occurredAt));
506
+ events.publish(journal.append(sessionId, 'org-plan.checkpoint-handoff-failed', {
507
+ requestId,
508
+ reason: 'checkpointHandoffFailed',
509
+ ...(autopilotStore.find(sessionId)?.checkpoints?.activeHandoffId
510
+ ? { traceId: autopilotStore.find(sessionId).checkpoints.activeHandoffId }
511
+ : {}),
512
+ }, occurredAt));
500
513
  publishAttentionSettlement(sessionId, requestId, occurredAt, 'failed');
501
514
  // Neither this timeout nor a failed transport may schedule continuation.
502
515
  // If a successful response is already blocked in the writable stream, drop
@@ -696,9 +709,16 @@ export async function composeRelayApp(options) {
696
709
  const rootCommandId = resolvedOrigin.kind === 'root' ? completedCommandId(notification) : null;
697
710
  if (rootCommandId)
698
711
  autopilot.rootProcessCompleted(sessionId, rootCommandId);
712
+ const refreshChildTopology = notification.method === 'item/completed' &&
713
+ activityFacts.some((fact) => fact.kind === 'collaboration' &&
714
+ (fact.collaborationAction === 'spawn_agent' ||
715
+ fact.collaborationAction === 'resume_agent' ||
716
+ fact.collaborationAction === 'close_agent'));
699
717
  if (!normalized) {
700
718
  for (const activityFact of activityFacts)
701
719
  activity.observe(activityFact);
720
+ if (refreshChildTopology)
721
+ void activity.refresh(sessionId);
702
722
  return;
703
723
  }
704
724
  let completedSession;
@@ -717,6 +737,19 @@ export async function composeRelayApp(options) {
717
737
  // activeTurnId while the completion fact makes the root appear idle.
718
738
  for (const activityFact of activityFacts)
719
739
  activity.observe(activityFact);
740
+ // Re-evaluate once both halves of the safe continuation boundary are
741
+ // observable. turnCompleted() releases the durable checkpoint before
742
+ // these facts project the idle root; its first evaluation can therefore
743
+ // still see the pre-completion activity snapshot. This second evaluation
744
+ // is idempotently fenced by the durable control row.
745
+ if (completedSession)
746
+ autopilot.evaluate(sessionId);
747
+ // Lifecycle notifications are fast transition evidence, while
748
+ // thread/list is the bounded topology authority. Reconcile after the
749
+ // mutation so canonical identity and every retained child reach both
750
+ // the GUI and Autopilot even when the notification is sparse.
751
+ if (refreshChildTopology)
752
+ void activity.refresh(sessionId);
720
753
  events.publish(journal.append(sessionId, normalized.type, normalized.payload, normalized.occurredAt));
721
754
  if (completedSession)
722
755
  events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
@@ -196,6 +196,14 @@ function childOutcome(status, action, previous) {
196
196
  return 'cancelled';
197
197
  if (status === 'completed' || status === 'idle')
198
198
  return 'partial';
199
+ // A follow-up reopens the same physical child. Do not keep presenting its
200
+ // previous boundary completion while its new turn is running.
201
+ if (action === 'resume_agent' ||
202
+ status === 'working' ||
203
+ status === 'active' ||
204
+ status === 'running' ||
205
+ status === 'pendingInit')
206
+ return undefined;
199
207
  return previous;
200
208
  }
201
209
  function childState(status, action, previous = 'working') {
@@ -102,6 +102,10 @@ export class AgentActivityRegistry {
102
102
  kind: 'collaboration',
103
103
  childId: child.id,
104
104
  childStatus: 'notLoaded',
105
+ // The bounded roster/process read is authoritative as a unit. If a
106
+ // child disappeared, its previous runtime-local process projection
107
+ // cannot remain eligible as live work after this reconciliation.
108
+ childOwnedProcesses: [],
105
109
  });
106
110
  // A malformed/unknown `thread/list` row is not proof that a child is
107
111
  // healthy. Preserve it as disconnected but keep the aggregate qualified
@@ -49,6 +49,47 @@ export function classifyAgentActivity(activity) {
49
49
  const subagentsSettled = activity.aggregateSubagents === 'idle' || activity.aggregateSubagents === 'blocked';
50
50
  return rootSettled && subagentsSettled ? 'settled' : 'observe';
51
51
  }
52
+ /**
53
+ * Classifies actor state against the plan position that can legally run next.
54
+ * Completed canonical executors remain visible evidence, but their terminal
55
+ * topology cannot indefinitely turn a fresh read into a reconciliation loop.
56
+ */
57
+ function classifySupervisedActivity(activity, plan) {
58
+ const selected = plan.steps.findIndex((step) => step.id === plan.currentStepId || step.state === 'WIP');
59
+ const index = selected >= 0 ? selected : plan.steps.findIndex((step) => step.state !== 'DONE');
60
+ if (index < 0)
61
+ return classifyAgentActivity(activity);
62
+ const position = index + 1;
63
+ const retained = activity.subagents.filter((child) => {
64
+ const match = /^L([1-9]\d*)$/.exec(child.canonicalPosition ?? '');
65
+ if (!match)
66
+ return true;
67
+ const historical = Number(match[1]) < position;
68
+ const supervisedProcess = child.ownedProcesses?.some((process) => process.ownership === 'supervisor' &&
69
+ (process.state === 'running' || process.state === 'detached-active'));
70
+ return !historical || supervisedProcess;
71
+ });
72
+ // An aggregate-only observation cannot prove which child is historical.
73
+ if (retained.length === activity.subagents.length)
74
+ return classifyAgentActivity(activity);
75
+ if (activity.root.state === 'awaitingHuman' ||
76
+ retained.some((child) => child.state === 'awaitingHuman'))
77
+ return 'attention';
78
+ if (activity.root.state === 'working' ||
79
+ retained.some((child) => child.state === 'working' ||
80
+ child.state === 'awaitingAgent' ||
81
+ child.ownedProcesses?.some((process) => (child.state !== 'disconnected' || process.ownership === 'supervisor') &&
82
+ (process.state === 'running' || process.state === 'detached-active'))))
83
+ return 'active';
84
+ if (activity.root.state === 'disconnected' ||
85
+ retained.some((child) => child.state === 'disconnected'))
86
+ return 'reconcile';
87
+ const rootSettled = activity.root.state === 'idle' ||
88
+ activity.root.state === 'blocked' ||
89
+ activity.root.state === 'awaitingAgent';
90
+ const childrenSettled = retained.every((child) => child.state === 'idle' || child.state === 'blocked');
91
+ return rootSettled && childrenSettled ? 'settled' : 'observe';
92
+ }
52
93
  /** A deliberately pure, exhaustive safety gate. Adapters may only enact this result. */
53
94
  export function decideAutopilot(input) {
54
95
  const { state, plan, activity, hasPendingInteraction, now, policy } = input;
@@ -93,7 +134,7 @@ export function decideAutopilot(input) {
93
134
  return { kind: 'reconcile' };
94
135
  if (Date.parse(now) - Date.parse(activity.root.observedAt) > policy.staleAfterMs)
95
136
  return { kind: 'reconcile' };
96
- const disposition = classifyAgentActivity(activity);
137
+ const disposition = classifySupervisedActivity(activity, plan);
97
138
  if (disposition === 'attention')
98
139
  return { kind: 'observe' };
99
140
  if (disposition === 'reconcile')
@@ -525,7 +525,12 @@ export class AutopilotCoordinator {
525
525
  {
526
526
  sessionId,
527
527
  type: 'autopilot.continuation-scheduled',
528
- payload: { controlId: scheduledControlId },
528
+ payload: {
529
+ controlId: scheduledControlId,
530
+ ...(prior.checkpoints?.activeHandoffId
531
+ ? { traceId: prior.checkpoints.activeHandoffId }
532
+ : {}),
533
+ },
529
534
  occurredAt: now,
530
535
  },
531
536
  ]);
@@ -639,7 +644,12 @@ export class AutopilotCoordinator {
639
644
  : pendingKind === 'terminalReviewAccepted'
640
645
  ? 'org-plan.terminal-review-reported'
641
646
  : 'org-plan.milestone-reported',
642
- payload: { turnId: state.checkpoints.pendingTurnId },
647
+ payload: {
648
+ turnId: state.checkpoints.pendingTurnId,
649
+ ...(state.checkpoints.activeHandoffId
650
+ ? { traceId: state.checkpoints.activeHandoffId }
651
+ : {}),
652
+ },
643
653
  occurredAt,
644
654
  },
645
655
  ]);
@@ -700,6 +710,7 @@ export class AutopilotCoordinator {
700
710
  ...(previous?.completionEpochs ?? []).filter((entry) => entry.target !== target),
701
711
  { target, epoch, reopened: false, completed: true },
702
712
  ];
713
+ const handoffId = `handoff-${key.slice(0, 24)}`;
703
714
  const checkpoints = {
704
715
  protocolVersion: 1,
705
716
  planIdentity: retained.identity,
@@ -710,6 +721,7 @@ export class AutopilotCoordinator {
710
721
  : reportedL1Ids,
711
722
  acceptedKeys: boundedUnique([...acceptedKeys, key], 768),
712
723
  pendingTurnId: turnId,
724
+ activeHandoffId: handoffId,
713
725
  pendingKind: checkpoint.kind,
714
726
  checkpointHandoffFailed: false,
715
727
  terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
@@ -746,10 +758,16 @@ export class AutopilotCoordinator {
746
758
  l2Id: checkpoint.l2Id,
747
759
  position: checkpoint.position,
748
760
  turnId,
761
+ traceId: handoffId,
749
762
  }
750
763
  : checkpoint.kind === 'l1Accepted'
751
- ? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
752
- : { turnId },
764
+ ? {
765
+ l1Id: checkpoint.l1Id,
766
+ position: checkpoint.position,
767
+ turnId,
768
+ traceId: handoffId,
769
+ }
770
+ : { turnId, traceId: handoffId },
753
771
  occurredAt,
754
772
  },
755
773
  ]);
@@ -1030,7 +1048,17 @@ export class AutopilotCoordinator {
1030
1048
  const issuedControl = { ...priorControl, status: 'issued', updatedAt: now };
1031
1049
  const events = [
1032
1050
  ...this.snapshotEvents(next, issuedControl),
1033
- { sessionId, type: 'autopilot.control-issued', payload: { controlId }, occurredAt: now },
1051
+ {
1052
+ sessionId,
1053
+ type: 'autopilot.control-issued',
1054
+ payload: {
1055
+ controlId,
1056
+ ...(prior.checkpoints?.activeHandoffId
1057
+ ? { traceId: prior.checkpoints.activeHandoffId }
1058
+ : {}),
1059
+ },
1060
+ occurredAt: now,
1061
+ },
1034
1062
  ];
1035
1063
  const control = this.deps.store.claimControlIssued
1036
1064
  ? this.deps.store.claimControlIssued(sessionId, controlId, now, next, events)
@@ -2140,8 +2168,19 @@ export class AutopilotCoordinator {
2140
2168
  sessionId,
2141
2169
  type: eventType,
2142
2170
  payload: eventType === 'autopilot.turn-failed'
2143
- ? { controlId, code: failureCode }
2144
- : { controlId },
2171
+ ? {
2172
+ controlId,
2173
+ code: failureCode,
2174
+ ...(this.deps.store.find(sessionId)?.checkpoints?.activeHandoffId
2175
+ ? { traceId: this.deps.store.find(sessionId).checkpoints.activeHandoffId }
2176
+ : {}),
2177
+ }
2178
+ : {
2179
+ controlId,
2180
+ ...(this.deps.store.find(sessionId)?.checkpoints?.activeHandoffId
2181
+ ? { traceId: this.deps.store.find(sessionId).checkpoints.activeHandoffId }
2182
+ : {}),
2183
+ },
2145
2184
  occurredAt,
2146
2185
  },
2147
2186
  ]
@@ -390,6 +390,11 @@ function parseCheckpoints(value) {
390
390
  const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
391
391
  if (pendingTurnId === undefined)
392
392
  return undefined;
393
+ const activeHandoffId = value.activeHandoffId === undefined || value.activeHandoffId === null
394
+ ? null
395
+ : boundedText(value.activeHandoffId);
396
+ if (activeHandoffId === undefined)
397
+ return undefined;
393
398
  if (value.checkpointHandoffFailed !== undefined &&
394
399
  typeof value.checkpointHandoffFailed !== 'boolean')
395
400
  return undefined;
@@ -437,6 +442,7 @@ function parseCheckpoints(value) {
437
442
  reportedL1Ids,
438
443
  acceptedKeys,
439
444
  pendingTurnId,
445
+ ...(activeHandoffId ? { activeHandoffId } : {}),
440
446
  pendingKind: pendingKind,
441
447
  ...(value.checkpointHandoffFailed === undefined
442
448
  ? {}
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ import { DatabaseSync } from 'node:sqlite';
7
+ const relevant = /^(?:org-plan\.|autopilot\.|agent\.activity\.|session\.status\.)/;
8
+ function objectPayload(value) {
9
+ try {
10
+ const parsed = JSON.parse(value);
11
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
12
+ ? parsed
13
+ : {};
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ function correlation(payload) {
20
+ for (const key of ['traceId', 'handoffId', 'controlId', 'turnId', 'requestId'])
21
+ if (typeof payload[key] === 'string' && payload[key])
22
+ return String(payload[key]);
23
+ return null;
24
+ }
25
+ function diagnose(events) {
26
+ const diagnoses = [];
27
+ if (!events.length)
28
+ return ['no control-plane events found for this session'];
29
+ const checkpointIndex = events.findLastIndex((event) => event.type.endsWith('-checkpointed'));
30
+ const currentHandoff = checkpointIndex < 0 ? events : events.slice(checkpointIndex);
31
+ const types = new Set(currentHandoff.map((event) => event.type));
32
+ if ([...types].some((type) => type.endsWith('-checkpointed')) &&
33
+ !types.has('autopilot.continuation-scheduled'))
34
+ diagnoses.push('checkpoint persisted, but no continuation was scheduled');
35
+ if (types.has('autopilot.continuation-scheduled') &&
36
+ !types.has('autopilot.control-issued') &&
37
+ !types.has('autopilot.executor-resumed'))
38
+ diagnoses.push('continuation scheduled, but no control or executor dispatch was observed');
39
+ if (types.has('autopilot.control-issued') &&
40
+ !types.has('autopilot.turn-started') &&
41
+ !types.has('autopilot.turn-failed'))
42
+ diagnoses.push('control issued, but no turn acceptance or failure was observed');
43
+ if (types.has('org-plan.checkpoint-handoff-failed'))
44
+ diagnoses.push('checkpoint response handoff failed; inspect the matching trace ID');
45
+ if (currentHandoff.some((event) => event.type === 'agent.activity.updated'))
46
+ diagnoses.push('backend agent projection was recorded; browser delivery still requires the client event cursor');
47
+ return diagnoses;
48
+ }
49
+ export function exportControlPlaneTrace(databasePath, sessionId, generatedAt = new Date().toISOString()) {
50
+ const database = new DatabaseSync(databasePath, { readOnly: true });
51
+ try {
52
+ const rows = database
53
+ .prepare('SELECT sequence,occurred_at,type,payload_json FROM session_events WHERE session_id = ? ORDER BY sequence')
54
+ .all(sessionId);
55
+ const events = rows
56
+ .filter((row) => relevant.test(row.type))
57
+ .map((row) => {
58
+ const payload = objectPayload(row.payload_json);
59
+ return {
60
+ sequence: row.sequence,
61
+ occurredAt: row.occurred_at,
62
+ type: row.type,
63
+ payload,
64
+ traceId: correlation(payload),
65
+ };
66
+ });
67
+ return { schemaVersion: 1, sessionId, generatedAt, events, diagnoses: diagnose(events) };
68
+ }
69
+ finally {
70
+ database.close();
71
+ }
72
+ }
73
+ export function formatControlPlaneTrace(trace) {
74
+ const lines = [
75
+ `Control-plane trace for ${trace.sessionId}`,
76
+ `Generated: ${trace.generatedAt}`,
77
+ '',
78
+ ];
79
+ for (const event of trace.events) {
80
+ const correlationLabel = event.traceId ? ` trace=${event.traceId}` : '';
81
+ lines.push(`${String(event.sequence).padStart(6)} ${event.occurredAt} ${event.type}${correlationLabel}`);
82
+ }
83
+ lines.push('', 'Diagnosis:');
84
+ if (!trace.diagnoses.length)
85
+ lines.push(' No incomplete control-plane transition detected.');
86
+ else
87
+ for (const diagnosis of trace.diagnoses)
88
+ lines.push(` - ${diagnosis}`);
89
+ return `${lines.join('\n')}\n`;
90
+ }
@@ -147,10 +147,12 @@ function collaborationAction(tool) {
147
147
  spawnAgent: 'spawn_agent',
148
148
  sendInput: 'send_input',
149
149
  resumeAgent: 'resume_agent',
150
+ followupTask: 'resume_agent',
150
151
  closeAgent: 'close_agent',
151
152
  spawn_agent: 'spawn_agent',
152
153
  send_input: 'send_input',
153
154
  resume_agent: 'resume_agent',
155
+ followup_task: 'resume_agent',
154
156
  close_agent: 'close_agent',
155
157
  wait: 'wait',
156
158
  }[tool];
@@ -10,81 +10,14 @@ const record = (value) => typeof value === 'object' && value !== null && !Array.
10
10
  export const gestaltOrgPlanCheckpointDynamicTool = {
11
11
  type: 'function',
12
12
  name: GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME,
13
- description: 'Record a validated supervised Org Plan report boundary. This is not review authority and must only be called by the active root after the Org Plan helper has recorded the matching state.',
13
+ description: 'Record a validated supervised Org Plan report boundary. Send only the boundary kind; Mobile derives and validates all details from authoritative control state.',
14
14
  inputSchema: {
15
15
  type: 'object',
16
16
  additionalProperties: false,
17
- required: ['version', 'kind', 'planIdentity'],
17
+ required: ['kind'],
18
18
  properties: {
19
- version: { const: 1 },
20
19
  kind: { type: 'string', enum: ['l2Completed', 'l1Accepted', 'terminalReviewAccepted'] },
21
- planIdentity: { type: 'string', minLength: 1, maxLength: 128 },
22
- l1Id: { type: 'string', minLength: 1, maxLength: 128 },
23
- l2Id: { type: 'string', minLength: 1, maxLength: 128 },
24
- position: { type: 'string', pattern: '^L[1-9][0-9]*(\\.[1-9][0-9]*)?$', maxLength: 32 },
25
- status: { const: 'DONE' },
26
- verdict: { const: 'ACCEPT' },
27
- commit: {
28
- oneOf: [
29
- {
30
- type: 'object',
31
- additionalProperties: false,
32
- required: ['kind', 'subject', 'shortHash'],
33
- properties: {
34
- kind: { const: 'created' },
35
- subject: { type: 'string', minLength: 1, maxLength: 160 },
36
- shortHash: { type: 'string', pattern: '^[0-9a-f]{7,16}$' },
37
- },
38
- },
39
- {
40
- type: 'object',
41
- additionalProperties: false,
42
- required: ['kind'],
43
- properties: { kind: { const: 'notRequired' } },
44
- },
45
- ],
46
- },
47
- findings: { type: 'string', minLength: 1, maxLength: 600 },
48
- changes: { type: 'string', minLength: 1, maxLength: 600 },
49
- files: { type: 'string', minLength: 1, maxLength: 600 },
50
- tests: { type: 'string', minLength: 1, maxLength: 600 },
51
20
  },
52
- oneOf: [
53
- {
54
- properties: { kind: { const: 'l2Completed' } },
55
- required: ['l1Id', 'l2Id', 'position', 'status', 'changes', 'files', 'tests'],
56
- not: {
57
- anyOf: [{ required: ['verdict'] }, { required: ['commit'] }, { required: ['findings'] }],
58
- },
59
- },
60
- {
61
- properties: { kind: { const: 'l1Accepted' } },
62
- required: ['l1Id', 'position', 'verdict', 'commit'],
63
- not: {
64
- anyOf: [
65
- { required: ['l2Id'] },
66
- { required: ['status'] },
67
- { required: ['changes'] },
68
- { required: ['files'] },
69
- ],
70
- },
71
- },
72
- {
73
- properties: { kind: { const: 'terminalReviewAccepted' } },
74
- required: ['verdict'],
75
- not: {
76
- anyOf: [
77
- { required: ['l1Id'] },
78
- { required: ['l2Id'] },
79
- { required: ['position'] },
80
- { required: ['status'] },
81
- { required: ['commit'] },
82
- { required: ['changes'] },
83
- { required: ['files'] },
84
- ],
85
- },
86
- },
87
- ],
88
21
  },
89
22
  };
90
23
  export function parseOrgPlanCheckpoint(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.41.3",
3
+ "version": "0.42.1",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",