agentxchain 2.155.73 → 2.157.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 +184 -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 +200 -0
- package/src/lib/claude-local-auth.js +75 -1
- package/src/lib/connector-probe.js +21 -0
- package/src/lib/continuous-run.js +75 -4
- 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/stream-json-cost-parser.js +169 -0
- package/src/lib/turn-checkpoint.js +21 -0
- package/src/lib/turn-result-validator.js +25 -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,10 +35,19 @@ 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';
|
|
47
|
+
import {
|
|
48
|
+
createStreamJsonCostParser,
|
|
49
|
+
buildCostFromStreamJson,
|
|
50
|
+
} from '../stream-json-cost-parser.js';
|
|
42
51
|
|
|
43
52
|
const DIAGNOSTIC_ENV_KEYS = [
|
|
44
53
|
'PATH',
|
|
@@ -52,6 +61,11 @@ const DIAGNOSTIC_STDERR_EXCERPT_LIMIT = 800;
|
|
|
52
61
|
const DEFAULT_STARTUP_WATCHDOG_MS = 180_000;
|
|
53
62
|
const DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS = 10_000;
|
|
54
63
|
const DEFAULT_STARTUP_HEARTBEAT_MS = 30_000;
|
|
64
|
+
// RB-6: liveness heartbeat interval. Unlike the startup heartbeat (which stops
|
|
65
|
+
// at first output), this keeps the dispatch-progress `last_activity_at` fresh
|
|
66
|
+
// for the whole lifetime of an alive subprocess, so a long output-silent turn
|
|
67
|
+
// (e.g. one running a slow test suite) is not misclassified as a stalled turn.
|
|
68
|
+
const DEFAULT_LIVENESS_HEARTBEAT_MS = 60_000;
|
|
55
69
|
|
|
56
70
|
/**
|
|
57
71
|
* Launch a local CLI subprocess for a governed turn.
|
|
@@ -101,6 +115,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
101
115
|
const startupWatchdogMs = startupWatchdogOverrideMs ?? resolveStartupWatchdogMs(config, runtime);
|
|
102
116
|
const startupWatchdogKillGraceMs = resolveStartupWatchdogKillGraceMs(options.startupWatchdogKillGraceMs);
|
|
103
117
|
const startupHeartbeatMs = resolveStartupHeartbeatMs(config, runtime, options.startupHeartbeatMs);
|
|
118
|
+
const livenessHeartbeatMs = resolveLivenessHeartbeatMs(config, runtime, options.livenessHeartbeatMs);
|
|
104
119
|
|
|
105
120
|
// Read the dispatch bundle prompt
|
|
106
121
|
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
@@ -136,6 +151,16 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
136
151
|
};
|
|
137
152
|
}
|
|
138
153
|
|
|
154
|
+
// Create stream-json cost parser for Claude runtimes with stream-json output.
|
|
155
|
+
// Only Claude runtimes emit parseable NDJSON with result events; other runtimes
|
|
156
|
+
// (Codex, Cursor, Windsurf, OpenCode) have different stdout formats.
|
|
157
|
+
const tokens = [command, ...args].filter((t) => typeof t === 'string');
|
|
158
|
+
const usesStreamJson = tokens.includes('--output-format=stream-json')
|
|
159
|
+
|| (tokens.includes('--output-format') && tokens[tokens.indexOf('--output-format') + 1] === 'stream-json');
|
|
160
|
+
const costParser = isClaudeLocalCliRuntime(runtime) && usesStreamJson
|
|
161
|
+
? createStreamJsonCostParser()
|
|
162
|
+
: null;
|
|
163
|
+
|
|
139
164
|
// Compute timeout from explicit dispatch deadline, turn deadline, or default (20 minutes).
|
|
140
165
|
const timeoutMs = options.dispatchTimeoutMs != null
|
|
141
166
|
? options.dispatchTimeoutMs
|
|
@@ -226,6 +251,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
226
251
|
let startupWatchdog = null;
|
|
227
252
|
let startupSigkillHandle = null;
|
|
228
253
|
let startupHeartbeat = null;
|
|
254
|
+
let livenessHeartbeat = null;
|
|
229
255
|
let startupTimedOut = false;
|
|
230
256
|
let startupFailureType = null;
|
|
231
257
|
let stdoutBytes = 0;
|
|
@@ -235,6 +261,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
235
261
|
const settle = (result) => {
|
|
236
262
|
if (settled) return;
|
|
237
263
|
settled = true;
|
|
264
|
+
clearLivenessHeartbeat();
|
|
238
265
|
resolve(result);
|
|
239
266
|
};
|
|
240
267
|
|
|
@@ -256,6 +283,48 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
256
283
|
}
|
|
257
284
|
};
|
|
258
285
|
|
|
286
|
+
const clearLivenessHeartbeat = () => {
|
|
287
|
+
if (livenessHeartbeat) {
|
|
288
|
+
clearInterval(livenessHeartbeat);
|
|
289
|
+
livenessHeartbeat = null;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// RB-6: keep `last_activity_at` fresh for the full lifetime of an alive
|
|
294
|
+
// subprocess (NOT cleared at first output, unlike the startup heartbeat), so
|
|
295
|
+
// a long output-silent turn — e.g. one running a slow test suite — is not
|
|
296
|
+
// misclassified as stalled by the stale-turn watchdog. Genuine hangs are
|
|
297
|
+
// still bounded by the per-turn hard timeout.
|
|
298
|
+
const armLivenessHeartbeat = () => {
|
|
299
|
+
if (livenessHeartbeat || !(livenessHeartbeatMs > 0 && Number.isFinite(livenessHeartbeatMs))) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
livenessHeartbeat = setInterval(() => {
|
|
303
|
+
if (settled) {
|
|
304
|
+
clearLivenessHeartbeat();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const payload = {
|
|
308
|
+
liveness_heartbeat_ms: livenessHeartbeatMs,
|
|
309
|
+
pid: child.pid ?? null,
|
|
310
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
311
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
312
|
+
first_output_at: firstOutputAt,
|
|
313
|
+
stdout_bytes: stdoutBytes,
|
|
314
|
+
stderr_bytes: stderrBytes,
|
|
315
|
+
};
|
|
316
|
+
appendDiagnostic(logs, 'liveness_heartbeat', payload);
|
|
317
|
+
if (options.onLivenessHeartbeat) {
|
|
318
|
+
try {
|
|
319
|
+
options.onLivenessHeartbeat(payload);
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
}, livenessHeartbeatMs);
|
|
323
|
+
if (typeof livenessHeartbeat.unref === 'function') {
|
|
324
|
+
livenessHeartbeat.unref();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
259
328
|
const armStartupHeartbeat = () => {
|
|
260
329
|
if (startupHeartbeat || !(startupHeartbeatMs > 0 && Number.isFinite(startupHeartbeatMs))) {
|
|
261
330
|
return;
|
|
@@ -360,6 +429,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
360
429
|
}
|
|
361
430
|
armStartupWatchdog();
|
|
362
431
|
armStartupHeartbeat();
|
|
432
|
+
armLivenessHeartbeat();
|
|
363
433
|
});
|
|
364
434
|
|
|
365
435
|
// Collect stdout/stderr
|
|
@@ -369,6 +439,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
369
439
|
stdoutBytes += Buffer.byteLength(text);
|
|
370
440
|
recordFirstOutput('stdout');
|
|
371
441
|
logs.push(text);
|
|
442
|
+
if (costParser) costParser.push(text);
|
|
372
443
|
if (onStdout) onStdout(text);
|
|
373
444
|
});
|
|
374
445
|
}
|
|
@@ -502,6 +573,28 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
502
573
|
appendDiagnostic(logs, 'process_exit', exitDiagnostic);
|
|
503
574
|
|
|
504
575
|
if (hasResult) {
|
|
576
|
+
// Enrich cost from stream-json if available (best-effort, never blocks settle)
|
|
577
|
+
if (costParser) {
|
|
578
|
+
try {
|
|
579
|
+
const parsedCost = costParser.getResult();
|
|
580
|
+
if (parsedCost) {
|
|
581
|
+
const stagingPath = join(root, getTurnStagingResultPath(turn.turn_id));
|
|
582
|
+
const raw = readFileSync(stagingPath, 'utf8');
|
|
583
|
+
const turnResult = JSON.parse(raw);
|
|
584
|
+
turnResult.cost = buildCostFromStreamJson(parsedCost, config);
|
|
585
|
+
writeFileSync(stagingPath, JSON.stringify(turnResult, null, 2) + '\n');
|
|
586
|
+
appendDiagnostic(logs, 'stream_json_cost_enriched', {
|
|
587
|
+
input_tokens: parsedCost.input_tokens,
|
|
588
|
+
output_tokens: parsedCost.output_tokens,
|
|
589
|
+
cost_usd: parsedCost.cost_usd,
|
|
590
|
+
model: parsedCost.model,
|
|
591
|
+
computed_usd: turnResult.cost.usd,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
// Cost enrichment is best-effort — if it fails, agent-reported cost is preserved
|
|
596
|
+
}
|
|
597
|
+
}
|
|
505
598
|
settle({ ok: true, exitCode, timedOut: false, aborted: false, logs, firstOutputAt });
|
|
506
599
|
} else if (isClaudeLocalCliRuntime(runtime) && hasClaudeAuthFailureOutput(logs)) {
|
|
507
600
|
const recovery = 'Refresh Claude credentials before resuming: export a valid ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, then run agentxchain step --resume.';
|
|
@@ -551,6 +644,26 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
551
644
|
error: `Claude local_cli runtime is using an incompatible Node.js version. ${recovery}`,
|
|
552
645
|
logs,
|
|
553
646
|
});
|
|
647
|
+
} else if (hasRateLimitOutput(logs)) {
|
|
648
|
+
const resetMs = parseRateLimitResetMs(logs.join('\n'));
|
|
649
|
+
const resetDetail = resetMs ? ` Resets in ${Math.ceil(resetMs / 1000)}s.` : '';
|
|
650
|
+
const recovery = `Provider rate-limited.${resetDetail} Will retry with backoff.`;
|
|
651
|
+
settle({
|
|
652
|
+
ok: false,
|
|
653
|
+
blocked: true,
|
|
654
|
+
exitCode,
|
|
655
|
+
timedOut: false,
|
|
656
|
+
aborted: false,
|
|
657
|
+
firstOutputAt,
|
|
658
|
+
classified: {
|
|
659
|
+
error_class: 'rate_limited',
|
|
660
|
+
retryable: true,
|
|
661
|
+
recovery,
|
|
662
|
+
reset_ms: resetMs,
|
|
663
|
+
},
|
|
664
|
+
error: `Subprocess rate-limited by provider. ${recovery}`,
|
|
665
|
+
logs,
|
|
666
|
+
});
|
|
554
667
|
} else if (startupTimedOut) {
|
|
555
668
|
settle({
|
|
556
669
|
ok: false,
|
|
@@ -749,6 +862,19 @@ function resolveStartupHeartbeatMs(config, runtime, override) {
|
|
|
749
862
|
return DEFAULT_STARTUP_HEARTBEAT_MS;
|
|
750
863
|
}
|
|
751
864
|
|
|
865
|
+
function resolveLivenessHeartbeatMs(config, runtime, override) {
|
|
866
|
+
if (Number.isInteger(override) && override > 0) {
|
|
867
|
+
return override;
|
|
868
|
+
}
|
|
869
|
+
if (runtime?.type === 'local_cli' && Number.isInteger(runtime?.liveness_heartbeat_ms) && runtime.liveness_heartbeat_ms > 0) {
|
|
870
|
+
return runtime.liveness_heartbeat_ms;
|
|
871
|
+
}
|
|
872
|
+
if (Number.isInteger(config?.run_loop?.liveness_heartbeat_ms) && config.run_loop.liveness_heartbeat_ms > 0) {
|
|
873
|
+
return config.run_loop.liveness_heartbeat_ms;
|
|
874
|
+
}
|
|
875
|
+
return DEFAULT_LIVENESS_HEARTBEAT_MS;
|
|
876
|
+
}
|
|
877
|
+
|
|
752
878
|
function resolveStartupWatchdogKillGraceMs(value) {
|
|
753
879
|
if (Number.isInteger(value) && value >= 0) {
|
|
754
880
|
return value;
|
|
@@ -827,6 +953,63 @@ function validateLocalCliCommandCompatibility({ command, args = [], runtimeId =
|
|
|
827
953
|
};
|
|
828
954
|
}
|
|
829
955
|
|
|
956
|
+
const usesCursor = isCursorLocalCliRuntime(runtimeShape);
|
|
957
|
+
if (usesCursor && !tokens.includes('--background-agent') && !tokens.includes('agent')) {
|
|
958
|
+
const runtimeLabel = runtimeId ? `Runtime "${runtimeId}"` : 'Cursor local_cli runtime';
|
|
959
|
+
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.`;
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
error_class: 'local_cli_command_incompatible',
|
|
963
|
+
recovery,
|
|
964
|
+
error: recovery,
|
|
965
|
+
diagnostic: {
|
|
966
|
+
runtime_id: runtimeId,
|
|
967
|
+
binary: binaryName,
|
|
968
|
+
rule: 'cursor_requires_agent_mode',
|
|
969
|
+
has_agent_flag: false,
|
|
970
|
+
recovery,
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const usesWindsurf = isWindsurfLocalCliRuntime(runtimeShape);
|
|
976
|
+
if (usesWindsurf && !tokens.includes('--agent')) {
|
|
977
|
+
const runtimeLabel = runtimeId ? `Runtime "${runtimeId}"` : 'Windsurf local_cli runtime';
|
|
978
|
+
const recovery = `${runtimeLabel} uses "windsurf" without an agent flag. Governed local runs require Windsurf's --agent mode for non-interactive execution.`;
|
|
979
|
+
return {
|
|
980
|
+
ok: false,
|
|
981
|
+
error_class: 'local_cli_command_incompatible',
|
|
982
|
+
recovery,
|
|
983
|
+
error: recovery,
|
|
984
|
+
diagnostic: {
|
|
985
|
+
runtime_id: runtimeId,
|
|
986
|
+
binary: binaryName,
|
|
987
|
+
rule: 'windsurf_requires_agent_mode',
|
|
988
|
+
has_agent_flag: false,
|
|
989
|
+
recovery,
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const usesOpenCode = isOpenCodeLocalCliRuntime(runtimeShape);
|
|
995
|
+
if (usesOpenCode && !tokens.includes('--non-interactive')) {
|
|
996
|
+
const runtimeLabel = runtimeId ? `Runtime "${runtimeId}"` : 'OpenCode local_cli runtime';
|
|
997
|
+
const recovery = `${runtimeLabel} uses "opencode" without --non-interactive. Governed local runs require OpenCode's non-interactive mode for unattended execution.`;
|
|
998
|
+
return {
|
|
999
|
+
ok: false,
|
|
1000
|
+
error_class: 'local_cli_command_incompatible',
|
|
1001
|
+
recovery,
|
|
1002
|
+
error: recovery,
|
|
1003
|
+
diagnostic: {
|
|
1004
|
+
runtime_id: runtimeId,
|
|
1005
|
+
binary: binaryName,
|
|
1006
|
+
rule: 'opencode_requires_non_interactive',
|
|
1007
|
+
has_non_interactive_flag: false,
|
|
1008
|
+
recovery,
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
830
1013
|
return { ok: true };
|
|
831
1014
|
}
|
|
832
1015
|
|
|
@@ -953,4 +1136,5 @@ export { resolveCommand };
|
|
|
953
1136
|
export { resolvePromptTransport };
|
|
954
1137
|
export { resolveStartupWatchdogMs };
|
|
955
1138
|
export { resolveStartupHeartbeatMs };
|
|
1139
|
+
export { resolveLivenessHeartbeatMs };
|
|
956
1140
|
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
|
+
}
|