agent-scenario-loop 0.1.2 → 0.1.4

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.
Files changed (87) hide show
  1. package/README.md +9 -9
  2. package/app/profile-session.ts +352 -12
  3. package/dist/core/agent-summary.d.ts +3 -2
  4. package/dist/core/agent-summary.js +44 -2
  5. package/dist/core/artifact-contract.d.ts +28 -8
  6. package/dist/core/artifact-contract.js +676 -26
  7. package/dist/core/comparison.d.ts +57 -3
  8. package/dist/core/comparison.js +113 -1
  9. package/dist/core/planner.d.ts +32 -1
  10. package/dist/core/planner.js +144 -0
  11. package/dist/core/run-index.d.ts +4 -0
  12. package/dist/core/run-index.js +55 -1
  13. package/dist/core/schema-validator.d.ts +2 -0
  14. package/dist/core/schema-validator.js +2 -0
  15. package/dist/runner/android-adb-driver.d.ts +7 -2
  16. package/dist/runner/android-adb-driver.js +7 -1
  17. package/dist/runner/android-adb.d.ts +40 -5
  18. package/dist/runner/android-adb.js +1046 -664
  19. package/dist/runner/compare-latest.d.ts +8 -4
  20. package/dist/runner/compare-latest.js +24 -5
  21. package/dist/runner/example-android-live.d.ts +10 -1
  22. package/dist/runner/example-android-live.js +55 -0
  23. package/dist/runner/example-ios-live.d.ts +10 -1
  24. package/dist/runner/example-ios-live.js +55 -0
  25. package/dist/runner/ios-simctl.d.ts +6 -0
  26. package/dist/runner/ios-simctl.js +7 -0
  27. package/dist/runner/live-comparison.d.ts +2 -2
  28. package/dist/runner/live-comparison.js +2 -1
  29. package/dist/runner/live-proof-summary.d.ts +5 -4
  30. package/dist/runner/live-proof-summary.js +12 -2
  31. package/dist/runner/live-proof.d.ts +3 -2
  32. package/dist/runner/live-proof.js +9 -2
  33. package/dist/runner/profile-android.d.ts +16 -1
  34. package/dist/runner/profile-android.js +364 -26
  35. package/dist/runner/profile-ios.d.ts +13 -2
  36. package/dist/runner/profile-ios.js +341 -19
  37. package/dist/runner/profile-mobile.d.ts +39 -3
  38. package/dist/runner/profile-mobile.js +1054 -42
  39. package/dist/runner/validate-project.js +3 -0
  40. package/dist/scripts/consumer-rehearsal.d.ts +119 -0
  41. package/dist/scripts/consumer-rehearsal.js +757 -0
  42. package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
  43. package/dist/scripts/downstream-local-package-gate.js +264 -0
  44. package/dist/scripts/package-smoke.d.ts +96 -0
  45. package/dist/scripts/package-smoke.js +2282 -0
  46. package/dist/scripts/release-readiness.d.ts +2 -0
  47. package/dist/scripts/release-readiness.js +520 -0
  48. package/docs/adapters.md +7 -1
  49. package/docs/api.md +2 -2
  50. package/docs/architecture.md +90 -0
  51. package/docs/authoring.md +39 -3
  52. package/docs/concepts.md +3 -24
  53. package/docs/consumer-rehearsal.md +31 -1
  54. package/docs/contracts.md +45 -101
  55. package/docs/external-adapter-protocol.md +219 -0
  56. package/docs/live-proofs.md +86 -3
  57. package/docs/principles.md +9 -15
  58. package/examples/mobile-app/README.md +12 -0
  59. package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
  60. package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
  61. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
  62. package/examples/runners/README.md +4 -3
  63. package/examples/runners/adb-android.json +1 -0
  64. package/examples/runners/agent-device-android.json +1 -0
  65. package/examples/runners/agent-device-ios.json +1 -0
  66. package/examples/runners/argent-android.json +1 -0
  67. package/examples/runners/argent-ios.json +1 -0
  68. package/examples/runners/axe-accessibility-provider.json +2 -2
  69. package/examples/runners/script-accessibility-provider.json +2 -2
  70. package/examples/runners/script-memory-provider.json +2 -2
  71. package/examples/runners/script-network-provider.json +2 -2
  72. package/examples/runners/script-profiler-provider.json +2 -2
  73. package/examples/runners/xcodebuildmcp-ios.json +1 -0
  74. package/package.json +12 -3
  75. package/schemas/causal-run.schema.json +85 -2
  76. package/schemas/comparison.schema.json +130 -2
  77. package/schemas/external-adapter-message.schema.json +693 -0
  78. package/schemas/health.schema.json +72 -0
  79. package/schemas/live-proof-set.schema.json +1 -1
  80. package/schemas/live-proof.schema.json +14 -6
  81. package/schemas/manifest.schema.json +515 -4
  82. package/schemas/profiler.schema.json +243 -0
  83. package/schemas/runner-capabilities.schema.json +28 -2
  84. package/schemas/scenario.schema.json +34 -2
  85. package/templates/evidence-provider.json +3 -3
  86. package/templates/primary-runner.json +1 -0
  87. package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
