@adhdev/daemon-standalone 1.0.49-rc.8 → 1.0.49
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/dist/index.js +2856 -1943
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/public/assets/index-B3OjPHzk.css +1 -0
- package/public/assets/index-BKYlsuSu.js +119 -0
- package/public/index.html +2 -2
- package/vendor/mcp-server/index.js +407 -80
- package/vendor/mcp-server/index.js.map +1 -1
- package/vendor/session-host-daemon/index.js +0 -0
- package/vendor/session-host-daemon/index.mjs +0 -0
- package/public/assets/index-BeC2kIKA.js +0 -116
- package/public/assets/index-CqyS0XiL.css +0 -1
package/public/index.html
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<meta name="description" content="ADHDev self-hosted dashboard for controlling AI agents" />
|
|
8
8
|
<link rel="icon" href="/otter-logo.png" />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BKYlsuSu.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendor-DgFmqOGd.js">
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B3OjPHzk.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<!-- Apply theme immediately to prevent FOIT (Flash of Incorrect Theme) -->
|
|
@@ -575,6 +575,7 @@ var CANONICAL_MESH_TOOL_NAMES = [
|
|
|
575
575
|
"mesh_status",
|
|
576
576
|
"mesh_list_nodes",
|
|
577
577
|
"mesh_enqueue_task",
|
|
578
|
+
"mesh_enqueue_batch",
|
|
578
579
|
"mesh_view_queue",
|
|
579
580
|
"mesh_queue_cancel",
|
|
580
581
|
"mesh_queue_requeue",
|
|
@@ -949,7 +950,7 @@ function isLocalControlPlaneNode(ctx, node) {
|
|
|
949
950
|
// src/tools/mesh-tool-schemas.ts
|
|
950
951
|
var MESH_STATUS_TOOL = {
|
|
951
952
|
name: "mesh_status",
|
|
952
|
-
description: `Get the current status of all nodes in the repo mesh \u2014 health, git state, active sessions, recovery hints, and recommended next steps. Use this to decide which node to send work to or how to recover from failures. Also reports the running daemon build per daemonId under top-level daemonBuilds ({commit, commitShort, version}); when a live daemon was built from a commit BEHIND its workspace HEAD it adds staleDaemonBuilds[] + staleDaemonBuildWarning \u2014 meaning a just-merged refinery/mesh-tool fix is NOT yet live on that daemon (awaiting deploy/restart; a local dist rebuild does not update a cloud daemon). When a daemon has a durable failed-upgrade notice on record it adds daemonUpgradeFailures{daemonId \u2192 {summary, recordedAt, ageLabel, targetVersion, noticePath, logPath}} + daemonUpgradeFailureWarning \u2014 meaning that daemon's LAST upgrade attempt failed and was rolled back, so it is still on the PREVIOUS version (an upgrade/restart response only ever reports "scheduled", never success). Do not repeatedly call this to wait for generating delegated work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.`,
|
|
953
|
+
description: `Get the current status of all nodes in the repo mesh \u2014 health, git state, active sessions, recovery hints, and recommended next steps. Use this to decide which node to send work to or how to recover from failures. Also reports the running daemon build per daemonId under top-level daemonBuilds ({commit, commitShort, version, track}); track is stable/preview when explicitly reported by that daemon and unknown for legacy peers \u2014 it is never inferred from an rc version suffix. When a live daemon was built from a commit BEHIND its workspace HEAD it adds staleDaemonBuilds[] + staleDaemonBuildWarning \u2014 meaning a just-merged refinery/mesh-tool fix is NOT yet live on that daemon (awaiting deploy/restart; a local dist rebuild does not update a cloud daemon). When a daemon has a durable failed-upgrade notice on record it adds daemonUpgradeFailures{daemonId \u2192 {summary, recordedAt, ageLabel, targetVersion, noticePath, logPath}} + daemonUpgradeFailureWarning \u2014 meaning that daemon's LAST upgrade attempt failed and was rolled back, so it is still on the PREVIOUS version (an upgrade/restart response only ever reports "scheduled", never success). Do not repeatedly call this to wait for generating delegated work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.`,
|
|
953
954
|
inputSchema: {
|
|
954
955
|
type: "object",
|
|
955
956
|
properties: {
|
|
@@ -974,7 +975,7 @@ var MESH_LIST_NODES_TOOL = {
|
|
|
974
975
|
};
|
|
975
976
|
var MESH_ENQUEUE_TASK_TOOL = {
|
|
976
977
|
name: "mesh_enqueue_task",
|
|
977
|
-
description: "Add a new task to the mesh work queue. Idle nodes will automatically pull and execute tasks from this queue. Use this instead of mesh_send_task when you do not need to target a specific node. Supports task-level priority (high tasks are pulled ahead of older normal/low tasks), not_before delayed execution (hold a task pending until a time), maxRetries (auto-fail after N requeues), and duplicate detection (by default warns in the response when an in-flight task with the same message+target already exists; pass block_duplicate=true to refuse instead, or allow_duplicate=true to silence the warning).",
|
|
978
|
+
description: "Add a new task to the mesh work queue. Idle nodes will automatically pull and execute tasks from this queue. Use this instead of mesh_send_task when you do not need to target a specific node. For a dependency-wired MULTI-task graph prefer mesh_enqueue_batch: it inserts the whole set atomically and lets depends_on reference batch-local refs, so no half-registered chain can be left behind. Supports task-level priority (high tasks are pulled ahead of older normal/low tasks), not_before delayed execution (hold a task pending until a time), maxRetries (auto-fail after N requeues), and duplicate detection (by default warns in the response when an in-flight task with the same message+target already exists; pass block_duplicate=true to refuse instead, or allow_duplicate=true to silence the warning).",
|
|
978
979
|
inputSchema: {
|
|
979
980
|
type: "object",
|
|
980
981
|
properties: {
|
|
@@ -1010,6 +1011,56 @@ var MESH_ENQUEUE_TASK_TOOL = {
|
|
|
1010
1011
|
required: ["message", "difficulty"]
|
|
1011
1012
|
}
|
|
1012
1013
|
};
|
|
1014
|
+
var MESH_ENQUEUE_BATCH_TOOL = {
|
|
1015
|
+
name: "mesh_enqueue_batch",
|
|
1016
|
+
description: "G5: atomically enqueue a dependency-wired set of tasks \u2014 either EVERY task in the batch is inserted or NONE is (a mid-batch error such as a dependency cycle, invalid difficulty, or guardrail violation rolls the whole batch back). Use this instead of N sequential mesh_enqueue_task calls whenever you are wiring a multi-task graph: give each task a batch-local `ref` label and name sibling refs in `depends_on` (forward references allowed \u2014 array order does not matter). A depends_on value that is not a ref in this batch must be an EXISTING queue task id; anything else is rejected. Per-task fields are the same as mesh_enqueue_task. Top-level mission_id applies to every task that lacks its own.",
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
type: "object",
|
|
1019
|
+
properties: {
|
|
1020
|
+
tasks: {
|
|
1021
|
+
type: "array",
|
|
1022
|
+
description: "The tasks to enqueue atomically (max 50). Each entry accepts the same fields as mesh_enqueue_task, plus an optional batch-local `ref`.",
|
|
1023
|
+
items: {
|
|
1024
|
+
type: "object",
|
|
1025
|
+
properties: {
|
|
1026
|
+
ref: { type: "string", description: `Batch-local label other entries' depends_on may name (e.g. "investigate", "fix", "verify"). Never persisted \u2014 resolved to the generated task id at insert.` },
|
|
1027
|
+
message: { type: "string", description: "The task instruction for the agent." },
|
|
1028
|
+
task_mode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "Optional task-mode contract (same semantics as mesh_enqueue_task)." },
|
|
1029
|
+
taskMode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "CamelCase alias for task_mode." },
|
|
1030
|
+
readonly: { type: "boolean", description: "Optional read-only axis (orthogonal to task_mode); same semantics as mesh_enqueue_task." },
|
|
1031
|
+
read_only: { type: "boolean", description: "Snake-case alias for readonly." },
|
|
1032
|
+
requiredTags: { type: "array", items: { type: "string" }, description: "Optional capability tags every eligible node must have, e.g. os=darwin, provider=codex-cli, worktree=<branch>." },
|
|
1033
|
+
required_tags: { type: "array", items: { type: "string" }, description: "Snake_case alias for requiredTags." },
|
|
1034
|
+
target_node_id: { type: "string", description: "Optional HARD pin: only this node may claim the task. An unresolvable id rejects the WHOLE batch (atomic)." },
|
|
1035
|
+
targetNodeId: { type: "string", description: "CamelCase alias for target_node_id." },
|
|
1036
|
+
prefer_worktree: { type: "boolean", description: "Route to the most recently cloned idle worktree node (no-op when none exists)." },
|
|
1037
|
+
preferWorktree: { type: "boolean", description: "CamelCase alias for prefer_worktree." },
|
|
1038
|
+
depends_on: { type: "array", items: { type: "string" }, description: "Refs of sibling entries in THIS batch (forward references allowed) and/or EXISTING queue task ids that must complete before this task becomes claimable. Cycles and unknown values reject the whole batch." },
|
|
1039
|
+
dependsOn: { type: "array", items: { type: "string" }, description: "CamelCase alias for depends_on." },
|
|
1040
|
+
mission_id: { type: "string", description: "Per-task mission override; defaults to the top-level mission_id." },
|
|
1041
|
+
missionId: { type: "string", description: "CamelCase alias for mission_id." },
|
|
1042
|
+
priority: { type: "string", enum: ["low", "normal", "high"], description: "G6 task-level scheduling priority (same semantics as mesh_enqueue_task)." },
|
|
1043
|
+
model: { type: "string", description: "Optional model override for the agent that runs this task (best-effort at launch)." },
|
|
1044
|
+
thinkingLevel: { type: "string", enum: ["low", "medium", "high"], description: "Optional reasoning-effort level (best-effort at launch)." },
|
|
1045
|
+
difficulty: { type: "string", enum: ["easy", "medium", "difficult", "freeform"], description: "REQUIRED per task \u2014 routing hint matched against node capability slots (same semantics as mesh_enqueue_task)." },
|
|
1046
|
+
not_before: { type: "number", description: "G7 delayed execution: hold the task pending until this time (epoch-ms, relative-ms, or ISO string)." },
|
|
1047
|
+
notBefore: { type: "number", description: "CamelCase alias for not_before. Also accepts an ISO-8601 timestamp string." },
|
|
1048
|
+
max_retries: { type: "number", description: "P3 retry cap (same semantics as mesh_enqueue_task)." },
|
|
1049
|
+
maxRetries: { type: "number", description: "CamelCase alias for max_retries." }
|
|
1050
|
+
},
|
|
1051
|
+
required: ["message", "difficulty"]
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
mission_id: { type: "string", description: "Mission every task in this batch belongs to unless an entry overrides it. For multi-task work, create the mission first (mesh_mission_upsert) and pass it here." },
|
|
1055
|
+
missionId: { type: "string", description: "CamelCase alias for mission_id." },
|
|
1056
|
+
block_duplicate: { type: "boolean", description: "G4: when any entry matches an in-flight task with the same message+target, refuse the WHOLE batch (it is atomic) with code duplicate_suspect. Default false = warn-only via duplicateSuspects in the response." },
|
|
1057
|
+
blockDuplicate: { type: "boolean", description: "CamelCase alias for block_duplicate." },
|
|
1058
|
+
allow_duplicate: { type: "boolean", description: "G4: skip duplicate detection entirely for every entry (intentional re-enqueue)." },
|
|
1059
|
+
allowDuplicate: { type: "boolean", description: "CamelCase alias for allow_duplicate." }
|
|
1060
|
+
},
|
|
1061
|
+
required: ["tasks"]
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1013
1064
|
var MESH_VIEW_QUEUE_TOOL = {
|
|
1014
1065
|
name: "mesh_view_queue",
|
|
1015
1066
|
description: "View the mesh work queue with source-of-truth active counts separated from historical completed/failed/cancelled records. Do not repeatedly call this to wait for generating assigned work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.",
|
|
@@ -1206,12 +1257,13 @@ var MESH_FAST_FORWARD_NODE_TOOL = {
|
|
|
1206
1257
|
};
|
|
1207
1258
|
var MESH_RESTART_DAEMON_TOOL = {
|
|
1208
1259
|
name: "mesh_restart_daemon",
|
|
1209
|
-
description: `Restart a mesh node's daemon, optionally updating it first \u2014 the same path as the dashboard "preview update" button, exposed as a mesh command so a coordinator can roll a worker daemon without a manual restart round-trip. No agent session is launched. mode="upgrade" (default): update to the latest published version on the release channel, then restart; already-latest is a no-op (no restart, returns alreadyLatest:true). mode="restart": pure re-spawn with no reinstall \u2014 restarts even when already latest, with much shorter downtime; use it to reset wedged daemon state (memory leaks, zombie sessions). Idle-gated: a node whose daemon has an active session (generating / waiting_approval / starting) is refused with code "blocking_sessions" so an in-flight turn is never interrupted. self_only=true waives ONLY this mesh's own coordinator session (the structural self-deadlock case \u2014 the coordinator is always generating while it calls). Other sessions still refuse. force=true bypasses the gate entirely: in-flight turns die and the unpersisted pendingOutboundQueue is lost. when_idle=true schedules the restart to run automatically once the daemon goes idle (the safest path \u2014 no queue loss); cancel_when_idle=true cancels it and every response reports the schedule under deferredRestart. kill_session_host=true additionally stops the session-host process, destroying ALL hosted CLI sessions (hard refresh; this is what Windows already does on every upgrade). Default off. Note: on Windows any daemon restart/upgrade terminates all hosted sessions regardless of options; on POSIX hosted sessions survive a plain restart and rebind on next boot. The channel parameter is DEPRECATED and ignored: since Phase 3 the release channel is a build-time identity of the installed binary (stable = adhdev/@latest, preview = adhdev-preview/@next), so an upgrade always targets the daemon's own build track and can never switch channels.`,
|
|
1260
|
+
description: `Restart a mesh node's daemon, optionally updating it first \u2014 the same path as the dashboard "preview update" button, exposed as a mesh command so a coordinator can roll a worker daemon without a manual restart round-trip. No agent session is launched. mode="upgrade" (default): update to the latest published version on the release channel, then restart; already-latest is a no-op (no restart, returns alreadyLatest:true). mode="restart": pure re-spawn with no reinstall \u2014 restarts even when already latest, with much shorter downtime; use it to reset wedged daemon state (memory leaks, zombie sessions). Idle-gated: a node whose daemon has an active session (generating / waiting_approval / starting) is refused with code "blocking_sessions" so an in-flight turn is never interrupted. self_only=true waives ONLY this mesh's own coordinator session (the structural self-deadlock case \u2014 the coordinator is always generating while it calls). Other sessions still refuse. force=true bypasses the gate entirely: in-flight turns die and the unpersisted pendingOutboundQueue is lost. when_idle=true schedules the restart to run automatically once the daemon goes idle (the safest path \u2014 no queue loss); cancel_when_idle=true cancels it and every response reports the schedule under deferredRestart. kill_session_host=true additionally stops the session-host process, destroying ALL hosted CLI sessions (hard refresh; this is what Windows already does on every upgrade). Default off. Note: on Windows any daemon restart/upgrade terminates all hosted sessions regardless of options; on POSIX hosted sessions survive a plain restart and rebind on next boot. Upgrade mode refuses a DOWNGRADE: if the target version resolved from the daemon's build track is OLDER than the running daemon, the call fails with code "downgrade_refused" and reports currentVersion / targetVersion / channel instead of rolling the node back. Pass allow_downgrade=true only for a deliberate rollback. The channel parameter is DEPRECATED and ignored: since Phase 3 the release channel is a build-time identity of the installed binary (stable = adhdev/@latest, preview = adhdev-preview/@next), so an upgrade always targets the daemon's own build track and can never switch channels. When you pass a channel that conflicts with the node's build track, the response now carries a channelOverride object saying so \u2014 the request is not silently honored. The response compares meshAttachedDaemon (the daemon that answered status immediately before the command) with restartTargetDaemon (the daemon process that accepted the lifecycle operation). daemonMismatch/trackMismatch=true and trackWarning surface a split but do not block the operation; null means an older/unreachable daemon did not report enough identity.`,
|
|
1210
1261
|
inputSchema: {
|
|
1211
1262
|
type: "object",
|
|
1212
1263
|
properties: {
|
|
1213
1264
|
node_id: { type: "string", description: "Target node ID \u2014 the daemon that owns this node is restarted (and updated, in upgrade mode)." },
|
|
1214
|
-
channel: { type: "string", enum: ["stable", "preview"], description: "DEPRECATED and ignored (upgrade mode only). Since Phase 3 the release channel is a build-time identity of the installed binary, so the daemon always upgrades on its own build track. Kept optional so older callers do not break." },
|
|
1265
|
+
channel: { type: "string", enum: ["stable", "preview"], description: "DEPRECATED and ignored (upgrade mode only). Since Phase 3 the release channel is a build-time identity of the installed binary, so the daemon always upgrades on its own build track. Kept optional so older callers do not break. A value conflicting with the node's build track is reported back as channelOverride rather than silently dropped \u2014 it does NOT switch the node's channel." },
|
|
1266
|
+
allow_downgrade: { type: "boolean", description: 'Permit an upgrade whose resolved target is OLDER than the running daemon (upgrade mode only). Default false: such a call is refused with code "downgrade_refused" so a mis-resolved track cannot silently roll a node back. Set true only for a deliberate rollback.' },
|
|
1215
1267
|
mode: { type: "string", enum: ["upgrade", "restart"], description: "upgrade (default): update to latest on channel, then restart (already-latest is a no-op). restart: pure re-spawn, no reinstall \u2014 restarts even when already latest." },
|
|
1216
1268
|
force: { type: "boolean", description: "Bypass the idle-gate entirely. Destructive: in-flight turns are killed and the in-memory pendingOutboundQueue is permanently lost. Default false." },
|
|
1217
1269
|
self_only: { type: "boolean", description: "Waive only this mesh's own coordinator session when it blocks the restart (the coordinator self-deadlock). Other nodes' active sessions still refuse. Default false." },
|
|
@@ -1566,7 +1618,7 @@ var MESH_PRUNE_STALE_DIRECT_TOOL = {
|
|
|
1566
1618
|
};
|
|
1567
1619
|
var MESH_REFINE_NODE_TOOL = {
|
|
1568
1620
|
name: "mesh_refine_node",
|
|
1569
|
-
description: "The Refinery: validate \u2192 merge \u2192 push \u2192 clean up a completed worktree node onto the base branch. Defaults to dry-run (plan only): returns the validation plan with mergeWillRun:false/cleanupWillRun:false and performs NO merge/push/cleanup. Pass execute=true to actually converge the node. execute=true is async: the immediate response includes async:true, status:'accepted', jobId, interactionId, target node, and startedAt; completion/failure evidence is delivered through pending mesh events and the mesh task ledger. dry_run=true overrides execute. Matches the mesh_refine_batch / mesh_fast_forward_node dry_run/execute contract.",
|
|
1621
|
+
description: "The Refinery: validate \u2192 merge \u2192 push \u2192 clean up a completed worktree node onto the base branch. Defaults to dry-run (plan only): returns the validation plan with mergeWillRun:false/cleanupWillRun:false and performs NO merge/push/cleanup. Pass execute=true to actually converge the node. execute=true is async: the immediate response includes async:true, status:'accepted', jobId, interactionId, target node, and startedAt; completion/failure evidence is delivered through pending mesh events and the mesh task ledger. dry_run=true overrides execute. Matches the mesh_refine_batch / mesh_fast_forward_node dry_run/execute contract. Converges ONE node: to land two or more sibling worktrees that share a base branch, use mesh_refine_batch instead of calling this repeatedly \u2014 it orders the nodes conflict-aware and auto-rebases each one onto the base its predecessors advanced, which repeated single-node calls leave to you (and which can contend on the base lease as base_locked).",
|
|
1570
1622
|
inputSchema: {
|
|
1571
1623
|
type: "object",
|
|
1572
1624
|
properties: {
|
|
@@ -1838,6 +1890,7 @@ var ALL_MESH_TOOLS = [
|
|
|
1838
1890
|
MESH_STATUS_TOOL,
|
|
1839
1891
|
MESH_LIST_NODES_TOOL,
|
|
1840
1892
|
MESH_ENQUEUE_TASK_TOOL,
|
|
1893
|
+
MESH_ENQUEUE_BATCH_TOOL,
|
|
1841
1894
|
MESH_VIEW_QUEUE_TOOL,
|
|
1842
1895
|
MESH_QUEUE_CANCEL_TOOL,
|
|
1843
1896
|
MESH_QUEUE_REQUEUE_TOOL,
|
|
@@ -3477,8 +3530,10 @@ async function collectLiveStatusSessionsVerified(ctx, node) {
|
|
|
3477
3530
|
async function collectLiveStatusProbe(ctx, node) {
|
|
3478
3531
|
try {
|
|
3479
3532
|
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {}, { statusProbe: true });
|
|
3533
|
+
const payload = unwrapCommandPayload(statusResult);
|
|
3480
3534
|
return {
|
|
3481
3535
|
sessions: extractStatusMetadataSessions(statusResult),
|
|
3536
|
+
...readString(payload?.status?.instanceId) ? { daemonId: readString(payload.status.instanceId) } : {},
|
|
3482
3537
|
daemonBuild: extractDaemonBuildInfo(statusResult),
|
|
3483
3538
|
upgradeFailure: extractUpgradeFailureSummary(statusResult)
|
|
3484
3539
|
};
|
|
@@ -3514,11 +3569,14 @@ function extractDaemonBuildInfo(value) {
|
|
|
3514
3569
|
if (!build) return void 0;
|
|
3515
3570
|
const commit = readString(build.commit);
|
|
3516
3571
|
if (!commit) return void 0;
|
|
3572
|
+
const reportedTrack = readString(build.track);
|
|
3573
|
+
const track = reportedTrack === "stable" || reportedTrack === "preview" ? reportedTrack : "unknown";
|
|
3517
3574
|
return {
|
|
3518
3575
|
commit,
|
|
3519
3576
|
commitShort: readString(build.commitShort) || commit.slice(0, 7),
|
|
3520
3577
|
version: readString(build.version) || "unknown",
|
|
3521
|
-
...readString(build.builtAt) ? { builtAt: readString(build.builtAt) } : {}
|
|
3578
|
+
...readString(build.builtAt) ? { builtAt: readString(build.builtAt) } : {},
|
|
3579
|
+
track
|
|
3522
3580
|
};
|
|
3523
3581
|
}
|
|
3524
3582
|
async function collectMeshViewQueueNodesWithLiveSessions(ctx) {
|
|
@@ -4533,14 +4591,14 @@ function buildUntargetedCodeChangeWorktreeAdvisory(input) {
|
|
|
4533
4591
|
worktreeRoutingAdvisory: 'This untargeted `code_change` will be claimed by whichever node polls first \u2014 usually the BASE node, which gives it no branch isolation. Base nodes are for environment-specific testing (win32 PATH/registry, clean install on one OS, that machine\'s package state, OS-dependent runtime behavior). If this task only changes code, cancel it (mesh_queue_cancel), call mesh_clone_node (~10s, auto-launch starts the session), and re-enqueue with target_node_id set to the returned worktree node id \u2014 or pass prefer_worktree: true to route to the most recent worktree. If it DOES need a specific machine, pin it with required_tags (e.g. ["os=win32"]) or target_node_id and this advisory will not appear.'
|
|
4534
4592
|
};
|
|
4535
4593
|
}
|
|
4536
|
-
|
|
4594
|
+
function normalizeEnqueueTaskArgs(ctx, args, callerLabel) {
|
|
4537
4595
|
const message = readString(args.message);
|
|
4538
4596
|
if (!message) {
|
|
4539
|
-
return
|
|
4540
|
-
|
|
4597
|
+
return {
|
|
4598
|
+
ok: false,
|
|
4541
4599
|
code: "invalid_message",
|
|
4542
|
-
error:
|
|
4543
|
-
}
|
|
4600
|
+
error: `${callerLabel} requires a non-empty string \`message\`.`
|
|
4601
|
+
};
|
|
4544
4602
|
}
|
|
4545
4603
|
const taskMode = readString(args.task_mode) || readString(args.taskMode);
|
|
4546
4604
|
const readonly = args.readonly === true || args.read_only === true;
|
|
@@ -4555,26 +4613,132 @@ async function meshEnqueueTask(ctx, args) {
|
|
|
4555
4613
|
const notBefore = (0, import_daemon_core5.resolveNotBefore)(notBeforeRaw);
|
|
4556
4614
|
const maxRetriesRaw = typeof args.maxRetries === "number" ? args.maxRetries : typeof args.max_retries === "number" ? args.max_retries : void 0;
|
|
4557
4615
|
const maxRetries = typeof maxRetriesRaw === "number" && Number.isFinite(maxRetriesRaw) && maxRetriesRaw >= 0 ? Math.floor(maxRetriesRaw) : void 0;
|
|
4558
|
-
const allowDuplicate = args.allowDuplicate === true || args.allow_duplicate === true;
|
|
4559
|
-
const blockDuplicate = args.blockDuplicate === true || args.block_duplicate === true;
|
|
4560
4616
|
const explicitTargetRaw = readString(args.targetNodeId) || readString(args.target_node_id) || readString(args.targetNode) || readString(args.target_node) || void 0;
|
|
4561
4617
|
const preferWorktree = args.preferWorktree === true || args.prefer_worktree === true;
|
|
4562
4618
|
let targetNodeId;
|
|
4563
4619
|
if (explicitTargetRaw) {
|
|
4564
4620
|
const matched = ctx.mesh.nodes.find((n) => (0, import_daemon_core5.meshNodeIdMatches)(n, explicitTargetRaw));
|
|
4565
4621
|
if (!matched) {
|
|
4566
|
-
return
|
|
4567
|
-
|
|
4622
|
+
return {
|
|
4623
|
+
ok: false,
|
|
4568
4624
|
code: "target_node_not_found",
|
|
4569
4625
|
error: `target node '${explicitTargetRaw}' is not a member of this mesh \u2014 refusing to enqueue an unpinned task (it could be claimed by any node, including a different machine). Use mesh_list_nodes to get a valid node id.`,
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4626
|
+
extra: {
|
|
4627
|
+
targetNodeId: explicitTargetRaw,
|
|
4628
|
+
availableNodeIds: ctx.mesh.nodes.map((n) => n.id).filter(Boolean)
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4573
4631
|
}
|
|
4574
4632
|
targetNodeId = readString(matched.id) || explicitTargetRaw;
|
|
4575
4633
|
} else if (preferWorktree) {
|
|
4576
4634
|
targetNodeId = resolvePreferredWorktreeNodeId(ctx) || void 0;
|
|
4577
4635
|
}
|
|
4636
|
+
return {
|
|
4637
|
+
ok: true,
|
|
4638
|
+
value: {
|
|
4639
|
+
message,
|
|
4640
|
+
taskMode,
|
|
4641
|
+
readonly,
|
|
4642
|
+
requiredTags,
|
|
4643
|
+
dependsOn,
|
|
4644
|
+
missionId,
|
|
4645
|
+
priority,
|
|
4646
|
+
model,
|
|
4647
|
+
thinkingLevel,
|
|
4648
|
+
difficulty,
|
|
4649
|
+
notBefore,
|
|
4650
|
+
maxRetries,
|
|
4651
|
+
explicitTargetRaw,
|
|
4652
|
+
preferWorktree,
|
|
4653
|
+
targetNodeId
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
function eagerPushTaskToRemoteNodes(ctx, task, message, targetNodeId, requiredTags, coordinatorDaemonId) {
|
|
4658
|
+
const dispatchPromises = [];
|
|
4659
|
+
for (const node of ctx.mesh.nodes) {
|
|
4660
|
+
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
4661
|
+
if (isLocalNode || !node.daemonId) continue;
|
|
4662
|
+
if (targetNodeId && node.id !== targetNodeId) continue;
|
|
4663
|
+
if (!(0, import_daemon_core5.nodeSatisfiesRequiredTags)(requiredTags, (0, import_daemon_core5.buildMeshNodeCapabilityTags)(node))) continue;
|
|
4664
|
+
dispatchPromises.push(
|
|
4665
|
+
ipcDispatchToRemoteAgent(ctx, node, {
|
|
4666
|
+
message,
|
|
4667
|
+
meshContext: {
|
|
4668
|
+
meshId: ctx.mesh.id,
|
|
4669
|
+
nodeId: node.id,
|
|
4670
|
+
taskId: task.id,
|
|
4671
|
+
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
4672
|
+
}
|
|
4673
|
+
}).then((result) => {
|
|
4674
|
+
if (result.success) {
|
|
4675
|
+
try {
|
|
4676
|
+
const providerType = result.providerType;
|
|
4677
|
+
const descriptor = summarizeTaskMessage(message);
|
|
4678
|
+
(0, import_daemon_core5.appendLedgerEntry)(ctx.mesh.id, {
|
|
4679
|
+
kind: "task_dispatched",
|
|
4680
|
+
nodeId: node.id,
|
|
4681
|
+
sessionId: result.sessionId,
|
|
4682
|
+
providerType,
|
|
4683
|
+
payload: {
|
|
4684
|
+
source: "queue",
|
|
4685
|
+
via: "p2p_direct",
|
|
4686
|
+
taskId: task.id,
|
|
4687
|
+
message,
|
|
4688
|
+
taskTitle: descriptor.taskTitle,
|
|
4689
|
+
taskSummary: descriptor.taskSummary,
|
|
4690
|
+
...task.taskMode ? { taskMode: task.taskMode } : {},
|
|
4691
|
+
...providerType ? { providerType } : {},
|
|
4692
|
+
targetSessionId: result.sessionId
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
} catch {
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
}).catch((err) => {
|
|
4699
|
+
try {
|
|
4700
|
+
(0, import_daemon_core5.appendLedgerEntry)(ctx.mesh.id, {
|
|
4701
|
+
kind: "p2p_dispatch_failed",
|
|
4702
|
+
nodeId: node.id,
|
|
4703
|
+
payload: {
|
|
4704
|
+
source: "queue",
|
|
4705
|
+
via: "p2p_direct",
|
|
4706
|
+
taskId: task.id,
|
|
4707
|
+
error: err?.message || String(err),
|
|
4708
|
+
dispatchFailedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4709
|
+
}
|
|
4710
|
+
});
|
|
4711
|
+
} catch {
|
|
4712
|
+
}
|
|
4713
|
+
})
|
|
4714
|
+
);
|
|
4715
|
+
}
|
|
4716
|
+
return dispatchPromises;
|
|
4717
|
+
}
|
|
4718
|
+
async function meshEnqueueTask(ctx, args) {
|
|
4719
|
+
const normalized = normalizeEnqueueTaskArgs(ctx, args, "mesh_enqueue_task");
|
|
4720
|
+
if (!normalized.ok) {
|
|
4721
|
+
return JSON.stringify({ success: false, code: normalized.code, error: normalized.error, ...normalized.extra ?? {} });
|
|
4722
|
+
}
|
|
4723
|
+
const {
|
|
4724
|
+
message,
|
|
4725
|
+
taskMode,
|
|
4726
|
+
readonly,
|
|
4727
|
+
requiredTags,
|
|
4728
|
+
dependsOn,
|
|
4729
|
+
missionId,
|
|
4730
|
+
priority,
|
|
4731
|
+
model,
|
|
4732
|
+
thinkingLevel,
|
|
4733
|
+
difficulty,
|
|
4734
|
+
notBefore,
|
|
4735
|
+
maxRetries,
|
|
4736
|
+
explicitTargetRaw,
|
|
4737
|
+
preferWorktree,
|
|
4738
|
+
targetNodeId
|
|
4739
|
+
} = normalized.value;
|
|
4740
|
+
const allowDuplicate = args.allowDuplicate === true || args.allow_duplicate === true;
|
|
4741
|
+
const blockDuplicate = args.blockDuplicate === true || args.block_duplicate === true;
|
|
4578
4742
|
const duplicateSuspect = allowDuplicate ? null : findInFlightDuplicate(ctx, message, targetNodeId);
|
|
4579
4743
|
if (duplicateSuspect && blockDuplicate) {
|
|
4580
4744
|
return JSON.stringify({
|
|
@@ -4640,65 +4804,7 @@ async function meshEnqueueTask(ctx, args) {
|
|
|
4640
4804
|
);
|
|
4641
4805
|
const eagerPushDeferred = !(0, import_daemon_core5.taskDependenciesSatisfied)(task, dependencyStatusById);
|
|
4642
4806
|
const coordinatorDaemonId = resolveCoordinatorDaemonId(ctx);
|
|
4643
|
-
const dispatchPromises = [];
|
|
4644
|
-
const eagerPushTargets = eagerPushDeferred ? [] : ctx.mesh.nodes;
|
|
4645
|
-
for (const node of eagerPushTargets) {
|
|
4646
|
-
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
4647
|
-
if (isLocalNode || !node.daemonId) continue;
|
|
4648
|
-
if (targetNodeId && node.id !== targetNodeId) continue;
|
|
4649
|
-
if (!(0, import_daemon_core5.nodeSatisfiesRequiredTags)(requiredTags, (0, import_daemon_core5.buildMeshNodeCapabilityTags)(node))) continue;
|
|
4650
|
-
dispatchPromises.push(
|
|
4651
|
-
ipcDispatchToRemoteAgent(ctx, node, {
|
|
4652
|
-
message,
|
|
4653
|
-
meshContext: {
|
|
4654
|
-
meshId: ctx.mesh.id,
|
|
4655
|
-
nodeId: node.id,
|
|
4656
|
-
taskId: task.id,
|
|
4657
|
-
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
4658
|
-
}
|
|
4659
|
-
}).then((result) => {
|
|
4660
|
-
if (result.success) {
|
|
4661
|
-
try {
|
|
4662
|
-
const providerType = result.providerType;
|
|
4663
|
-
const descriptor = summarizeTaskMessage(message);
|
|
4664
|
-
(0, import_daemon_core5.appendLedgerEntry)(ctx.mesh.id, {
|
|
4665
|
-
kind: "task_dispatched",
|
|
4666
|
-
nodeId: node.id,
|
|
4667
|
-
sessionId: result.sessionId,
|
|
4668
|
-
providerType,
|
|
4669
|
-
payload: {
|
|
4670
|
-
source: "queue",
|
|
4671
|
-
via: "p2p_direct",
|
|
4672
|
-
taskId: task.id,
|
|
4673
|
-
message,
|
|
4674
|
-
taskTitle: descriptor.taskTitle,
|
|
4675
|
-
taskSummary: descriptor.taskSummary,
|
|
4676
|
-
...task.taskMode ? { taskMode: task.taskMode } : {},
|
|
4677
|
-
...providerType ? { providerType } : {},
|
|
4678
|
-
targetSessionId: result.sessionId
|
|
4679
|
-
}
|
|
4680
|
-
});
|
|
4681
|
-
} catch {
|
|
4682
|
-
}
|
|
4683
|
-
}
|
|
4684
|
-
}).catch((err) => {
|
|
4685
|
-
try {
|
|
4686
|
-
(0, import_daemon_core5.appendLedgerEntry)(ctx.mesh.id, {
|
|
4687
|
-
kind: "p2p_dispatch_failed",
|
|
4688
|
-
nodeId: node.id,
|
|
4689
|
-
payload: {
|
|
4690
|
-
source: "queue",
|
|
4691
|
-
via: "p2p_direct",
|
|
4692
|
-
taskId: task.id,
|
|
4693
|
-
error: err?.message || String(err),
|
|
4694
|
-
dispatchFailedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4695
|
-
}
|
|
4696
|
-
});
|
|
4697
|
-
} catch {
|
|
4698
|
-
}
|
|
4699
|
-
})
|
|
4700
|
-
);
|
|
4701
|
-
}
|
|
4807
|
+
const dispatchPromises = eagerPushDeferred ? [] : eagerPushTaskToRemoteNodes(ctx, task, message, targetNodeId, requiredTags, coordinatorDaemonId);
|
|
4702
4808
|
Promise.all(dispatchPromises).catch(() => {
|
|
4703
4809
|
});
|
|
4704
4810
|
return JSON.stringify({
|
|
@@ -4730,6 +4836,178 @@ async function meshEnqueueTask(ctx, args) {
|
|
|
4730
4836
|
return JSON.stringify({ success: false, error: message2 });
|
|
4731
4837
|
}
|
|
4732
4838
|
}
|
|
4839
|
+
var BATCH_ENQUEUE_ERROR_CODES = [
|
|
4840
|
+
"live_debug_readonly_guardrail_violation",
|
|
4841
|
+
"dependency_cycle_detected",
|
|
4842
|
+
"unknown_dependency",
|
|
4843
|
+
"duplicate_task_ref",
|
|
4844
|
+
"duplicate_task_id",
|
|
4845
|
+
"task_graph_too_large",
|
|
4846
|
+
"empty_task_graph",
|
|
4847
|
+
"missing_task_difficulty",
|
|
4848
|
+
"invalid_task_difficulty"
|
|
4849
|
+
];
|
|
4850
|
+
async function meshEnqueueBatch(ctx, args) {
|
|
4851
|
+
const rawTasks = Array.isArray(args.tasks) ? args.tasks : void 0;
|
|
4852
|
+
if (!rawTasks || rawTasks.length === 0) {
|
|
4853
|
+
return JSON.stringify({
|
|
4854
|
+
success: false,
|
|
4855
|
+
code: "empty_task_graph",
|
|
4856
|
+
error: "mesh_enqueue_batch requires a non-empty `tasks` array."
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
if (rawTasks.length > import_daemon_core5.MESH_TASK_GRAPH_MAX_TASKS) {
|
|
4860
|
+
return JSON.stringify({
|
|
4861
|
+
success: false,
|
|
4862
|
+
code: "task_graph_too_large",
|
|
4863
|
+
error: `mesh_enqueue_batch accepts at most ${import_daemon_core5.MESH_TASK_GRAPH_MAX_TASKS} tasks per call (got ${rawTasks.length}). Split the graph, or reconsider whether one batch really needs this many tasks.`
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
const batchMissionId = readString(args.missionId) || readString(args.mission_id) || void 0;
|
|
4867
|
+
const allowDuplicate = args.allowDuplicate === true || args.allow_duplicate === true;
|
|
4868
|
+
const blockDuplicate = args.blockDuplicate === true || args.block_duplicate === true;
|
|
4869
|
+
const specs = [];
|
|
4870
|
+
const normalizedEntries = [];
|
|
4871
|
+
const duplicateSuspects = [];
|
|
4872
|
+
for (let i = 0; i < rawTasks.length; i++) {
|
|
4873
|
+
const entry = rawTasks[i] ?? {};
|
|
4874
|
+
const ref = readString(entry.ref) || void 0;
|
|
4875
|
+
const label = ref ? `task '${ref}'` : `task #${i}`;
|
|
4876
|
+
const normalized = normalizeEnqueueTaskArgs(ctx, entry, `mesh_enqueue_batch ${label}`);
|
|
4877
|
+
if (!normalized.ok) {
|
|
4878
|
+
return JSON.stringify({
|
|
4879
|
+
success: false,
|
|
4880
|
+
code: normalized.code,
|
|
4881
|
+
error: normalized.error,
|
|
4882
|
+
taskIndex: i,
|
|
4883
|
+
...ref ? { ref } : {},
|
|
4884
|
+
...normalized.extra ?? {},
|
|
4885
|
+
enqueued: 0,
|
|
4886
|
+
atomic: true
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
const v = normalized.value;
|
|
4890
|
+
if (!allowDuplicate) {
|
|
4891
|
+
const suspect = findInFlightDuplicate(ctx, v.message, v.targetNodeId);
|
|
4892
|
+
if (suspect) {
|
|
4893
|
+
duplicateSuspects.push({
|
|
4894
|
+
taskIndex: i,
|
|
4895
|
+
...ref ? { ref } : {},
|
|
4896
|
+
duplicateOf: { taskId: suspect.id, status: suspect.status, assignedNodeId: suspect.assignedNodeId, targetNodeId: suspect.targetNodeId }
|
|
4897
|
+
});
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
normalizedEntries.push({ ...v, ...ref ? { ref } : {} });
|
|
4901
|
+
specs.push({
|
|
4902
|
+
...ref ? { ref } : {},
|
|
4903
|
+
message: v.message,
|
|
4904
|
+
taskMode: v.taskMode,
|
|
4905
|
+
...v.readonly ? { readonly: true } : {},
|
|
4906
|
+
requiredTags: v.requiredTags,
|
|
4907
|
+
dependsOn: v.dependsOn,
|
|
4908
|
+
missionId: v.missionId ?? batchMissionId,
|
|
4909
|
+
targetNodeId: v.targetNodeId,
|
|
4910
|
+
...v.priority ? { priority: v.priority } : {},
|
|
4911
|
+
...v.model ? { model: v.model } : {},
|
|
4912
|
+
...v.thinkingLevel ? { thinkingLevel: v.thinkingLevel } : {},
|
|
4913
|
+
...v.difficulty ? { difficulty: v.difficulty } : {},
|
|
4914
|
+
...v.notBefore ? { notBefore: v.notBefore } : {},
|
|
4915
|
+
...v.maxRetries !== void 0 ? { maxRetries: v.maxRetries } : {},
|
|
4916
|
+
...ctx.coordinatorSessionId ? { sourceCoordinatorSessionId: ctx.coordinatorSessionId } : {}
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
if (duplicateSuspects.length > 0 && blockDuplicate) {
|
|
4920
|
+
return JSON.stringify({
|
|
4921
|
+
success: false,
|
|
4922
|
+
code: "duplicate_suspect",
|
|
4923
|
+
error: `${duplicateSuspects.length} task(s) in this batch match an in-flight task with the same message (and target node when pinned). Refusing the WHOLE batch because block_duplicate=true and the batch is atomic. Cancel the in-flight duplicates, wait for them, or re-send with allow_duplicate=true.`,
|
|
4924
|
+
duplicateSuspects,
|
|
4925
|
+
enqueued: 0,
|
|
4926
|
+
atomic: true
|
|
4927
|
+
});
|
|
4928
|
+
}
|
|
4929
|
+
let tasks;
|
|
4930
|
+
try {
|
|
4931
|
+
tasks = (0, import_daemon_core5.enqueueTaskGraph)(ctx.mesh.id, specs);
|
|
4932
|
+
} catch (e) {
|
|
4933
|
+
const message = e?.message || String(e);
|
|
4934
|
+
const code = BATCH_ENQUEUE_ERROR_CODES.find((c) => message.includes(c));
|
|
4935
|
+
return JSON.stringify({
|
|
4936
|
+
success: false,
|
|
4937
|
+
...code ? { code } : {},
|
|
4938
|
+
error: message,
|
|
4939
|
+
enqueued: 0,
|
|
4940
|
+
atomic: true
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4943
|
+
const distinctMissionIds = [...new Set(specs.map((s) => s.missionId).filter((m) => !!m))];
|
|
4944
|
+
const missionWarning = distinctMissionIds.map((missionId) => buildMissionInactiveWarning(ctx, missionId)).find((w) => w !== null) ?? {};
|
|
4945
|
+
const advisoryTasks = [];
|
|
4946
|
+
normalizedEntries.forEach((entry, i) => {
|
|
4947
|
+
const advisory = buildUntargetedCodeChangeWorktreeAdvisory({
|
|
4948
|
+
taskMode: entry.taskMode,
|
|
4949
|
+
readonly: entry.readonly,
|
|
4950
|
+
requiredTags: entry.requiredTags,
|
|
4951
|
+
targetNodeId: entry.targetNodeId,
|
|
4952
|
+
preferWorktree: entry.preferWorktree
|
|
4953
|
+
});
|
|
4954
|
+
if (advisory) advisoryTasks.push(entry.ref ? `'${entry.ref}'` : `#${i}`);
|
|
4955
|
+
});
|
|
4956
|
+
const worktreeAdvisory = advisoryTasks.length > 0 ? {
|
|
4957
|
+
...buildUntargetedCodeChangeWorktreeAdvisory({ taskMode: "code_change", readonly: false, preferWorktree: false }),
|
|
4958
|
+
worktreeRoutingAdvisoryTasks: advisoryTasks
|
|
4959
|
+
} : {};
|
|
4960
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
4961
|
+
let eagerPushDeferredCount = 0;
|
|
4962
|
+
if (ctx.transport instanceof IpcTransport) {
|
|
4963
|
+
const dependencyStatusById = new Map(
|
|
4964
|
+
(0, import_daemon_core5.getQueue)(ctx.mesh.id).map((t) => [t.id, t.status])
|
|
4965
|
+
);
|
|
4966
|
+
const coordinatorDaemonId = resolveCoordinatorDaemonId(ctx);
|
|
4967
|
+
const dispatchPromises = [];
|
|
4968
|
+
tasks.forEach((task, i) => {
|
|
4969
|
+
if (!(0, import_daemon_core5.taskDependenciesSatisfied)(task, dependencyStatusById)) {
|
|
4970
|
+
eagerPushDeferredCount++;
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
dispatchPromises.push(...eagerPushTaskToRemoteNodes(
|
|
4974
|
+
ctx,
|
|
4975
|
+
task,
|
|
4976
|
+
normalizedEntries[i].message,
|
|
4977
|
+
normalizedEntries[i].targetNodeId,
|
|
4978
|
+
normalizedEntries[i].requiredTags,
|
|
4979
|
+
coordinatorDaemonId
|
|
4980
|
+
));
|
|
4981
|
+
});
|
|
4982
|
+
Promise.all(dispatchPromises).catch(() => {
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
return JSON.stringify({
|
|
4986
|
+
success: true,
|
|
4987
|
+
source: "queue",
|
|
4988
|
+
atomic: true,
|
|
4989
|
+
enqueued: tasks.length,
|
|
4990
|
+
tasks: tasks.map((task, i) => ({
|
|
4991
|
+
...normalizedEntries[i].ref ? { ref: normalizedEntries[i].ref } : {},
|
|
4992
|
+
taskId: task.id,
|
|
4993
|
+
status: task.status,
|
|
4994
|
+
taskMode: task.taskMode,
|
|
4995
|
+
...Array.isArray(task.dependsOn) && task.dependsOn.length > 0 ? { dependsOn: task.dependsOn } : {},
|
|
4996
|
+
...task.targetNodeId ? { targetNodeId: task.targetNodeId } : {},
|
|
4997
|
+
...task.priority ? { priority: task.priority } : {},
|
|
4998
|
+
...task.notBefore ? { notBefore: task.notBefore } : {}
|
|
4999
|
+
})),
|
|
5000
|
+
...duplicateSuspects.length > 0 ? {
|
|
5001
|
+
duplicateSuspects,
|
|
5002
|
+
duplicateSuspectHint: "In-flight task(s) with the same message+target already exist. The batch was enqueued anyway (warn-only). Cancel duplicates via mesh_queue_cancel, or pass allow_duplicate=true / block_duplicate=true to silence or refuse next time."
|
|
5003
|
+
} : {},
|
|
5004
|
+
...missionWarning,
|
|
5005
|
+
...worktreeAdvisory,
|
|
5006
|
+
...eagerPushDeferredCount > 0 ? { eagerPushDeferred: eagerPushDeferredCount, eagerPushDeferredReason: "dependencies_unsatisfied" } : {},
|
|
5007
|
+
queueTrigger,
|
|
5008
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
5009
|
+
});
|
|
5010
|
+
}
|
|
4733
5011
|
async function meshViewQueue(ctx, args) {
|
|
4734
5012
|
const rateResult = (0, import_daemon_core5.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_view_queue" });
|
|
4735
5013
|
const compact = args.verbose === true ? false : args.compact ?? true;
|
|
@@ -8145,6 +8423,10 @@ async function meshFastForwardNode(ctx, args) {
|
|
|
8145
8423
|
async function meshRestartDaemon(ctx, args) {
|
|
8146
8424
|
await refreshMeshFromDaemon(ctx);
|
|
8147
8425
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
8426
|
+
const observedProbe = await collectLiveStatusProbe(ctx, node);
|
|
8427
|
+
const meshAttachedTrack = observedProbe.daemonBuild?.track ?? "unknown";
|
|
8428
|
+
const configuredDaemonId = typeof node.daemonId === "string" && node.daemonId.trim() ? node.daemonId.trim() : "unknown";
|
|
8429
|
+
const meshAttachedDaemonId = observedProbe.daemonId ?? "unknown";
|
|
8148
8430
|
try {
|
|
8149
8431
|
const result = await commandForNode(ctx, node, "restart_daemon_node", {
|
|
8150
8432
|
meshId: ctx.mesh.id,
|
|
@@ -8157,16 +8439,58 @@ async function meshRestartDaemon(ctx, args) {
|
|
|
8157
8439
|
...args.when_idle === true ? { whenIdle: true } : {},
|
|
8158
8440
|
...args.cancel_when_idle === true ? { cancelWhenIdle: true } : {},
|
|
8159
8441
|
...typeof args.timeout_ms === "number" ? { timeoutMs: args.timeout_ms } : {},
|
|
8160
|
-
...args.kill_session_host === true ? { killSessionHost: true } : {}
|
|
8442
|
+
...args.kill_session_host === true ? { killSessionHost: true } : {},
|
|
8443
|
+
// Forwarded only when explicitly set, so the default stays "refuse
|
|
8444
|
+
// a downgrade" for every caller that does not opt in.
|
|
8445
|
+
...args.allow_downgrade === true ? { allowDowngrade: true } : {}
|
|
8161
8446
|
});
|
|
8162
|
-
|
|
8447
|
+
const payload = unwrapCommandPayload(result);
|
|
8448
|
+
const rawTarget = payload?.restartTargetDaemon && typeof payload.restartTargetDaemon === "object" ? payload.restartTargetDaemon : {};
|
|
8449
|
+
const targetTrack = rawTarget.track === "stable" || rawTarget.track === "preview" ? rawTarget.track : "unknown";
|
|
8450
|
+
const targetDaemonId = typeof rawTarget.daemonId === "string" && rawTarget.daemonId.trim() ? rawTarget.daemonId.trim() : "unknown";
|
|
8451
|
+
const targetNpmTag = typeof rawTarget.npmTag === "string" && rawTarget.npmTag.trim() ? rawTarget.npmTag.trim() : typeof payload?.npmTag === "string" && payload.npmTag.trim() ? payload.npmTag.trim() : "unknown";
|
|
8452
|
+
const trackMismatch = meshAttachedTrack === "unknown" || targetTrack === "unknown" ? null : meshAttachedTrack !== targetTrack;
|
|
8453
|
+
const daemonMismatch = meshAttachedDaemonId === "unknown" || targetDaemonId === "unknown" ? null : !(0, import_daemon_core5.daemonIdsEquivalent)(meshAttachedDaemonId, targetDaemonId);
|
|
8454
|
+
const routingMismatch = trackMismatch === true || daemonMismatch === true;
|
|
8455
|
+
return JSON.stringify({
|
|
8456
|
+
...payload,
|
|
8457
|
+
meshAttachedDaemon: {
|
|
8458
|
+
daemonId: meshAttachedDaemonId,
|
|
8459
|
+
configuredDaemonId,
|
|
8460
|
+
track: meshAttachedTrack
|
|
8461
|
+
},
|
|
8462
|
+
restartTargetDaemon: {
|
|
8463
|
+
daemonId: targetDaemonId,
|
|
8464
|
+
track: targetTrack,
|
|
8465
|
+
npmTag: targetNpmTag
|
|
8466
|
+
},
|
|
8467
|
+
daemonMismatch,
|
|
8468
|
+
trackMismatch,
|
|
8469
|
+
...routingMismatch ? {
|
|
8470
|
+
trackWarning: `DAEMON/TRACK MISMATCH: mesh status was answered by ${meshAttachedDaemonId} on the '${meshAttachedTrack}' track, but restart/upgrade was accepted by ${targetDaemonId} on the '${targetTrack}' track. The operation was not blocked; verify the intended daemon before interpreting version changes.`
|
|
8471
|
+
} : {},
|
|
8472
|
+
...!routingMismatch && (trackMismatch === null || daemonMismatch === null) ? {
|
|
8473
|
+
trackWarning: "Daemon/track match is unknown because the mesh-attached daemon or restart target did not report enough identity. No track was inferred from the version string."
|
|
8474
|
+
} : {}
|
|
8475
|
+
}, null, 2);
|
|
8163
8476
|
} catch (e) {
|
|
8164
8477
|
const failure = buildCoordinatorP2pRelayFailure(e, {
|
|
8165
8478
|
command: "restart_daemon_node",
|
|
8166
8479
|
targetDaemonId: node.daemonId,
|
|
8167
8480
|
nodeId: args.node_id
|
|
8168
8481
|
});
|
|
8169
|
-
return JSON.stringify(
|
|
8482
|
+
return JSON.stringify({
|
|
8483
|
+
...failure,
|
|
8484
|
+
meshAttachedDaemon: {
|
|
8485
|
+
daemonId: meshAttachedDaemonId,
|
|
8486
|
+
configuredDaemonId,
|
|
8487
|
+
track: meshAttachedTrack
|
|
8488
|
+
},
|
|
8489
|
+
restartTargetDaemon: { daemonId: "unknown", track: "unknown", npmTag: "unknown" },
|
|
8490
|
+
daemonMismatch: null,
|
|
8491
|
+
trackMismatch: null,
|
|
8492
|
+
trackWarning: "Restart/upgrade target track is unknown because the command was not accepted. No track was inferred from the version string."
|
|
8493
|
+
}, null, 2);
|
|
8170
8494
|
}
|
|
8171
8495
|
}
|
|
8172
8496
|
async function meshCheckpoint(ctx, args) {
|
|
@@ -9872,6 +10196,9 @@ async function startMcpServer(opts) {
|
|
|
9872
10196
|
case "mesh_enqueue_task":
|
|
9873
10197
|
text = await meshEnqueueTask(meshCtx, a);
|
|
9874
10198
|
break;
|
|
10199
|
+
case "mesh_enqueue_batch":
|
|
10200
|
+
text = await meshEnqueueBatch(meshCtx, a);
|
|
10201
|
+
break;
|
|
9875
10202
|
case "mesh_view_queue":
|
|
9876
10203
|
text = await meshViewQueue(meshCtx, a);
|
|
9877
10204
|
break;
|