autokap 1.0.8 → 1.1.0
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/assets/skill/OPCODE-REFERENCE.md +29 -1
- package/assets/skill/SKILL.md +2 -1
- package/dist/auth-capture.js +35 -2
- package/dist/billing-operation-logging.d.ts +4 -3
- package/dist/billing-operation-logging.js +3 -2
- package/dist/browser.d.ts +10 -10
- package/dist/browser.js +32 -28
- package/dist/capture-encryption.d.ts +3 -1
- package/dist/capture-encryption.js +21 -6
- package/dist/capture-strategy.js +3 -2
- package/dist/cli-config.d.ts +2 -1
- package/dist/cli-config.js +51 -2
- package/dist/cli-contract.d.ts +5 -1
- package/dist/cli-contract.js +7 -1
- package/dist/cli-runner-local.js +16 -3
- package/dist/cli-runner.js +165 -18
- package/dist/cli.js +25 -19
- package/dist/clip-begin-frame-recorder.d.ts +44 -0
- package/dist/clip-begin-frame-recorder.js +250 -0
- package/dist/clip-capture-backend.d.ts +25 -0
- package/dist/clip-capture-backend.js +189 -0
- package/dist/clip-capture-loop.d.ts +61 -0
- package/dist/clip-capture-loop.js +111 -0
- package/dist/clip-frame-recorder.d.ts +63 -0
- package/dist/clip-frame-recorder.js +305 -0
- package/dist/clip-postprocess.d.ts +31 -2
- package/dist/clip-postprocess.js +174 -57
- package/dist/clip-runtime.d.ts +18 -0
- package/dist/clip-runtime.js +67 -0
- package/dist/clip-scale.d.ts +10 -0
- package/dist/clip-scale.js +21 -0
- package/dist/clip-screencast-recorder.d.ts +42 -0
- package/dist/clip-screencast-recorder.js +242 -0
- package/dist/clip-sidecar.d.ts +54 -0
- package/dist/clip-sidecar.js +208 -0
- package/dist/cost-logging.d.ts +1 -1
- package/dist/env-validation.js +38 -4
- package/dist/execution-schema.d.ts +690 -360
- package/dist/execution-schema.js +98 -42
- package/dist/execution-types.d.ts +53 -3
- package/dist/execution-types.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/llm-healer.d.ts +2 -10
- package/dist/llm-healer.js +109 -62
- package/dist/llm-provider.js +3 -0
- package/dist/opcode-actions.js +13 -0
- package/dist/opcode-runner.js +21 -12
- package/dist/program-signing.d.ts +1094 -0
- package/dist/program-signing.js +140 -0
- package/dist/provider-config.d.ts +5 -0
- package/dist/provider-config.js +28 -1
- package/dist/recovery-chain.js +40 -16
- package/dist/server-credit-usage.d.ts +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/web-playwright-local.d.ts +31 -1
- package/dist/web-playwright-local.js +207 -37
- package/package.json +12 -2
package/dist/llm-healer.js
CHANGED
|
@@ -3,19 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Last-resort recovery: when all deterministic strategies fail,
|
|
5
5
|
* the healer asks an LLM to analyze the current page state and
|
|
6
|
-
* produce a
|
|
7
|
-
*
|
|
8
|
-
* Constraints:
|
|
9
|
-
* - Cannot change the preset's intention
|
|
10
|
-
* - Cannot change variant order
|
|
11
|
-
* - Cannot invent new capture targets
|
|
12
|
-
* - Cannot skip the failed opcode's postcondition
|
|
13
|
-
* - Max 1 LLM call per healing attempt
|
|
14
|
-
* - Max 3 healing attempts per run
|
|
6
|
+
* produce a bounded selector patch.
|
|
15
7
|
*/
|
|
16
|
-
import { ExecutionOpcodeSchema } from './execution-schema.js';
|
|
17
8
|
/**
|
|
18
|
-
* The LLM Healer — a constrained agent that repairs failed
|
|
9
|
+
* The LLM Healer — a constrained agent that repairs failed selectors.
|
|
19
10
|
*/
|
|
20
11
|
export class LLMHealer {
|
|
21
12
|
llmProvider;
|
|
@@ -36,34 +27,27 @@ export class LLMHealer {
|
|
|
36
27
|
try {
|
|
37
28
|
const prompt = buildHealerPrompt(context);
|
|
38
29
|
const { response, llmResult } = await this.llmProvider.call(prompt, context.screenshot);
|
|
39
|
-
|
|
40
|
-
const parsed = parseHealerResponse(response, context);
|
|
30
|
+
const parsed = parseHealerResponse(response);
|
|
41
31
|
if (!parsed) {
|
|
42
32
|
return { healed: false, reason: 'healer produced invalid response', llmResult };
|
|
43
33
|
}
|
|
44
34
|
if (parsed.cannotHeal) {
|
|
45
35
|
return { healed: false, reason: parsed.reason ?? 'healer cannot resolve this failure', llmResult };
|
|
46
36
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const result = ExecutionOpcodeSchema.safeParse(opcode);
|
|
51
|
-
if (!result.success) {
|
|
52
|
-
return { healed: false, reason: `healer produced invalid opcode: ${result.error}`, llmResult };
|
|
53
|
-
}
|
|
54
|
-
validOpcodes.push(result.data);
|
|
55
|
-
}
|
|
56
|
-
if (validOpcodes.length === 0 || validOpcodes.length > 3) {
|
|
57
|
-
return { healed: false, reason: `healer produced ${validOpcodes.length} opcodes (expected 1-3)`, llmResult };
|
|
37
|
+
const replacementOpcode = buildReplacementOpcode(context.failedOpcode, parsed);
|
|
38
|
+
if (!replacementOpcode) {
|
|
39
|
+
return { healed: false, reason: 'healer could not produce a bounded selector patch', llmResult };
|
|
58
40
|
}
|
|
59
41
|
const patch = {
|
|
60
42
|
opcodeIndex: context.opcodeIndex,
|
|
61
43
|
originalOpcode: context.failedOpcode,
|
|
62
|
-
replacementOpcodes:
|
|
63
|
-
|
|
44
|
+
replacementOpcodes: [replacementOpcode],
|
|
45
|
+
patchType: 'selector_patch',
|
|
46
|
+
interactionMode: parsed.interactionMode,
|
|
47
|
+
reason: parsed.reason ?? 'healer selector repair',
|
|
64
48
|
patchedAt: new Date().toISOString(),
|
|
65
49
|
};
|
|
66
|
-
return { healed: true, patch, reason:
|
|
50
|
+
return { healed: true, patch, reason: patch.reason, llmResult };
|
|
67
51
|
}
|
|
68
52
|
catch (err) {
|
|
69
53
|
return {
|
|
@@ -73,48 +57,50 @@ export class LLMHealer {
|
|
|
73
57
|
}
|
|
74
58
|
}
|
|
75
59
|
}
|
|
76
|
-
// ── Prompt construction ─────────────────────────────────────────────
|
|
77
60
|
function buildHealerPrompt(context) {
|
|
78
61
|
const { failedOpcode, errorMessage, akTreeSerialized, currentUrl, surroundingOpcodes } = context;
|
|
62
|
+
const selector = hasPatchableSelector(failedOpcode) ? failedOpcode.selector : null;
|
|
79
63
|
const surroundingDesc = surroundingOpcodes
|
|
80
|
-
.map((op, i) => ` ${i + 1}. ${op.kind}: ${op.description}`)
|
|
64
|
+
.map((op, i) => ` ${i + 1}. ${op.kind}: ${sanitizePromptText(op.description)}`)
|
|
81
65
|
.join('\n');
|
|
82
|
-
return `You are a web automation repair agent. An opcode in a deterministic browser automation program has failed. Your job is to
|
|
66
|
+
return `You are a web automation repair agent. An opcode in a deterministic browser automation program has failed. Your job is to repair the selector only.
|
|
83
67
|
|
|
84
68
|
## Failed Opcode
|
|
85
69
|
Kind: ${failedOpcode.kind}
|
|
86
|
-
Description: ${failedOpcode.description}
|
|
87
|
-
|
|
88
|
-
${failedOpcode.kind === 'TYPE' ? `Selector: ${failedOpcode.selector}, Text: ${failedOpcode.text}` : ''}
|
|
89
|
-
${failedOpcode.kind === 'WAIT_FOR' ? `Selector: ${failedOpcode.selector}` : ''}
|
|
70
|
+
Description: ${sanitizePromptText(failedOpcode.description)}
|
|
71
|
+
Selector: ${selector ?? '<unsupported>'}
|
|
90
72
|
|
|
91
73
|
## Error
|
|
92
|
-
|
|
74
|
+
<<<ERROR>>>
|
|
75
|
+
${sanitizePromptText(errorMessage)}
|
|
76
|
+
<<<END_ERROR>>>
|
|
93
77
|
|
|
94
78
|
## Current Page
|
|
95
|
-
URL: ${currentUrl}
|
|
79
|
+
URL: ${sanitizeUrlForPrompt(currentUrl)}
|
|
96
80
|
|
|
97
81
|
## AKTree (current page structure)
|
|
98
|
-
|
|
82
|
+
<<<AKTREE>>>
|
|
83
|
+
${sanitizePromptText(akTreeSerialized).slice(0, 8000)}
|
|
84
|
+
<<<END_AKTREE>>>
|
|
99
85
|
|
|
100
86
|
## Surrounding Opcodes (context)
|
|
101
87
|
${surroundingDesc}
|
|
102
88
|
|
|
103
89
|
## Rules
|
|
104
|
-
1. You MUST
|
|
105
|
-
2. You
|
|
106
|
-
3. You
|
|
107
|
-
4.
|
|
108
|
-
5.
|
|
90
|
+
1. You MUST NOT emit opcodes or navigation targets.
|
|
91
|
+
2. You may only return a selector patch for the existing opcode.
|
|
92
|
+
3. You may optionally return a bounded interaction mode: default, keyboard, js_dispatch, or coordinates.
|
|
93
|
+
4. Use selectors you can actually see in the AKTree above.
|
|
94
|
+
5. If the opcode kind is unsupported or there is no reliable selector, return cannotHeal=true.
|
|
109
95
|
|
|
110
96
|
## Response Format
|
|
111
97
|
Respond with a JSON object:
|
|
112
98
|
{
|
|
113
99
|
"cannotHeal": false,
|
|
114
100
|
"reason": "explanation of what you changed",
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
"selector": "[data-ak='submit']",
|
|
102
|
+
"selectorAlternates": ["button[type='submit']"],
|
|
103
|
+
"interactionMode": "default"
|
|
118
104
|
}
|
|
119
105
|
|
|
120
106
|
Or if you cannot fix it:
|
|
@@ -125,42 +111,103 @@ Or if you cannot fix it:
|
|
|
125
111
|
|
|
126
112
|
Respond ONLY with the JSON object, no markdown, no explanation outside JSON.`;
|
|
127
113
|
}
|
|
128
|
-
function parseHealerResponse(response
|
|
114
|
+
function parseHealerResponse(response) {
|
|
129
115
|
try {
|
|
130
|
-
// Extract JSON from response (handle markdown code blocks)
|
|
131
116
|
const jsonStr = response
|
|
132
117
|
.replace(/```json\s*/g, '')
|
|
133
118
|
.replace(/```\s*/g, '')
|
|
134
119
|
.trim();
|
|
135
120
|
const parsed = JSON.parse(jsonStr);
|
|
136
|
-
if (typeof parsed !== 'object'
|
|
121
|
+
if (!parsed || typeof parsed !== 'object')
|
|
137
122
|
return null;
|
|
138
123
|
if (parsed.cannotHeal) {
|
|
139
124
|
return {
|
|
140
125
|
cannotHeal: true,
|
|
141
|
-
reason: parsed.reason
|
|
142
|
-
|
|
126
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : null,
|
|
127
|
+
selector: null,
|
|
128
|
+
selectorAlternates: [],
|
|
129
|
+
interactionMode: 'default',
|
|
143
130
|
};
|
|
144
131
|
}
|
|
145
|
-
|
|
132
|
+
const selector = typeof parsed.selector === 'string' && parsed.selector.trim()
|
|
133
|
+
? parsed.selector.trim()
|
|
134
|
+
: null;
|
|
135
|
+
if (!selector)
|
|
146
136
|
return null;
|
|
147
|
-
// Inject the original postcondition and recovery into replacement opcodes
|
|
148
|
-
// (healer cannot skip postconditions)
|
|
149
|
-
const enriched = parsed.replacementOpcodes.map((op) => ({
|
|
150
|
-
...op,
|
|
151
|
-
postcondition: op.postcondition ?? context.failedOpcode.postcondition,
|
|
152
|
-
recovery: op.recovery ?? context.failedOpcode.recovery,
|
|
153
|
-
timeoutMs: op.timeoutMs ?? context.failedOpcode.timeoutMs,
|
|
154
|
-
maxFailures: op.maxFailures ?? context.failedOpcode.maxFailures,
|
|
155
|
-
}));
|
|
156
137
|
return {
|
|
157
138
|
cannotHeal: false,
|
|
158
|
-
reason: parsed.reason
|
|
159
|
-
|
|
139
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : null,
|
|
140
|
+
selector,
|
|
141
|
+
selectorAlternates: Array.isArray(parsed.selectorAlternates)
|
|
142
|
+
? parsed.selectorAlternates
|
|
143
|
+
.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
144
|
+
.map((entry) => entry.trim())
|
|
145
|
+
.slice(0, 5)
|
|
146
|
+
: [],
|
|
147
|
+
interactionMode: parsed.interactionMode === 'keyboard'
|
|
148
|
+
|| parsed.interactionMode === 'js_dispatch'
|
|
149
|
+
|| parsed.interactionMode === 'coordinates'
|
|
150
|
+
? parsed.interactionMode
|
|
151
|
+
: 'default',
|
|
160
152
|
};
|
|
161
153
|
}
|
|
162
154
|
catch {
|
|
163
155
|
return null;
|
|
164
156
|
}
|
|
165
157
|
}
|
|
158
|
+
function hasPatchableSelector(opcode) {
|
|
159
|
+
return ((opcode.kind === 'CLICK'
|
|
160
|
+
|| opcode.kind === 'TYPE'
|
|
161
|
+
|| opcode.kind === 'WAIT_FOR'
|
|
162
|
+
|| opcode.kind === 'HOVER'
|
|
163
|
+
|| opcode.kind === 'SELECT_OPTION'
|
|
164
|
+
|| opcode.kind === 'CHECK'
|
|
165
|
+
|| opcode.kind === 'DOUBLE_CLICK')
|
|
166
|
+
&& typeof opcode.selector === 'string'
|
|
167
|
+
&& opcode.selector.trim().length > 0);
|
|
168
|
+
}
|
|
169
|
+
function buildReplacementOpcode(failedOpcode, parsed) {
|
|
170
|
+
if (!hasPatchableSelector(failedOpcode) || !parsed.selector) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
switch (failedOpcode.kind) {
|
|
174
|
+
case 'CLICK':
|
|
175
|
+
case 'TYPE':
|
|
176
|
+
case 'WAIT_FOR':
|
|
177
|
+
case 'HOVER':
|
|
178
|
+
case 'SELECT_OPTION':
|
|
179
|
+
case 'CHECK':
|
|
180
|
+
case 'DOUBLE_CLICK':
|
|
181
|
+
return {
|
|
182
|
+
...failedOpcode,
|
|
183
|
+
selector: parsed.selector,
|
|
184
|
+
...('selectorAlternates' in failedOpcode
|
|
185
|
+
? { selectorAlternates: parsed.selectorAlternates }
|
|
186
|
+
: {}),
|
|
187
|
+
};
|
|
188
|
+
default:
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function sanitizePromptText(value) {
|
|
193
|
+
return value
|
|
194
|
+
.replace(/\{\{(email|password|loginUrl)\}\}/g, '<credential-placeholder>')
|
|
195
|
+
.replace(/data:[^"' )]+/gi, '<data-url>')
|
|
196
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (match) => sanitizeUrlForPrompt(match))
|
|
197
|
+
.replace(/[^\x09\x0a\x0d\x20-\x7e]/g, ' ')
|
|
198
|
+
.slice(0, 12_000);
|
|
199
|
+
}
|
|
200
|
+
function sanitizeUrlForPrompt(value) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = new URL(value);
|
|
203
|
+
parsed.username = '';
|
|
204
|
+
parsed.password = '';
|
|
205
|
+
parsed.search = parsed.search ? '?<redacted>' : '';
|
|
206
|
+
parsed.hash = parsed.hash ? '#<redacted>' : '';
|
|
207
|
+
return parsed.toString();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return value.slice(0, 512);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
166
213
|
//# sourceMappingURL=llm-healer.js.map
|
package/dist/llm-provider.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import OpenAI from 'openai';
|
|
8
8
|
import { createOpenRouterCompletion } from './openrouter-client.js';
|
|
9
|
+
import { assertZdrCompatibleModel, zdrParam } from './provider-config.js';
|
|
9
10
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
10
11
|
const DEFAULT_MODEL = 'google/gemini-2.5-flash';
|
|
11
12
|
/**
|
|
@@ -19,6 +20,7 @@ export async function callLLM(config, systemPrompt, userContent, image) {
|
|
|
19
20
|
});
|
|
20
21
|
const model = config.model ?? DEFAULT_MODEL;
|
|
21
22
|
const maxTokens = config.maxTokens ?? 200;
|
|
23
|
+
assertZdrCompatibleModel(model);
|
|
22
24
|
const messages = [
|
|
23
25
|
{ role: 'system', content: systemPrompt },
|
|
24
26
|
];
|
|
@@ -43,6 +45,7 @@ export async function callLLM(config, systemPrompt, userContent, image) {
|
|
|
43
45
|
messages,
|
|
44
46
|
max_tokens: maxTokens,
|
|
45
47
|
temperature: 0.1,
|
|
48
|
+
provider: Object.keys(zdrParam()).length > 0 ? zdrParam() : undefined,
|
|
46
49
|
};
|
|
47
50
|
const controller = new AbortController();
|
|
48
51
|
const timer = setTimeout(() => controller.abort(), config.timeoutMs ?? 15000);
|
package/dist/opcode-actions.js
CHANGED
|
@@ -171,6 +171,19 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
|
|
|
171
171
|
return { success: false, error: 'adapter does not support DOUBLE_CLICK' };
|
|
172
172
|
await adapter.doubleClick(opcode.selector);
|
|
173
173
|
break;
|
|
174
|
+
case 'DRAG':
|
|
175
|
+
if (!adapter.drag)
|
|
176
|
+
return { success: false, error: 'adapter does not support DRAG' };
|
|
177
|
+
await adapter.drag({
|
|
178
|
+
selector: opcode.selector,
|
|
179
|
+
target: opcode.target,
|
|
180
|
+
selectorAlternates: opcode.selectorAlternates,
|
|
181
|
+
toSelector: opcode.toSelector,
|
|
182
|
+
toTarget: opcode.toTarget,
|
|
183
|
+
toSelectorAlternates: opcode.toSelectorAlternates,
|
|
184
|
+
offset: opcode.offset,
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
174
187
|
case 'CLONE_ELEMENT':
|
|
175
188
|
if (!adapter.cloneElement) {
|
|
176
189
|
return { success: false, error: 'adapter does not support CLONE_ELEMENT' };
|
package/dist/opcode-runner.js
CHANGED
|
@@ -53,6 +53,13 @@ export class NoOpRecoveryChain {
|
|
|
53
53
|
return { recovered: false, reason: 'no recovery chain configured' };
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
const MIN_CLIP_FINALIZATION_TIMEOUT_MS = 30000;
|
|
57
|
+
function resolveOpcodeTimeoutMs(opcode) {
|
|
58
|
+
if (opcode.kind === 'END_CLIP') {
|
|
59
|
+
return Math.max(opcode.timeoutMs, MIN_CLIP_FINALIZATION_TIMEOUT_MS);
|
|
60
|
+
}
|
|
61
|
+
return opcode.timeoutMs;
|
|
62
|
+
}
|
|
56
63
|
// ── Main execution function ─────────────────────────────────────────
|
|
57
64
|
export async function executeProgram(program, createAdapter, options = {}) {
|
|
58
65
|
const recoveryChain = options.recoveryChain ?? new NoOpRecoveryChain();
|
|
@@ -238,7 +245,8 @@ function softSkipResult(opcode, index, startTime, reason, telemetry) {
|
|
|
238
245
|
}
|
|
239
246
|
async function executeOpcode(opcode, index, adapter, verifier, breaker, recoveryChain, telemetry, healerPatches, artifacts, options, variantId, executionState, artifactPlan, mockDataGroups, currentVariant, credentials) {
|
|
240
247
|
const startTime = Date.now();
|
|
241
|
-
const
|
|
248
|
+
const effectiveTimeoutMs = resolveOpcodeTimeoutMs(opcode);
|
|
249
|
+
const deadlineMs = startTime + effectiveTimeoutMs;
|
|
242
250
|
const isInteraction = ['CLICK', 'TYPE', 'PRESS_KEY', 'SCROLL'].includes(opcode.kind);
|
|
243
251
|
const isSoft = isSoftOpcodeKind(opcode.kind);
|
|
244
252
|
// Track page context for circuit breaker
|
|
@@ -249,7 +257,7 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
249
257
|
catch { /* ignore */ }
|
|
250
258
|
// Execute with timeout
|
|
251
259
|
try {
|
|
252
|
-
logger.debug(`[opcode ${index}] ${opcode.kind} start — budget ${
|
|
260
|
+
logger.debug(`[opcode ${index}] ${opcode.kind} start — budget ${effectiveTimeoutMs}ms${formatOpcodeDebug(opcode)}`);
|
|
253
261
|
if (isInteraction) {
|
|
254
262
|
const beforeStart = Date.now();
|
|
255
263
|
await verifier.captureBeforeState(adapter);
|
|
@@ -257,11 +265,11 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
257
265
|
}
|
|
258
266
|
const actionBudgetMs = getRemainingTimeMs(deadlineMs);
|
|
259
267
|
if (actionBudgetMs <= 0) {
|
|
260
|
-
const reason = `timeout after ${
|
|
268
|
+
const reason = `timeout after ${effectiveTimeoutMs}ms`;
|
|
261
269
|
logger.debug(`[opcode ${index}] no budget left after captureBeforeState (deadline=${deadlineMs}, now=${Date.now()})`);
|
|
262
270
|
if (isSoft)
|
|
263
271
|
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
264
|
-
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
272
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
|
|
265
273
|
}
|
|
266
274
|
logger.debug(`[opcode ${index}] action exec start — actionBudget ${actionBudgetMs}ms`);
|
|
267
275
|
const actionStart = Date.now();
|
|
@@ -271,16 +279,16 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
271
279
|
const reason = result.error ?? 'action failed';
|
|
272
280
|
if (isSoft)
|
|
273
281
|
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
274
|
-
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
282
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
|
|
275
283
|
}
|
|
276
284
|
// Verify postcondition
|
|
277
285
|
const postconditionBudgetMs = getRemainingTimeMs(deadlineMs);
|
|
278
286
|
if (postconditionBudgetMs <= 0) {
|
|
279
|
-
const reason = `timeout after ${
|
|
287
|
+
const reason = `timeout after ${effectiveTimeoutMs}ms`;
|
|
280
288
|
logger.debug(`[opcode ${index}] no budget left for postcondition check`);
|
|
281
289
|
if (isSoft)
|
|
282
290
|
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
283
|
-
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
291
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
|
|
284
292
|
}
|
|
285
293
|
const postStart = Date.now();
|
|
286
294
|
const postcondition = await evaluatePostcondition(adapter, withClampedPostconditionTimeout(opcode.postcondition, postconditionBudgetMs));
|
|
@@ -289,13 +297,13 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
289
297
|
const reason = `postcondition failed: ${postcondition.reason}`;
|
|
290
298
|
if (isSoft)
|
|
291
299
|
return softSkipResult(opcode, index, startTime, reason, telemetry);
|
|
292
|
-
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, reason);
|
|
300
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
|
|
293
301
|
}
|
|
294
302
|
// Verify action had effect (for interaction opcodes)
|
|
295
303
|
if (isInteraction) {
|
|
296
304
|
const verification = await verifier.verifyAfterAction(adapter);
|
|
297
305
|
if (!verification.hadEffect && opcode.postcondition.type !== 'always' && opcode.postcondition.type !== 'any_change') {
|
|
298
|
-
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, `action had no effect: ${verification.summary}`);
|
|
306
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, `action had no effect: ${verification.summary}`);
|
|
299
307
|
}
|
|
300
308
|
}
|
|
301
309
|
// Record successful mock data application
|
|
@@ -317,11 +325,11 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
|
|
|
317
325
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
318
326
|
if (isSoft)
|
|
319
327
|
return softSkipResult(opcode, index, startTime, errorMsg, telemetry);
|
|
320
|
-
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, errorMsg);
|
|
328
|
+
return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, errorMsg);
|
|
321
329
|
}
|
|
322
330
|
}
|
|
323
331
|
// ── Failure handling with recovery ──────────────────────────────────
|
|
324
|
-
async function handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, errorMsg) {
|
|
332
|
+
async function handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, errorMsg) {
|
|
325
333
|
const breakerState = breaker.recordFailure(index, opcode.maxFailures);
|
|
326
334
|
if (breakerState.tripped) {
|
|
327
335
|
telemetry.circuitBreakerTrips++;
|
|
@@ -342,7 +350,7 @@ async function handleFailure(opcode, index, adapter, verifier, isInteraction, br
|
|
|
342
350
|
status: 'failed',
|
|
343
351
|
durationMs: Date.now() - startTime,
|
|
344
352
|
recoveryAttempts: 0,
|
|
345
|
-
error: `${errorMsg} (timeout after ${
|
|
353
|
+
error: `${errorMsg} (timeout after ${effectiveTimeoutMs}ms)`,
|
|
346
354
|
};
|
|
347
355
|
}
|
|
348
356
|
// Attempt recovery
|
|
@@ -424,6 +432,7 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
|
|
|
424
432
|
case 'SELECT_OPTION':
|
|
425
433
|
case 'CHECK':
|
|
426
434
|
case 'DOUBLE_CLICK':
|
|
435
|
+
case 'DRAG':
|
|
427
436
|
case 'CLONE_ELEMENT':
|
|
428
437
|
case 'INJECT_MOCK_DATA':
|
|
429
438
|
case 'REMOVE_ELEMENT':
|