browserzap 0.5.5 → 0.6.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/CHANGELOG.md +56 -0
- package/bridge-server/background-runs.js +91 -4
- package/bridge-server/cli-run.js +2 -0
- package/bridge-server/limits.js +27 -1
- package/bridge-server/mcp-server.js +25 -9
- package/bridge-server/panel.js +14 -3
- package/bridge-server/resolvers.js +287 -0
- package/bridge-server/tools.js +43 -4
- package/bridge-server/workflows.js +298 -11
- package/extension/background.js +1 -1
- package/extension/engine.js +1 -1
- package/extension/manifest.json +1 -1
- package/extension/sidepanel.css +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1 (2026-10-30)
|
|
4
|
+
|
|
5
|
+
The changes from three real-Chrome runs in the owner's own browser (docs/REVIEW_LATER.md, K-7…K-10). Checked in the
|
|
6
|
+
owner's Chrome: `npm run check:chrome` 9/9, approval through the page card, the test tab closed at session end.
|
|
7
|
+
|
|
8
|
+
- **No more tabs left behind.** The tab group is the contract: while the agent is connected its tabs stay open in the
|
|
9
|
+
BrowserZap group, and when the session ends **every tab still in that group closes — including the one on screen**.
|
|
10
|
+
Before this the active tab of the focused window was kept and ungrouped, so every run left a duplicate tab and
|
|
11
|
+
repeated runs piled them up (K-7). A tab you drag **out of** the group is yours and stays open. The one other
|
|
12
|
+
exception is a hand-off you were actually typing in (or a page that moved while it was open): that page is kept,
|
|
13
|
+
ungrouped, and closed never.
|
|
14
|
+
- **A note instead of silence.** On a tab kept that way, a short dismissal note says *"The agent finished — this tab is
|
|
15
|
+
yours."* It goes on its own after a few seconds (K-10).
|
|
16
|
+
- **Approve on the page.** A consequential action now draws an **Approve once** / **Deny** card beside the element it
|
|
17
|
+
marks, so the side panel never has to be opened by hand — Chrome will not open it on its own, which is why the ringed
|
|
18
|
+
button looked like the thing to press (K-8). The card, like the Stop pill and Done, acts only on a real click, is out
|
|
19
|
+
of the tab order and out of the agent's reach (it steps aside when the agent's own click is due to land there), and
|
|
20
|
+
the extension re-checks the approval is still pending for that session and tab. The toolbar icon is badged while a
|
|
21
|
+
decision or a hand-off waits, and cleared after. The side panel's **Approve once** still works.
|
|
22
|
+
- **The "Your turn" card says what happens next.** It now reads *type here, then press Done — don't submit the form; the
|
|
23
|
+
agent continues after you press Done*. If the page moves while the hand-off is open (a login redirect, or you submit
|
|
24
|
+
the form yourself), the card on the new page says the page changed and **Done** still hands control back (K-9).
|
|
25
|
+
- `npm run check:chrome` puts every instruction on the page it is about, not only in the terminal, and says explicitly
|
|
26
|
+
to press **Approve once** on the card next to the button — or in the side panel — and not the page's own button.
|
|
27
|
+
|
|
28
|
+
## 0.6.0 (2026-10-30)
|
|
29
|
+
|
|
30
|
+
A replay now waits for what the step is actually blocked on, and retries that step instead of going back.
|
|
31
|
+
|
|
32
|
+
- **The resolver engine.** When a step fails, or a click lands but changes nothing, BrowserZap works out what it is
|
|
33
|
+
waiting for — a slow request, a spinner, a target that is not usable yet, a target that has not appeared — waits for
|
|
34
|
+
exactly that, then retries the **same step**. Up to twice per step and 45 seconds of waiting, inside the run's own
|
|
35
|
+
limit. A failed step is never replayed from step 1 unless that is where it failed.
|
|
36
|
+
- **Clicks are still never repeated blindly.** A consequential click (submit, pay, delete, or one that needed your
|
|
37
|
+
approval) is never retried. A click is retried only when it provably did not happen, or when it was reversible,
|
|
38
|
+
changed nothing at all **and started no request**. A click that started a request is never clicked again.
|
|
39
|
+
- **New wait conditions:** an element is usable (visible, enabled, uncovered and not moving), a spinner or "Loading…"
|
|
40
|
+
is gone, the page's network is quiet, or a specific request has answered. A 4xx/5xx answers at once instead of
|
|
41
|
+
waiting. A dashboard that refreshes itself every few seconds no longer looks busy forever, and analytics traffic is
|
|
42
|
+
ignored.
|
|
43
|
+
- **Learns each step.** After three runs a step's wait budget comes from what it usually needs; a blocker it hit in
|
|
44
|
+
at least 60 % of its **last 10 runs** is waited for *before* acting, and one that stops happening stops being waited
|
|
45
|
+
for, so a site that was fixed is not slowed down; a wait that never helps is skipped there. You can override with
|
|
46
|
+
`patience` (`fast`/`normal`/`patient`) or `maxWaitMs` on a step. The saved workflow is never changed automatically;
|
|
47
|
+
hints appear in the run result.
|
|
48
|
+
- **Remembers what a step produced.** The recorder saves the dialog that appeared, the request the step started
|
|
49
|
+
(method plus a path with ids replaced by `*`) and how long the page took to settle. On replay a step is done when
|
|
50
|
+
that is there, waiting at most the settle the recording measured (or your `maxWaitMs`) — a bounded check that ends
|
|
51
|
+
as soon as the evidence appears, never a pause. Never a value, a query string or page text.
|
|
52
|
+
- **Re-run failed resumes at the step that failed** when the page has not moved since the last verified step;
|
|
53
|
+
otherwise at the nearest earlier page load. It first checks, with a short bounded wait, that the failed step's
|
|
54
|
+
target is really on the page now — a page that moved goes back to the page load, and a workflow with no page load
|
|
55
|
+
of its own fails that set with `RESUME_UNVERIFIED` rather than acting on a page nobody checked. The batch summary
|
|
56
|
+
says how many sets resumed.
|
|
57
|
+
- No new tools and no new permission: still 34 tools, and the bridge stays loopback-only and authenticated.
|
|
58
|
+
|
|
3
59
|
## 0.5.5 (2026-10-30)
|
|
4
60
|
|
|
5
61
|
Comment mode, debugging tools, files, Edge and WSL, a much better doctor, and the Chrome Web Store build. Known items to review are listed in docs/REVIEW_LATER.md (upload approval visibility, a file the person picks themselves, a download re-test, wider Comment screenshots, a calmer side panel).
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
15
|
const crypto = require('crypto');
|
|
16
|
+
const { LIMITS } = require('./limits');
|
|
17
|
+
const { resolveParams, substitute } = require('./workflows');
|
|
16
18
|
|
|
17
19
|
/** Finished runs kept for workflow_status and rerunFailed. */
|
|
18
20
|
const MAX_KEPT = 10;
|
|
@@ -25,6 +27,85 @@ class RunError extends Error {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** Steps that only read the page: a resume may start at one without a probe. */
|
|
31
|
+
const RESUME_WITHOUT_PROBE = new Set(['wait_for_state', 'page_inspect', 'screenshot_capture', 'page_console', 'page_network']);
|
|
32
|
+
|
|
33
|
+
/** The nearest `browser_navigate` at or before `index`; -1 when there is none. */
|
|
34
|
+
function navigationAnchor(wf, index) {
|
|
35
|
+
let found = -1;
|
|
36
|
+
for (let i = 0; i <= index && Array.isArray(wf?.steps) && i < wf.steps.length; i++) {
|
|
37
|
+
if (wf.steps[i].tool === 'browser_navigate') found = i;
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a set that did not complete may resume (0.6.0). It stays at the failed
|
|
44
|
+
* step when the page has not navigated since the last verified step; otherwise
|
|
45
|
+
* it starts at the nearest navigation anchor at or before the failure. Never
|
|
46
|
+
* step 1 unless that is the anchor.
|
|
47
|
+
*/
|
|
48
|
+
function resumeAtFor(wf, run) {
|
|
49
|
+
if (!wf || !run || !Number.isInteger(run.stoppedAt)) return 0;
|
|
50
|
+
const checkpoint = Number.isInteger(run.checkpoint) ? run.checkpoint : -1;
|
|
51
|
+
const lastNavigation = navigationAnchor(wf, run.stoppedAt);
|
|
52
|
+
if (checkpoint >= 0 && checkpoint >= lastNavigation) return run.stoppedAt;
|
|
53
|
+
if (lastNavigation >= 0) return lastNavigation;
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A resume: where to start (`at`), the navigation anchor to fall back to when
|
|
59
|
+
* the page has moved (`anchor`, -1 when the workflow has none) and the failed
|
|
60
|
+
* step's own target (`target`), which is what a resume at that step must find
|
|
61
|
+
* actionable before it runs (A6-09). Values are not substituted here: the
|
|
62
|
+
* caller resolves them per parameter set.
|
|
63
|
+
*/
|
|
64
|
+
function resumePlan(wf, run) {
|
|
65
|
+
const at = resumeAtFor(wf, run);
|
|
66
|
+
const anchor = Number.isInteger(run?.stoppedAt) ? navigationAnchor(wf, run.stoppedAt) : -1;
|
|
67
|
+
const target = wf?.steps?.[at]?.args?.target;
|
|
68
|
+
return { at, anchor, target: at > 0 && target && typeof target === 'object' ? target : null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Prove the page is still where a resume at the failed step expects it.
|
|
73
|
+
*
|
|
74
|
+
* A real `wait_for_state { actionable: <the step's target> }` through the
|
|
75
|
+
* normal tool path, bounded by LIMITS.RESUME_PROBE_MS (a poll that ends as
|
|
76
|
+
* soon as the target is ready, never a pause). When it does not hold, the set
|
|
77
|
+
* starts at its navigation anchor instead. With no anchor there is nothing
|
|
78
|
+
* safe to start from, so it throws RESUME_UNVERIFIED: a set is never resumed
|
|
79
|
+
* with a real click on a page nobody checked.
|
|
80
|
+
*
|
|
81
|
+
* Returns the step to start at.
|
|
82
|
+
*/
|
|
83
|
+
async function probeResume({ wf, plan, params = {}, execute, allowedOrigins = null, timeoutMs = LIMITS.RESUME_PROBE_MS }) {
|
|
84
|
+
const at = plan?.at ?? 0;
|
|
85
|
+
if (!(at > 0) || !execute) return at;
|
|
86
|
+
const tool = wf?.steps?.[at]?.tool;
|
|
87
|
+
let reason;
|
|
88
|
+
if (plan?.target) {
|
|
89
|
+
const target = substitute(plan.target, resolveParams(wf, params));
|
|
90
|
+
const env = await execute('wait_for_state', { actionable: target, timeoutMs }, { allowedOrigins, index: at });
|
|
91
|
+
if (env.ok) return at;
|
|
92
|
+
reason = env.error?.code ?? 'the probe did not hold';
|
|
93
|
+
} else if (tool === 'browser_navigate' || RESUME_WITHOUT_PROBE.has(tool)) {
|
|
94
|
+
return at;
|
|
95
|
+
} else {
|
|
96
|
+
// A key press with no target (Enter, say) acts on whatever has focus:
|
|
97
|
+
// there is nothing to probe, so it is never the first thing a resume does.
|
|
98
|
+
reason = 'the step has no target to check';
|
|
99
|
+
}
|
|
100
|
+
if (plan.anchor >= 0) return plan.anchor;
|
|
101
|
+
throw new RunError(
|
|
102
|
+
'RESUME_UNVERIFIED',
|
|
103
|
+
`The page is not where step ${at + 1} of "${wf.name}" expects it (${reason}), ` +
|
|
104
|
+
'and the workflow has no earlier page load to go back to, so nothing was run.',
|
|
105
|
+
'Open the page the workflow starts on, or save it with a browser_navigate first, then run it from the start.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
28
109
|
/**
|
|
29
110
|
* @param {object} deps
|
|
30
111
|
* @param {(job: object) => Promise<object>} deps.execute runs the batch for a job; resolves with the bulk result
|
|
@@ -43,7 +124,7 @@ function createRunRegistry({ execute, onFinish = () => {}, now = Date.now }) {
|
|
|
43
124
|
* Start a batch. `paramSets` and `labels` are kept in memory only.
|
|
44
125
|
* Throws RunError(TAB_BUSY) while another run is going.
|
|
45
126
|
*/
|
|
46
|
-
function start({ wf, paramSets, labels = [], onError = 'continue' }) {
|
|
127
|
+
function start({ wf, paramSets, labels = [], onError = 'continue', resume = [] }) {
|
|
47
128
|
const running = active();
|
|
48
129
|
if (running) {
|
|
49
130
|
throw new RunError('TAB_BUSY', `"${running.workflow}" is still running in the background (${running.runId}).`,
|
|
@@ -57,6 +138,7 @@ function createRunRegistry({ execute, onFinish = () => {}, now = Date.now }) {
|
|
|
57
138
|
total: paramSets.length,
|
|
58
139
|
paramSets,
|
|
59
140
|
labels,
|
|
141
|
+
resume,
|
|
60
142
|
wf,
|
|
61
143
|
onError,
|
|
62
144
|
state: 'running',
|
|
@@ -130,15 +212,19 @@ function createRunRegistry({ execute, onFinish = () => {}, now = Date.now }) {
|
|
|
130
212
|
throw new RunError('TAB_BUSY', `"${job.workflow}" (${runId}) is still running.`, 'Wait for it to finish, or cancel it.');
|
|
131
213
|
}
|
|
132
214
|
const runs = job.result?.evidence?.runs ?? [];
|
|
133
|
-
const finished = new Map(runs.map((r) => [r.index, r
|
|
215
|
+
const finished = new Map(runs.map((r) => [r.index, r]));
|
|
134
216
|
const indexes = [];
|
|
135
|
-
for (let i = 0; i < job.total; i++) if (finished.get(i) !== true) indexes.push(i);
|
|
217
|
+
for (let i = 0; i < job.total; i++) if (finished.get(i)?.ok !== true) indexes.push(i);
|
|
136
218
|
return {
|
|
137
219
|
job,
|
|
138
220
|
indexes,
|
|
139
221
|
paramSets: indexes.map((i) => job.paramSets[i]),
|
|
140
222
|
// A set keeps its name from the first run ("#3" stays "#3").
|
|
141
223
|
labels: indexes.map((i) => job.labels[i] ?? `#${i + 1}`),
|
|
224
|
+
// Where each set may resume (0.6.0): the failed step, or the nearest
|
|
225
|
+
// navigation anchor when the page has moved since the last verified step.
|
|
226
|
+
// A resume at the failed step is probed first (probeResume, A6-09).
|
|
227
|
+
resume: indexes.map((i) => resumePlan(job.wf, finished.get(i))),
|
|
142
228
|
};
|
|
143
229
|
}
|
|
144
230
|
|
|
@@ -194,6 +280,7 @@ function compactBatch(result) {
|
|
|
194
280
|
needAttention: ev.needAttention ?? attention.length,
|
|
195
281
|
notRun: ev.notRun ?? 0,
|
|
196
282
|
skippedSteps: (ev.runs ?? []).reduce((n, r) => n + (r.skipped?.length ?? 0), 0),
|
|
283
|
+
resolvedSteps: (ev.runs ?? []).reduce((n, r) => n + (r.resolvedSteps ?? 0), 0),
|
|
197
284
|
recovered: (ev.runs ?? []).filter((r) => r.recoveries?.length).length,
|
|
198
285
|
elapsedMs: ev.elapsedMs ?? null,
|
|
199
286
|
report: ev.report ?? null,
|
|
@@ -201,4 +288,4 @@ function compactBatch(result) {
|
|
|
201
288
|
};
|
|
202
289
|
}
|
|
203
290
|
|
|
204
|
-
module.exports = { createRunRegistry, compactBatch, RunError };
|
|
291
|
+
module.exports = { createRunRegistry, compactBatch, resumeAtFor, resumePlan, navigationAnchor, probeResume, RunError };
|
package/bridge-server/cli-run.js
CHANGED
|
@@ -75,6 +75,8 @@ function describe(env) {
|
|
|
75
75
|
`${f?.likelyCause ? `, ${f.likelyCause}` : ''}${f?.screenshot ? `\n screenshot: ${f.screenshot}` : ''}`);
|
|
76
76
|
}
|
|
77
77
|
if (env.evidence?.report) lines.push(`Report: ${env.evidence.report} (browserzap --runs)`);
|
|
78
|
+
const resolved = runs.reduce((n, r) => n + (r.resolvedSteps ?? 0), 0);
|
|
79
|
+
if (resolved) lines.push(` ${resolved} step(s) resolved in place (a wait, then the same step again).`);
|
|
78
80
|
if (!runs.length && !env.ok && env.nextAction) lines.push(env.nextAction);
|
|
79
81
|
return lines.join('\n');
|
|
80
82
|
}
|
package/bridge-server/limits.js
CHANGED
|
@@ -26,6 +26,13 @@ const LIMITS = Object.freeze({
|
|
|
26
26
|
SETTLE_QUIET_MS: 500,
|
|
27
27
|
/** How long that settle waits for a busy page (it costs nothing on a quiet one). */
|
|
28
28
|
SETTLE_MAX_MS: 5000,
|
|
29
|
+
/** A wait whose condition makes no progress for this long ends early: a page that is stuck does not get better. */
|
|
30
|
+
STALL_MS: 3000,
|
|
31
|
+
/**
|
|
32
|
+
* Before "Re-run failed" resumes at the step that failed, that step's target
|
|
33
|
+
* must be actionable within this. A bounded probe, not a pause (A6-09).
|
|
34
|
+
*/
|
|
35
|
+
RESUME_PROBE_MS: 3000,
|
|
29
36
|
/** Longest per-step pacing a person may set on a saved workflow step. */
|
|
30
37
|
MAX_STEP_PACING_MS: 30000,
|
|
31
38
|
/** A tab with no agent action for this long is detached, so Chrome's "debugging" bar goes away (D-35). */
|
|
@@ -60,6 +67,16 @@ const STATUS_DEADLINE_MS = 4_000;
|
|
|
60
67
|
*/
|
|
61
68
|
const RUN_DEADLINE_MS = 600_000;
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Resolver engine bounds (`bridge-server/resolvers.js`, 0.6.0, D-37).
|
|
72
|
+
* A step may spend at most MAX_STEP_RESOLVE_MS resolving blockers in place,
|
|
73
|
+
* across at most MAX_RESOLVER_ATTEMPTS attempts, and a reversible click with
|
|
74
|
+
* no effect at all is repeated at most MAX_INPLACE_CLICK_RETRIES times (AU-08).
|
|
75
|
+
*/
|
|
76
|
+
const MAX_STEP_RESOLVE_MS = 45_000;
|
|
77
|
+
const MAX_RESOLVER_ATTEMPTS = 2;
|
|
78
|
+
const MAX_INPLACE_CLICK_RETRIES = 2;
|
|
79
|
+
|
|
63
80
|
/**
|
|
64
81
|
* How long the caller waits for the extension to answer one call: the sum of
|
|
65
82
|
* every wait the call can do, plus a margin, and never less than
|
|
@@ -97,4 +114,13 @@ function callDeadlineMs(tool, args = {}) {
|
|
|
97
114
|
return Math.max(CALL_DEADLINE_MS, waits + DEADLINE_MARGIN_MS);
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
module.exports = {
|
|
117
|
+
module.exports = {
|
|
118
|
+
LIMITS,
|
|
119
|
+
CALL_DEADLINE_MS,
|
|
120
|
+
STATUS_DEADLINE_MS,
|
|
121
|
+
RUN_DEADLINE_MS,
|
|
122
|
+
MAX_STEP_RESOLVE_MS,
|
|
123
|
+
MAX_RESOLVER_ATTEMPTS,
|
|
124
|
+
MAX_INPLACE_CLICK_RETRIES,
|
|
125
|
+
callDeadlineMs,
|
|
126
|
+
};
|
|
@@ -41,7 +41,7 @@ const workflows = require('./workflows');
|
|
|
41
41
|
const screenshots = require('./screenshots');
|
|
42
42
|
const feedback = require('./feedback');
|
|
43
43
|
const downloads = require('./downloads');
|
|
44
|
-
const { createRunRegistry, compactBatch, RunError } = require('./background-runs');
|
|
44
|
+
const { createRunRegistry, compactBatch, probeResume, RunError } = require('./background-runs');
|
|
45
45
|
|
|
46
46
|
const { WorkflowError, Recorder } = workflows;
|
|
47
47
|
|
|
@@ -929,7 +929,7 @@ async function runTool(requestId, traceId, args, progress) {
|
|
|
929
929
|
* redaction as screenshot_capture).
|
|
930
930
|
*/
|
|
931
931
|
async function runBatchOrOnce({
|
|
932
|
-
wf, params, paramSets, labels, onError, startAt = 0, approvalId, signal,
|
|
932
|
+
wf, params, paramSets, labels, onError, startAt = 0, approvalId, signal, resume = [],
|
|
933
933
|
onCall = () => {}, onStep = () => {}, onSet = () => {}, onSetDone = () => {},
|
|
934
934
|
}) {
|
|
935
935
|
const execute = async (tool, stepArgs, { allowedOrigins }) => {
|
|
@@ -946,14 +946,14 @@ async function runBatchOrOnce({
|
|
|
946
946
|
const { env } = await executeTool('screenshot_capture', {}, { allowedOrigins: ALLOWED_ORIGINS });
|
|
947
947
|
return env.ok ? env.screenshotPath ?? null : null;
|
|
948
948
|
};
|
|
949
|
-
const runOnce = async (setParams, first) => {
|
|
949
|
+
const runOnce = async (setParams, first, setStartAt = 0) => {
|
|
950
950
|
const startedAt = new Date().toISOString();
|
|
951
951
|
const history = workflows.runHistory(wf);
|
|
952
952
|
const result = await workflows.run(wf, {
|
|
953
953
|
history,
|
|
954
954
|
onStep,
|
|
955
955
|
params: setParams,
|
|
956
|
-
startAt: first ? startAt : 0,
|
|
956
|
+
startAt: setStartAt || (first ? startAt : 0),
|
|
957
957
|
approvalId: first ? approvalId : undefined,
|
|
958
958
|
execute,
|
|
959
959
|
capture,
|
|
@@ -964,7 +964,7 @@ async function runBatchOrOnce({
|
|
|
964
964
|
service.recordRun({ wf, result, params: setParams, startedAt });
|
|
965
965
|
return result;
|
|
966
966
|
};
|
|
967
|
-
if (paramSets === undefined) return runOnce(params, true);
|
|
967
|
+
if (paramSets === undefined) return runOnce(params, true, startAt);
|
|
968
968
|
let first = true;
|
|
969
969
|
return pro.runBulk({
|
|
970
970
|
wf,
|
|
@@ -972,9 +972,22 @@ async function runBatchOrOnce({
|
|
|
972
972
|
labels,
|
|
973
973
|
onError,
|
|
974
974
|
signal,
|
|
975
|
+
resume,
|
|
976
|
+
// A resume at the failed step is probed first, as a real wait_for_state
|
|
977
|
+
// through executeTool; the page has moved, so the set starts at the
|
|
978
|
+
// navigation anchor instead (A6-09). With no anchor it fails without acting.
|
|
979
|
+
resumeStart: ({ plan, params: setParams }) => probeResume({
|
|
980
|
+
wf, plan, params: setParams, execute, allowedOrigins: ALLOWED_ORIGINS,
|
|
981
|
+
}),
|
|
975
982
|
onSet,
|
|
976
983
|
onSetDone,
|
|
977
|
-
|
|
984
|
+
// A re-run of the sets that failed resumes at the step that failed where
|
|
985
|
+
// the page allows it (0.6.0), instead of starting every set at step 1.
|
|
986
|
+
runOnce: (setParams, setStartAt) => {
|
|
987
|
+
const r = runOnce(setParams, first, setStartAt ?? 0);
|
|
988
|
+
first = false;
|
|
989
|
+
return r;
|
|
990
|
+
},
|
|
978
991
|
});
|
|
979
992
|
}
|
|
980
993
|
|
|
@@ -985,6 +998,7 @@ const backgroundRuns = createRunRegistry({
|
|
|
985
998
|
paramSets: job.paramSets,
|
|
986
999
|
labels: job.labels,
|
|
987
1000
|
onError: job.onError,
|
|
1001
|
+
resume: job.resume,
|
|
988
1002
|
signal: job.controller.signal,
|
|
989
1003
|
onCall: (c) => { job.call = c; },
|
|
990
1004
|
onStep: ({ index }) => backgroundRuns.progress(job, { stepIndex: index }),
|
|
@@ -1040,14 +1054,16 @@ function statusOfRuns(traceId, args) {
|
|
|
1040
1054
|
}
|
|
1041
1055
|
|
|
1042
1056
|
function rerunFailedTool(traceId, args) {
|
|
1043
|
-
const { job: previous, paramSets, labels } = backgroundRuns.failedSets(args.rerunFailed);
|
|
1057
|
+
const { job: previous, paramSets, labels, resume } = backgroundRuns.failedSets(args.rerunFailed);
|
|
1044
1058
|
if (previous.workflow !== args.name) {
|
|
1045
1059
|
return errorEnvelope(traceId, 'INVALID_ARGUMENT', `${previous.runId} ran "${previous.workflow}", not "${args.name}".`);
|
|
1046
1060
|
}
|
|
1047
1061
|
if (paramSets.length === 0) return okEnvelope(traceId, `Every set of ${previous.runId} completed; nothing to run again.`);
|
|
1048
|
-
const job = backgroundRuns.start({ wf: workflows.load(previous.workflow), paramSets, labels, onError: 'continue' });
|
|
1062
|
+
const job = backgroundRuns.start({ wf: workflows.load(previous.workflow), paramSets, labels, onError: 'continue', resume });
|
|
1063
|
+
const resumed = resume.filter((plan) => plan?.at > 0).length;
|
|
1049
1064
|
return okEnvelope(traceId,
|
|
1050
|
-
`Running the ${paramSets.length} set(s) of ${previous.runId} that did not complete
|
|
1065
|
+
`Running the ${paramSets.length} set(s) of ${previous.runId} that did not complete` +
|
|
1066
|
+
`${resumed ? `, ${resumed} from the step that failed` : ''}, in the background (${job.runId}).`,
|
|
1051
1067
|
{ runId: job.runId }, { paramSets: paramSets.length, rerunOf: previous.runId });
|
|
1052
1068
|
}
|
|
1053
1069
|
|
package/bridge-server/panel.js
CHANGED
|
@@ -38,6 +38,7 @@ const { CALL_DEADLINE_MS } = require('./limits');
|
|
|
38
38
|
const { createWorkflowService, checkStep, stepLogEntry } = require('./workflow-service');
|
|
39
39
|
const { createSessionLog, describeResult } = require('./session-log');
|
|
40
40
|
const { parseParamSets } = require('./param-sets');
|
|
41
|
+
const { resumePlan, probeResume } = require('./background-runs');
|
|
41
42
|
const screenshots = require('./screenshots');
|
|
42
43
|
const feedback = require('./feedback');
|
|
43
44
|
|
|
@@ -218,7 +219,13 @@ function createPanelRunner({ deliver, toExtension, log = () => {} }) {
|
|
|
218
219
|
if (ok) current.batch.done += 1;
|
|
219
220
|
else current.batch.failed += 1;
|
|
220
221
|
},
|
|
221
|
-
|
|
222
|
+
// "Re-run failed" probes the failed step's target before resuming there,
|
|
223
|
+
// as a real wait_for_state; a page that has moved starts at the
|
|
224
|
+
// navigation anchor, and with no anchor the set fails without acting (A6-09).
|
|
225
|
+
resumeStart: ({ plan, params: setParams }) => probeResume({
|
|
226
|
+
wf, plan, params: setParams, execute, allowedOrigins: wf.origins,
|
|
227
|
+
}),
|
|
228
|
+
runOnce: (setParams, setStartAt) => runOnce(setParams, setStartAt ?? 0),
|
|
222
229
|
});
|
|
223
230
|
const runs = result.evidence?.runs ?? [];
|
|
224
231
|
lastBatch = {
|
|
@@ -226,6 +233,9 @@ function createPanelRunner({ deliver, toExtension, log = () => {} }) {
|
|
|
226
233
|
paramSets: batch.paramSets,
|
|
227
234
|
labels: batch.labels,
|
|
228
235
|
unfinished: batch.paramSets.map((_, i) => i).filter((i) => !runs.some((r) => r.index === i && r.ok)),
|
|
236
|
+
// Where each set may resume when the person presses "Re-run failed" (0.6.0):
|
|
237
|
+
// the plan a probe proves before it is used (A6-09).
|
|
238
|
+
resumeByIndex: Object.fromEntries(runs.filter((r) => !r.ok).map((r) => [r.index, resumePlan(wf, r)])),
|
|
229
239
|
};
|
|
230
240
|
// What the person needs to find each problem: step, cause, screenshot. Never values.
|
|
231
241
|
const attention = runs.filter((r) => !r.ok).map((r) => ({
|
|
@@ -241,7 +251,7 @@ function createPanelRunner({ deliver, toExtension, log = () => {} }) {
|
|
|
241
251
|
status: result.ok ? 'completed' : controller.signal.aborted ? 'cancelled' : 'failed',
|
|
242
252
|
summary: result.summary,
|
|
243
253
|
code: result.error?.code ?? null,
|
|
244
|
-
nextAction: lastBatch.unfinished.length ? `${lastBatch.unfinished.length} set(s) did not complete. "Re-run failed" runs just those, from the
|
|
254
|
+
nextAction: lastBatch.unfinished.length ? `${lastBatch.unfinished.length} set(s) did not complete. "Re-run failed" runs just those, from the step that failed where the page allows it.` : null,
|
|
245
255
|
canContinueFrom: null,
|
|
246
256
|
batch: { ...current.batch, attention, report: result.evidence?.report ?? null, canRerun: lastBatch.unfinished.length > 0 },
|
|
247
257
|
finishedAt: new Date().toISOString(),
|
|
@@ -396,7 +406,8 @@ function createPanelRunner({ deliver, toExtension, log = () => {} }) {
|
|
|
396
406
|
const wf = workflows.load(lastBatch.workflow);
|
|
397
407
|
const paramSets = lastBatch.unfinished.map((i) => lastBatch.paramSets[i]);
|
|
398
408
|
const labels = lastBatch.unfinished.map((i) => lastBatch.labels[i] ?? `#${i + 1}`);
|
|
399
|
-
|
|
409
|
+
const resume = lastBatch.unfinished.map((i) => lastBatch.resumeByIndex?.[i] ?? null);
|
|
410
|
+
runWorkflow(wf, {}, 0, profile, { paramSets, labels, resume }).catch((err) => log('panel_run_error', { message: err.message }));
|
|
400
411
|
return { ok: true, data: { runId: current.runId, sets: paramSets.length } };
|
|
401
412
|
}
|
|
402
413
|
case 'run':
|