package/README.md CHANGED
@@ -14,12 +14,14 @@ Execution tools can change. The scenario and evidence contract should not.
14
14
  | --- | --- |
15
15
  | Understand the idea in plain language | [Concepts](docs/concepts.md) |
16
16
  | Understand the project doctrine | [Principles](docs/principles.md) |
17
- | Write your first scenario | [Scenario Authoring](docs/authoring.md) |
18
- | Rehearse adoption in an existing app | [Consumer App Rehearsal](docs/consumer-rehearsal.md) |
17
+ | Understand why ASL is a protocol, not a TypeScript-only library | [Architecture](docs/architecture.md) |
18
+ | Implement or evaluate an out-of-process adapter in any language | [External Adapter Protocol](docs/external-adapter-protocol.md) |
19
19
  | Inspect artifacts, schemas, and supported surfaces | [Contracts](docs/contracts.md) |
20
- | Use the package from code | [Public API](docs/api.md) |
20
+ | Write your first scenario | [Scenario Authoring](docs/authoring.md) |
21
21
  | Add a runner or evidence provider | [Adapter Onboarding](docs/adapters.md) |
22
+ | Rehearse adoption in an existing app | [Consumer App Rehearsal](docs/consumer-rehearsal.md) |
22
23
  | Run fixture, Android, or iOS proofs | [Live Proofs](docs/live-proofs.md) |
24
+ | Use the package from code | [Public API](docs/api.md) |
23
25
  | Inspect runner behavior and limits | [Runner docs](runner/README.md) |
24
26
  | Explore the neutral dogfood app | [examples/mobile-app](examples/mobile-app/README.md) |
25
27
  | See runner and provider fixtures | [examples/runners](examples/runners/README.md) |
@@ -73,12 +75,6 @@ No simulator or device available yet? Run the fixture loop:
73
75
  pnpm demo:loop -- --out artifacts/demo-loop
74
76
  ```
75
77
 
76
- Read next:
77
-
78
- - [Scenario Authoring](docs/authoring.md) for scenario shape and truth events
79
- - [Consumer App Rehearsal](docs/consumer-rehearsal.md) for adoption in an existing app
80
- - [Live Proofs](docs/live-proofs.md) for Android, iOS, comparison, and release-proof paths
81
-
82
78
  ## Package Surface
83
79
 
84
80
  The root package exports stable core contracts:
@@ -123,3 +119,7 @@ pnpm release:check
123
119
  ```
124
120
 
125
121
  The package should remain product-neutral. Product-specific selectors, routes, auth assumptions, accounts, and scenario data belong in the consuming app, not in this repository.
