@adhdev/daemon-standalone 1.0.49-rc.11 → 1.0.49-rc.13

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/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-BeC2kIKA.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-QzpIT-yd.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendor-DgFmqOGd.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-CqyS0XiL.css">
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,7 +1257,7 @@ 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. 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.`,
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: {
@@ -1567,7 +1618,7 @@ var MESH_PRUNE_STALE_DIRECT_TOOL = {
1567
1618
  };
1568
1619
  var MESH_REFINE_NODE_TOOL = {
1569
1620
  name: "mesh_refine_node",
1570
- 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).",
1571
1622
  inputSchema: {
1572
1623
  type: "object",
1573
1624
  properties: {
@@ -1839,6 +1890,7 @@ var ALL_MESH_TOOLS = [
1839
1890
  MESH_STATUS_TOOL,
1840
1891
  MESH_LIST_NODES_TOOL,
1841
1892
  MESH_ENQUEUE_TASK_TOOL,
1893
+ MESH_ENQUEUE_BATCH_TOOL,
1842
1894
  MESH_VIEW_QUEUE_TOOL,
1843
1895
  MESH_QUEUE_CANCEL_TOOL,
1844
1896
  MESH_QUEUE_REQUEUE_TOOL,
@@ -3478,8 +3530,10 @@ async function collectLiveStatusSessionsVerified(ctx, node) {
3478
3530
  async function collectLiveStatusProbe(ctx, node) {
3479
3531
  try {
3480
3532
  const statusResult = await commandForNode(ctx, node, "get_status_metadata", {}, { statusProbe: true });
3533
+ const payload = unwrapCommandPayload(statusResult);
3481
3534
  return {
3482
3535
  sessions: extractStatusMetadataSessions(statusResult),
3536
+ ...readString(payload?.status?.instanceId) ? { daemonId: readString(payload.status.instanceId) } : {},
3483
3537
  daemonBuild: extractDaemonBuildInfo(statusResult),
3484
3538
  upgradeFailure: extractUpgradeFailureSummary(statusResult)
3485
3539
  };
@@ -3515,11 +3569,14 @@ function extractDaemonBuildInfo(value) {
3515
3569
  if (!build) return void 0;
3516
3570
  const commit = readString(build.commit);
3517
3571
  if (!commit) return void 0;
3572
+ const reportedTrack = readString(build.track);
3573
+ const track = reportedTrack === "stable" || reportedTrack === "preview" ? reportedTrack : "unknown";
3518
3574
  return {
3519
3575
  commit,
3520
3576
  commitShort: readString(build.commitShort) || commit.slice(0, 7),
3521
3577
  version: readString(build.version) || "unknown",
3522
- ...readString(build.builtAt) ? { builtAt: readString(build.builtAt) } : {}
3578
+ ...readString(build.builtAt) ? { builtAt: readString(build.builtAt) } : {},
3579
+ track
3523
3580
  };
3524
3581
  }
3525
3582
  async function collectMeshViewQueueNodesWithLiveSessions(ctx) {
@@ -4534,14 +4591,14 @@ function buildUntargetedCodeChangeWorktreeAdvisory(input) {
4534
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.'
4535
4592
  };
4536
4593
  }
4537
- async function meshEnqueueTask(ctx, args) {
4594
+ function normalizeEnqueueTaskArgs(ctx, args, callerLabel) {
4538
4595
  const message = readString(args.message);
4539
4596
  if (!message) {
4540
- return JSON.stringify({
4541
- success: false,
4597
+ return {
4598
+ ok: false,
4542
4599
  code: "invalid_message",
4543
- error: "mesh_enqueue_task requires a non-empty string `message`."
4544
- });
4600
+ error: `${callerLabel} requires a non-empty string \`message\`.`
4601
+ };
4545
4602
  }
4546
4603
  const taskMode = readString(args.task_mode) || readString(args.taskMode);
4547
4604
  const readonly = args.readonly === true || args.read_only === true;
