ccqa 1.8.2 → 1.8.3

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/dist/bin/ccqa.mjs CHANGED
@@ -6932,6 +6932,68 @@ function verifySessionRestores(statePath, verifyUrl) {
6932
6932
  ]);
6933
6933
  }
6934
6934
  }
6935
+ /**
6936
+ * Non-destructive mid-run health probe of an already-running live session.
6937
+ * Reads the current page URL (`eval location.href` — the same read
6938
+ * {@link verifySessionRestores} makes, and it does NOT navigate, so it never
6939
+ * clobbers the page the model is working on) and detects only the unambiguous
6940
+ * signals that agent-browser's daemon was replaced/wedged mid-run:
6941
+ *
6942
+ * - the probe exits non-zero — the daemon is wedged or was replaced
6943
+ * (`spawnAB`'s hard timeout bounds a hung daemon, so this returns rather
6944
+ * than hanging the run);
6945
+ * - the page is blank/absent (`about:blank`, empty, `chrome://…`) — a
6946
+ * restarted daemon comes up with no page and no in-memory auth-state.
6947
+ *
6948
+ * Deliberately NOT flagged: a non-blank page that left the verify URL's origin.
6949
+ * That can't be told apart from a spec legitimately roaming to another origin
6950
+ * mid-flow (e.g. Slack → a separate admin app it logs into at runtime), and a
6951
+ * false "unhealthy" would re-inject the saved state and wipe auth the spec
6952
+ * acquired live — breaking a spec that was fine. Missing a same-origin-ish
6953
+ * sign-in wall just leaves that step failing as before (no regression), so the
6954
+ * asymmetry favours only firing on a provably dead daemon. `verifyUrl` is still
6955
+ * required (it's the re-anchor target for recovery) but no longer compared here.
6956
+ */
6957
+ function checkLiveSessionHealth(sessionName) {
6958
+ const probe = spawnAB([
6959
+ "--session",
6960
+ sessionName,
6961
+ "eval",
6962
+ "location.href"
6963
+ ]);
6964
+ if (probe.status !== 0) return {
6965
+ healthy: false,
6966
+ reason: (probe.stderr || probe.stdout || `probe exited ${probe.status}`).trim()
6967
+ };
6968
+ const href = unwrapEvalString(probe.stdout);
6969
+ if (!href || href === "about:blank" || href.startsWith("chrome://") || href.startsWith("chrome-error://")) return {
6970
+ healthy: false,
6971
+ reason: `blank/absent page (${href || "empty"})`
6972
+ };
6973
+ return { healthy: true };
6974
+ }
6975
+ /**
6976
+ * Recover a live session whose daemon was replaced mid-run (detected by
6977
+ * {@link checkLiveSessionHealth}). The restart drops the in-memory auth-state
6978
+ * injected at run start, so the session fell to a sign-in wall. Re-boot +
6979
+ * re-attach the saved state ({@link loadStateIntoSession} is idempotent —
6980
+ * `state load` is load-only, never writes back) and then navigate to
6981
+ * `verifyUrl`, a known signed-in page, so the retrying model has an
6982
+ * authenticated anchor to continue from instead of a login screen. Returns the
6983
+ * injection result; the trailing `open` is best-effort (a failed nav still
6984
+ * leaves the state attached for the model's own next navigation).
6985
+ */
6986
+ function recoverLiveSession(sessionName, statePath, verifyUrl) {
6987
+ const injected = loadStateIntoSession(sessionName, statePath);
6988
+ if (!injected.ok) return injected;
6989
+ spawnAB([
6990
+ "--session",
6991
+ sessionName,
6992
+ "open",
6993
+ verifyUrl
6994
+ ]);
6995
+ return { ok: true };
6996
+ }
6935
6997
  /** Take the last non-empty line of `agent-browser eval` stdout and JSON-unquote it. */