122
+
123
+ ## Read next
124
+
125
+ - [Concepts](docs/concepts.md) for the plain-language model
@@ -12,11 +12,17 @@ export type ProfileSessionState = {
12
12
 
13
13
  export type ProfileSessionCommand = {
14
14
  id: string;
15
+ commandId?: string;
15
16
  scenario?: string;
16
17
  runId?: string;
17
18
  command: string;
19
+ queueId?: string;
20
+ sequence?: number;
18
21
  source?: 'deeplink' | 'storage';
19
22
  timestamp: number;
23
+ waitForMilestone?: string;
24
+ waitMs?: number;
25
+ waitTimeoutMs?: number;
20
26
  };
21
27
 
22
28
  export type ProfileSignalKind = 'js' | 'memory' | 'network';
@@ -62,13 +68,35 @@ type StoredProfileSessionEntry = {
62
68
  scenario: string;
63
69
  runId: string;
64
70
  timestamp: number;
71
+ atMs?: number;
65
72
  startedAt?: number;
66
73
  stoppedAt?: number;
67
74
  command?: string;
75
+ commandId?: string;
68
76
  id?: string;
77
+ queueId?: string;
78
+ reason?: string;
79
+ result?: string;
80
+ sequence?: number;
81
+ source?: 'deeplink' | 'storage';
82
+ status?: 'received' | 'queued' | 'delivered' | 'completed' | 'skipped';
83
+ waitForMilestone?: string;
84
+ waitMs?: number;
85
+ waitTimeoutMs?: number;
69
86
  };
70
87
 
71
88
  type StoredProfileSignals = Record<ProfileSignalKind, Record<string, unknown>>;
89
+ type ProfileCommandMilestoneGate = {
90
+ commandId?: string;
91
+ id: string;
92
+ milestone: string;
93
+ queueId?: string;
94
+ runId?: string;
95
+ scenario?: string;
96
+ sequence?: number;
97
+ timeoutId?: ReturnType<typeof setTimeout>;
98
+ waitMs?: number;
99
+ };
72
100
 
73
101
  const INITIAL_STATE: ProfileSessionState = {
74
102
  active: false,
@@ -97,9 +125,15 @@ const listeners = new Set<() => void>();
97
125
  const profileCommandListeners = new Set<(command: ProfileSessionCommand) => void>();
98
126
  const profileCommandTargetHandlers = new Map<string, () => void>();
99
127
  const pendingProfileCommands: ProfileSessionCommand[] = [];
128
+ const sequencedProfileCommands: ProfileSessionCommand[] = [];
129
+ const observedProfileEvents: StoredProfileEvent[] = [];
100
130
  const processedProfileCommandIds = new Set<string>();
101
131
  let lastProfileCommandSignature: string | null = null;
102
132
  let lastProfileCommandTimestamp = 0;
133
+ let profileCommandMilestoneGate: ProfileCommandMilestoneGate | null = null;
134
+ let profileCommandProcessingScheduled = false;
135
+ let profileCommandProcessingTimeoutId: ReturnType<typeof setTimeout> | null = null;
136
+ let profileCommandProcessingAvailableAt = 0;
103
137
 
104
138
  function writeProfileLog(line: string) {
105
139
  if (Platform.OS === 'ios') {
@@ -191,6 +225,7 @@ function appendStoredProfileEvent(event: StoredProfileEvent) {
191
225
  }
192
226
 
193
227
  function resetStoredProfileArtifacts() {
228
+ observedProfileEvents.length = 0;
194
229
  queueProfileStorageMutation(async () => {
195
230
  await Promise.all([
196
231
  AsyncStorage.removeItem(PROFILE_EVENT_STORAGE_KEY),
@@ -202,6 +237,9 @@ function resetStoredProfileArtifacts() {
202
237
 
203
238
  function clearPendingProfileCommands() {
204
239
  pendingProfileCommands.length = 0;
240
+ sequencedProfileCommands.length = 0;
241
+ clearProfileCommandMilestoneGate();
242
+ clearProfileCommandProcessingSchedule();
205
243
  queueProfileStorageMutation(async () => {
206
244
  await AsyncStorage.removeItem(PROFILE_COMMAND_STORAGE_KEY);
207
245
  });
@@ -285,7 +323,21 @@ export function isProfileSessionFresh(
285
323
  }
286
324
 
287
325
  function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<string, unknown>) {
288
- writeProfileLog(buildLogLine('profile-session', { kind, ...payload }));
326
+ const timestamp = Date.now();
327
+ const sessionStartedAt = readProfileSessionStartedAt(profileSessionState);
328
+ const atMs =
329
+ typeof payload.atMs === 'number' && Number.isFinite(payload.atMs)
330
+ ? payload.atMs
331
+ : sessionStartedAt !== null
332
+ ? Math.max(0, timestamp - sessionStartedAt)
333
+ : undefined;
334
+ const logPayload = {
335
+ kind,
336
+ ...payload,
337
+ timestamp,
338
+ ...(atMs !== undefined ? { atMs } : {}),
339
+ };
340
+ writeProfileLog(buildLogLine('profile-session', logPayload));
289
341
 
290
342
  const scenario = typeof payload.scenario === 'string' ? payload.scenario : null;
291
343
  const runId = typeof payload.runId === 'string' ? payload.runId : null;
@@ -293,12 +345,12 @@ function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<s
293
345
  return;
294
346
  }
295
347
 
296
- const timestamp = Date.now();
297
348
  const entry: StoredProfileSessionEntry = {
298
349
  kind,
299
350
  scenario,
300
351
  runId,
301
352
  timestamp,
353
+ ...(atMs !== undefined ? { atMs } : {}),
302
354
  };
303
355
 
304
356
  if (kind === 'start' && typeof payload.startedAt === 'number') {
@@ -316,6 +368,44 @@ function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<s
316
368
  if (typeof payload.id === 'string') {
317
369
  entry.id = payload.id;
318
370
  }
371
+ if (typeof payload.commandId === 'string') {
372
+ entry.commandId = payload.commandId;
373
+ } else if (typeof payload.id === 'string') {
374
+ entry.commandId = payload.id;
375
+ }
376
+ if (typeof payload.queueId === 'string') {
377
+ entry.queueId = payload.queueId;
378
+ }
379
+ if (typeof payload.sequence === 'number') {
380
+ entry.sequence = payload.sequence;
381
+ }
382
+ if (payload.source === 'deeplink' || payload.source === 'storage') {
383
+ entry.source = payload.source;
384
+ }
385
+ if (
386
+ payload.status === 'received' ||
387
+ payload.status === 'queued' ||
388
+ payload.status === 'delivered' ||
389
+ payload.status === 'completed' ||
390
+ payload.status === 'skipped'
391
+ ) {
392
+ entry.status = payload.status;
393
+ }
394
+ if (typeof payload.reason === 'string') {
395
+ entry.reason = payload.reason;
396
+ }
397
+ if (typeof payload.result === 'string') {
398
+ entry.result = payload.result;
399
+ }
400
+ if (typeof payload.waitForMilestone === 'string') {
401
+ entry.waitForMilestone = payload.waitForMilestone;
402
+ }
403
+ if (typeof payload.waitMs === 'number') {
404
+ entry.waitMs = payload.waitMs;
405
+ }
406
+ if (typeof payload.waitTimeoutMs === 'number') {
407
+ entry.waitTimeoutMs = payload.waitTimeoutMs;
408
+ }
319
409
  }
320
410
 
321
411
  appendStoredProfileSessionEntry(entry);
@@ -326,6 +416,12 @@ function getProfileSessionRoute(url: string): {
326
416
  scenario?: string;
327
417
  runId?: string;
328
418
  command?: string;
419
+ commandId?: string;
420
+ queueId?: string;
421
+ sequence?: number;
422
+ waitForMilestone?: string;
423
+ waitMs?: number;
424
+ waitTimeoutMs?: number;
329
425
  } | null {
330
426
  const parsed = ExpoLinking.parse(url);
331
427
  const segments = [parsed.hostname, parsed.path]
@@ -347,8 +443,26 @@ function getProfileSessionRoute(url: string): {
347
443
  typeof parsed.queryParams?.runId === 'string' ? parsed.queryParams.runId : undefined;
348
444
  const command =
349
445
  typeof parsed.queryParams?.command === 'string' ? parsed.queryParams.command : undefined;
350
-
351
- return { action, scenario, runId, command };
446
+ const commandId =
447
+ typeof parsed.queryParams?.commandId === 'string' ? parsed.queryParams.commandId : undefined;
448
+ const sequence =
449
+ typeof parsed.queryParams?.sequence === 'string' && Number.isInteger(Number(parsed.queryParams.sequence))
450
+ ? Number(parsed.queryParams.sequence)
451
+ : undefined;
452
+ const queueId =
453
+ typeof parsed.queryParams?.queueId === 'string' ? parsed.queryParams.queueId : undefined;
454
+ const waitForMilestone =
455
+ typeof parsed.queryParams?.waitForMilestone === 'string' ? parsed.queryParams.waitForMilestone : undefined;
456
+ const waitTimeoutMs =
457
+ typeof parsed.queryParams?.waitTimeoutMs === 'string' && Number.isInteger(Number(parsed.queryParams.waitTimeoutMs))
458
+ ? Number(parsed.queryParams.waitTimeoutMs)
459
+ : undefined;
460
+ const waitMs =
461
+ typeof parsed.queryParams?.waitMs === 'string' && Number.isInteger(Number(parsed.queryParams.waitMs))
462
+ ? Number(parsed.queryParams.waitMs)
463
+ : undefined;
464
+
465
+ return { action, scenario, runId, command, commandId, queueId, sequence, waitForMilestone, waitMs, waitTimeoutMs };
352
466
  }
353
467
 
354
468
  function queuePendingProfileCommand(command: ProfileSessionCommand) {
@@ -440,6 +554,132 @@ function markProfileCommandIdProcessed(command: ProfileSessionCommand) {
440
554
  }
441
555
  }
442
556
 
557
+ function compareProfileCommands(left: ProfileSessionCommand, right: ProfileSessionCommand): number {
558
+ const leftSequence = typeof left.sequence === 'number' ? left.sequence : Number.POSITIVE_INFINITY;
559
+ const rightSequence = typeof right.sequence === 'number' ? right.sequence : Number.POSITIVE_INFINITY;
560
+ if (leftSequence !== rightSequence) {
561
+ return leftSequence - rightSequence;
562
+ }
563
+
564
+ return left.timestamp - right.timestamp;
565
+ }
566
+
567
+ function shouldQueueProfileCommand(command: ProfileSessionCommand): boolean {
568
+ return !hasProcessedProfileCommandId(command) &&
569
+ !sequencedProfileCommands.some((queuedCommand) => queuedCommand.id === command.id);
570
+ }
571
+
572
+ function buildProfileCommandMilestoneGate(command: ProfileSessionCommand): ProfileCommandMilestoneGate | null {
573
+ if (typeof command.waitForMilestone !== 'string' || command.waitForMilestone.length === 0) {
574
+ return null;
575
+ }
576
+
577
+ return {
578
+ id: command.id,
579
+ milestone: command.waitForMilestone,
580
+ ...(typeof command.commandId === 'string' ? { commandId: command.commandId } : {}),
581
+ ...(typeof command.queueId === 'string' ? { queueId: command.queueId } : {}),
582
+ ...(typeof command.runId === 'string' ? { runId: command.runId } : {}),
583
+ ...(typeof command.scenario === 'string' ? { scenario: command.scenario } : {}),
584
+ ...(typeof command.sequence === 'number' ? { sequence: command.sequence } : {}),
585
+ ...(typeof command.waitMs === 'number' && command.waitMs > 0 ? { waitMs: command.waitMs } : {}),
586
+ };
587
+ }
588
+
589
+ function hasObservedProfileCommandMilestone(command: ProfileSessionCommand): boolean {
590
+ if (typeof command.waitForMilestone !== 'string' || command.waitForMilestone.length === 0) {
591
+ return false;
592
+ }
593
+ if (typeof command.sequence === 'number' && command.sequence > 1) {
594
+ return false;
595
+ }
596
+
597
+ return observedProfileEvents.some((eventPayload) => (
598
+ eventPayload.event === command.waitForMilestone &&
599
+ (!command.runId || eventPayload.runId === command.runId) &&
600
+ (!command.scenario || eventPayload.scenario === command.scenario)
601
+ ));
602
+ }
603
+
604
+ function clearProfileCommandMilestoneGate() {
605
+ if (profileCommandMilestoneGate?.timeoutId) {
606
+ clearTimeout(profileCommandMilestoneGate.timeoutId);
607
+ }
608
+ profileCommandMilestoneGate = null;
609
+ }
610
+
611
+ function clearProfileCommandProcessingSchedule() {
612
+ if (profileCommandProcessingTimeoutId) {
613
+ clearTimeout(profileCommandProcessingTimeoutId);
614
+ }
615
+ profileCommandProcessingTimeoutId = null;
616
+ profileCommandProcessingScheduled = false;
617
+ profileCommandProcessingAvailableAt = 0;
618
+ }
619
+
620
+ function startProfileCommandMilestoneTimeout(command: ProfileSessionCommand) {
621
+ if (
622
+ !profileCommandMilestoneGate ||
623
+ typeof command.waitTimeoutMs !== 'number' ||
624
+ command.waitTimeoutMs <= 0
625
+ ) {
626
+ return;
627
+ }
628
+
629
+ profileCommandMilestoneGate.timeoutId = setTimeout(() => {
630
+ if (!profileCommandMilestoneGate || profileCommandMilestoneGate.id !== command.id) {
631
+ return;
632
+ }
633
+
634
+ logProfileSession('command', {
635
+ ...command,
636
+ status: 'skipped',
637
+ reason: 'wait-for-milestone-timeout',
638
+ });
639
+ clearProfileCommandMilestoneGate();
640
+ processSequencedProfileCommands();
641
+ }, command.waitTimeoutMs);
642
+ }
643
+
644
+ function scheduleProfileCommandProcessing(waitMs = 0) {
645
+ const availableAt = waitMs > 0 ? Date.now() + waitMs : 0;
646
+ if (availableAt > profileCommandProcessingAvailableAt) {
647
+ profileCommandProcessingAvailableAt = availableAt;
648
+ }
649
+
650
+ const delayMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
651
+ if (profileCommandProcessingTimeoutId) {
652
+ clearTimeout(profileCommandProcessingTimeoutId);
653
+ profileCommandProcessingTimeoutId = null;
654
+ }
655
+
656
+ profileCommandProcessingScheduled = true;
657
+ const run = () => {
658
+ profileCommandProcessingTimeoutId = null;
659
+ const remainingMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
660
+ if (remainingMs > 0) {
661
+ profileCommandProcessingScheduled = false;
662
+ scheduleProfileCommandProcessing(remainingMs);
663
+ return;
664
+ }
665
+ profileCommandProcessingAvailableAt = 0;
666
+ profileCommandProcessingScheduled = false;
667
+ processSequencedProfileCommands();
668
+ };
669
+
670
+ if (delayMs > 0) {
671
+ profileCommandProcessingTimeoutId = setTimeout(run, delayMs);
672
+ return;
673
+ }
674
+
675
+ if (typeof queueMicrotask === 'function') {
676
+ queueMicrotask(run);
677
+ return;
678
+ }
679
+
680
+ Promise.resolve().then(run);
681
+ }
682
+
443
683
  function notifyProfileCommandListeners(command: ProfileSessionCommand) {
444
684
  const commandTimestamp = Number.isFinite(command.timestamp) ? command.timestamp : Date.now();
445
685
  if (shouldSkipProfileCommandForDuplicateWindow(command, commandTimestamp)) {
@@ -456,17 +696,108 @@ function notifyProfileCommandListeners(command: ProfileSessionCommand) {
456
696
 
457
697
  const targetDispatched = dispatchProfileCommandTarget(command);
458
698
  if (targetDispatched) {
699
+ logProfileSession('command', {
700
+ ...command,
701
+ status: 'completed',
702
+ result: 'target-dispatched',
703
+ });
459
704
  return;
460
705
  }
461
706
 
462
707
  if (profileCommandListeners.size === 0) {
463
708
  queuePendingProfileCommand(command);
709
+ logProfileSession('command', {
710
+ ...command,
711
+ status: 'queued',
712
+ reason: 'no-command-listener',
713
+ });
464
714
  return;
465
715
  }
466
716
 
467
717
  for (const listener of profileCommandListeners) {
468
718
  listener(command);
469
719
  }
720
+ logProfileSession('command', {
721
+ ...command,
722
+ status: 'delivered',
723
+ result: 'listener-notified',
724
+ });
725
+ }
726
+
727
+ function processSequencedProfileCommands() {
728
+ if (profileCommandProcessingScheduled) {
729
+ return;
730
+ }
731
+ const remainingMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
732
+ if (remainingMs > 0) {
733
+ scheduleProfileCommandProcessing(remainingMs);
734
+ return;
735
+ }
736
+ if (profileCommandMilestoneGate) {
737
+ return;
738
+ }
739
+
740
+ while (sequencedProfileCommands.length > 0) {
741
+ const command = sequencedProfileCommands.shift();
742
+ if (!command || hasProcessedProfileCommandId(command)) {
743
+ continue;
744
+ }
745
+
746
+ markProfileCommandIdProcessed(command);
747
+ const nextGate = hasObservedProfileCommandMilestone(command)
748
+ ? null
749
+ : buildProfileCommandMilestoneGate(command);
750
+ profileCommandMilestoneGate = nextGate;
751
+ logProfileSession('command', {
752
+ ...command,
753
+ status: 'received',
754
+ });
755
+ startProfileCommandMilestoneTimeout(command);
756
+ notifyProfileCommandListeners(command);
757
+
758
+ if (profileCommandMilestoneGate) {
759
+ return;
760
+ }
761
+ if (typeof command.waitMs === 'number' && command.waitMs > 0) {
762
+ scheduleProfileCommandProcessing(command.waitMs);
763
+ return;
764
+ }
765
+ }
766
+ }
767
+
768
+ function enqueueSequencedProfileCommands(commands: ProfileSessionCommand[]) {
769
+ const nextCommands = commands.filter(shouldQueueProfileCommand);
770
+ if (nextCommands.length === 0) {
771
+ return;
772
+ }
773
+
774
+ sequencedProfileCommands.push(...nextCommands);
775
+ sequencedProfileCommands.sort(compareProfileCommands);
776
+ processSequencedProfileCommands();
777
+ }
778
+
779
+ function releaseProfileCommandMilestoneGate(eventPayload: StoredProfileEvent) {
780
+ if (!profileCommandMilestoneGate || profileCommandMilestoneGate.milestone !== eventPayload.event) {
781
+ return;
782
+ }
783
+ if (
784
+ profileCommandMilestoneGate.runId &&
785
+ profileCommandMilestoneGate.runId !== eventPayload.runId
786
+ ) {
787
+ return;
788
+ }
789
+ if (
790
+ profileCommandMilestoneGate.scenario &&
791
+ profileCommandMilestoneGate.scenario !== eventPayload.scenario
792
+ ) {
793
+ return;
794
+ }
795
+
796
+ const waitMs = typeof profileCommandMilestoneGate.waitMs === 'number'
797
+ ? profileCommandMilestoneGate.waitMs
798
+ : 0;
799
+ clearProfileCommandMilestoneGate();
800
+ scheduleProfileCommandProcessing(waitMs);
470
801
  }
471
802
 
472
803
  function flushPendingProfileCommands(listener: (command: ProfileSessionCommand) => void) {
@@ -560,11 +891,16 @@ export function applyProfileSessionUrl(url: string | null | undefined): boolean
560
891
  scenario: route.scenario,
561
892
  runId: route.runId,
562
893
  command: route.command,
894
+ ...(route.commandId ? { commandId: route.commandId } : {}),
895
+ ...(route.queueId ? { queueId: route.queueId } : {}),
896
+ ...(typeof route.sequence === 'number' ? { sequence: route.sequence } : {}),
563
897
  source: 'deeplink' as const,
564
898
  timestamp,
899
+ ...(route.waitForMilestone ? { waitForMilestone: route.waitForMilestone } : {}),
900
+ ...(typeof route.waitMs === 'number' ? { waitMs: route.waitMs } : {}),
901
+ ...(typeof route.waitTimeoutMs === 'number' ? { waitTimeoutMs: route.waitTimeoutMs } : {}),
565
902
  };
566
- logProfileSession('command', command);
567
- notifyProfileCommandListeners(command);
903
+ enqueueSequencedProfileCommands([command]);
568
904
  return true;
569
905
  }
570
906
 
@@ -607,7 +943,12 @@ export function emitProfileEvent(event: string, metadata?: ProfileEventMetadata)
607
943
  };
608
944
 
609
945
  writeProfileLog(buildLogLine('profile-event', eventPayload));
946
+ observedProfileEvents.push(eventPayload);
947
+ while (observedProfileEvents.length > MAX_STORED_PROFILE_EVENTS) {
948
+ observedProfileEvents.shift();
949
+ }
610
950
  appendStoredProfileEvent(eventPayload);
951
+ releaseProfileCommandMilestoneGate(eventPayload);
611
952
  }
612
953
 
613
954
  /**
@@ -747,7 +1088,7 @@ export function useProfileSessionBootstrap(): void {
747
1088
  return;
748
1089
  }
749
1090
 
750
- clearPendingProfileCommands();
1091
+ const nextCommands: ProfileSessionCommand[] = [];
751
1092
  for (const command of storedCommands) {
752
1093
  if (
753
1094
  !command ||
@@ -760,8 +1101,7 @@ export function useProfileSessionBootstrap(): void {
760
1101
 
761
1102
  if (
762
1103
  command.scenario !== activeSession.scenario ||
763
- command.runId !== activeSession.runId ||
764
- (typeof activeSession.startedAt === 'number' && command.timestamp < activeSession.startedAt)
1104
+ command.runId !== activeSession.runId
765
1105
  ) {
766
1106
  continue;
767
1107
  }
@@ -775,10 +1115,10 @@ export function useProfileSessionBootstrap(): void {
775
1115
  continue;
776
1116
  }
777
1117
 
778
- markProfileCommandIdProcessed(storageCommand);
779
- logProfileSession('command', storageCommand);
780
- notifyProfileCommandListeners(storageCommand);
1118
+ nextCommands.push(storageCommand);
781
1119
  }
1120
+
1121
+ enqueueSequencedProfileCommands(nextCommands);
782
1122
  };
783
1123
 
784
1124
  syncStoredProfileState()
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Builds the minimum agent-facing markdown summary for a run.
3
3
  *
4
- * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null}} options
4
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null, manifest?: Record<string, unknown> | null}} options
5
5
  * @returns {string}
6
6
  */
7
- declare function buildAgentSummaryMarkdown({ health, verdict, comparison }: AgentSummaryInput): string;
7
+ declare function buildAgentSummaryMarkdown({ health, verdict, comparison, manifest }: AgentSummaryInput): string;
8
8
  export { buildAgentSummaryMarkdown, };
9
9
  export type { AgentSummaryInput, SummaryRecord, };
10
10
  type SummaryRecord = Record<string, unknown>;
@@ -12,4 +12,5 @@ type AgentSummaryInput = {
12
12
  health: SummaryRecord;
13
13
  verdict: SummaryRecord;
14
14
  comparison?: SummaryRecord | null;
15
+ manifest?: SummaryRecord | null;
15
16
  };
@@ -122,13 +122,54 @@ function formatComparisonBasis(comparison) {
122
122
  }
123
123
  return lines;
124
124
  }
125
+ /**
126
+ * Formats attempt terminal semantics for agent-readable summaries.
127
+ *
128
+ * @param {SummaryRecord | null | undefined} manifest
129
+ * @returns {string[]}
130
+ */
131
+ function formatAttempt(manifest) {
132
+ const attempt = manifest?.attempt;
133
+ if (!attempt || typeof attempt !== 'object' || Array.isArray(attempt)) {
134
+ return [];
135
+ }
136
+ const attemptRecord = attempt;
137
+ const classification = attemptRecord.classification && typeof attemptRecord.classification === 'object' && !Array.isArray(attemptRecord.classification)
138
+ ? attemptRecord.classification
139
+ : {};
140
+ const cleanup = attemptRecord.cleanup && typeof attemptRecord.cleanup === 'object' && !Array.isArray(attemptRecord.cleanup)
141
+ ? attemptRecord.cleanup
142
+ : {};
143
+ const partialArtifacts = attemptRecord.partialArtifacts && typeof attemptRecord.partialArtifacts === 'object' && !Array.isArray(attemptRecord.partialArtifacts)
144
+ ? attemptRecord.partialArtifacts
145
+ : {};
146
+ const retryOfAttemptId = firstString([attemptRecord.retryOfAttemptId], '');
147
+ const retryReason = firstString([attemptRecord.retryReason], '');
148
+ const lines = [
149
+ '',
150
+ '## attempt',
151
+ '',
152
+ `- Attempt: ${code(firstString([attemptRecord.attemptId], 'unknown-attempt'))} (${attemptRecord.attemptNumber ?? 'unknown'}/${attemptRecord.maxAttempts ?? 'unknown'})`,
153
+ `- Terminal state: ${code(firstString([attemptRecord.terminalState], 'unknown'))}`,
154
+ `- Classification: ${code(firstString([classification.category], 'unknown'))}${classification.code ? ` ${code(classification.code)}` : ''}`,
155
+ `- Cleanup: ${code(firstString([cleanup.status], 'unknown'))}`,
156
+ `- Partial artifacts valid: ${partialArtifacts.valid === true ? 'true' : 'false'} - ${firstString([partialArtifacts.reason], 'no reason recorded')}`,
157
+ ];
158
+ if (retryOfAttemptId || retryReason) {
159
+ lines.push(`- Retry lineage: previous=${code(retryOfAttemptId || 'unknown')} reason=${retryReason || 'not recorded'}`);
160
+ }
161
+ if (Array.isArray(partialArtifacts.paths) && partialArtifacts.paths.length > 0) {
162
+ lines.push(`- Partial artifact paths: ${partialArtifacts.paths.map((item) => code(item)).join(', ')}`);
163
+ }
164
+ return lines;
165
+ }
125
166
  /**
126
167
  * Builds the minimum agent-facing markdown summary for a run.
127
168
  *
128
- * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null}} options
169
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null, manifest?: Record<string, unknown> | null}} options
129
170
  * @returns {string}
130
171
  */
131
- function buildAgentSummaryMarkdown({ health, verdict, comparison = null }) {
172
+ function buildAgentSummaryMarkdown({ health, verdict, comparison = null, manifest = null }) {
132
173
  const scenarioId = firstString([health?.scenarioId, verdict?.scenarioId], 'unknown-scenario');
133
174
  const runId = firstString([health?.runId, verdict?.runId], 'unknown-run');
134
175
  const healthStatus = firstString([health?.healthStatus], 'failed');
@@ -169,6 +210,7 @@ function buildAgentSummaryMarkdown({ health, verdict, comparison = null }) {
169
210
  if (failedBudgets.length > 0) {
170
211
  lines.push('', '## failed budgets', '', ...failedBudgets);
171
212
  }
213
+ lines.push(...formatAttempt(manifest));
172
214
  if (comparison) {
173
215
  lines.push('', '## comparison', '', firstString([comparison.summary], 'No comparison summary provided.'));
174
216
  lines.push(...formatComparisonBasis(comparison));