agentxchain 2.155.73 → 2.156.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/bin/agentxchain.js +22 -0
- package/dashboard/app.js +54 -0
- package/dashboard/components/org-audit-trail.js +161 -0
- package/dashboard/components/org-history.js +140 -0
- package/dashboard/components/org-overview.js +145 -0
- package/dashboard/components/org-runs.js +168 -0
- package/dashboard/index.html +4 -0
- package/package.json +2 -1
- package/src/commands/ci-report.js +80 -0
- package/src/commands/doctor.js +22 -1
- package/src/commands/intake-approve.js +1 -0
- package/src/commands/replay.js +1 -0
- package/src/commands/run.js +47 -0
- package/src/commands/serve.js +64 -0
- package/src/commands/step.js +16 -0
- package/src/commands/verify.js +1 -0
- package/src/lib/adapters/local-cli-adapter.js +147 -0
- package/src/lib/api/execution-worker.js +192 -0
- package/src/lib/api/hosted-runner.js +494 -0
- package/src/lib/api/job-queue.js +152 -0
- package/src/lib/api/org-state-aggregator.js +428 -0
- package/src/lib/api/project-registry.js +148 -0
- package/src/lib/api/protocol-bridge.js +476 -0
- package/src/lib/approval-policy.js +12 -0
- package/src/lib/ci-reporter.js +188 -0
- package/src/lib/claude-local-auth.js +75 -1
- package/src/lib/connector-probe.js +21 -0
- package/src/lib/continuous-run.js +9 -0
- package/src/lib/dashboard/bridge-server.js +10 -5
- package/src/lib/governed-state.js +1 -1
- package/src/lib/intake.js +32 -6
- package/src/lib/normalized-config.js +2 -0
- package/src/lib/scope-overlap.js +214 -0
- package/src/lib/turn-checkpoint.js +21 -0
- package/src/lib/turn-result-validator.js +5 -0
- package/src/lib/validation.js +11 -3
- package/src/lib/verification-replay.js +125 -4
- package/src/lib/vision-reader.js +7 -2
|
@@ -35,8 +35,13 @@ import {
|
|
|
35
35
|
hasClaudeAuthenticationFailureText,
|
|
36
36
|
hasClaudeNodeIncompatibilityText,
|
|
37
37
|
hasCodexAuthenticationFailureText,
|
|
38
|
+
hasRateLimitOutput,
|
|
38
39
|
isClaudeLocalCliRuntime,
|
|
39
40
|
isCodexLocalCliRuntime,
|
|
41
|
+
isCursorLocalCliRuntime,
|
|
42
|
+
isWindsurfLocalCliRuntime,
|
|
43
|
+
isOpenCodeLocalCliRuntime,
|
|
44
|
+
parseRateLimitResetMs,
|
|
40
45
|
resolveClaudeCompatibleNodeBinary,
|
|
41
46
|
} from '../claude-local-auth.js';
|
|
42
47
|
|
|
@@ -52,6 +57,11 @@ const DIAGNOSTIC_STDERR_EXCERPT_LIMIT = 800;
|
|
|
52
57
|
const DEFAULT_STARTUP_WATCHDOG_MS = 180_000;
|
|
53
58
|
const DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS = 10_000;
|
|
54
59
|
const DEFAULT_STARTUP_HEARTBEAT_MS = 30_000;
|
|
60
|
+
// RB-6: liveness heartbeat interval. Unlike the startup heartbeat (which stops
|
|
61
|
+
// at first output), this keeps the dispatch-progress `last_activity_at` fresh
|
|
62
|
+
// for the whole lifetime of an alive subprocess, so a long output-silent turn
|
|
63
|
+
// (e.g. one running a slow test suite) is not misclassified as a stalled turn.
|
|
64
|
+
const DEFAULT_LIVENESS_HEARTBEAT_MS = 60_000;
|
|
55
65
|
|
|
56
66
|
/**
|
|
57
67
|
* Launch a local CLI subprocess for a governed turn.
|
|
@@ -101,6 +111,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
101
111
|
const startupWatchdogMs = startupWatchdogOverrideMs ?? resolveStartupWatchdogMs(config, runtime);
|
|
102
112
|
const startupWatchdogKillGraceMs = resolveStartupWatchdogKillGraceMs(options.startupWatchdogKillGraceMs);
|
|
103
113
|
const startupHeartbeatMs = resolveStartupHeartbeatMs(config, runtime, options.startupHeartbeatMs);
|
|
114
|
+
const livenessHeartbeatMs = resolveLivenessHeartbeatMs(config, runtime, options.livenessHeartbeatMs);
|
|
104
115
|
|
|
105
116
|
// Read the dispatch bundle prompt
|
|
106
117
|
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
@@ -226,6 +237,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
226
237
|
let startupWatchdog = null;
|
|
227
238
|
let startupSigkillHandle = null;
|
|
228
239
|
let startupHeartbeat = null;
|
|
240
|
+
let livenessHeartbeat = null;
|
|
229
241
|
let startupTimedOut = false;
|
|
230
242
|
let startupFailureType = null;
|
|
231
243
|
let stdoutBytes = 0;
|
|
@@ -235,6 +247,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
235
247
|
const settle = (result) => {
|
|
236
248
|
if (settled) return;
|
|
237
249
|
settled = true;
|
|
250
|
+
clearLivenessHeartbeat();
|
|
238
251
|
resolve(result);
|
|
239
252
|
};
|
|
240
253
|
|
|
@@ -256,6 +269,48 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
256
269
|
}
|
|
257
270
|
};
|
|
258
271
|
|
|
272
|
+
const clearLivenessHeartbeat = () => {
|
|
273
|
+
if (livenessHeartbeat) {
|
|
274
|
+
clearInterval(livenessHeartbeat);
|
|
275
|
+
livenessHeartbeat = null;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// RB-6: keep `last_activity_at` fresh for the full lifetime of an alive
|
|
280
|
+
// subprocess (NOT cleared at first output, unlike the startup heartbeat), so
|
|
281
|
+
// a long output-silent turn — e.g. one running a slow test suite — is not
|
|
282
|
+
// misclassified as stalled by the stale-turn watchdog. Genuine hangs are
|
|
283
|
+
// still bounded by the per-turn hard timeout.
|
|
284
|
+
const armLivenessHeartbeat = () => {
|
|
285
|
+
if (livenessHeartbeat || !(livenessHeartbeatMs > 0 && Number.isFinite(livenessHeartbeatMs))) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
livenessHeartbeat = setInterval(() => {
|
|
289
|
+
if (settled) {
|
|
290
|
+
clearLivenessHeartbeat();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const payload = {
|
|
294
|
+
liveness_heartbeat_ms: livenessHeartbeatMs,
|
|
295
|
+
pid: child.pid ?? null,
|
|
296
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
297
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
298
|
+
first_output_at: firstOutputAt,
|
|
299
|
+
stdout_bytes: stdoutBytes,
|
|
300
|
+
stderr_bytes: stderrBytes,
|
|
301
|
+
};
|
|
302
|
+
appendDiagnostic(logs, 'liveness_heartbeat', payload);
|
|
303
|
+
if (options.onLivenessHeartbeat) {
|
|
304
|
+
try {
|
|
305
|
+
options.onLivenessHeartbeat(payload);
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
308
|
+
}, livenessHeartbeatMs);
|
|
309
|
+
if (typeof livenessHeartbeat.unref === 'function') {
|
|
310
|
+
livenessHeartbeat.unref();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
259
314
|
const armStartupHeartbeat = () => {
|
|
260
315
|
if (startupHeartbeat || !(startupHeartbeatMs > 0 && Number.isFinite(startupHeartbeatMs))) {
|
|
261
316
|
return;
|
|
@@ -360,6 +415,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
360
415
|
}
|
|
361
416
|
armStartupWatchdog();
|
|
362
417
|
armStartupHeartbeat();
|
|
418
|
+
armLivenessHeartbeat();
|
|
363
419
|
});
|
|
364
420
|
|
|
365
421
|
// Collect stdout/stderr
|
|
@@ -551,6 +607,26 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
551
607
|
error: `Claude local_cli runtime is using an incompatible Node.js version. ${recovery}`,
|
|
552
608
|
logs,
|
|
553
609
|
});
|
|
610
|
+
} else if (hasRateLimitOutput(logs)) {
|
|
611
|
+
const resetMs = parseRateLimitResetMs(logs.join('\n'));
|
|
612
|
+
const resetDetail = resetMs ? ` Resets in ${Math.ceil(resetMs / 1000)}s.` : '';
|
|
613
|
+
const recovery = `Provider rate-limited.${resetDetail} Will retry with backoff.`;
|
|
614
|
+
settle({
|
|
615
|
+
ok: false,
|
|
616
|
+
blocked: true,
|
|
617
|
+
exitCode,
|
|
618
|
+
timedOut: false,
|
|
619
|
+
aborted: false,
|
|
620
|
+
firstOutputAt,
|
|
621
|
+
classified: {
|
|
622
|
+
error_class: 'rate_limited',
|
|
623
|
+
retryable: true,
|
|
624
|
+
recovery,
|
|
625
|
+
reset_ms: resetMs,
|
|
626
|
+
},
|
|
627
|
+
error: `Subprocess rate-limited by provider. ${recovery}`,
|
|
628
|
+
logs,
|
|
629
|
+
});
|
|
554
630
|
} else if (startupTimedOut) {
|
|
555
631
|
settle({
|
|
556
632
|
ok: false,
|
|
@@ -749,6 +825,19 @@ function resolveStartupHeartbeatMs(config, runtime, override) {
|
|
|
749
825
|
return DEFAULT_STARTUP_HEARTBEAT_MS;
|
|
750
826
|
}
|
|
751
827
|
|
|
828
|
+
function resolveLivenessHeartbeatMs(config, runtime, override) {
|
|
829
|
+
if (Number.isInteger(override) && override > 0) {
|
|
830
|
+
return override;
|
|
831
|
+
}
|
|
832
|
+
if (runtime?.type === 'local_cli' && Number.isInteger(runtime?.liveness_heartbeat_ms) && runtime.liveness_heartbeat_ms > 0) {
|
|
833
|
+
return runtime.liveness_heartbeat_ms;
|
|
834
|
+
}
|
|
835
|
+
if (Number.isInteger(config?.run_loop?.liveness_heartbeat_ms) && config.run_loop.liveness_heartbeat_ms > 0) {
|
|
836
|
+
return config.run_loop.liveness_heartbeat_ms;
|
|
837
|
+
}
|
|
838
|
+
return DEFAULT_LIVENESS_HEARTBEAT_MS;
|
|
839
|
+
}
|
|
840
|
+
|
|
752
841
|
function resolveStartupWatchdogKillGraceMs(value) {
|
|
753
842
|
if (Number.isInteger(value) && value >= 0) {
|
|
754
843
|
return value;
|
|
@@ -827,6 +916,63 @@ function validateLocalCliCommandCompatibility({ command, args = [], runtimeId =
|
|
|
827
916
|
};
|
|
828
917
|
}
|
|
829
918
|
|
|
919
|
+
const usesCursor = isCursorLocalCliRuntime(runtimeShape);
|
|
920
|
+
if (usesCursor && !tokens.includes('--background-agent') && !tokens.includes('agent')) {
|
|
921
|
+
const runtimeLabel = runtimeId ? `Runtime "${runtimeId}"` : 'Cursor local_cli runtime';
|
|
922
|
+
const recovery = `${runtimeLabel} uses "cursor" without a background agent flag. Governed local runs require Cursor's agent or background-agent mode for non-interactive execution.`;
|
|
923
|
+
return {
|
|
924
|
+
ok: false,
|
|
925
|
+
error_class: 'local_cli_command_incompatible',
|
|
926
|
+
recovery,
|
|
927
|
+
error: recovery,
|
|
928
|
+
diagnostic: {
|
|
929
|
+
runtime_id: runtimeId,
|
|
930
|
+
binary: binaryName,
|
|
931
|
+
rule: 'cursor_requires_agent_mode',
|
|
932
|
+
has_agent_flag: false,
|
|
933
|
+
recovery,
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const usesWindsurf = isWindsurfLocalCliRuntime(runtimeShape);
|
|
939
|
+
if (usesWindsurf && !tokens.includes('--agent')) {
|
|
940
|
+
const runtimeLabel = runtimeId ? `Runtime "${runtimeId}"` : 'Windsurf local_cli runtime';
|
|
941
|
+
const recovery = `${runtimeLabel} uses "windsurf" without an agent flag. Governed local runs require Windsurf's --agent mode for non-interactive execution.`;
|
|
942
|
+
return {
|
|
943
|
+
ok: false,
|
|
944
|
+
error_class: 'local_cli_command_incompatible',
|
|
945
|
+
recovery,
|
|
946
|
+
error: recovery,
|
|
947
|
+
diagnostic: {
|
|
948
|
+
runtime_id: runtimeId,
|
|
949
|
+
binary: binaryName,
|
|
950
|
+
rule: 'windsurf_requires_agent_mode',
|
|
951
|
+
has_agent_flag: false,
|
|
952
|
+
recovery,
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const usesOpenCode = isOpenCodeLocalCliRuntime(runtimeShape);
|
|
958
|
+
if (usesOpenCode && !tokens.includes('--non-interactive')) {
|
|
959
|
+
const runtimeLabel = runtimeId ? `Runtime "${runtimeId}"` : 'OpenCode local_cli runtime';
|
|
960
|
+
const recovery = `${runtimeLabel} uses "opencode" without --non-interactive. Governed local runs require OpenCode's non-interactive mode for unattended execution.`;
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
error_class: 'local_cli_command_incompatible',
|
|
964
|
+
recovery,
|
|
965
|
+
error: recovery,
|
|
966
|
+
diagnostic: {
|
|
967
|
+
runtime_id: runtimeId,
|
|
968
|
+
binary: binaryName,
|
|
969
|
+
rule: 'opencode_requires_non_interactive',
|
|
970
|
+
has_non_interactive_flag: false,
|
|
971
|
+
recovery,
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
830
976
|
return { ok: true };
|
|
831
977
|
}
|
|
832
978
|
|
|
@@ -953,4 +1099,5 @@ export { resolveCommand };
|
|
|
953
1099
|
export { resolvePromptTransport };
|
|
954
1100
|
export { resolveStartupWatchdogMs };
|
|
955
1101
|
export { resolveStartupHeartbeatMs };
|
|
1102
|
+
export { resolveLivenessHeartbeatMs };
|
|
956
1103
|
export { validateLocalCliCommandCompatibility };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Worker — in-process worker that polls the job queue and executes
|
|
3
|
+
* governed turns via run-loop + api_proxy adapter composition.
|
|
4
|
+
*
|
|
5
|
+
* Design rules:
|
|
6
|
+
* - Composes run-loop directly (protocol parity invariant)
|
|
7
|
+
* - Single turn per job (maxTurns: 1)
|
|
8
|
+
* - Heartbeat-based liveness per execution plane spec rule 3
|
|
9
|
+
* - Structured execution events per spec rule 6
|
|
10
|
+
*
|
|
11
|
+
* @module execution-worker
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { runLoop } from '../run-loop.js';
|
|
19
|
+
import { dispatchApiProxy } from '../adapters/api-proxy-adapter.js';
|
|
20
|
+
import { getTurnStagingResultPath } from '../runner-interface.js';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
23
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an execution worker.
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* @param {string} options.root - project root directory
|
|
29
|
+
* @param {object} options.config - normalized config
|
|
30
|
+
* @param {object} options.queue - job queue instance
|
|
31
|
+
* @param {function} [options.onEvent] - structured event callback
|
|
32
|
+
* @param {string} [options.workerId] - worker identifier
|
|
33
|
+
* @param {number} [options.pollIntervalMs=2000] - queue poll interval
|
|
34
|
+
* @returns {{ start(): void, stop(): void, getStatus(): object }}
|
|
35
|
+
*/
|
|
36
|
+
export function createExecutionWorker(options) {
|
|
37
|
+
const {
|
|
38
|
+
root,
|
|
39
|
+
config,
|
|
40
|
+
queue,
|
|
41
|
+
onEvent,
|
|
42
|
+
workerId = `worker-${randomUUID().slice(0, 8)}`,
|
|
43
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
let running = false;
|
|
47
|
+
let pollTimer = null;
|
|
48
|
+
let currentJob = null;
|
|
49
|
+
let currentAbort = null;
|
|
50
|
+
|
|
51
|
+
function emit(type, data = {}) {
|
|
52
|
+
if (!onEvent) return;
|
|
53
|
+
try {
|
|
54
|
+
onEvent({ type, worker_id: workerId, timestamp: new Date().toISOString(), ...data });
|
|
55
|
+
} catch {
|
|
56
|
+
// swallow event handler errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function executeJob(job, lease) {
|
|
61
|
+
currentJob = job;
|
|
62
|
+
const abortController = new AbortController();
|
|
63
|
+
currentAbort = abortController;
|
|
64
|
+
|
|
65
|
+
emit('execution_started', { job_id: job.job_id, run_id: job.run_id, role: job.role });
|
|
66
|
+
|
|
67
|
+
// Heartbeat interval
|
|
68
|
+
const heartbeatTimer = setInterval(() => {
|
|
69
|
+
const alive = queue.heartbeat(lease.lease_id);
|
|
70
|
+
if (!alive) {
|
|
71
|
+
abortController.abort();
|
|
72
|
+
}
|
|
73
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = await runLoop(root, config, {
|
|
77
|
+
selectRole(state) {
|
|
78
|
+
return job.role;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async dispatch(context) {
|
|
82
|
+
const adapterResult = await dispatchApiProxy(root, context.state, config, {
|
|
83
|
+
turnId: context.turn.turn_id,
|
|
84
|
+
signal: abortController.signal,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!adapterResult.ok) {
|
|
88
|
+
return { accept: false, reason: adapterResult.error || 'api_proxy dispatch failed' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// api_proxy stages to disk; read back for run-loop acceptance
|
|
92
|
+
const stagingFile = join(root, getTurnStagingResultPath(context.turn.turn_id));
|
|
93
|
+
if (!existsSync(stagingFile)) {
|
|
94
|
+
return { accept: false, reason: 'adapter completed but no staged result found' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let turnResult;
|
|
98
|
+
try {
|
|
99
|
+
turnResult = JSON.parse(readFileSync(stagingFile, 'utf8'));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return { accept: false, reason: `failed to parse staged result: ${err.message}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { accept: true, turnResult };
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
approveGate() {
|
|
108
|
+
return true; // auto-approve in hosted mode
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
onEvent(event) {
|
|
112
|
+
emit('execution_progress', { job_id: job.job_id, event });
|
|
113
|
+
},
|
|
114
|
+
}, { maxTurns: 1 });
|
|
115
|
+
|
|
116
|
+
clearInterval(heartbeatTimer);
|
|
117
|
+
|
|
118
|
+
const success = result.ok;
|
|
119
|
+
queue.finalize(lease.lease_id, success ? 'completed' : 'failed');
|
|
120
|
+
|
|
121
|
+
emit(success ? 'execution_completed' : 'execution_interrupted', {
|
|
122
|
+
job_id: job.job_id,
|
|
123
|
+
run_id: job.run_id,
|
|
124
|
+
result_status: result.stop_reason || (success ? 'completed' : 'failed'),
|
|
125
|
+
turns_executed: result.turnsExecuted,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
clearInterval(heartbeatTimer);
|
|
131
|
+
queue.finalize(lease.lease_id, 'failed');
|
|
132
|
+
|
|
133
|
+
emit('execution_interrupted', {
|
|
134
|
+
job_id: job.job_id,
|
|
135
|
+
run_id: job.run_id,
|
|
136
|
+
error: err.message,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
} finally {
|
|
141
|
+
currentJob = null;
|
|
142
|
+
currentAbort = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function poll() {
|
|
147
|
+
if (!running) return;
|
|
148
|
+
|
|
149
|
+
const claimed = queue.claim(workerId);
|
|
150
|
+
if (claimed) {
|
|
151
|
+
await executeJob(claimed.job, claimed.lease);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Expire stale leases periodically
|
|
155
|
+
queue.expireStaleLeases();
|
|
156
|
+
|
|
157
|
+
// Schedule next poll
|
|
158
|
+
if (running) {
|
|
159
|
+
pollTimer = setTimeout(poll, pollIntervalMs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function start() {
|
|
164
|
+
if (running) return;
|
|
165
|
+
running = true;
|
|
166
|
+
emit('worker_started', {});
|
|
167
|
+
// Start polling immediately
|
|
168
|
+
pollTimer = setTimeout(poll, 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stop() {
|
|
172
|
+
running = false;
|
|
173
|
+
if (pollTimer) {
|
|
174
|
+
clearTimeout(pollTimer);
|
|
175
|
+
pollTimer = null;
|
|
176
|
+
}
|
|
177
|
+
if (currentAbort) {
|
|
178
|
+
currentAbort.abort();
|
|
179
|
+
}
|
|
180
|
+
emit('worker_stopped', {});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getStatus() {
|
|
184
|
+
return {
|
|
185
|
+
worker_id: workerId,
|
|
186
|
+
running,
|
|
187
|
+
current_job: currentJob ? currentJob.job_id : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { start, stop, getStatus };
|
|
192
|
+
}
|