@@ -4556,26 +4613,132 @@ async function meshEnqueueTask(ctx, args) {
4556
4613
  const notBefore = (0, import_daemon_core5.resolveNotBefore)(notBeforeRaw);
4557
4614
  const maxRetriesRaw = typeof args.maxRetries === "number" ? args.maxRetries : typeof args.max_retries === "number" ? args.max_retries : void 0;
4558
4615
  const maxRetries = typeof maxRetriesRaw === "number" && Number.isFinite(maxRetriesRaw) && maxRetriesRaw >= 0 ? Math.floor(maxRetriesRaw) : void 0;
4559
- const allowDuplicate = args.allowDuplicate === true || args.allow_duplicate === true;
4560
- const blockDuplicate = args.blockDuplicate === true || args.block_duplicate === true;
4561
4616
  const explicitTargetRaw = readString(args.targetNodeId) || readString(args.target_node_id) || readString(args.targetNode) || readString(args.target_node) || void 0;
4562
4617
  const preferWorktree = args.preferWorktree === true || args.prefer_worktree === true;
4563
4618
  let targetNodeId;
4564
4619
  if (explicitTargetRaw) {
4565
4620
  const matched = ctx.mesh.nodes.find((n) => (0, import_daemon_core5.meshNodeIdMatches)(n, explicitTargetRaw));
4566
4621
  if (!matched) {
4567
- return JSON.stringify({
4568
- success: false,
4622
+ return {
4623
+ ok: false,
4569
4624
  code: "target_node_not_found",
4570
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.`,
4571
- targetNodeId: explicitTargetRaw,
4572
- availableNodeIds: ctx.mesh.nodes.map((n) => n.id).filter(Boolean)
4573
- });
4626
+ extra: {
4627
+ targetNodeId: explicitTargetRaw,
4628
+ availableNodeIds: ctx.mesh.nodes.map((n) => n.id).filter(Boolean)
4629
+ }
4630
+ };
4574
4631
  }
4575
4632
  targetNodeId = readString(matched.id) || explicitTargetRaw;
4576
4633
  } else if (preferWorktree) {
4577
4634
  targetNodeId = resolvePreferredWorktreeNodeId(ctx) || void 0;
4578
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;
4579
4742
  const duplicateSuspect = allowDuplicate ? null : findInFlightDuplicate(ctx, message, targetNodeId);
4580
4743
  if (duplicateSuspect && blockDuplicate) {
4581
4744
  return JSON.stringify({
@@ -4641,65 +4804,7 @@ async function meshEnqueueTask(ctx, args) {
4641
4804
  );
4642
4805
  const eagerPushDeferred = !(0, import_daemon_core5.taskDependenciesSatisfied)(task, dependencyStatusById);
4643
4806
  const coordinatorDaemonId = resolveCoordinatorDaemonId(ctx);
4644
- const dispatchPromises = [];
4645
- const eagerPushTargets = eagerPushDeferred ? [] : ctx.mesh.nodes;
4646
- for (const node of eagerPushTargets) {
4647
- const isLocalNode = isLocalControlPlaneNode(ctx, node);
4648
- if (isLocalNode || !node.daemonId) continue;
4649
- if (targetNodeId && node.id !== targetNodeId) continue;
4650
- if (!(0, import_daemon_core5.nodeSatisfiesRequiredTags)(requiredTags, (0, import_daemon_core5.buildMeshNodeCapabilityTags)(node))) continue;
4651
- dispatchPromises.push(
4652
- ipcDispatchToRemoteAgent(ctx, node, {
4653
- message,
4654
- meshContext: {
4655
- meshId: ctx.mesh.id,
4656
- nodeId: node.id,
4657
- taskId: task.id,
4658
- ...coordinatorDaemonId ? { coordinatorDaemonId } : {}
4659
- }
4660
- }).then((result) => {
4661
- if (result.success) {
4662
- try {
4663
- const providerType = result.providerType;
4664
- const descriptor = summarizeTaskMessage(message);
4665
- (0, import_daemon_core5.appendLedgerEntry)(ctx.mesh.id, {
4666
- kind: "task_dispatched",
4667
- nodeId: node.id,
4668
- sessionId: result.sessionId,
4669
- providerType,
4670
- payload: {
4671
- source: "queue",
4672
- via: "p2p_direct",
4673
- taskId: task.id,
4674
- message,
4675
- taskTitle: descriptor.taskTitle,
4676
- taskSummary: descriptor.taskSummary,
4677
- ...task.taskMode ? { taskMode: task.taskMode } : {},
4678
- ...providerType ? { providerType } : {},
4679
- targetSessionId: result.sessionId
4680
- }
4681
- });
4682
- } catch {
4683
- }
4684
- }
4685
- }).catch((err) => {
4686
- try {
4687
- (0, import_daemon_core5.appendLedgerEntry)(ctx.mesh.id, {
4688
- kind: "p2p_dispatch_failed",
4689
- nodeId: node.id,
4690
- payload: {
4691
- source: "queue",
4692
- via: "p2p_direct",
4693
- taskId: task.id,
4694
- error: err?.message || String(err),
4695
- dispatchFailedAt: (/* @__PURE__ */ new Date()).toISOString()
4696
- }
4697
- });
4698
- } catch {
4699
- }
4700
- })
4701
- );
4702
- }
4807
+ const dispatchPromises = eagerPushDeferred ? [] : eagerPushTaskToRemoteNodes(ctx, task, message, targetNodeId, requiredTags, coordinatorDaemonId);
4703
4808
  Promise.all(dispatchPromises).catch(() => {
4704
4809
  });
4705
4810
  return JSON.stringify({
@@ -4731,6 +4836,178 @@ async function meshEnqueueTask(ctx, args) {
4731
4836
  return JSON.stringify({ success: false, error: message2 });
4732
4837
  }
4733
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
+ }
4734
5011
  async function meshViewQueue(ctx, args) {
4735
5012
  const rateResult = (0, import_daemon_core5.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_view_queue" });
4736
5013
  const compact = args.verbose === true ? false : args.compact ?? true;
@@ -8146,6 +8423,10 @@ async function meshFastForwardNode(ctx, args) {
8146
8423
  async function meshRestartDaemon(ctx, args) {
8147
8424
  await refreshMeshFromDaemon(ctx);
8148
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";
8149
8430
  try {
8150
8431
  const result = await commandForNode(ctx, node, "restart_daemon_node", {
8151
8432
  meshId: ctx.mesh.id,
@@ -8163,14 +8444,53 @@ async function meshRestartDaemon(ctx, args) {
8163
8444
  // a downgrade" for every caller that does not opt in.
8164
8445
  ...args.allow_downgrade === true ? { allowDowngrade: true } : {}
8165
8446
  });
8166
- return JSON.stringify(unwrapCommandPayload(result), null, 2);
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);
8167
8476
  } catch (e) {
8168
8477
  const failure = buildCoordinatorP2pRelayFailure(e, {
8169
8478
  command: "restart_daemon_node",
8170
8479
  targetDaemonId: node.daemonId,
8171
8480
  nodeId: args.node_id
8172
8481
  });
8173
- return JSON.stringify(failure, null, 2);
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);
8174
8494
  }
8175
8495
  }
8176
8496
  async function meshCheckpoint(ctx, args) {
@@ -9876,6 +10196,9 @@ async function startMcpServer(opts) {
9876
10196
  case "mesh_enqueue_task":
9877
10197
  text = await meshEnqueueTask(meshCtx, a);
9878
10198
  break;
10199
+ case "mesh_enqueue_batch":
10200
+ text = await meshEnqueueBatch(meshCtx, a);
10201
+ break;
9879
10202
  case "mesh_view_queue":
9880
10203
  text = await meshViewQueue(meshCtx, a);
9881
10204
  break;