6936
6998
  function unwrapEvalString(stdout) {
6937
6999
  const lines = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
@@ -7514,6 +7576,7 @@ async function runLiveExecutor(input) {
7514
7576
  const stepResults = [];
7515
7577
  let overallFailed = false;
7516
7578
  const statePath = input.statePath ?? null;
7579
+ const verifyUrl = input.verifyUrl ?? null;
7517
7580
  const promptPrefix = buildLiveSystemPromptPrefix({
7518
7581
  title: input.spec.title,
7519
7582
  allSteps: input.steps,
@@ -7545,12 +7608,25 @@ async function runLiveExecutor(input) {
7545
7608
  const systemPrompt = promptPrefix + buildLiveSystemPromptStepSection(step$1) + suffixBlock + langDirective;
7546
7609
  const userPrompt = buildLiveUserPrompt(step$1);
7547
7610
  let attempt = 0;
7611
+ let recoveredOnce = false;
7548
7612
  let lastOutcome = null;
7549
- while (attempt <= retries) {
7550
- if (attempt > 0) info(` retry ${attempt}/${retries} for ${step$1.id}`);
7613
+ for (;;) {
7551
7614
  lastOutcome = await executeStepAttempt(step$1, paths, systemPrompt, userPrompt);
7552
7615
  if (lastOutcome.status === "passed") break;
7616
+ if (!recoveredOnce && statePath && verifyUrl) {
7617
+ const health = checkLiveSessionHealth(input.sessionName);
7618
+ if (!health.healthy) {
7619
+ warn(`session lost mid-step for ${step$1.id} (${health.reason}); re-injecting auth-state and retrying`);
7620
+ const rec = recoverLiveSession(input.sessionName, statePath, verifyUrl);
7621
+ if (!rec.ok) warn(`session recovery failed: ${rec.error}`);
7622
+ recoveredOnce = true;
7623
+ attempt++;
7624
+ continue;
7625
+ }
7626
+ }
7627
+ if (attempt >= retries) break;
7553
7628
  attempt++;
7629
+ info(` retry ${attempt}/${retries} for ${step$1.id}`);
7554
7630
  }
7555
7631
  const outcome = lastOutcome;
7556
7632
  stepResults.push({
@@ -8101,6 +8177,7 @@ async function resolveSessionState(names, hubCtx, profile, verify = verifySessio
8101
8177
  const profileFlag = profile ? ` --profile ${profile}` : "";
8102
8178
  const loaded = [];
8103
8179
  const broken = [];
8180
+ let verifyUrl;
8104
8181
  for (const name of names) {
8105
8182
  let state;
8106
8183
  try {
@@ -8115,6 +8192,7 @@ async function resolveSessionState(names, hubCtx, profile, verify = verifySessio
8115
8192
  }
8116
8193
  const embedded = state[SESSION_VERIFY_URL_KEY];
8117
8194
  if (typeof embedded === "string") {
8195
+ verifyUrl ??= embedded;
8118
8196
  const memoKey = `${resolvedProfile}/${name}`;
8119
8197
  if (!verifiedSessions.has(memoKey)) {
8120
8198
  const tmp = await writeMergedTempState(mergeStorageStates([state]));
@@ -8139,6 +8217,7 @@ async function resolveSessionState(names, hubCtx, profile, verify = verifySessio
8139
8217
  return {
8140
8218
  ok: true,
8141
8219
  statePath,
8220
+ ...verifyUrl ? { verifyUrl } : {},
8142
8221
  cleanup: () => removeTempStateDir(statePath)
8143
8222
  };
8144
8223
  }
@@ -8167,6 +8246,7 @@ async function runOneSpec(args) {
8167
8246
  meta("session", sessionName);
8168
8247
  opts.teardown?.trackSession(sessionName);
8169
8248
  let statePath = null;
8249
+ let verifyUrl = null;
8170
8250
  let cleanupSession = null;
8171
8251
  if (spec.session && spec.session.length > 0) {
8172
8252
  const resolution = await resolveSessionState(spec.session, opts.hubContext ?? null, opts.profile);
@@ -8181,6 +8261,7 @@ async function runOneSpec(args) {
8181
8261
  };
8182
8262
  }
8183
8263
  statePath = resolution.statePath;
8264
+ verifyUrl = resolution.verifyUrl ?? null;
8184
8265
  cleanupSession = resolution.cleanup;
8185
8266
  meta("state", spec.session.join(", "));
8186
8267
  }
@@ -8196,6 +8277,7 @@ async function runOneSpec(args) {
8196
8277
  runDir,
8197
8278
  sessionName,
8198
8279
  statePath,
8280
+ verifyUrl,
8199
8281
  systemPromptSuffix: userPromptSuffix,
8200
8282
  model: opts.model,
8201
8283
  language: opts.language,
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {