agent-scenario-loop 0.1.5 → 0.1.7

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.
@@ -167,6 +167,7 @@ function parseKeyValueProfileSessionEntry(payload) {
167
167
  return null;
168
168
  }
169
169
  const timestamp = coerceNumber(entry.timestamp);
170
+ const startedAt = coerceNumber(entry.startedAt);
170
171
  const atMs = coerceNumber(entry.atMs);
171
172
  const sequence = coerceNumber(entry.sequence);
172
173
  const waitMs = coerceNumber(entry.waitMs);
@@ -177,6 +178,9 @@ function parseKeyValueProfileSessionEntry(payload) {
177
178
  if (timestamp !== null) {
178
179
  entry.timestamp = timestamp;
179
180
  }
181
+ if (startedAt !== null) {
182
+ entry.startedAt = startedAt;
183
+ }
180
184
  if (sequence !== null) {
181
185
  entry.sequence = sequence;
182
186
  }
@@ -473,10 +477,15 @@ function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterat
473
477
  (typeof record.milestoneCount === 'number' &&
474
478
  record.milestoneCount >= requiredMilestoneEventsPerIteration)) &&
475
479
  record.milestoneAt >= 0;
476
- if (hasMilestoneDuration) {
480
+ if (hasMilestoneDuration && expectedIterations === 1) {
477
481
  durationsMs.push(roundMs(record.milestoneAt));
478
482
  openDurationsMs.push(roundMs(record.milestoneAt));
479
483
  }
484
+ else if (hasMilestoneDuration) {
485
+ // Repeated completion-only milestones prove that cycles finished, but
486
+ // their atMs values are positions on the session timeline, not per-cycle
487
+ // latency samples. Repeated latency budgets need explicit interval anchors.
488
+ }
480
489
  else if (hasCycleDuration) {
481
490
  durationsMs.push(roundMs(record.dismissedAt - record.presentRequestedAt));
482
491
  }
@@ -126,6 +126,17 @@ type ProviderCommandFailure = {
126
126
  providerId: string;
127
127
  rawPath?: string;
128
128
  };
129
+ type ProfileSessionSeed = {
130
+ runId: string;
131
+ scenario: string;
132
+ startedAt: number;
133
+ };
134
+ type ProfileSessionFreshness = {
135
+ appStartedAt?: number;
136
+ reason?: string;
137
+ seed: ProfileSessionSeed;
138
+ status: 'fresh' | 'missing-app-session' | 'stale';
139
+ };
129
140
  /**
130
141
  * Prints CLI usage to stderr.
131
142
  *
@@ -173,10 +184,10 @@ declare function resolveAttachedEvidence({ args, layout, providerInputs, }: {
173
184
  /**
174
185
  * Builds scenario health from profile metrics.
175
186
  *
176
- * @param {{scenario: Record<string, unknown>, runId: string, metrics: Record<string, unknown>, diagnostics?: DiagnosticInventoryEntry[], profileEventCount?: number, profileSessionEntryCount?: number, commandTransport?: string, sessionEntries?: Record<string, unknown>[]}} options
187
+ * @param {{scenario: Record<string, unknown>, runId: string, metrics: Record<string, unknown>, diagnostics?: DiagnosticInventoryEntry[], profileEventCount?: number, profileSessionEntryCount?: number, commandTransport?: string, sessionEntries?: Record<string, unknown>[], sessionFreshness?: ProfileSessionFreshness | null}} options
177
188
  * @returns {Record<string, unknown>}
178
189
  */
179
- declare function buildProfileHealth({ scenario, runId, metrics, diagnostics, profileEventCount, profileSessionEntryCount, commandTransport, sessionEntries, }: {
190
+ declare function buildProfileHealth({ scenario, runId, metrics, diagnostics, profileEventCount, profileSessionEntryCount, commandTransport, sessionEntries, sessionFreshness, }: {
180
191
  scenario: Record<string, any>;
181
192
  runId: string;
182
193
  metrics: Record<string, any>;
@@ -185,6 +196,7 @@ declare function buildProfileHealth({ scenario, runId, metrics, diagnostics, pro
185
196
  profileSessionEntryCount?: number;
186
197
  commandTransport?: string;
187
198
  sessionEntries?: Record<string, any>[];
199
+ sessionFreshness?: ProfileSessionFreshness | null;
188
200
  }): Record<string, unknown>;
189
201
  /**
190
202
  * Builds failed scenario health from evidence-provider command failures.
@@ -20,7 +20,7 @@ exports.runProfileCli = runProfileCli;
20
20
  exports.runProfileMobile = runProfileMobile;
21
21
  exports.hashScenarioContract = hashScenarioContract;
22
22
  exports.usage = usage;
23
- const { execFile } = require('node:child_process');
23
+ const { spawn } = require('node:child_process');
24
24
  const fs = require('node:fs');
25
25
  const fsp = require('node:fs/promises');
26
26
  const path = require('node:path');
@@ -35,6 +35,7 @@ const { writeUsage } = require('./cli');
35
35
  const CAPTURE_EVIDENCE_KINDS = new Set(['screenshot', 'uiTree', 'video']);
36
36
  const PROVIDER_EVIDENCE_KINDS = new Set(['accessibility', 'logs', 'profiler']);
37
37
  const SIGNAL_EVIDENCE_KINDS = new Set(['js', 'memory', 'network']);
38
+ const DEFAULT_PROVIDER_COMMAND_TIMEOUT_MS = 180_000;
38
39
  /**
39
40
  * Prints CLI usage to stderr.
40
41
  *
@@ -188,23 +189,83 @@ async function hashFileSha256(filePath) {
188
189
  return crypto.createHash('sha256').update(content).digest('hex');
189
190
  }
190
191
  /**
191
- * Runs one provider command without a shell and captures its output.
192
+ * Resolves the timeout applied to provider commands.
192
193
  *
193
- * @param {{command: string, args: string[], cwd?: string, env?: Record<string, string>}} options
194
+ * @returns {number}
195
+ */
196
+ function resolveProviderCommandTimeoutMs() {
197
+ return readPositiveInteger(process.env.ASL_PROVIDER_COMMAND_TIMEOUT_MS, DEFAULT_PROVIDER_COMMAND_TIMEOUT_MS);
198
+ }
199
+ /**
200
+ * Runs one provider command without a shell, streaming output to raw files.
201
+ *
202
+ * @param {{command: string, args: string[], cwd?: string, env?: Record<string, string>, stderrPath: string, stdoutPath: string, timeoutMs: number}} options
194
203
  * @returns {Promise<ProviderCommandResult>}
195
204
  */
196
- function execProviderCommand({ args, command, cwd, env, }) {
205
+ function execProviderCommand({ args, command, cwd, env, stderrPath, stdoutPath, timeoutMs, }) {
197
206
  return new Promise((resolve) => {
198
- execFile(command, args, {
207
+ const child = spawn(command, args, {
199
208
  ...(cwd ? { cwd } : {}),
200
209
  env: env ? { ...process.env, ...env } : process.env,
201
- }, (error, stdout, stderr) => {
210
+ shell: false,
211
+ stdio: ['ignore', 'pipe', 'pipe'],
212
+ });
213
+ const stdoutChunks = [];
214
+ const stderrChunks = [];
215
+ let timedOut = false;
216
+ let settled = false;
217
+ const timeout = setTimeout(() => {
218
+ timedOut = true;
219
+ child.kill('SIGTERM');
220
+ setTimeout(() => {
221
+ if (!settled) {
222
+ child.kill('SIGKILL');
223
+ }
224
+ }, 1000).unref();
225
+ }, timeoutMs);
226
+ timeout.unref();
227
+ child.stdout.on('data', (chunk) => {
228
+ stdoutChunks.push(chunk);
229
+ fs.appendFileSync(stdoutPath, chunk);
230
+ });
231
+ child.stderr.on('data', (chunk) => {
232
+ stderrChunks.push(chunk);
233
+ fs.appendFileSync(stderrPath, chunk);
234
+ });
235
+ child.on('error', (error) => {
236
+ if (settled) {
237
+ return;
238
+ }
239
+ clearTimeout(timeout);
240
+ settled = true;
241
+ const stderr = error.message;
242
+ fs.appendFileSync(stderrPath, `${stderr}\n`, 'utf8');
202
243
  resolve({
203
244
  args,
204
245
  command,
205
- exitCode: error && typeof error.code === 'number' ? error.code : error ? 1 : 0,
246
+ exitCode: 1,
247
+ signal: null,
248
+ stderr,
249
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
250
+ timedOut,
251
+ });
252
+ });
253
+ child.on('close', (exitCode, signal) => {
254
+ if (settled) {
255
+ return;
256
+ }
257
+ clearTimeout(timeout);
258
+ settled = true;
259
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
260
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
261
+ resolve({
262
+ args,
263
+ command,
264
+ exitCode: typeof exitCode === 'number' ? exitCode : timedOut ? 124 : 1,
265
+ signal,
206
266
  stderr,
207
267
  stdout,
268
+ timedOut,
208
269
  });
209
270
  });
210
271
  });
@@ -396,32 +457,66 @@ async function executeProviderCommands({ args, layout, platform, runDir, runId,
396
457
  ? resolveProviderPath({ context, manifestDir, value: providerCommand.cwd })
397
458
  : manifestDir;
398
459
  const resolvedEnv = Object.fromEntries(Object.entries(providerCommand.env ?? {}).map(([key, value]) => [key, applyProviderPlaceholders(value, context)]));
460
+ const commandRecordFileName = `${providerId}-${providerCommand.id}.json`;
461
+ const stdoutFileName = `${providerId}-${providerCommand.id}.stdout.txt`;
462
+ const stderrFileName = `${providerId}-${providerCommand.id}.stderr.txt`;
463
+ const commandRecordPath = path.join(commandRecordDir, commandRecordFileName);
464
+ const stdoutPath = path.join(commandRecordDir, stdoutFileName);
465
+ const stderrPath = path.join(commandRecordDir, stderrFileName);
466
+ const timeoutMs = resolveProviderCommandTimeoutMs();
467
+ const startedAt = new Date().toISOString();
468
+ await fsp.writeFile(stdoutPath, '', 'utf8');
469
+ await fsp.writeFile(stderrPath, '', 'utf8');
470
+ await fsp.writeFile(commandRecordPath, `${JSON.stringify({
471
+ args: resolvedArgs,
472
+ command: resolvedCommand,
473
+ phase: providerCommand.phase,
474
+ providerId,
475
+ startedAt,
476
+ status: 'started',
477
+ stderrPath: `raw/provider-commands/${stderrFileName}`,
478
+ stdoutPath: `raw/provider-commands/${stdoutFileName}`,
479
+ timeoutMs,
480
+ }, null, 2)}\n`, 'utf8');
399
481
  const commandResult = await execProviderCommand({
400
482
  args: resolvedArgs,
401
483
  command: resolvedCommand,
402
484
  cwd: resolvedCwd,
403
485
  env: resolvedEnv,
486
+ stderrPath,
487
+ stdoutPath,
488
+ timeoutMs,
404
489
  });
405
- const commandRecordFileName = `${providerId}-${providerCommand.id}.json`;
406
- const commandRecordPath = path.join(commandRecordDir, commandRecordFileName);
407
490
  await fsp.writeFile(commandRecordPath, `${JSON.stringify({
408
491
  args: commandResult.args,
409
492
  command: commandResult.command,
493
+ endedAt: new Date().toISOString(),
410
494
  exitCode: commandResult.exitCode,
411
495
  phase: providerCommand.phase,
412
496
  providerId,
497
+ signal: commandResult.signal,
413
498
  stderr: commandResult.stderr,
499
+ stderrPath: `raw/provider-commands/${stderrFileName}`,
500
+ status: commandResult.timedOut ? 'timed_out' : commandResult.exitCode === 0 ? 'completed' : 'failed',
414
501
  stdout: commandResult.stdout,
502
+ stdoutPath: `raw/provider-commands/${stdoutFileName}`,
503
+ timedOut: commandResult.timedOut,
504
+ timeoutMs,
415
505
  }, null, 2)}\n`, 'utf8');
416
506
  if (commandResult.exitCode !== 0) {
507
+ const timedOut = commandResult.timedOut;
417
508
  failures.push({
418
509
  commandId: providerCommand.id,
419
- code: 'provider_command_failed',
510
+ code: timedOut ? 'provider_liveness_timeout' : 'provider_command_failed',
420
511
  exitCode: commandResult.exitCode,
421
- message: `Evidence provider command ${providerId}/${providerCommand.id} failed with exit code ${commandResult.exitCode}.`,
512
+ message: timedOut
513
+ ? `Evidence provider command ${providerId}/${providerCommand.id} did not finish before the ${timeoutMs}ms timeout.`
514
+ : `Evidence provider command ${providerId}/${providerCommand.id} failed with exit code ${commandResult.exitCode}.`,
422
515
  name: 'evidence_provider_command_completed',
423
- nextAction: `Inspect raw/provider-commands/${commandRecordFileName}, fix the provider command or its environment, then rerun the profile.`,
424
- nextActionCode: 'fix_provider_command',
516
+ nextAction: timedOut
517
+ ? `Inspect raw/provider-commands/${commandRecordFileName}, raw/provider-commands/${stdoutFileName}, and raw/provider-commands/${stderrFileName}; fix the provider liveness issue or increase ASL_PROVIDER_COMMAND_TIMEOUT_MS only if the provider is making progress.`
518
+ : `Inspect raw/provider-commands/${commandRecordFileName}, fix the provider command or its environment, then rerun the profile.`,
519
+ nextActionCode: timedOut ? 'fix_provider_liveness' : 'fix_provider_command',
425
520
  phase: providerCommand.phase,
426
521
  providerId,
427
522
  rawPath: `raw/provider-commands/${commandRecordFileName}`,
@@ -1109,10 +1204,10 @@ function buildRequiredDiagnosticHealthChecks(diagnostics = []) {
1109
1204
  /**
1110
1205
  * Builds scenario health from profile metrics.
1111
1206
  *
1112
- * @param {{scenario: Record<string, unknown>, runId: string, metrics: Record<string, unknown>, diagnostics?: DiagnosticInventoryEntry[], profileEventCount?: number, profileSessionEntryCount?: number, commandTransport?: string, sessionEntries?: Record<string, unknown>[]}} options
1207
+ * @param {{scenario: Record<string, unknown>, runId: string, metrics: Record<string, unknown>, diagnostics?: DiagnosticInventoryEntry[], profileEventCount?: number, profileSessionEntryCount?: number, commandTransport?: string, sessionEntries?: Record<string, unknown>[], sessionFreshness?: ProfileSessionFreshness | null}} options
1113
1208
  * @returns {Record<string, unknown>}
1114
1209
  */
1115
- function buildProfileHealth({ scenario, runId, metrics, diagnostics = [], profileEventCount, profileSessionEntryCount, commandTransport, sessionEntries = [], }) {
1210
+ function buildProfileHealth({ scenario, runId, metrics, diagnostics = [], profileEventCount, profileSessionEntryCount, commandTransport, sessionEntries = [], sessionFreshness = null, }) {
1116
1211
  const passed = metrics.status === 'passed';
1117
1212
  const metadata = {
1118
1213
  failures: typeof metrics.failures === 'number' ? metrics.failures : null,
@@ -1175,7 +1270,39 @@ function buildProfileHealth({ scenario, runId, metrics, diagnostics = [], profil
1175
1270
  const commandChecksPassed = commandChecks.every((check) => check.status === 'passed');
1176
1271
  const diagnosticChecks = buildRequiredDiagnosticHealthChecks(diagnostics);
1177
1272
  const diagnosticChecksPassed = diagnosticChecks.every((check) => check.status === 'passed');
1178
- const healthPassed = passed && commandChecksPassed && diagnosticChecksPassed;
1273
+ const sessionFreshnessChecks = sessionFreshness
1274
+ ? [
1275
+ {
1276
+ name: 'profile_session_freshness',
1277
+ status: sessionFreshness.status === 'fresh'
1278
+ ? 'passed'
1279
+ : sessionFreshness.status === 'missing-app-session'
1280
+ ? 'warning'
1281
+ : 'failed',
1282
+ source: 'runner',
1283
+ code: sessionFreshness.status === 'fresh'
1284
+ ? 'profile_session_fresh'
1285
+ : sessionFreshness.status === 'missing-app-session'
1286
+ ? 'profile_session_start_missing'
1287
+ : 'profile_session_stale',
1288
+ message: sessionFreshness.status === 'fresh'
1289
+ ? 'App-side profile-session start matched the runner-written session seed.'
1290
+ : sessionFreshness.reason ?? 'App-side profile-session evidence did not match the runner-written session seed.',
1291
+ metadata: {
1292
+ appStartedAt: sessionFreshness.appStartedAt ?? null,
1293
+ nextAction: sessionFreshness.status === 'fresh'
1294
+ ? 'No action required.'
1295
+ : 'Clear stale app/session state, reload the expected app bundle, and rerun before treating profile events or metrics as product evidence.',
1296
+ nextActionCode: sessionFreshness.status === 'fresh'
1297
+ ? 'none'
1298
+ : 'rerun_with_fresh_profile_session',
1299
+ seedStartedAt: sessionFreshness.seed.startedAt,
1300
+ },
1301
+ },
1302
+ ]
1303
+ : [];
1304
+ const sessionFreshnessChecksPassed = sessionFreshnessChecks.every((check) => check.status !== 'failed');
1305
+ const healthPassed = passed && commandChecksPassed && diagnosticChecksPassed && sessionFreshnessChecksPassed;
1179
1306
  return assertValidJson({
1180
1307
  schemaVersion: '1.0.0',
1181
1308
  scenarioId: scenario.name,
@@ -1193,6 +1320,7 @@ function buildProfileHealth({ scenario, runId, metrics, diagnostics = [], profil
1193
1320
  : 'Profile events did not complete every expected iteration.',
1194
1321
  metadata,
1195
1322
  },
1323
+ ...sessionFreshnessChecks,
1196
1324
  ...commandChecks,
1197
1325
  ...diagnosticChecks,
1198
1326
  ],
@@ -1446,6 +1574,141 @@ function resolveProfileSessionEntriesPath({ args, platform }) {
1446
1574
  }
1447
1575
  return null;
1448
1576
  }
1577
+ /**
1578
+ * Reads one JSON object candidate from raw command text.
1579
+ *
1580
+ * @param {string} text
1581
+ * @returns {Record<string, unknown>[]}
1582
+ */
1583
+ function parseJsonObjectsFromText(text) {
1584
+ const matches = text.match(/\{[^{}\n]*\}/gu) ?? [];
1585
+ const objects = [];
1586
+ for (const match of matches) {
1587
+ try {
1588
+ const parsed = JSON.parse(match);
1589
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1590
+ objects.push(parsed);
1591
+ }
1592
+ }
1593
+ catch {
1594
+ // Raw command files can contain shell syntax around JSON payloads.
1595
+ }
1596
+ }
1597
+ return objects;
1598
+ }
1599
+ /**
1600
+ * Reads an Android profile-session seed from adb AsyncStorage raw artifacts.
1601
+ *
1602
+ * @param {{sidecarRoot: string, runId: string, scenarioName: string}} options
1603
+ * @returns {ProfileSessionSeed | null}
1604
+ */
1605
+ function readAndroidProfileSessionSeed({ runId, scenarioName, sidecarRoot, }) {
1606
+ const rawDir = path.resolve(sidecarRoot, 'raw');
1607
+ if (!fs.existsSync(rawDir)) {
1608
+ return null;
1609
+ }
1610
+ for (const fileName of fs.readdirSync(rawDir).filter((entry) => /^adb-async-storage-write-\d+\.txt$/u.test(entry)).sort()) {
1611
+ const rawText = fs.readFileSync(path.join(rawDir, fileName), 'utf8');
1612
+ for (const candidate of parseJsonObjectsFromText(rawText)) {
1613
+ if (candidate.runId === runId &&
1614
+ candidate.scenario === scenarioName &&
1615
+ typeof candidate.startedAt === 'number' &&
1616
+ Number.isFinite(candidate.startedAt)) {
1617
+ return {
1618
+ runId,
1619
+ scenario: scenarioName,
1620
+ startedAt: candidate.startedAt,
1621
+ };
1622
+ }
1623
+ }
1624
+ }
1625
+ return null;
1626
+ }
1627
+ /**
1628
+ * Reads an iOS profile-session seed from simctl storage artifacts.
1629
+ *
1630
+ * @param {{sidecarRoot: string, runId: string, scenarioName: string}} options
1631
+ * @returns {ProfileSessionSeed | null}
1632
+ */
1633
+ function readIosProfileSessionSeed({ runId, scenarioName, sidecarRoot, }) {
1634
+ const seedPath = path.resolve(sidecarRoot, 'raw', 'ios-profile-session-seed.json');
1635
+ const seed = readOptionalJsonObject(seedPath);
1636
+ const session = seed?.session;
1637
+ if (!session || typeof session !== 'object' || Array.isArray(session)) {
1638
+ return null;
1639
+ }
1640
+ const record = session;
1641
+ if (record.runId === runId &&
1642
+ record.scenario === scenarioName &&
1643
+ typeof record.startedAt === 'number' &&
1644
+ Number.isFinite(record.startedAt)) {
1645
+ return {
1646
+ runId,
1647
+ scenario: scenarioName,
1648
+ startedAt: record.startedAt,
1649
+ };
1650
+ }
1651
+ return null;
1652
+ }
1653
+ /**
1654
+ * Reads the profile-session seed written by a platform sidecar, when present.
1655
+ *
1656
+ * @param {{args: CliArgs, platform: ProfilePlatform, runId: string, scenarioName: string}} options
1657
+ * @returns {ProfileSessionSeed | null}
1658
+ */
1659
+ function resolveProfileSessionSeed({ args, platform, runId, scenarioName, }) {
1660
+ if (platform === 'android' && typeof args['adb-artifacts'] === 'string') {
1661
+ return readAndroidProfileSessionSeed({
1662
+ runId,
1663
+ scenarioName,
1664
+ sidecarRoot: path.resolve(args['adb-artifacts']),
1665
+ });
1666
+ }
1667
+ if (platform === 'ios' && typeof args['simctl-artifacts'] === 'string') {
1668
+ return readIosProfileSessionSeed({
1669
+ runId,
1670
+ scenarioName,
1671
+ sidecarRoot: path.resolve(args['simctl-artifacts']),
1672
+ });
1673
+ }
1674
+ return null;
1675
+ }
1676
+ /**
1677
+ * Compares the sidecar-written profile session to the app-emitted session.
1678
+ *
1679
+ * @param {{seed: ProfileSessionSeed | null, sessionEntries: Record<string, unknown>[]}} options
1680
+ * @returns {ProfileSessionFreshness | null}
1681
+ */
1682
+ function resolveProfileSessionFreshness({ seed, sessionEntries, }) {
1683
+ if (!seed) {
1684
+ return null;
1685
+ }
1686
+ const appStart = sessionEntries.find((entry) => (entry?.kind === 'start' &&
1687
+ entry.runId === seed.runId &&
1688
+ entry.scenario === seed.scenario &&
1689
+ typeof entry.startedAt === 'number' &&
1690
+ Number.isFinite(entry.startedAt)));
1691
+ if (!appStart || typeof appStart.startedAt !== 'number') {
1692
+ return {
1693
+ seed,
1694
+ status: 'missing-app-session',
1695
+ reason: 'The runner wrote a profile-session seed, but no matching app-side start entry was observed.',
1696
+ };
1697
+ }
1698
+ if (appStart.startedAt !== seed.startedAt) {
1699
+ return {
1700
+ appStartedAt: appStart.startedAt,
1701
+ seed,
1702
+ status: 'stale',
1703
+ reason: 'The app-side profile-session start did not match the runner-written seed.',
1704
+ };
1705
+ }
1706
+ return {
1707
+ appStartedAt: appStart.startedAt,
1708
+ seed,
1709
+ status: 'fresh',
1710
+ };
1711
+ }
1449
1712
  /**
1450
1713
  * Resolves the run id used by rehydrated sidecar evidence.
1451
1714
  *
@@ -2319,6 +2582,16 @@ async function runProfileMobile(args, options) {
2319
2582
  })
2320
2583
  : []),
2321
2584
  ];
2585
+ const profileSessionSeed = resolveProfileSessionSeed({
2586
+ args,
2587
+ platform: options.platform,
2588
+ runId: evidenceFilterRunId,
2589
+ scenarioName,
2590
+ });
2591
+ const sessionFreshness = resolveProfileSessionFreshness({
2592
+ seed: profileSessionSeed,
2593
+ sessionEntries,
2594
+ });
2322
2595
  const runtimeTarget = resolveRuntimeTarget({ args, platform: options.platform });
2323
2596
  const metrics = buildMetricsFromProfileEvents({
2324
2597
  scenario: scenarioName,
@@ -2447,6 +2720,7 @@ async function runProfileMobile(args, options) {
2447
2720
  profileSessionEntryCount: sessionEntries.length,
2448
2721
  commandTransport,
2449
2722
  sessionEntries,
2723
+ sessionFreshness,
2450
2724
  });
2451
2725
  const verdict = buildProfileVerdict({ scenario: profileScenario, runId, health, metrics });
2452
2726
  const agentSummary = buildAgentSummaryMarkdown({ health, verdict, manifest });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-scenario-loop",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "description": "Scenario orchestration and evidence collection for agent-driven software development. Bring your own runner. Keep your scenarios. Keep your evidence.",
6
6
  "license": "MIT",