llm-relay 0.85.0 → 0.86.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/README.md +39 -62
- package/THIRD_PARTY_NOTICES.md +5 -4
- package/dist/accounting-state.js.map +1 -1
- package/dist/accounting-store-schema.js.map +1 -1
- package/dist/accounting-store.js.map +1 -1
- package/dist/accounting.js.map +1 -1
- package/dist/authEnv.js.map +1 -1
- package/dist/availability-snapshot.js.map +1 -1
- package/dist/availability.js.map +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/benchmarks.js.map +1 -1
- package/dist/breaker-persistence.js.map +1 -1
- package/dist/candidate-runner.d.ts +6 -12
- package/dist/candidate-runner.js +16 -4
- package/dist/candidate-runner.js.map +1 -1
- package/dist/candidates.js.map +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/circuit-breaker.d.ts +14 -17
- package/dist/circuit-breaker.js +46 -6
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/claude-hook.js.map +1 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +127 -12
- package/dist/cli.js.map +1 -1
- package/dist/config/routing-parser.d.ts +3 -13
- package/dist/config/routing-parser.js +16 -10
- package/dist/config/routing-parser.js.map +1 -1
- package/dist/config-reload.d.ts +34 -0
- package/dist/config-reload.js +180 -0
- package/dist/config-reload.js.map +1 -0
- package/dist/config-types.d.ts +26 -65
- package/dist/config-types.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/configured-lane-execution-launcher.d.ts +36 -0
- package/dist/configured-lane-execution-launcher.js +133 -0
- package/dist/configured-lane-execution-launcher.js.map +1 -0
- package/dist/context-limits.js.map +1 -1
- package/dist/control-authorization.js.map +1 -1
- package/dist/cooldown-clear.js.map +1 -1
- package/dist/credential-fleet.js.map +1 -1
- package/dist/credential-select.js.map +1 -1
- package/dist/daemon-dispatch-view.d.ts +21 -0
- package/dist/daemon-dispatch-view.js +16 -0
- package/dist/daemon-dispatch-view.js.map +1 -0
- package/dist/dashboard/.vite/dashboard-bundle-graph.json +2 -1
- package/dist/dashboard/.vite/manifest.json +2 -2
- package/dist/dashboard/assets/index-BoSmrF3q.js +123 -0
- package/dist/dashboard/assets/index-DKPeE07T.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard-auth.js.map +1 -1
- package/dist/dashboard-contract.d.ts +2 -2
- package/dist/dashboard-routes.js.map +1 -1
- package/dist/dashboard-snapshot.js.map +1 -1
- package/dist/dashboard-static.js.map +1 -1
- package/dist/delegate-gate/cli.js.map +1 -1
- package/dist/dispatch-exhaustion-persistence.d.ts +1 -0
- package/dist/dispatch-exhaustion-persistence.js +7 -2
- package/dist/dispatch-exhaustion-persistence.js.map +1 -1
- package/dist/dispatch-lane-stats.d.ts +10 -17
- package/dist/dispatch-lane-stats.js +0 -5
- package/dist/dispatch-lane-stats.js.map +1 -1
- package/dist/dispatch.d.ts +32 -78
- package/dist/dispatch.js +51 -54
- package/dist/dispatch.js.map +1 -1
- package/dist/documents.js.map +1 -1
- package/dist/dotenv.js.map +1 -1
- package/dist/dynamic-pools.js.map +1 -1
- package/dist/executable-lookup.d.ts +2 -0
- package/dist/executable-lookup.js +7 -4
- package/dist/executable-lookup.js.map +1 -1
- package/dist/host-routing.js.map +1 -1
- package/dist/installed-hosts.js.map +1 -1
- package/dist/json-shape.js.map +1 -1
- package/dist/key-checker.js.map +1 -1
- package/dist/key-import.js.map +1 -1
- package/dist/keys-cli.js.map +1 -1
- package/dist/keystore.js.map +1 -1
- package/dist/lane-activity.d.ts +8 -0
- package/dist/lane-activity.js +27 -2
- package/dist/lane-activity.js.map +1 -1
- package/dist/lane-affinity.d.ts +4 -4
- package/dist/lane-affinity.js.map +1 -1
- package/dist/lane-cadence.js.map +1 -1
- package/dist/lane-execution-broker.d.ts +124 -0
- package/dist/lane-execution-broker.js +479 -0
- package/dist/lane-execution-broker.js.map +1 -0
- package/dist/lane-launch-env.d.ts +14 -0
- package/dist/lane-launch-env.js +52 -0
- package/dist/lane-launch-env.js.map +1 -0
- package/dist/lane-manifest.js.map +1 -1
- package/dist/lane-probe.js.map +1 -1
- package/dist/lane-quota-probe.js.map +1 -1
- package/dist/log.js.map +1 -1
- package/dist/mcp/job-archive.d.ts +12 -9
- package/dist/mcp/job-archive.js +44 -37
- package/dist/mcp/job-archive.js.map +1 -1
- package/dist/mcp/job-journal.d.ts +42 -2
- package/dist/mcp/job-journal.js +212 -32
- package/dist/mcp/job-journal.js.map +1 -1
- package/dist/mcp/lane-execution-client.d.ts +26 -0
- package/dist/mcp/lane-execution-client.js +117 -0
- package/dist/mcp/lane-execution-client.js.map +1 -0
- package/dist/mcp/lane-runner.d.ts +140 -25
- package/dist/mcp/lane-runner.js +361 -49
- package/dist/mcp/lane-runner.js.map +1 -1
- package/dist/mcp/persistence-lock.d.ts +13 -0
- package/dist/mcp/persistence-lock.js +4 -0
- package/dist/mcp/persistence-lock.js.map +1 -0
- package/dist/mcp/process-cpu.d.ts +18 -0
- package/dist/mcp/process-cpu.js +133 -0
- package/dist/mcp/process-cpu.js.map +1 -0
- package/dist/mcp/readonly-boundary.js.map +1 -1
- package/dist/mcp/server.d.ts +97 -3
- package/dist/mcp/server.js +547 -61
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tree-delta.js.map +1 -1
- package/dist/mcp/windows-npm-shim.d.ts +32 -0
- package/dist/mcp/windows-npm-shim.js +92 -0
- package/dist/mcp/windows-npm-shim.js.map +1 -0
- package/dist/metadata.js.map +1 -1
- package/dist/openai-dialect.js.map +1 -1
- package/dist/openai-request.js.map +1 -1
- package/dist/os-keyring.js.map +1 -1
- package/dist/ping/cadence.js.map +1 -1
- package/dist/ping/metrics.js.map +1 -1
- package/dist/ping/ping.js.map +1 -1
- package/dist/ping/probe-cache.js.map +1 -1
- package/dist/ping/quota.js.map +1 -1
- package/dist/ping/runtime-telemetry.js.map +1 -1
- package/dist/pool-health.js.map +1 -1
- package/dist/process-safety-net.js +18 -10
- package/dist/process-safety-net.js.map +1 -1
- package/dist/quota-observation.js.map +1 -1
- package/dist/rate-limits.js.map +1 -1
- package/dist/refusal-interpretation.js.map +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/request-log.js.map +1 -1
- package/dist/reshaper.js.map +1 -1
- package/dist/resolved-attempt.js.map +1 -1
- package/dist/responses-request.js.map +1 -1
- package/dist/routes/admin.d.ts +8 -0
- package/dist/routes/admin.js +54 -8
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/messages.js +10 -10
- package/dist/routes/messages.js.map +1 -1
- package/dist/routes/openai-front.js +4 -4
- package/dist/routes/openai-front.js.map +1 -1
- package/dist/secret-file-acl.js.map +1 -1
- package/dist/self-update.js.map +1 -1
- package/dist/server.d.ts +19 -0
- package/dist/server.js +70 -4
- package/dist/server.js.map +1 -1
- package/dist/session-pin.js.map +1 -1
- package/dist/setup-claude.d.ts +6 -1
- package/dist/setup-claude.js +3 -3
- package/dist/setup-claude.js.map +1 -1
- package/dist/spend-headroom.js.map +1 -1
- package/dist/sse.js.map +1 -1
- package/dist/state-paths.js.map +1 -1
- package/dist/storage/file-lock.d.ts +26 -0
- package/dist/storage/file-lock.js +127 -0
- package/dist/storage/file-lock.js.map +1 -0
- package/dist/storage/json-store.d.ts +33 -0
- package/dist/storage/json-store.js +68 -1
- package/dist/storage/json-store.js.map +1 -1
- package/dist/stream-commit.js.map +1 -1
- package/dist/stream-pipeline.js.map +1 -1
- package/dist/target-facts.js.map +1 -1
- package/dist/telemetry.d.ts +3 -1
- package/dist/telemetry.js +2 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tier-data.js.map +1 -1
- package/dist/tool-dialects.js.map +1 -1
- package/dist/usage-observer.js.map +1 -1
- package/dist/winenv.js.map +1 -1
- package/docs/README.md +4 -2
- package/docs/dashboard-bundle-inventory.json +5 -4
- package/package.json +15 -14
- package/scripts/install-skill.mjs +4 -2
- package/skills/llm-relay/SKILL.md +10 -8
- package/skills/llm-relay/references/dispatch-lanes.md +18 -1
- package/dist/dashboard/assets/index-C-I6hPeY.js +0 -403
- package/dist/dashboard/assets/index-CrSNlLMD.css +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { buildLaneEnv } from "../lane-launch-env.js";
|
|
2
|
+
export { buildLaneEnv, laneCredentialEnvNames } from "../lane-launch-env.js";
|
|
3
|
+
import { createLaneActivityTag, LANE_ACTIVITY_HEADER, withLaneActivityHeader, } from "../lane-activity.js";
|
|
3
4
|
import { DEFAULT_MCP_BLOCKING_WAIT_MS, DEFAULT_MCP_MAX_WAIT_MS, EFFORT_LEVELS } from "../config-types.js";
|
|
4
5
|
import { isToolUseBlock } from "../anthropic.js";
|
|
5
6
|
import { formatLaneStats, isPassThroughSpec, LANE_UNRELIABLE_STREAK, mcpPassThroughReason } from "../dispatch.js";
|
|
@@ -11,6 +12,8 @@ import { readOnlyInvoke, readOnlyVerdict } from "./readonly-boundary.js";
|
|
|
11
12
|
import { agyQuotaStatement } from "./agy-quota-log.js";
|
|
12
13
|
import { nullJobJournal } from "./job-journal.js";
|
|
13
14
|
import { nullJobArchive } from "./job-archive.js";
|
|
15
|
+
import { UNKNOWN_LANE_EXECUTION_MESSAGE } from "../lane-execution-broker.js";
|
|
16
|
+
import { createLaneExecutionId, } from "./lane-execution-client.js";
|
|
14
17
|
import { RPC_INTERNAL_ERROR, RPC_INVALID_PARAMS, RPC_METHOD_NOT_FOUND, RPC_PARSE_ERROR, encodeMessage, errorResponse, isJsonRpcNotification, isJsonRpcRequest, logStderr, negotiateProtocolVersion, resultResponse, splitMessages, } from "./protocol.js";
|
|
15
18
|
export const MCP_SERVER_NAME = "llm-relay";
|
|
16
19
|
export const DEFAULT_ANSWER_MAX_TOKENS = 4096;
|
|
@@ -32,7 +35,11 @@ export const MCP_INSTRUCTIONS = "Use dispatch from every MCP host — Claude, Co
|
|
|
32
35
|
"Call dispatch instead. If this MCP server is unavailable, run `llm-relay dispatch " +
|
|
33
36
|
"--next-command -t <task>` and follow its returned command or target; do not guess from the host.\n\n" +
|
|
34
37
|
"Let dispatch choose the lane. If a lane stops showing activity, dispatch moves to the next one " +
|
|
35
|
-
"itself and prefers the lane that answered on your next call.
|
|
38
|
+
"itself and prefers the lane that answered on your next call. For a running job, follow " +
|
|
39
|
+
"`walk-verdict`: `keep-running` means continue polling, `no-idle-stop` means the walk will " +
|
|
40
|
+
"not stop this attempt for idleness, and `unavailable` means another MCP process owns the live " +
|
|
41
|
+
"job so you should keep polling rather than infer. Do not infer liveness from elapsed time, output silence, " +
|
|
42
|
+
"historical duration, or activity diagnostics. Follow the advice at the end of a " +
|
|
36
43
|
"reply: when it says a lane was stopped while it was still working, dispatch again with that " +
|
|
37
44
|
"lane named so it can finish; when it says every lane ran and failed, do the work here — " +
|
|
38
45
|
"re-dispatching the same task picks the same lanes. To run one specific model instead of the " +
|
|
@@ -153,8 +160,12 @@ const TOOLS = [
|
|
|
153
160
|
{
|
|
154
161
|
name: "dispatch_status",
|
|
155
162
|
title: "Check a dispatched job",
|
|
156
|
-
description: "Report a dispatched job. While it runs
|
|
157
|
-
"
|
|
163
|
+
description: "Report a dispatched job. While it runs, `walk-verdict` is the authoritative liveness " +
|
|
164
|
+
"decision: keep-running means continue polling; no-idle-stop means this attempt is exempt " +
|
|
165
|
+
"from idle stopping; unavailable means another MCP process owns the live job, so keep polling. " +
|
|
166
|
+
"Activity may read advancing while the walk changes lanes; keep-running still means poll. " +
|
|
167
|
+
"Elapsed time, output silence, historical duration and activity diagnostics are diagnostics only. " +
|
|
168
|
+
"Once it has ended: its full answer, exactly as dispatch_result returns it. Wait at least a few " +
|
|
158
169
|
"seconds between polls. Without jobId: list the recent jobs of every llm-relay MCP server " +
|
|
159
170
|
"on this machine, so a lost jobId can be found again.",
|
|
160
171
|
inputSchema: {
|
|
@@ -314,6 +325,31 @@ function relayProvenanceLines(job) {
|
|
|
314
325
|
lines.push(`degraded: ${relay.degraded}`);
|
|
315
326
|
return lines;
|
|
316
327
|
}
|
|
328
|
+
function describeLiveness(job, now) {
|
|
329
|
+
if (job.status !== "running")
|
|
330
|
+
return [];
|
|
331
|
+
const liveness = job.liveness;
|
|
332
|
+
if (liveness === undefined) {
|
|
333
|
+
return ["activity: starting", "walk-verdict: keep-running", "activity-checked: pending"];
|
|
334
|
+
}
|
|
335
|
+
const checkedAgo = Math.max(0, Math.round((now - liveness.checkedAt) / 1000));
|
|
336
|
+
const lines = [
|
|
337
|
+
`activity: ${liveness.activity}`,
|
|
338
|
+
`walk-verdict: ${liveness.verdict}`,
|
|
339
|
+
`activity-checked: ${checkedAgo}s ago`,
|
|
340
|
+
];
|
|
341
|
+
lines.push(`activity-basis: ${liveness.source}`);
|
|
342
|
+
if (liveness.lastActivityAt !== null) {
|
|
343
|
+
const activityAgeAtCheck = Math.max(0, liveness.checkedAt - liveness.lastActivityAt);
|
|
344
|
+
lines.push(`last-activity-at-check: ${Math.round(activityAgeAtCheck / 1000)}s ago`);
|
|
345
|
+
}
|
|
346
|
+
if (liveness.verdict === "keep-running" && liveness.idleMs !== null && liveness.activity !== "advancing") {
|
|
347
|
+
const baselineAgeAtCheck = Math.max(0, liveness.checkedAt - liveness.idleBaselineAt);
|
|
348
|
+
const remainingAtCheck = Math.max(0, liveness.idleMs - baselineAgeAtCheck);
|
|
349
|
+
lines.push(`idle-stop-in-at-check: ${Math.ceil(remainingAtCheck / 1000)}s`);
|
|
350
|
+
}
|
|
351
|
+
return lines;
|
|
352
|
+
}
|
|
317
353
|
function describeJob(job, now) {
|
|
318
354
|
const elapsed = Math.round(((job.endedAt ?? now) - job.startedAt) / 1000);
|
|
319
355
|
const head = [
|
|
@@ -331,17 +367,20 @@ function describeJob(job, now) {
|
|
|
331
367
|
head.push(...relayProvenanceLines(job));
|
|
332
368
|
if (job.dispatchSource === "fallback")
|
|
333
369
|
head.push("dispatch-source: local-fallback (daemon unreachable)");
|
|
370
|
+
if (job.executionOwner === "relay-daemon")
|
|
371
|
+
head.push("execution-owner: relay-daemon (survives MCP host restart)");
|
|
372
|
+
if (job.executionOwner === "local-fallback")
|
|
373
|
+
head.push("execution-owner: local-fallback (does NOT survive MCP host restart)");
|
|
334
374
|
if (job.restored === true) {
|
|
335
|
-
head.push(
|
|
375
|
+
head.push(job.status === "running"
|
|
376
|
+
? "record: recovered after MCP restart — this job's process tree is owned by the relay daemon"
|
|
377
|
+
: "record: restored from disk — this job ended in an earlier llm-relay MCP server process or in another host's one");
|
|
336
378
|
}
|
|
337
379
|
if (job.readOnly)
|
|
338
380
|
head.push(`read-only: ${job.readOnly.binding}`);
|
|
339
381
|
if (job.launch?.length)
|
|
340
382
|
head.push(`launch: ${job.launch.join("; ")}`);
|
|
341
|
-
|
|
342
|
-
const ago = Math.max(0, Math.round((now - job.lastActive.at) / 1000));
|
|
343
|
-
head.push(`last activity: ${ago}s ago (${job.lastActive.source})`);
|
|
344
|
-
}
|
|
383
|
+
head.push(...describeLiveness(job, now));
|
|
345
384
|
const usually = runningTimeToAnswer(job);
|
|
346
385
|
if (usually !== null)
|
|
347
386
|
head.push(usually);
|
|
@@ -409,7 +448,9 @@ function jobAnswerBody(job, now) {
|
|
|
409
448
|
return `${header}\n\nStill running. Poll dispatch_status, then call dispatch_result.`;
|
|
410
449
|
}
|
|
411
450
|
if (job.status === "killed") {
|
|
412
|
-
return
|
|
451
|
+
return job.restored === true
|
|
452
|
+
? `${header}\n\nThe daemon-owned lane execution could no longer be recovered. No terminal result was collectable, so re-dispatch from scratch and check the working directory for partial files first.`
|
|
453
|
+
: `${header}\n\nThe lane was KILLED when the llm-relay MCP server restarted — it did not fail and it did not time out. Its process is gone. Re-dispatch from scratch, and check the working directory first: only files the lane wrote before the restart survive.`;
|
|
413
454
|
}
|
|
414
455
|
if (job.status === "timed_out") {
|
|
415
456
|
const partial = isContentEmpty(body) ? "" : `\n\nPartial output before the timeout:\n\n${body}`;
|
|
@@ -422,6 +463,7 @@ function jobAnswerBody(job, now) {
|
|
|
422
463
|
}
|
|
423
464
|
return `${header}\n\n${body}${exhausted}`;
|
|
424
465
|
}
|
|
466
|
+
export const BROKER_STATUS_POLL_MS = 1_000;
|
|
425
467
|
function emptyRun() {
|
|
426
468
|
return { code: null, stdout: "", stderr: "", timedOut: false };
|
|
427
469
|
}
|
|
@@ -437,7 +479,7 @@ function attemptReason(status, outcome, idleMs) {
|
|
|
437
479
|
return undefined;
|
|
438
480
|
if (status === "abandoned") {
|
|
439
481
|
const quiet = idleMs === null ? "a long time" : `${Math.round(idleMs / 1000)}s`;
|
|
440
|
-
return `no activity for ${quiet} (no relay traffic, output or file change), so the next lane was started`;
|
|
482
|
+
return `no activity for ${quiet} (no relay traffic, output, process CPU or file change), so the next lane was started`;
|
|
441
483
|
}
|
|
442
484
|
if (outcome.semanticFailure !== undefined)
|
|
443
485
|
return outcome.semanticFailure;
|
|
@@ -456,10 +498,7 @@ function flagValue(args, flag) {
|
|
|
456
498
|
return joined === undefined ? null : joined.slice(flag.length + 1);
|
|
457
499
|
}
|
|
458
500
|
export const IDLE_POLL_MS = 15_000;
|
|
459
|
-
|
|
460
|
-
const line = `${LANE_ACTIVITY_HEADER}: ${tag}`;
|
|
461
|
-
return existing === undefined || existing.trim() === "" ? line : `${existing}\n${line}`;
|
|
462
|
-
}
|
|
501
|
+
const PROCESS_CPU_ACTIVITY_MS = 1_000;
|
|
463
502
|
function newestActivity(seen) {
|
|
464
503
|
let best = null;
|
|
465
504
|
for (const s of seen) {
|
|
@@ -469,9 +508,6 @@ function newestActivity(seen) {
|
|
|
469
508
|
}
|
|
470
509
|
return best;
|
|
471
510
|
}
|
|
472
|
-
function newActivityTag() {
|
|
473
|
-
return randomUUID().replaceAll("-", "");
|
|
474
|
-
}
|
|
475
511
|
function pollTimer(ms) {
|
|
476
512
|
return new Promise((resolve) => {
|
|
477
513
|
const t = setTimeout(() => resolve({ kind: "poll" }), ms);
|
|
@@ -482,16 +518,6 @@ function pollTimer(ms) {
|
|
|
482
518
|
function laneAnswered(r) {
|
|
483
519
|
return r.code === 0 && !r.timedOut && !isContentEmpty(r.stdout);
|
|
484
520
|
}
|
|
485
|
-
function applyLaneEnv(base, deltas) {
|
|
486
|
-
const env = { ...base };
|
|
487
|
-
for (const [name, value] of Object.entries(deltas ?? {})) {
|
|
488
|
-
if (value === null)
|
|
489
|
-
delete env[name];
|
|
490
|
-
else
|
|
491
|
-
env[name] = value;
|
|
492
|
-
}
|
|
493
|
-
return env;
|
|
494
|
-
}
|
|
495
521
|
function laneSummary(lane, inFlight) {
|
|
496
522
|
const bits = [`${lane.position}. ${lane.id}`, `[${lane.state}]`];
|
|
497
523
|
if (lane.spec)
|
|
@@ -504,6 +530,11 @@ function laneSummary(lane, inFlight) {
|
|
|
504
530
|
bits.push(`unreachable: ${lane.unreachable}`);
|
|
505
531
|
if (lane.note)
|
|
506
532
|
bits.push(lane.note);
|
|
533
|
+
if (lane.capabilityBasis !== undefined) {
|
|
534
|
+
bits.push(lane.capability === undefined
|
|
535
|
+
? "capability: unknown (no evidence-qualified limit)"
|
|
536
|
+
: `capability: ${lane.capability} (${lane.capabilityBasis})`);
|
|
537
|
+
}
|
|
507
538
|
bits.push(...laneEvidenceBits(lane));
|
|
508
539
|
if (lane.maxConcurrent !== null && lane.maxConcurrent !== undefined) {
|
|
509
540
|
bits.push(`in flight: ${inFlight} of ${lane.maxConcurrent}`);
|
|
@@ -535,6 +566,11 @@ export class McpDispatchServer {
|
|
|
535
566
|
cancellable = new Map();
|
|
536
567
|
trees = new Map();
|
|
537
568
|
activityTags = new Map();
|
|
569
|
+
processCpu = new Map();
|
|
570
|
+
brokerCpu = new Map();
|
|
571
|
+
liveBrokerSnapshots = new Map();
|
|
572
|
+
startup;
|
|
573
|
+
brokerStartup;
|
|
538
574
|
constructor(deps) {
|
|
539
575
|
this.deps = deps;
|
|
540
576
|
this.jobs = new LaneJobStore(deps.journal ?? nullJobJournal, deps.archive ?? nullJobArchive);
|
|
@@ -543,6 +579,9 @@ export class McpDispatchServer {
|
|
|
543
579
|
this.now = deps.now ?? Date.now;
|
|
544
580
|
this.cwd = deps.cwd ?? (() => process.cwd());
|
|
545
581
|
this.maxDepth = deps.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
582
|
+
const killedTreeStartup = this.restoreKilledTreeDeltas();
|
|
583
|
+
this.brokerStartup = this.restoreBrokerOrphans();
|
|
584
|
+
this.startup = Promise.all([killedTreeStartup, this.brokerStartup]).then(() => undefined);
|
|
546
585
|
}
|
|
547
586
|
ingest(chunk) {
|
|
548
587
|
this.buffer += chunk;
|
|
@@ -661,10 +700,13 @@ export class McpDispatchServer {
|
|
|
661
700
|
case "dispatch":
|
|
662
701
|
return this.toolDispatch(args, ctx);
|
|
663
702
|
case "dispatch_status":
|
|
703
|
+
await this.startup;
|
|
664
704
|
return this.toolStatus(args);
|
|
665
705
|
case "dispatch_result":
|
|
706
|
+
await this.startup;
|
|
666
707
|
return this.toolResult(args);
|
|
667
708
|
case "dispatch_cancel":
|
|
709
|
+
await this.brokerStartup;
|
|
668
710
|
return this.toolCancel(args);
|
|
669
711
|
case "dispatch_lanes":
|
|
670
712
|
return this.toolLanes(args);
|
|
@@ -706,14 +748,17 @@ export class McpDispatchServer {
|
|
|
706
748
|
const next = view.next ? `\n\nnext: ${view.next.id} — ${view.reason}` : `\n\nnext: none — ${view.reason}`;
|
|
707
749
|
const walk = this.deps.config.routing?.dispatchWalk;
|
|
708
750
|
const rule = walk?.enabled === true
|
|
709
|
-
? `\nwalk: a lane is stopped after ${Math.round(walk.idleMs / 1000)}s with no activity (relay traffic, output or file change); the last lane is never stopped`
|
|
751
|
+
? `\nwalk: a lane is stopped after ${Math.round(walk.idleMs / 1000)}s with no activity (relay traffic, output, owned process CPU or file change); the last lane is never stopped`
|
|
710
752
|
: "\nwalk: off — only the first lane runs";
|
|
711
753
|
return textResult(`tier: ${view.tier ?? "default"}${rule}\n\n${lines.join("\n")}${next}`);
|
|
712
754
|
}
|
|
713
|
-
toolStatus(args) {
|
|
755
|
+
async toolStatus(args) {
|
|
714
756
|
const jobId = readString(args, "jobId");
|
|
715
|
-
if (!jobId)
|
|
757
|
+
if (!jobId) {
|
|
758
|
+
await this.refreshAllBrokerJobs();
|
|
716
759
|
return textResult(this.recentJobs());
|
|
760
|
+
}
|
|
761
|
+
await this.refreshBrokerJob(jobId);
|
|
717
762
|
const job = this.jobs.find(jobId);
|
|
718
763
|
if (!job)
|
|
719
764
|
return this.unknownJob(jobId);
|
|
@@ -721,10 +766,11 @@ export class McpDispatchServer {
|
|
|
721
766
|
return textResult(describeJob(job, this.now()));
|
|
722
767
|
return textResult(jobAnswer(job, this.now()), isFailureStatus(job.status));
|
|
723
768
|
}
|
|
724
|
-
toolResult(args) {
|
|
769
|
+
async toolResult(args) {
|
|
725
770
|
const jobId = readString(args, "jobId");
|
|
726
771
|
if (!jobId)
|
|
727
772
|
return textResult("dispatch_result requires jobId", true);
|
|
773
|
+
await this.refreshBrokerJob(jobId);
|
|
728
774
|
const job = this.jobs.find(jobId);
|
|
729
775
|
if (!job)
|
|
730
776
|
return this.unknownJob(jobId);
|
|
@@ -753,18 +799,55 @@ export class McpDispatchServer {
|
|
|
753
799
|
`lane: ${elsewhere.laneId}${elsewhere.spec ? ` (${elsewhere.spec})` : ""}`,
|
|
754
800
|
"status: running",
|
|
755
801
|
`elapsed: ${elapsed}s`,
|
|
802
|
+
"activity: unavailable",
|
|
803
|
+
"walk-verdict: unavailable",
|
|
756
804
|
`record: running in another llm-relay MCP server process (pid ${elsewhere.pid})`,
|
|
757
805
|
].join("\n") +
|
|
758
|
-
"\n\nThis process cannot
|
|
759
|
-
"
|
|
806
|
+
"\n\nThis process cannot observe that process's live activity or cancel it. " +
|
|
807
|
+
"`walk-verdict: unavailable` means keep polling rather than infer from elapsed time or silence; " +
|
|
808
|
+
"its answer appears here once it ends.");
|
|
760
809
|
}
|
|
761
|
-
toolCancel(args) {
|
|
810
|
+
async toolCancel(args) {
|
|
762
811
|
const jobId = readString(args, "jobId");
|
|
763
812
|
if (!jobId)
|
|
764
813
|
return textResult("dispatch_cancel requires jobId", true);
|
|
765
814
|
const job = this.jobs.find(jobId);
|
|
766
815
|
if (!job)
|
|
767
816
|
return this.unknownJob(jobId);
|
|
817
|
+
if (job.status !== "running") {
|
|
818
|
+
return textResult(`${jobId} was already ${job.status}`);
|
|
819
|
+
}
|
|
820
|
+
const executionId = this.jobs.brokerExecution(jobId);
|
|
821
|
+
if (executionId !== undefined) {
|
|
822
|
+
const client = this.deps.laneExecutionClient;
|
|
823
|
+
if (client === undefined) {
|
|
824
|
+
return textResult(`could not cancel ${jobId}: the daemon execution client is unavailable; the job was left running`, true);
|
|
825
|
+
}
|
|
826
|
+
let result;
|
|
827
|
+
try {
|
|
828
|
+
result = await client.request({ action: "cancel", executionId });
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
return textResult(`could not cancel ${jobId}: the daemon execution client failed; the job was left running`, true);
|
|
832
|
+
}
|
|
833
|
+
if (!result.ok) {
|
|
834
|
+
if (result.kind === "rejected" && result.status === 404 && result.message === UNKNOWN_LANE_EXECUTION_MESSAGE) {
|
|
835
|
+
this.jobs.markBrokerKilled(jobId, "the relay daemon is reachable but no longer knows this recovered lane execution");
|
|
836
|
+
await this.recordTreeDelta(jobId);
|
|
837
|
+
return textResult(`${jobId} was already killed`, true);
|
|
838
|
+
}
|
|
839
|
+
return textResult(`could not cancel ${jobId}: ${result.message}; the daemon-owned execution was left untouched`, true);
|
|
840
|
+
}
|
|
841
|
+
if (!this.jobs.applyBrokerSnapshot(jobId, result.execution)) {
|
|
842
|
+
return textResult(`could not confirm cancellation of ${jobId}: the daemon returned a mismatched execution snapshot`, true);
|
|
843
|
+
}
|
|
844
|
+
if (result.execution.status !== "running")
|
|
845
|
+
await this.recordTreeDelta(jobId);
|
|
846
|
+
const current = this.jobs.find(jobId);
|
|
847
|
+
return textResult(current?.status === "cancelled"
|
|
848
|
+
? `cancelled ${jobId}`
|
|
849
|
+
: `${jobId} was already ${current?.status ?? result.execution.status}`);
|
|
850
|
+
}
|
|
768
851
|
const cancelled = this.jobs.cancel(jobId);
|
|
769
852
|
if (cancelled)
|
|
770
853
|
void this.recordTreeDelta(jobId);
|
|
@@ -815,6 +898,7 @@ export class McpDispatchServer {
|
|
|
815
898
|
readOnly: args["readOnly"] === true,
|
|
816
899
|
scope: readStringArray(args, "scope"),
|
|
817
900
|
tier: view.tier ?? undefined,
|
|
901
|
+
host: view.host,
|
|
818
902
|
timeoutMs: readNumber(args, "timeoutMs") ?? DEFAULT_LANE_TIMEOUT_MS,
|
|
819
903
|
idleMs: walk !== null ? walk.idleMs : null,
|
|
820
904
|
system: readString(args, "system"),
|
|
@@ -830,6 +914,7 @@ export class McpDispatchServer {
|
|
|
830
914
|
});
|
|
831
915
|
if (!readOnlyVerdictResult.ok)
|
|
832
916
|
return textResult(readOnlyVerdictResult.refusal, true);
|
|
917
|
+
await this.brokerStartup;
|
|
833
918
|
const first = view.ladder.find((l) => l.id === ordered[0]) ?? view.next;
|
|
834
919
|
const job = this.jobs.create(first.id, first.spec, opts.cwd, opts.dispatchSource, taskLabel(task));
|
|
835
920
|
this.jobs.noteWalkScope(job.id, { enabled: walk !== null, lanesNotTried: notTried, forced });
|
|
@@ -846,7 +931,10 @@ export class McpDispatchServer {
|
|
|
846
931
|
this.jobs.fail(job.id, `dispatch walk failed: ${e.message}`);
|
|
847
932
|
return "done";
|
|
848
933
|
})
|
|
849
|
-
.finally(() =>
|
|
934
|
+
.finally(() => {
|
|
935
|
+
this.activityTags.delete(job.id);
|
|
936
|
+
this.processCpu.delete(job.id);
|
|
937
|
+
});
|
|
850
938
|
return this.awaitOrPoll(job.id, settled, waitMs.waitMs, {
|
|
851
939
|
clamp: waitMs.clamped ? { requested: waitMs.requested, ceiling, key: ceilingKey } : undefined,
|
|
852
940
|
progressToken: blocking?.progress === true ? ctx.progressToken : undefined,
|
|
@@ -871,6 +959,7 @@ export class McpDispatchServer {
|
|
|
871
959
|
}
|
|
872
960
|
async runWalk(jobId, view, laneIds, task, opts) {
|
|
873
961
|
await this.startTreeDelta(jobId, opts);
|
|
962
|
+
let ranAttempt = false;
|
|
874
963
|
for (let i = 0; i < laneIds.length; i++) {
|
|
875
964
|
if (this.jobs.get(jobId)?.status !== "running")
|
|
876
965
|
return "done";
|
|
@@ -883,6 +972,12 @@ export class McpDispatchServer {
|
|
|
883
972
|
const readOnly = this.bindReadOnlyOrSkip(jobId, lane, opts);
|
|
884
973
|
if (readOnly.skipped)
|
|
885
974
|
continue;
|
|
975
|
+
if (ranAttempt) {
|
|
976
|
+
await this.resetLaneTreeActivity(jobId);
|
|
977
|
+
if (this.jobs.get(jobId)?.status !== "running")
|
|
978
|
+
return "done";
|
|
979
|
+
}
|
|
980
|
+
ranAttempt = true;
|
|
886
981
|
this.jobs.setCurrentLane(jobId, lane.id, lane.spec, lane.timeToAnswer);
|
|
887
982
|
if (readOnly.bound !== undefined)
|
|
888
983
|
this.jobs.noteReadOnly(jobId, lane.id, readOnly.bound.binding);
|
|
@@ -919,6 +1014,11 @@ export class McpDispatchServer {
|
|
|
919
1014
|
requestedMode: opts.mode,
|
|
920
1015
|
adHoc: lane.adHoc === true,
|
|
921
1016
|
}, outcome.run.stdout.length, { status, wallClockMs: elapsedMs, exitCode: outcome.run.code });
|
|
1017
|
+
if (status !== "completed" && !isLast && this.jobs.brokerExecution(jobId) !== undefined) {
|
|
1018
|
+
this.jobs.clearBrokerExecution(jobId);
|
|
1019
|
+
this.liveBrokerSnapshots.delete(jobId);
|
|
1020
|
+
this.brokerCpu.delete(jobId);
|
|
1021
|
+
}
|
|
922
1022
|
if (status === "completed" || isLast) {
|
|
923
1023
|
await this.recordTreeDelta(jobId);
|
|
924
1024
|
this.jobs.complete(jobId, outcome.run, status === "completed" ? undefined : (outcome.semanticFailure ?? reason), outcome.relay);
|
|
@@ -941,8 +1041,168 @@ export class McpDispatchServer {
|
|
|
941
1041
|
if (opts.mode !== "agent")
|
|
942
1042
|
return;
|
|
943
1043
|
const before = await this.readTree(opts.cwd);
|
|
944
|
-
if (before !== null)
|
|
945
|
-
this.trees.set(jobId, { cwd: opts.cwd, before, scope: opts.scope });
|
|
1044
|
+
if (before !== null) {
|
|
1045
|
+
this.trees.set(jobId, { cwd: opts.cwd, before, scope: opts.scope, activityLastSeen: before });
|
|
1046
|
+
this.jobs.noteStartingTree(jobId, before, opts.scope);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async restoreBrokerOrphans() {
|
|
1050
|
+
const rows = this.jobs.takeBrokerOrphans();
|
|
1051
|
+
await Promise.all(rows.map(async (row) => {
|
|
1052
|
+
const job = this.jobs.adoptBrokerRunning(row);
|
|
1053
|
+
if (job === undefined)
|
|
1054
|
+
return;
|
|
1055
|
+
if (row.startingTree !== undefined) {
|
|
1056
|
+
const before = {
|
|
1057
|
+
prefix: row.startingTree.prefix,
|
|
1058
|
+
entries: new Map(row.startingTree.entries),
|
|
1059
|
+
};
|
|
1060
|
+
this.trees.set(row.jobId, {
|
|
1061
|
+
cwd: row.cwd,
|
|
1062
|
+
before,
|
|
1063
|
+
scope: row.startingTree.scope,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
this.noteBrokerRecoveryUnavailable(row.jobId, "recovered-broker-pending");
|
|
1067
|
+
}));
|
|
1068
|
+
}
|
|
1069
|
+
async refreshAllBrokerJobs() {
|
|
1070
|
+
const pending = this.jobs
|
|
1071
|
+
.list()
|
|
1072
|
+
.filter((job) => job.status === "running" && this.jobs.brokerExecution(job.id) !== undefined)
|
|
1073
|
+
.map((job) => this.refreshBrokerJob(job.id));
|
|
1074
|
+
await Promise.all(pending);
|
|
1075
|
+
}
|
|
1076
|
+
async refreshBrokerJob(jobId) {
|
|
1077
|
+
const job = this.jobs.get(jobId);
|
|
1078
|
+
if (job !== undefined && job.restored !== true)
|
|
1079
|
+
return false;
|
|
1080
|
+
const executionId = this.jobs.brokerExecution(jobId);
|
|
1081
|
+
if (executionId === undefined)
|
|
1082
|
+
return false;
|
|
1083
|
+
const client = this.deps.laneExecutionClient;
|
|
1084
|
+
if (client === undefined) {
|
|
1085
|
+
this.noteBrokerRecoveryUnavailable(jobId, "broker-client-unavailable");
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
let result;
|
|
1089
|
+
try {
|
|
1090
|
+
result = await client.request({ action: "status", executionId });
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
this.noteBrokerRecoveryUnavailable(jobId, "broker-client-threw");
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
if (!result.ok) {
|
|
1097
|
+
if (result.kind === "rejected" && result.status === 404 && result.message === UNKNOWN_LANE_EXECUTION_MESSAGE) {
|
|
1098
|
+
const killed = this.jobs.markBrokerKilled(jobId, "the relay daemon is reachable but no longer knows this recovered lane execution; " +
|
|
1099
|
+
"the execution ended outside the retained broker window or the daemon restarted");
|
|
1100
|
+
if (killed) {
|
|
1101
|
+
this.brokerCpu.delete(jobId);
|
|
1102
|
+
await this.recordTreeDelta(jobId);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
this.noteBrokerRecoveryUnavailable(jobId, result.kind === "invalid-response" ? "broker-response-invalid" : "broker-unavailable");
|
|
1107
|
+
}
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
const snapshot = result.execution;
|
|
1111
|
+
if (!this.jobs.applyBrokerSnapshot(jobId, snapshot)) {
|
|
1112
|
+
this.noteBrokerRecoveryUnavailable(jobId, "broker-response-mismatch");
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
if (snapshot.status === "running") {
|
|
1116
|
+
await this.noteRecoveredBrokerLiveness(jobId, snapshot);
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
this.brokerCpu.delete(jobId);
|
|
1120
|
+
await this.recordTreeDelta(jobId);
|
|
1121
|
+
}
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
noteBrokerRecoveryUnavailable(jobId, source) {
|
|
1125
|
+
const job = this.jobs.get(jobId);
|
|
1126
|
+
if (job === undefined || job.status !== "running")
|
|
1127
|
+
return;
|
|
1128
|
+
this.jobs.noteLiveness(jobId, {
|
|
1129
|
+
activity: "unmonitored",
|
|
1130
|
+
verdict: "no-idle-stop",
|
|
1131
|
+
checkedAt: this.now(),
|
|
1132
|
+
idleBaselineAt: job.activity?.attemptStartedAt ?? job.startedAt,
|
|
1133
|
+
lastActivityAt: job.liveness?.lastActivityAt ?? null,
|
|
1134
|
+
source,
|
|
1135
|
+
idleMs: null,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
async noteRecoveredBrokerLiveness(jobId, snapshot) {
|
|
1139
|
+
const now = this.now();
|
|
1140
|
+
const seen = [
|
|
1141
|
+
{ at: snapshot.lastOutputAt, source: "lane-output" },
|
|
1142
|
+
snapshot.relayInFlight !== undefined && snapshot.relayInFlight > 0
|
|
1143
|
+
? { at: now, source: "relay-in-flight" }
|
|
1144
|
+
: { at: snapshot.relayLastActivityAt, source: "relay-traffic" },
|
|
1145
|
+
];
|
|
1146
|
+
if (snapshot.cpuMs !== undefined) {
|
|
1147
|
+
const previous = this.brokerCpu.get(jobId);
|
|
1148
|
+
this.brokerCpu.set(jobId, snapshot.cpuMs);
|
|
1149
|
+
if (previous !== undefined && snapshot.cpuMs - previous >= PROCESS_CPU_ACTIVITY_MS) {
|
|
1150
|
+
seen.push({ at: now, source: "process-cpu" });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const tree = this.trees.get(jobId);
|
|
1154
|
+
const reading = tree === undefined ? null : await this.readTree(tree.cwd);
|
|
1155
|
+
if (tree !== undefined && reading !== null) {
|
|
1156
|
+
const previous = tree.activityLastSeen;
|
|
1157
|
+
tree.activityLastSeen = reading;
|
|
1158
|
+
if (previous !== undefined) {
|
|
1159
|
+
seen.push(sameTree(previous, reading)
|
|
1160
|
+
? { at: newestChangeMs(tree.cwd, reading), source: "file-change" }
|
|
1161
|
+
: { at: now, source: "working-tree" });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const previous = this.jobs.get(jobId)?.liveness;
|
|
1165
|
+
if (previous?.lastActivityAt !== null && previous?.lastActivityAt !== undefined) {
|
|
1166
|
+
seen.push({ at: previous.lastActivityAt, source: previous.source });
|
|
1167
|
+
}
|
|
1168
|
+
const latest = newestActivity(seen);
|
|
1169
|
+
const activity = snapshot.relayInFlight !== undefined && snapshot.relayInFlight > 0
|
|
1170
|
+
? "active"
|
|
1171
|
+
: latest === null
|
|
1172
|
+
? "unmonitored"
|
|
1173
|
+
: now - latest.at < IDLE_POLL_MS
|
|
1174
|
+
? "active"
|
|
1175
|
+
: "quiet";
|
|
1176
|
+
this.jobs.noteLiveness(jobId, {
|
|
1177
|
+
activity,
|
|
1178
|
+
verdict: "no-idle-stop",
|
|
1179
|
+
checkedAt: now,
|
|
1180
|
+
idleBaselineAt: snapshot.startedAt,
|
|
1181
|
+
lastActivityAt: latest?.at ?? null,
|
|
1182
|
+
source: latest?.source ?? "recovered-broker",
|
|
1183
|
+
idleMs: null,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
async restoreKilledTreeDeltas() {
|
|
1187
|
+
const adopted = this.jobs.takeAdoptedStartingTrees();
|
|
1188
|
+
await Promise.all(adopted.map(async ({ jobId, cwd, startingTree }) => {
|
|
1189
|
+
const after = await this.readTree(cwd);
|
|
1190
|
+
if (after === null)
|
|
1191
|
+
return;
|
|
1192
|
+
const before = { prefix: startingTree.prefix, entries: new Map(startingTree.entries) };
|
|
1193
|
+
const delta = renderTreeDelta(cwd, before, after, startingTree.scope);
|
|
1194
|
+
this.jobs.noteTreeDelta(jobId, `${delta}\n note: measured at restart adoption time, not at the time the job was killed`);
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
async resetLaneTreeActivity(jobId) {
|
|
1198
|
+
const tree = this.trees.get(jobId);
|
|
1199
|
+
if (tree === undefined)
|
|
1200
|
+
return;
|
|
1201
|
+
const reading = await this.readTree(tree.cwd);
|
|
1202
|
+
if (reading === null)
|
|
1203
|
+
delete tree.activityLastSeen;
|
|
1204
|
+
else
|
|
1205
|
+
tree.activityLastSeen = reading;
|
|
946
1206
|
}
|
|
947
1207
|
async recordTreeDelta(jobId) {
|
|
948
1208
|
const tree = this.trees.get(jobId);
|
|
@@ -999,7 +1259,8 @@ export class McpDispatchServer {
|
|
|
999
1259
|
spec: lane.spec,
|
|
1000
1260
|
status: SKIPPED_LANE_STATUS,
|
|
1001
1261
|
elapsedMs: 0,
|
|
1002
|
-
reason: `lane "${lane.id}"
|
|
1262
|
+
reason: `lane "${lane.id}" has derived capability ${lane.capability}` +
|
|
1263
|
+
`${lane.capabilityBasis ? ` (${lane.capabilityBasis})` : ""}, below this ${opts.tier} dispatch`,
|
|
1003
1264
|
});
|
|
1004
1265
|
this.jobs.noteSkippedLane(jobId);
|
|
1005
1266
|
return true;
|
|
@@ -1007,26 +1268,56 @@ export class McpDispatchServer {
|
|
|
1007
1268
|
async latestActivity(jobId) {
|
|
1008
1269
|
const now = this.now();
|
|
1009
1270
|
const seen = [
|
|
1010
|
-
{ at: this.jobs.get(jobId)?.activity?.lastOutputAt, source: "lane
|
|
1271
|
+
{ at: this.jobs.get(jobId)?.activity?.lastOutputAt, source: "lane-output" },
|
|
1011
1272
|
];
|
|
1273
|
+
const broker = this.liveBrokerSnapshots.get(jobId);
|
|
1274
|
+
if (broker !== undefined) {
|
|
1275
|
+
seen.push(broker.relayInFlight !== undefined && broker.relayInFlight > 0
|
|
1276
|
+
? { at: now, source: "relay-in-flight" }
|
|
1277
|
+
: { at: broker.relayLastActivityAt, source: "relay-traffic" });
|
|
1278
|
+
if (broker.cpuMs !== undefined) {
|
|
1279
|
+
const previous = this.brokerCpu.get(jobId);
|
|
1280
|
+
this.brokerCpu.set(jobId, broker.cpuMs);
|
|
1281
|
+
if (previous !== undefined && broker.cpuMs - previous >= PROCESS_CPU_ACTIVITY_MS) {
|
|
1282
|
+
seen.push({ at: now, source: "process-cpu" });
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1012
1286
|
const tag = this.activityTags.get(jobId);
|
|
1013
|
-
if (tag !== undefined && this.deps.readLaneActivity !== undefined) {
|
|
1287
|
+
if (broker === undefined && tag !== undefined && this.deps.readLaneActivity !== undefined) {
|
|
1014
1288
|
const traffic = await this.deps.readLaneActivity(tag).catch(() => null);
|
|
1015
1289
|
seen.push(traffic !== null && traffic.inFlight > 0
|
|
1016
|
-
? { at: now, source: "
|
|
1017
|
-
: { at: traffic?.lastActivityAt, source: "relay
|
|
1290
|
+
? { at: now, source: "relay-in-flight" }
|
|
1291
|
+
: { at: traffic?.lastActivityAt, source: "relay-traffic" });
|
|
1018
1292
|
}
|
|
1019
1293
|
const early = newestActivity(seen);
|
|
1020
1294
|
if (early !== null && now - early.at < IDLE_POLL_MS)
|
|
1021
1295
|
return early;
|
|
1296
|
+
const pids = broker === undefined ? this.jobs.runningPids(jobId) : [];
|
|
1297
|
+
if (pids.length > 0 && this.deps.readProcessCpu !== undefined) {
|
|
1298
|
+
const cpuMs = await this.deps.readProcessCpu(pids).catch(() => null);
|
|
1299
|
+
if (typeof cpuMs === "number" && Number.isFinite(cpuMs) && cpuMs >= 0) {
|
|
1300
|
+
const previous = this.processCpu.get(jobId);
|
|
1301
|
+
this.processCpu.set(jobId, cpuMs);
|
|
1302
|
+
if (previous !== undefined && cpuMs - previous >= PROCESS_CPU_ACTIVITY_MS) {
|
|
1303
|
+
seen.push({ at: this.now(), source: "process-cpu" });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const afterCpu = newestActivity(seen);
|
|
1308
|
+
const afterCpuNow = this.now();
|
|
1309
|
+
if (afterCpu !== null && afterCpuNow - afterCpu.at < IDLE_POLL_MS)
|
|
1310
|
+
return afterCpu;
|
|
1022
1311
|
const tree = this.trees.get(jobId);
|
|
1023
1312
|
const reading = tree === undefined ? null : await this.readTree(tree.cwd);
|
|
1024
1313
|
if (tree !== undefined && reading !== null) {
|
|
1025
|
-
const previous = tree.
|
|
1026
|
-
tree.
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1314
|
+
const previous = tree.activityLastSeen;
|
|
1315
|
+
tree.activityLastSeen = reading;
|
|
1316
|
+
if (previous !== undefined) {
|
|
1317
|
+
seen.push(sameTree(previous, reading)
|
|
1318
|
+
? { at: newestChangeMs(tree.cwd, reading), source: "file-change" }
|
|
1319
|
+
: { at: now, source: "working-tree" });
|
|
1320
|
+
}
|
|
1030
1321
|
}
|
|
1031
1322
|
return newestActivity(seen);
|
|
1032
1323
|
}
|
|
@@ -1052,19 +1343,48 @@ export class McpDispatchServer {
|
|
|
1052
1343
|
}
|
|
1053
1344
|
async runOneLane(jobId, lane, task, opts, idleMs, invoke) {
|
|
1054
1345
|
const attemptStart = this.now();
|
|
1055
|
-
const started = this.startLane(jobId, lane, task, opts, invoke);
|
|
1346
|
+
const started = await this.startLane(jobId, lane, task, opts, invoke);
|
|
1056
1347
|
if ("refusal" in started)
|
|
1057
1348
|
return { run: emptyRun(), abandoned: false, refusal: started.refusal };
|
|
1058
|
-
|
|
1349
|
+
if (started.brokerExecutionId === undefined) {
|
|
1350
|
+
this.jobs.registerProcess(jobId, started);
|
|
1351
|
+
}
|
|
1352
|
+
this.processCpu.delete(jobId);
|
|
1353
|
+
this.brokerCpu.delete(jobId);
|
|
1059
1354
|
const guarded = started.result.catch((e) => ({
|
|
1060
1355
|
run: { code: null, stdout: "", stderr: e.message, timedOut: false },
|
|
1061
1356
|
abandoned: false,
|
|
1062
1357
|
semanticFailure: e.message,
|
|
1063
1358
|
}));
|
|
1064
|
-
if (idleMs === null)
|
|
1359
|
+
if (idleMs === null) {
|
|
1360
|
+
this.jobs.noteLiveness(jobId, {
|
|
1361
|
+
activity: "unmonitored",
|
|
1362
|
+
verdict: "no-idle-stop",
|
|
1363
|
+
checkedAt: attemptStart,
|
|
1364
|
+
idleBaselineAt: attemptStart,
|
|
1365
|
+
lastActivityAt: null,
|
|
1366
|
+
source: "attempt-start",
|
|
1367
|
+
idleMs: null,
|
|
1368
|
+
});
|
|
1065
1369
|
return guarded;
|
|
1066
|
-
|
|
1370
|
+
}
|
|
1371
|
+
this.jobs.noteLiveness(jobId, {
|
|
1372
|
+
activity: "starting",
|
|
1373
|
+
verdict: "keep-running",
|
|
1374
|
+
checkedAt: attemptStart,
|
|
1375
|
+
idleBaselineAt: attemptStart,
|
|
1376
|
+
lastActivityAt: null,
|
|
1377
|
+
source: "attempt-start",
|
|
1378
|
+
idleMs,
|
|
1379
|
+
});
|
|
1380
|
+
let settledOutcome = null;
|
|
1381
|
+
const settled = guarded.then((o) => {
|
|
1382
|
+
settledOutcome = o;
|
|
1383
|
+
return { kind: "settled", outcome: o };
|
|
1384
|
+
});
|
|
1067
1385
|
let lastActive = attemptStart;
|
|
1386
|
+
let lastRealActivity = null;
|
|
1387
|
+
let lastSource = "attempt-start";
|
|
1068
1388
|
for (;;) {
|
|
1069
1389
|
const raced = await Promise.race([settled, pollTimer(Math.min(IDLE_POLL_MS, idleMs))]);
|
|
1070
1390
|
if (raced.kind === "settled")
|
|
@@ -1072,17 +1392,177 @@ export class McpDispatchServer {
|
|
|
1072
1392
|
if (this.jobs.get(jobId)?.status !== "running")
|
|
1073
1393
|
break;
|
|
1074
1394
|
const seen = await this.latestActivity(jobId);
|
|
1075
|
-
|
|
1395
|
+
const afterProbe = await Promise.race([settled, pollTimer(0)]);
|
|
1396
|
+
if (afterProbe.kind === "settled")
|
|
1397
|
+
return afterProbe.outcome;
|
|
1398
|
+
if (settledOutcome !== null)
|
|
1399
|
+
return settledOutcome;
|
|
1400
|
+
if (this.jobs.get(jobId)?.status !== "running")
|
|
1401
|
+
break;
|
|
1402
|
+
const fresh = seen !== null && seen.at > lastActive;
|
|
1403
|
+
if (fresh && seen !== null) {
|
|
1076
1404
|
lastActive = seen.at;
|
|
1077
|
-
|
|
1405
|
+
lastRealActivity = seen.at;
|
|
1406
|
+
lastSource = seen.source;
|
|
1078
1407
|
}
|
|
1079
|
-
|
|
1408
|
+
const checkedAt = this.now();
|
|
1409
|
+
const idle = checkedAt - lastActive >= idleMs;
|
|
1410
|
+
if (idle) {
|
|
1411
|
+
this.jobs.noteLiveness(jobId, {
|
|
1412
|
+
activity: "advancing",
|
|
1413
|
+
verdict: "keep-running",
|
|
1414
|
+
checkedAt,
|
|
1415
|
+
idleBaselineAt: lastActive,
|
|
1416
|
+
lastActivityAt: lastRealActivity,
|
|
1417
|
+
source: lastSource,
|
|
1418
|
+
idleMs,
|
|
1419
|
+
});
|
|
1080
1420
|
break;
|
|
1421
|
+
}
|
|
1422
|
+
this.jobs.noteLiveness(jobId, {
|
|
1423
|
+
activity: fresh ? "active" : "quiet",
|
|
1424
|
+
verdict: "keep-running",
|
|
1425
|
+
checkedAt,
|
|
1426
|
+
idleBaselineAt: lastActive,
|
|
1427
|
+
lastActivityAt: lastRealActivity,
|
|
1428
|
+
source: lastSource,
|
|
1429
|
+
idleMs,
|
|
1430
|
+
});
|
|
1081
1431
|
}
|
|
1082
1432
|
started.kill();
|
|
1433
|
+
if (started.waitForKill !== undefined)
|
|
1434
|
+
await started.waitForKill();
|
|
1083
1435
|
return { run: emptyRun(), abandoned: true };
|
|
1084
1436
|
}
|
|
1085
|
-
|
|
1437
|
+
async brokerRequest(request) {
|
|
1438
|
+
const client = this.deps.laneExecutionClient;
|
|
1439
|
+
if (client === undefined) {
|
|
1440
|
+
return { ok: false, kind: "unavailable", status: null, message: "lane execution broker client is unavailable" };
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
return await client.request(request);
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
return { ok: false, kind: "unavailable", status: null, message: "lane execution broker client failed" };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
brokerSnapshotMatches(snapshot, jobId, laneId, executionId) {
|
|
1450
|
+
return snapshot.executionId === executionId && snapshot.jobId === jobId && snapshot.laneId === laneId;
|
|
1451
|
+
}
|
|
1452
|
+
brokerRun(snapshot) {
|
|
1453
|
+
return {
|
|
1454
|
+
code: snapshot.code ?? null,
|
|
1455
|
+
stdout: snapshot.stdout ?? "",
|
|
1456
|
+
stderr: snapshot.stderr ?? "",
|
|
1457
|
+
timedOut: snapshot.timedOut ?? snapshot.status === "timed_out",
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
async waitBrokerLane(jobId, lane, invoke, opts, startedAt, startRequest, initial) {
|
|
1461
|
+
let response = initial;
|
|
1462
|
+
let startConfirmed = initial.ok;
|
|
1463
|
+
for (;;) {
|
|
1464
|
+
if (response.ok) {
|
|
1465
|
+
const snapshot = response.execution;
|
|
1466
|
+
if (!this.brokerSnapshotMatches(snapshot, jobId, lane.id, startRequest.executionId)) {
|
|
1467
|
+
await pollTimer(BROKER_STATUS_POLL_MS);
|
|
1468
|
+
response = await this.brokerRequest({ action: "status", executionId: startRequest.executionId });
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
startConfirmed = true;
|
|
1472
|
+
if (snapshot.status === "running") {
|
|
1473
|
+
this.liveBrokerSnapshots.set(jobId, snapshot);
|
|
1474
|
+
this.jobs.applyBrokerSnapshot(jobId, snapshot);
|
|
1475
|
+
await pollTimer(BROKER_STATUS_POLL_MS);
|
|
1476
|
+
response = await this.brokerRequest({ action: "status", executionId: startRequest.executionId });
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
this.liveBrokerSnapshots.delete(jobId);
|
|
1480
|
+
this.brokerCpu.delete(jobId);
|
|
1481
|
+
return this.settleSpawnedRun(jobId, lane, invoke, opts, startedAt, this.brokerRun(snapshot));
|
|
1482
|
+
}
|
|
1483
|
+
if (!startConfirmed) {
|
|
1484
|
+
if (response.kind === "rejected" && response.status !== null) {
|
|
1485
|
+
this.jobs.clearBrokerExecution(jobId);
|
|
1486
|
+
return failedOutcome(response.message);
|
|
1487
|
+
}
|
|
1488
|
+
await pollTimer(BROKER_STATUS_POLL_MS);
|
|
1489
|
+
response = await this.brokerRequest(startRequest);
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (response.kind === "rejected") {
|
|
1493
|
+
this.jobs.clearBrokerExecution(jobId);
|
|
1494
|
+
this.liveBrokerSnapshots.delete(jobId);
|
|
1495
|
+
this.brokerCpu.delete(jobId);
|
|
1496
|
+
return failedOutcome(response.status === 404
|
|
1497
|
+
? "the relay daemon lost the brokered lane execution before it returned a terminal result"
|
|
1498
|
+
: response.message);
|
|
1499
|
+
}
|
|
1500
|
+
await pollTimer(BROKER_STATUS_POLL_MS);
|
|
1501
|
+
response = await this.brokerRequest({ action: "status", executionId: startRequest.executionId });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
async stopBrokerLane(executionId) {
|
|
1505
|
+
for (;;) {
|
|
1506
|
+
const response = await this.brokerRequest({ action: "cancel", executionId });
|
|
1507
|
+
if (response.ok)
|
|
1508
|
+
return;
|
|
1509
|
+
if (response.kind === "rejected" && ((response.status === 404 && response.message === UNKNOWN_LANE_EXECUTION_MESSAGE) || response.status === 503))
|
|
1510
|
+
return;
|
|
1511
|
+
await pollTimer(BROKER_STATUS_POLL_MS);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async tryStartBrokerLane(jobId, lane, task, opts, invoke) {
|
|
1515
|
+
if (this.deps.laneExecutionClient === undefined)
|
|
1516
|
+
return null;
|
|
1517
|
+
let executionId = createLaneExecutionId();
|
|
1518
|
+
for (let collision = 0; collision < 3; collision += 1) {
|
|
1519
|
+
const probe = await this.brokerRequest({ action: "status", executionId });
|
|
1520
|
+
if (!probe.ok && probe.kind === "rejected" && probe.status === 404 && probe.message === UNKNOWN_LANE_EXECUTION_MESSAGE)
|
|
1521
|
+
break;
|
|
1522
|
+
if (!probe.ok)
|
|
1523
|
+
return { fallback: probe.message };
|
|
1524
|
+
executionId = createLaneExecutionId();
|
|
1525
|
+
if (collision === 2)
|
|
1526
|
+
return { fallback: "could not allocate a unique daemon execution id" };
|
|
1527
|
+
}
|
|
1528
|
+
const startRequest = {
|
|
1529
|
+
action: "start",
|
|
1530
|
+
executionId,
|
|
1531
|
+
jobId,
|
|
1532
|
+
laneId: lane.id,
|
|
1533
|
+
task,
|
|
1534
|
+
cwd: opts.cwd,
|
|
1535
|
+
timeoutMs: opts.timeoutMs,
|
|
1536
|
+
depth: opts.depth,
|
|
1537
|
+
...(opts.tier === undefined ? {} : { tier: opts.tier }),
|
|
1538
|
+
...(opts.readOnly ? { readOnly: true, callerRoot: this.cwd() } : {}),
|
|
1539
|
+
host: opts.host,
|
|
1540
|
+
};
|
|
1541
|
+
this.jobs.noteBrokerExecution(jobId, executionId);
|
|
1542
|
+
this.jobs.noteExecutionOwner(jobId, "relay-daemon");
|
|
1543
|
+
const startedAt = this.now();
|
|
1544
|
+
const initial = await this.brokerRequest(startRequest);
|
|
1545
|
+
if (!initial.ok && initial.kind === "rejected" && initial.status !== null) {
|
|
1546
|
+
this.jobs.clearBrokerExecution(jobId);
|
|
1547
|
+
if (initial.status === 503)
|
|
1548
|
+
return { fallback: initial.message };
|
|
1549
|
+
return {
|
|
1550
|
+
result: Promise.resolve(failedOutcome(initial.message)),
|
|
1551
|
+
kill: () => { },
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
let cancelPromise;
|
|
1555
|
+
const result = this.waitBrokerLane(jobId, lane, invoke, opts, startedAt, startRequest, initial);
|
|
1556
|
+
return {
|
|
1557
|
+
result,
|
|
1558
|
+
brokerExecutionId: executionId,
|
|
1559
|
+
kill: () => {
|
|
1560
|
+
cancelPromise ??= this.stopBrokerLane(executionId);
|
|
1561
|
+
},
|
|
1562
|
+
waitForKill: () => cancelPromise ?? Promise.resolve(),
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
async startLane(jobId, lane, task, opts, invokeOverride) {
|
|
1086
1566
|
if (opts.mode === "answer" && lane.kind === "relay") {
|
|
1087
1567
|
if (!lane.spec) {
|
|
1088
1568
|
return {
|
|
@@ -1092,7 +1572,7 @@ export class McpDispatchServer {
|
|
|
1092
1572
|
}
|
|
1093
1573
|
const controller = new AbortController();
|
|
1094
1574
|
const signal = AbortSignal.any([AbortSignal.timeout(opts.timeoutMs), controller.signal]);
|
|
1095
|
-
const tag =
|
|
1575
|
+
const tag = createLaneActivityTag();
|
|
1096
1576
|
this.activityTags.set(jobId, tag);
|
|
1097
1577
|
const result = this.runAnswerFetch(lane.spec, task, opts, signal, tag).then((o) => {
|
|
1098
1578
|
const semanticFailure = o.semanticFailure === undefined && !o.run.timedOut && isContentEmpty(o.run.stdout)
|
|
@@ -1121,14 +1601,20 @@ export class McpDispatchServer {
|
|
|
1121
1601
|
kill: () => { },
|
|
1122
1602
|
};
|
|
1123
1603
|
}
|
|
1604
|
+
const broker = await this.tryStartBrokerLane(jobId, lane, task, opts, declared);
|
|
1605
|
+
if (broker !== null && !("fallback" in broker))
|
|
1606
|
+
return broker;
|
|
1607
|
+
if (broker !== null && "fallback" in broker) {
|
|
1608
|
+
this.jobs.noteExecutionOwner(jobId, "local-fallback");
|
|
1609
|
+
}
|
|
1124
1610
|
const agy = agyWorkingDirInvoke(declared, opts.cwd);
|
|
1125
1611
|
const invoke = agy?.invoke ?? declared;
|
|
1126
|
-
const expansion = expandEnvReferences(
|
|
1612
|
+
const expansion = expandEnvReferences(buildLaneEnv(process.env, invoke.env, this.deps.config, this.deps.platform), this.deps.platform);
|
|
1127
1613
|
const env = expansion.env;
|
|
1128
1614
|
env[DEPTH_ENV] = String(opts.depth + 1);
|
|
1129
|
-
const tag =
|
|
1615
|
+
const tag = createLaneActivityTag();
|
|
1130
1616
|
this.activityTags.set(jobId, tag);
|
|
1131
|
-
env["ANTHROPIC_CUSTOM_HEADERS"] =
|
|
1617
|
+
env["ANTHROPIC_CUSTOM_HEADERS"] = withLaneActivityHeader(env["ANTHROPIC_CUSTOM_HEADERS"], tag);
|
|
1132
1618
|
this.jobs.noteLaunch(jobId, [...expansion.notes, ...(agy === null ? [] : [agy.note])]);
|
|
1133
1619
|
const startedAt = this.now();
|
|
1134
1620
|
this.jobs.beginAttemptActivity(jobId, startedAt);
|