@debugg-ai/debugg-ai-mcp 3.5.1 → 3.5.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/README.md
CHANGED
|
@@ -226,6 +226,7 @@ Response-shape changes: the bare `count` field on list responses is gone — use
|
|
|
226
226
|
| `DEBUGGAI_API_KEY` | yes | Backend API key. Aliases: `DEBUGGAI_API_TOKEN`, `DEBUGGAI_JWT_TOKEN`. |
|
|
227
227
|
| `DEBUGGAI_API_URL` | no | Backend base URL. Defaults to `https://api.debugg.ai`. |
|
|
228
228
|
| `DEBUGGAI_TOKEN_TYPE` | no | `token` (default) or `bearer`. |
|
|
229
|
+
| `DEBUGGAI_EVAL_TEMPLATE` | no | Override the App Evaluation workflow **slug** that `check_app_in_browser` dispatches to. Defaults to `flow/e2es/app-eval`. Dispatch pins to this slug so a backend template rename can't break it. |
|
|
229
230
|
| `LOG_LEVEL` | no | `error` / `warn` / `info` (default) / `debug`. |
|
|
230
231
|
| `POSTHOG_API_KEY` | no | Override the embedded telemetry project key (e.g. private fork). |
|
|
231
232
|
| `DEBUGGAI_TELEMETRY_DISABLED` | no | Set to `1` / `true` / `yes` / `on` to disable telemetry entirely. |
|
|
@@ -8,6 +8,8 @@ import { Logger } from '../utils/logger.js';
|
|
|
8
8
|
import { handleExternalServiceError } from '../utils/errors.js';
|
|
9
9
|
import { fetchImageAsBase64, imageContentBlock, resourceLinkBlock, artifactResourceLinks } from '../utils/imageUtils.js';
|
|
10
10
|
import { DebuggAIServerClient } from '../services/index.js';
|
|
11
|
+
import { getEvalTemplateSlug } from '../services/workflows.js';
|
|
12
|
+
import { adaptVerdict } from '../services/verdictAdapter.js';
|
|
11
13
|
import { TunnelProvisionError } from '../services/tunnels.js';
|
|
12
14
|
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
|
|
13
15
|
import { detectRepoName } from '../utils/gitContext.js';
|
|
@@ -18,7 +20,6 @@ import { getCachedTemplateUuid, getCachedProjectUuid, invalidateTemplateCache, i
|
|
|
18
20
|
import { isTransientWorkflowError, transientReasonTag } from '../utils/transientErrors.js';
|
|
19
21
|
import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
|
|
20
22
|
const logger = new Logger({ module: 'testPageChangesHandler' });
|
|
21
|
-
const TEMPLATE_NAME = 'app evaluation';
|
|
22
23
|
// Bead kbxy: bounded retry on known transient backend signatures (Pydantic
|
|
23
24
|
// JSON parse errors, 502s, ECONNRESETs). Default 1 retry; env-overridable
|
|
24
25
|
// up to 3 to balance reliability vs quota cost. Conservative: only retries
|
|
@@ -203,7 +204,10 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
203
204
|
}
|
|
204
205
|
const repoName = input.repoName || detectRepoName();
|
|
205
206
|
const [templateUuid, projectUuid] = await Promise.all([
|
|
206
|
-
|
|
207
|
+
// Cache key = the dispatch slug so the cache key and the lookup can never
|
|
208
|
+
// drift apart (bug clka: the key used to be a decoupled 'app evaluation'
|
|
209
|
+
// literal while the lookup searched a different string).
|
|
210
|
+
getCachedTemplateUuid(getEvalTemplateSlug(), async () => {
|
|
207
211
|
return client.workflows.findEvaluationTemplate();
|
|
208
212
|
}),
|
|
209
213
|
repoName
|
|
@@ -233,6 +237,7 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
233
237
|
if (projectUuid) {
|
|
234
238
|
contextData.projectId = projectUuid;
|
|
235
239
|
}
|
|
240
|
+
contextData.headless = true; // D7: the MCP always runs headless — no opt-out.
|
|
236
241
|
// --- Build env (credentials/environment) ---
|
|
237
242
|
const env = {};
|
|
238
243
|
if (input.environmentId)
|
|
@@ -365,6 +370,10 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
365
370
|
// result the agent reached.
|
|
366
371
|
if (attempt > MAX_RETRIES)
|
|
367
372
|
break;
|
|
373
|
+
// A poll-deadline timeout (bead 56kd.3) is never retried — surface the
|
|
374
|
+
// partial result instead of burning another 10 minutes.
|
|
375
|
+
if (finalExecution.timedOut)
|
|
376
|
+
break;
|
|
368
377
|
if (!isTransientWorkflowError(finalExecution))
|
|
369
378
|
break;
|
|
370
379
|
logger.warn(`Transient backend error detected (${transientReasonTag(finalExecution) ?? 'unknown'}) — ` +
|
|
@@ -372,7 +381,6 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
372
381
|
}
|
|
373
382
|
const duration = Date.now() - startTime;
|
|
374
383
|
// --- Format result ---
|
|
375
|
-
const outcome = finalExecution.state?.outcome ?? finalExecution.status;
|
|
376
384
|
const nodes = finalExecution.nodeExecutions ?? [];
|
|
377
385
|
// subworkflow.run is the current graph shape — carries outcome, actionHistory, screenshot
|
|
378
386
|
const subworkflowNode = nodes.find(n => n.nodeType === 'subworkflow.run');
|
|
@@ -427,43 +435,41 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
427
435
|
reason: sw.error || undefined,
|
|
428
436
|
};
|
|
429
437
|
}
|
|
430
|
-
|
|
431
|
-
|
|
438
|
+
// --- Relay the backend's explicit verdict (bead 56kd.2) ---
|
|
439
|
+
// ONE adapter owns the backend-field → MCP mapping (services/verdictAdapter).
|
|
440
|
+
// We consume the verdict/budget/evidence VERBATIM — no fabricated outcome,
|
|
441
|
+
// no success default-false, no synthesized 'assertion-mismatch'. A
|
|
442
|
+
// missing/unknown verdict surfaces as 'inconclusive', never as a failure.
|
|
443
|
+
// On a poll-deadline timeout (bead 56kd.3) pollExecution returns the last
|
|
444
|
+
// observed execution flagged `timedOut`; force outcome 'timeout' since there
|
|
445
|
+
// is no terminal backend verdict, and shape its partial evidence below.
|
|
446
|
+
const timedOut = finalExecution.timedOut === true;
|
|
447
|
+
const verdict = adaptVerdict(finalExecution, {
|
|
448
|
+
fallbackBudget: MAX_EXEC_STEPS,
|
|
449
|
+
outcomeOverride: timedOut ? 'timeout' : undefined,
|
|
450
|
+
});
|
|
451
|
+
// Evidence: prefer the backend's contract evidence.actionTrace; fall back to
|
|
452
|
+
// the legacy node-extracted trace while the backend contract deploys.
|
|
453
|
+
const relayActionTrace = verdict.actionTrace ?? actionTrace;
|
|
432
454
|
const responsePayload = {
|
|
433
|
-
outcome,
|
|
434
|
-
success,
|
|
455
|
+
outcome: verdict.outcome,
|
|
456
|
+
success: verdict.success,
|
|
435
457
|
status: finalExecution.status,
|
|
436
|
-
stepsTaken,
|
|
437
|
-
stepsBudget:
|
|
438
|
-
stepsRemaining:
|
|
458
|
+
stepsTaken: verdict.stepsTaken,
|
|
459
|
+
stepsBudget: verdict.stepsBudget, // from the response (bead 56kd.2)
|
|
460
|
+
stepsRemaining: verdict.stepsRemaining,
|
|
439
461
|
targetUrl: originalUrl,
|
|
440
462
|
executionId: executionUuid,
|
|
441
463
|
durationMs: finalExecution.durationMs ?? duration,
|
|
442
464
|
};
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// backend to distinguish from assertion-mismatch reliably; today's
|
|
452
|
-
// inferrable info is too fragile.)
|
|
453
|
-
// Field is OMITTED on success (no failure to categorize).
|
|
454
|
-
if (!success) {
|
|
455
|
-
// state.error is the AGENT's narrative — it can describe assertion
|
|
456
|
-
// failures ("expected heading to contain Welcome") OR infrastructure
|
|
457
|
-
// failures ("Pydantic JSON parse error"). Without a structured signal,
|
|
458
|
-
// we only count it as 'agent-error' when paired with workflow-level
|
|
459
|
-
// failure (status='failed') or transient signature.
|
|
460
|
-
// status='failed' or errorMessage set → workflow-level / transport error.
|
|
461
|
-
const hasInfraFailure = finalExecution.status === 'failed'
|
|
462
|
-
|| !!finalExecution.errorMessage;
|
|
463
|
-
responsePayload.failureCategory = hasInfraFailure ? 'agent-error' : 'assertion-mismatch';
|
|
464
|
-
}
|
|
465
|
-
if (actionTrace.length > 0)
|
|
466
|
-
responsePayload.actionTrace = actionTrace;
|
|
465
|
+
// failureCategory = the outcome verbatim (fail | inconclusive | error |
|
|
466
|
+
// timeout); OMITTED on success. No inference, no 'assertion-mismatch'.
|
|
467
|
+
if (verdict.failureCategory)
|
|
468
|
+
responsePayload.failureCategory = verdict.failureCategory;
|
|
469
|
+
if (verdict.reason)
|
|
470
|
+
responsePayload.reason = verdict.reason;
|
|
471
|
+
if (Array.isArray(relayActionTrace) && relayActionTrace.length > 0)
|
|
472
|
+
responsePayload.actionTrace = relayActionTrace;
|
|
467
473
|
if (evaluation)
|
|
468
474
|
responsePayload.evaluation = evaluation;
|
|
469
475
|
if (finalExecution.state?.error)
|
|
@@ -504,14 +510,29 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
504
510
|
const GIF_KEYS = ['runGif', 'gifUrl', 'gif', 'videoUrl', 'recordingUrl'];
|
|
505
511
|
let screenshotEmbedded = false;
|
|
506
512
|
let gifUrl = null;
|
|
513
|
+
let screenshotUrl = null;
|
|
514
|
+
// Contract evidence.screenshot (bead 56kd.2/.3) is the preferred source and
|
|
515
|
+
// is present on EVERY terminal state — including fail and timeout — so we
|
|
516
|
+
// always have the last screenshot on non-success. Base64 embeds inline; an
|
|
517
|
+
// http(s) URL is fetched below via the same path as legacy node URLs.
|
|
518
|
+
const evidenceScreenshot = verdict.screenshot;
|
|
519
|
+
if (typeof evidenceScreenshot === 'string' && evidenceScreenshot) {
|
|
520
|
+
if (/^https?:\/\//i.test(evidenceScreenshot)) {
|
|
521
|
+
screenshotUrl = evidenceScreenshot;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
logger.info('Embedding inline base64 screenshot from backend evidence');
|
|
525
|
+
content.push(imageContentBlock(evidenceScreenshot, 'image/png'));
|
|
526
|
+
screenshotEmbedded = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
507
529
|
// subworkflow.run carries screenshotB64 directly — no fetch needed
|
|
508
530
|
const screenshotB64 = subworkflowNode?.outputData?.screenshotB64;
|
|
509
|
-
if (typeof screenshotB64 === 'string' && screenshotB64) {
|
|
531
|
+
if (!screenshotEmbedded && !screenshotUrl && typeof screenshotB64 === 'string' && screenshotB64) {
|
|
510
532
|
logger.info('Embedding inline base64 screenshot from subworkflow.run');
|
|
511
533
|
content.push(imageContentBlock(screenshotB64, 'image/png'));
|
|
512
534
|
screenshotEmbedded = true;
|
|
513
535
|
}
|
|
514
|
-
let screenshotUrl = null;
|
|
515
536
|
for (const node of nodes) {
|
|
516
537
|
const data = node.outputData ?? {};
|
|
517
538
|
if (!screenshotEmbedded && !screenshotUrl) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict adapter (bead 56kd.2).
|
|
3
|
+
*
|
|
4
|
+
* THE ONE place that maps the backend explicit-verdict + budget + evidence
|
|
5
|
+
* contract onto the MCP relay fields. If the backend's final field shape
|
|
6
|
+
* differs, re-align it here and nowhere else.
|
|
7
|
+
*
|
|
8
|
+
* Backend contract (camelCase after axiosTransport conversion). The containers
|
|
9
|
+
* are TOP-LEVEL siblings of `state` on the execution-detail response:
|
|
10
|
+
* execution.verdict: { outcome: pass|fail|inconclusive|error|timeout, reason }
|
|
11
|
+
* execution.budget: { maxSteps, usedSteps }
|
|
12
|
+
* execution.evidence: { screenshot, actionTrace }
|
|
13
|
+
* `verdict` is SINGULAR — distinct from the pre-existing plural `verdicts`
|
|
14
|
+
* (RunVerdict array) and the raw `outcome` string, neither of which we read.
|
|
15
|
+
*
|
|
16
|
+
* Principle: relay, never invent.
|
|
17
|
+
* - `verdict.outcome` is relayed VERBATIM; `success` = (outcome === 'pass').
|
|
18
|
+
* - Anything else (fail/inconclusive/error/timeout) → success:false with
|
|
19
|
+
* `failureCategory = outcome` — NOT a fabricated 'assertion-mismatch'.
|
|
20
|
+
* - A missing / null / unrecognized verdict maps to `inconclusive`, never to
|
|
21
|
+
* `fail` and never to the raw execution status.
|
|
22
|
+
* - Budget comes from the response, not a client-side constant (the constant
|
|
23
|
+
* is only a fallback for pre-contract backends).
|
|
24
|
+
*/
|
|
25
|
+
/** The verdict enum the backend emits (bead sentinal-k8x1f.2). */
|
|
26
|
+
export const KNOWN_OUTCOMES = ['pass', 'fail', 'inconclusive', 'error', 'timeout'];
|
|
27
|
+
const KNOWN = new Set(KNOWN_OUTCOMES);
|
|
28
|
+
/**
|
|
29
|
+
* Map a workflow execution onto the MCP relay verdict. Never throws.
|
|
30
|
+
*/
|
|
31
|
+
export function adaptVerdict(execution, opts = {}) {
|
|
32
|
+
const state = execution?.state ?? null;
|
|
33
|
+
// Contract containers live at the TOP LEVEL of the execution response
|
|
34
|
+
// (siblings of `state`), NOT nested under state.
|
|
35
|
+
const verdict = execution?.verdict ?? null;
|
|
36
|
+
const budget = execution?.budget ?? null;
|
|
37
|
+
const evidence = execution?.evidence ?? null;
|
|
38
|
+
// --- Outcome (verbatim relay, inconclusive on anything unrecognized) ---
|
|
39
|
+
let outcome;
|
|
40
|
+
if (opts.outcomeOverride) {
|
|
41
|
+
outcome = opts.outcomeOverride;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Prefer the explicit verdict; fall back to the legacy per-run outcome
|
|
45
|
+
// field; NEVER fall back to execution.status. Unknown values → inconclusive.
|
|
46
|
+
const raw = verdict?.outcome ?? state?.outcome;
|
|
47
|
+
outcome = typeof raw === 'string' && KNOWN.has(raw) ? raw : 'inconclusive';
|
|
48
|
+
}
|
|
49
|
+
const success = outcome === 'pass';
|
|
50
|
+
// --- Budget (from the response; constant is a fallback only) ---
|
|
51
|
+
const fallbackBudget = opts.fallbackBudget ?? 0;
|
|
52
|
+
const stepsBudget = typeof budget?.maxSteps === 'number' ? budget.maxSteps : fallbackBudget;
|
|
53
|
+
const stepsTaken = typeof budget?.usedSteps === 'number'
|
|
54
|
+
? budget.usedSteps
|
|
55
|
+
: (typeof state?.stepsTaken === 'number' ? state.stepsTaken : 0);
|
|
56
|
+
const stepsRemaining = Math.max(0, stepsBudget - stepsTaken);
|
|
57
|
+
const relay = {
|
|
58
|
+
outcome,
|
|
59
|
+
success,
|
|
60
|
+
stepsBudget,
|
|
61
|
+
stepsTaken,
|
|
62
|
+
stepsRemaining,
|
|
63
|
+
};
|
|
64
|
+
if (!success)
|
|
65
|
+
relay.failureCategory = outcome;
|
|
66
|
+
if (typeof verdict?.reason === 'string' && verdict.reason)
|
|
67
|
+
relay.reason = verdict.reason;
|
|
68
|
+
if (typeof evidence?.screenshot === 'string' && evidence.screenshot)
|
|
69
|
+
relay.screenshot = evidence.screenshot;
|
|
70
|
+
if (Array.isArray(evidence?.actionTrace))
|
|
71
|
+
relay.actionTrace = evidence.actionTrace;
|
|
72
|
+
return relay;
|
|
73
|
+
}
|
|
@@ -12,6 +12,22 @@ const POLL_INTERVAL_INITIAL_MS = 1000;
|
|
|
12
12
|
const POLL_INTERVAL_MAX_MS = 5000;
|
|
13
13
|
const POLL_BACKOFF_MULTIPLIER = 1.5;
|
|
14
14
|
const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
|
|
15
|
+
/**
|
|
16
|
+
* Stable dispatch slug for the App Evaluation workflow (bug clka). Dispatch
|
|
17
|
+
* pins to this slug so a backend display-name change never breaks
|
|
18
|
+
* check_app_in_browser. Backend contract sentinal-k8x1f.8 exposes the slug on
|
|
19
|
+
* template results; until that deploy lands the resolver falls back to a
|
|
20
|
+
* name-substring search (see EVAL_TEMPLATE_NAME_FALLBACK).
|
|
21
|
+
*
|
|
22
|
+
* Override with the DEBUGGAI_EVAL_TEMPLATE env var to pin a different slug.
|
|
23
|
+
*/
|
|
24
|
+
export const EVAL_TEMPLATE_SLUG = 'flow/e2es/app-eval';
|
|
25
|
+
/** Interim name keyword used ONLY when the backend hasn't exposed slugs yet. */
|
|
26
|
+
export const EVAL_TEMPLATE_NAME_FALLBACK = 'app evaluation workflow';
|
|
27
|
+
/** The slug the eval-template dispatch pins to (env-overridable). */
|
|
28
|
+
export function getEvalTemplateSlug() {
|
|
29
|
+
return process.env.DEBUGGAI_EVAL_TEMPLATE || EVAL_TEMPLATE_SLUG;
|
|
30
|
+
}
|
|
15
31
|
export const createWorkflowsService = (tx) => {
|
|
16
32
|
const service = {
|
|
17
33
|
async findTemplateByName(keyword) {
|
|
@@ -41,11 +57,37 @@ export const createWorkflowsService = (tx) => {
|
|
|
41
57
|
`Available templates: ${seenNames.map(n => `"${n}"`).join(', ')}. ` +
|
|
42
58
|
`Ensure the backend has a template with "${keyword}" in its name.`);
|
|
43
59
|
},
|
|
60
|
+
async findTemplateBySlug(slug) {
|
|
61
|
+
// Pin dispatch to the stable slug (bug clka). We pass `slug` as a query
|
|
62
|
+
// param so a slug-aware backend can filter server-side; on backends that
|
|
63
|
+
// ignore it we still walk the pages and match client-side on the `slug`
|
|
64
|
+
// field. Returns null if NO result carries a slug field (backend hasn't
|
|
65
|
+
// deployed the slug contract yet) — the caller then falls back to name
|
|
66
|
+
// search. `next`-paging mirrors findTemplateByName for the same reason
|
|
67
|
+
// (the backend caps page size).
|
|
68
|
+
const MAX_PAGES = 50;
|
|
69
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
70
|
+
const response = await tx.get('api/v1/workflows/', { isTemplate: true, slug, page });
|
|
71
|
+
const templates = response?.results ?? [];
|
|
72
|
+
for (const t of templates) {
|
|
73
|
+
if (t.slug === slug)
|
|
74
|
+
return t;
|
|
75
|
+
}
|
|
76
|
+
if (!response?.next)
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
44
81
|
async findEvaluationTemplate() {
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
82
|
+
// Primary: resolve by the stable slug so a backend rename can't break us.
|
|
83
|
+
const slug = getEvalTemplateSlug();
|
|
84
|
+
const bySlug = await service.findTemplateBySlug(slug);
|
|
85
|
+
if (bySlug)
|
|
86
|
+
return bySlug;
|
|
87
|
+
// Fallback (interim, until backend contract sentinal-k8x1f.8 lands): the
|
|
88
|
+
// name is specific enough to skip 'App Evaluation Brain' (subworkflow, no
|
|
89
|
+
// browser lifecycle) which also contains 'app evaluation'.
|
|
90
|
+
return service.findTemplateByName(EVAL_TEMPLATE_NAME_FALLBACK);
|
|
49
91
|
},
|
|
50
92
|
async executeWorkflow(workflowUuid, contextData, env) {
|
|
51
93
|
const body = { contextData };
|
|
@@ -105,11 +147,16 @@ export const createWorkflowsService = (tx) => {
|
|
|
105
147
|
const pollStart = Date.now();
|
|
106
148
|
let pollCount = 0;
|
|
107
149
|
let intervalMs = POLL_INTERVAL_INITIAL_MS;
|
|
150
|
+
// Track the most recent observation so a poll-deadline timeout can return
|
|
151
|
+
// partial results (screenshot + trace) instead of discarding them (bead
|
|
152
|
+
// 56kd.3).
|
|
153
|
+
let lastExecution;
|
|
108
154
|
while (Date.now() < deadline) {
|
|
109
155
|
if (signal?.aborted) {
|
|
110
156
|
throw new Error(`Polling cancelled for execution ${executionUuid}`);
|
|
111
157
|
}
|
|
112
158
|
const execution = await service.getExecution(executionUuid);
|
|
159
|
+
lastExecution = execution;
|
|
113
160
|
pollCount++;
|
|
114
161
|
if (onUpdate) {
|
|
115
162
|
await onUpdate(execution).catch(() => { });
|
|
@@ -147,6 +194,23 @@ export const createWorkflowsService = (tx) => {
|
|
|
147
194
|
// past terminal-state achievement on the longest runs.
|
|
148
195
|
intervalMs = Math.min(Math.round(intervalMs * POLL_BACKOFF_MULTIPLIER), POLL_INTERVAL_MAX_MS);
|
|
149
196
|
}
|
|
197
|
+
// Deadline hit. Return the last observed execution flagged `timedOut` so
|
|
198
|
+
// the handler shapes a partial 'timeout' result (bead 56kd.3) rather than
|
|
199
|
+
// discarding the screenshot + trace we already captured. Only throw if we
|
|
200
|
+
// never observed anything to shape.
|
|
201
|
+
if (lastExecution) {
|
|
202
|
+
Telemetry.capture(TelemetryEvents.WORKFLOW_EXECUTED, {
|
|
203
|
+
status: lastExecution.status,
|
|
204
|
+
success: false,
|
|
205
|
+
outcome: 'timeout',
|
|
206
|
+
stepsTaken: lastExecution.state?.stepsTaken ?? 0,
|
|
207
|
+
durationMs: Date.now() - pollStart,
|
|
208
|
+
pollCount,
|
|
209
|
+
finalIntervalMs: intervalMs,
|
|
210
|
+
timedOut: true,
|
|
211
|
+
});
|
|
212
|
+
return { ...lastExecution, timedOut: true };
|
|
213
|
+
}
|
|
150
214
|
throw new Error(`Execution ${executionUuid} timed out after ${EXECUTION_TIMEOUT_MS / 1000}s`);
|
|
151
215
|
}
|
|
152
216
|
};
|