@adhdev/daemon-standalone 0.9.82-rc.261 → 0.9.82-rc.263

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-standalone",
3
- "version": "0.9.82-rc.261",
3
+ "version": "0.9.82-rc.263",
4
4
  "description": "ADHDev standalone daemon — embedded HTTP/WS server for local dashboard",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1119,6 +1119,28 @@ function buildCompactGitSnapshot(status) {
1119
1119
  }
1120
1120
  return slim;
1121
1121
  }
1122
+ function summarizeNodeSessions(sessions) {
1123
+ const list = Array.isArray(sessions) ? sessions : [];
1124
+ const byStatus = {};
1125
+ const providerCounts = {};
1126
+ const selfCoordinatorSessionIds = [];
1127
+ for (const s of list) {
1128
+ const status = typeof s?.status === "string" && s.status ? s.status : "unknown";
1129
+ byStatus[status] = (byStatus[status] ?? 0) + 1;
1130
+ const provider = typeof s?.providerType === "string" && s.providerType ? s.providerType : "unknown";
1131
+ providerCounts[provider] = (providerCounts[provider] ?? 0) + 1;
1132
+ if (s?.isSelfCoordinator === true && s.id) selfCoordinatorSessionIds.push(String(s.id));
1133
+ }
1134
+ const summary = {
1135
+ total: list.length,
1136
+ byStatus,
1137
+ providerCounts
1138
+ };
1139
+ if (selfCoordinatorSessionIds.length > 0) {
1140
+ summary.selfCoordinatorSessionIds = selfCoordinatorSessionIds;
1141
+ }
1142
+ return summary;
1143
+ }
1122
1144
  function extractLaunchPayload(value) {
1123
1145
  return findNestedPayload(value, (payload) => Boolean(payload?.sessionId || payload?.id || payload?.runtimeSessionId));
1124
1146
  }
@@ -1705,6 +1727,30 @@ async function collectLiveStatusSessions(ctx, node) {
1705
1727
  return [];
1706
1728
  }
1707
1729
  }
1730
+ async function collectLiveStatusProbe(ctx, node) {
1731
+ try {
1732
+ const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
1733
+ return {
1734
+ sessions: extractStatusMetadataSessions(statusResult),
1735
+ daemonBuild: extractDaemonBuildInfo(statusResult)
1736
+ };
1737
+ } catch {
1738
+ return { sessions: [] };
1739
+ }
1740
+ }
1741
+ function extractDaemonBuildInfo(value) {
1742
+ const payload = unwrapCommandPayload(value);
1743
+ const build = payload?.daemonBuild && typeof payload.daemonBuild === "object" ? payload.daemonBuild : value?.daemonBuild && typeof value.daemonBuild === "object" ? value.daemonBuild : void 0;
1744
+ if (!build) return void 0;
1745
+ const commit = readString(build.commit);
1746
+ if (!commit) return void 0;
1747
+ return {
1748
+ commit,
1749
+ commitShort: readString(build.commitShort) || commit.slice(0, 7),
1750
+ version: readString(build.version) || "unknown",
1751
+ ...readString(build.builtAt) ? { builtAt: readString(build.builtAt) } : {}
1752
+ };
1753
+ }
1708
1754
  async function collectMeshViewQueueNodesWithLiveSessions(ctx) {
1709
1755
  const nodes = await Promise.all(ctx.mesh.nodes.map(async (node) => {
1710
1756
  const liveSessions = await collectLiveStatusSessions(ctx, node);
@@ -1957,13 +2003,14 @@ function buildRemoveNodeArgs(ctx, nodeId, sessionCleanupMode) {
1957
2003
  }
1958
2004
  var MESH_STATUS_TOOL = {
1959
2005
  name: "mesh_status",
1960
- 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. Do not repeatedly call this to wait for generating delegated work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.",
2006
+ 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). Do not repeatedly call this to wait for generating delegated work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.",
1961
2007
  inputSchema: {
1962
2008
  type: "object",
1963
2009
  properties: {
1964
2010
  _gemini_compat: { type: "string", description: "Dummy property for Gemini compatibility. Ignore this." },
1965
2011
  includeStaleDirectWorkDetails: { type: "boolean", description: "Opt in to the full staleDirectWork array. Defaults false; normal status returns compact staleDirectWorkSummary only." },
1966
- compact: { type: "boolean", description: "Slim payload for LLM callers. Default true. Set false (or verbose=true) for the full dashboard-grade payload." },
2012
+ includeSessions: { type: "boolean", description: "Opt in to per-node live session arrays. Default false: compact mode returns a per-node sessionSummary (counts) and de-duplicated full session lists under top-level daemonSessions keyed by daemonId (sessions are not repeated for every node that shares a daemon). Set true to also include the full session array on each node." },
2013
+ compact: { type: "boolean", description: "Slim payload for LLM callers. Default true. Folds per-node session arrays to sessionSummary and de-duplicates daemon-shared sessions into daemonSessions. Set false (or verbose=true) for the full dashboard-grade payload." },
1967
2014
  verbose: { type: "boolean", description: "Force the full payload; overrides compact." }
1968
2015
  }
1969
2016
  }
@@ -2119,15 +2166,17 @@ var MESH_GIT_STATUS_TOOL = {
2119
2166
  };
2120
2167
  var MESH_FAST_FORWARD_NODE_TOOL = {
2121
2168
  name: "mesh_fast_forward_node",
2122
- description: "Safely dry-run or execute an obvious direct fast-forward for a mesh node without launching an agent session. Defaults to dry-run; execution requires execute=true. Never pushes, rebases, resets, cleans, or checks out arbitrary revisions.",
2169
+ description: 'Safely dry-run or execute an obvious direct fast-forward for a mesh node without launching an agent session. mode="merge" (default) absorbs upstream commits into the local branch via git merge --ff-only (ahead=0, behind>0). mode="push" publishes local commits to origin via a strict ff-only push (HEAD must be a descendant of origin/<branch>). Defaults to dry-run; execution requires execute=true. Never force-pushes, rebases, resets, cleans, or checks out arbitrary revisions. When the merge path finds the branch ahead with nothing to merge, it returns code "ahead_needs_push" pointing at mode="push".',
2123
2170
  inputSchema: {
2124
2171
  type: "object",
2125
2172
  properties: {
2126
2173
  node_id: { type: "string", description: "Target node ID." },
2174
+ mode: { type: "string", enum: ["merge", "push"], description: "merge (default): git merge --ff-only to absorb upstream. push: strict ff-only push of local commits to origin/<branch>; refuses any non-fast-forward." },
2127
2175
  branch: { type: "string", description: "Optional guard: require the node's current branch to match this branch before planning/executing." },
2128
- execute: { type: "boolean", description: "When true, apply the fast-forward if all safety gates pass. Defaults false/dry-run." },
2176
+ execute: { type: "boolean", description: "When true, apply the fast-forward/push if all safety gates pass. Defaults false/dry-run." },
2129
2177
  dry_run: { type: "boolean", description: "Preview only. Defaults true unless execute=true; dry_run=true overrides execute." },
2130
- update_submodules: { type: "boolean", description: "When true, if the root fast-forward changes gitlinks, run only git submodule update --init --recursive and verify submodules clean." }
2178
+ update_submodules: { type: "boolean", description: 'mode="merge" only: when true, if the root fast-forward changes gitlinks, run only git submodule update --init --recursive and verify submodules clean.' },
2179
+ push_submodules: { type: "boolean", description: 'mode="push" only: also ff-only push submodule HEADs to their origin main. Gated by mesh policy allowAutoPublishSubmoduleMainCommits \u2014 skipped unless that policy is enabled. Defaults false (root push only).' }
2131
2180
  },
2132
2181
  required: ["node_id"]
2133
2182
  }
@@ -2228,8 +2277,10 @@ var MESH_TASK_HISTORY_TOOL = {
2228
2277
  inputSchema: {
2229
2278
  type: "object",
2230
2279
  properties: {
2231
- tail: { type: "number", description: "Number of recent entries to return (default: 20)." },
2232
- kind: { type: "string", description: "Filter by entry kind: task_dispatched, task_completed, task_failed, task_stalled, session_launched, checkpoint_created, node_cloned, node_removed, direct_fast_forward." }
2280
+ tail: { type: "number", description: "Number of recent entries to return (default: 20; clamped to 40 in compact mode, 200 in verbose)." },
2281
+ kind: { type: "string", description: "Filter by entry kind: task_dispatched, task_completed, task_failed, task_stalled, session_launched, checkpoint_created, node_cloned, node_removed, direct_fast_forward." },
2282
+ compact: { type: "boolean", description: "Slim payload for LLM callers. Default true. Truncates long payload strings (message/taskSummary \u2264200, finalSummary \u2264300) and drops large nested evidence blobs (accessible via mesh_reconcile_ledger). Set false (or verbose=true) for full untruncated payloads." },
2283
+ verbose: { type: "boolean", description: "Force the full untruncated payload; overrides compact." }
2233
2284
  }
2234
2285
  }
2235
2286
  };
@@ -2247,6 +2298,18 @@ var MESH_RECONCILE_LEDGER_TOOL = {
2247
2298
  }
2248
2299
  }
2249
2300
  };
2301
+ var MESH_PRUNE_STALE_DIRECT_TOOL = {
2302
+ name: "mesh_prune_stale_direct",
2303
+ description: "Prune orphaned staleDirect dispatch records \u2014 direct task dispatches whose original node/session is no longer present in the live mesh. dry_run (default) reports exactly which records would be pruned without mutating anything; pass execute=true to delete them. Active/pending/assigned/generating work and fresh unacknowledged dispatch failures (node/session still live) are always preserved. The append-only mesh ledger audit history is left intact.",
2304
+ inputSchema: {
2305
+ type: "object",
2306
+ properties: {
2307
+ execute: { type: "boolean", description: "When true, actually delete the orphaned records. Defaults false (dry run). Ignored when dry_run=true." },
2308
+ dry_run: { type: "boolean", description: "Force a preview without mutation even if execute=true. Defaults to dry-run behavior when execute is not set." },
2309
+ include_terminal: { type: "boolean", description: "Also prune terminal (completed/failed) direct dispatch store rows in addition to orphans. Defaults false." }
2310
+ }
2311
+ }
2312
+ };
2250
2313
  var MESH_REFINE_NODE_TOOL = {
2251
2314
  name: "mesh_refine_node",
2252
2315
  description: "The Refinery: Accept an async validation/merge/cleanup job for a completed worktree node. 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.",
@@ -2260,7 +2323,7 @@ var MESH_REFINE_NODE_TOOL = {
2260
2323
  };
2261
2324
  var MESH_REFINE_BATCH_TOOL = {
2262
2325
  name: "mesh_refine_batch",
2263
- description: "Batch Refinery: converge multiple sibling worktree nodes onto the base branch in one conflict-aware sequential pipeline. Orders nodes by change-area (non-submodule nodes first, submodule-touching nodes serialized last) so each merged sibling advances the base and the next node auto-rebases + re-checks patch-equivalence before its own merge. Each node runs the same validation/patch-equivalence/submodule-reachability/merge/cleanup gates as mesh_refine_node. Conflicting or blocked nodes are isolated as blocked_review while the rest of the batch proceeds. Defaults to dry-run (plan only); set execute=true to converge. Never force-pushes or resets.",
2326
+ description: "Batch Refinery: converge multiple sibling worktree nodes onto the base branch in one conflict-aware sequential pipeline. Orders nodes by change-area (non-submodule nodes first, submodule-touching nodes serialized last) so each merged sibling advances the base and the next node auto-rebases + re-checks patch-equivalence before its own merge. Each node runs the same validation/patch-equivalence/submodule-reachability/merge/cleanup gates as mesh_refine_node. Conflicting or blocked nodes are isolated as blocked_review while the rest of the batch proceeds. Defaults to dry-run (plan only); set execute=true to converge. Never force-pushes or resets. execute=true is async: the immediate response is async:true / status:'accepted' with the batch jobId and ordered target node list; per-node convergence runs in the background and the aggregate completion/failure (with per-node merged / blocked_review / not_mergeable results) is delivered as a terminal refine event via pending mesh events and the ledger \u2014 do not re-invoke while a batch is in flight. dry_run returns the plan synchronously.",
2264
2327
  inputSchema: {
2265
2328
  type: "object",
2266
2329
  properties: {
@@ -2347,6 +2410,7 @@ var ALL_MESH_TOOLS = [
2347
2410
  MESH_SUGGEST_REFINE_CONFIG_TOOL,
2348
2411
  MESH_REFINE_PLAN_TOOL,
2349
2412
  MESH_CLEANUP_SESSIONS_TOOL,
2413
+ MESH_PRUNE_STALE_DIRECT_TOOL,
2350
2414
  MESH_TASK_HISTORY_TOOL,
2351
2415
  MESH_RECONCILE_LEDGER_TOOL,
2352
2416
  MESH_MISSION_UPSERT_TOOL,
@@ -2384,6 +2448,9 @@ async function meshStatus(ctx, args = {}) {
2384
2448
  entry.isDirty = dirty;
2385
2449
  entry.uncommittedChanges = uncommittedChanges;
2386
2450
  entry.branchConvergence = buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges);
2451
+ if (status?.daemonBuildBehind && typeof status.daemonBuildBehind === "object") {
2452
+ entry.staleDaemonBuild = status.daemonBuildBehind;
2453
+ }
2387
2454
  const submodules = extractSubmodules(statusResult, node.policy?.submoduleIgnorePaths || []);
2388
2455
  if (submodules && submodules.some((s) => s?.outOfSync)) {
2389
2456
  entry.submoduleWarning = "One or more submodules are out of sync with the parent repo. Run `git submodule update` or check deployment readiness.";
@@ -2451,7 +2518,9 @@ async function meshStatus(ctx, args = {}) {
2451
2518
  }
2452
2519
  const relatedRepos = await collectRelatedRepoStatuses(ctx, node);
2453
2520
  if (relatedRepos.length) entry.relatedRepos = relatedRepos;
2454
- const liveSessions = await collectLiveStatusSessions(ctx, node);
2521
+ const statusProbe = await collectLiveStatusProbe(ctx, node);
2522
+ const liveSessions = statusProbe.sessions;
2523
+ if (statusProbe.daemonBuild) entry.daemonBuild = statusProbe.daemonBuild;
2455
2524
  if (liveSessions.length > 0) {
2456
2525
  entry.sessions = liveSessions.map((s) => {
2457
2526
  const coordinatorMeshId = typeof s.coordinator?.meshId === "string" ? s.coordinator.meshId : void 0;
@@ -2501,10 +2570,58 @@ async function meshStatus(ctx, args = {}) {
2501
2570
  }
2502
2571
  }
2503
2572
  }
2573
+ const includeSessions = args.includeSessions === true;
2574
+ const daemonSessions = {};
2575
+ if (compact) {
2576
+ const seenDaemons = /* @__PURE__ */ new Set();
2577
+ for (const entry of results) {
2578
+ const daemonId = typeof entry?.daemonId === "string" && entry.daemonId ? entry.daemonId : "";
2579
+ const sessions = Array.isArray(entry?.sessions) ? entry.sessions : [];
2580
+ if (daemonId && sessions.length > 0 && !seenDaemons.has(daemonId)) {
2581
+ seenDaemons.add(daemonId);
2582
+ daemonSessions[daemonId] = includeSessions ? sessions : summarizeNodeSessions(sessions);
2583
+ }
2584
+ }
2585
+ }
2586
+ const daemonBuilds = {};
2587
+ for (const entry of results) {
2588
+ const daemonId = typeof entry?.daemonId === "string" && entry.daemonId ? entry.daemonId : "";
2589
+ if (daemonId && entry?.daemonBuild && !(daemonId in daemonBuilds)) {
2590
+ daemonBuilds[daemonId] = entry.daemonBuild;
2591
+ }
2592
+ }
2593
+ const staleDaemonBuilds = [];
2594
+ const seenStale = /* @__PURE__ */ new Set();
2595
+ for (const entry of results) {
2596
+ const behind = entry?.staleDaemonBuild;
2597
+ if (!behind || typeof behind !== "object") continue;
2598
+ const daemonId = typeof entry?.daemonId === "string" ? entry.daemonId : "";
2599
+ const key = `${daemonId}::${behind.scope ?? ""}::${behind.buildCommit ?? ""}::${behind.head ?? ""}`;
2600
+ if (seenStale.has(key)) continue;
2601
+ seenStale.add(key);
2602
+ staleDaemonBuilds.push({
2603
+ daemonId,
2604
+ nodeId: entry.nodeId,
2605
+ scope: behind.scope,
2606
+ liveBuildCommit: behind.buildCommit,
2607
+ liveBuildCommitShort: behind.buildCommitShort,
2608
+ head: behind.head,
2609
+ warning: behind.warning
2610
+ });
2611
+ }
2504
2612
  const nodesForResponse = compact ? results.map((entry) => {
2505
- if (!entry || typeof entry !== "object" || entry.git === void 0) return entry;
2506
- const slimGit = buildCompactGitSnapshot(entry.git);
2507
- return slimGit ? { ...entry, git: slimGit } : entry;
2613
+ if (!entry || typeof entry !== "object") return entry;
2614
+ const next = { ...entry };
2615
+ if (next.git !== void 0) {
2616
+ const slimGit = buildCompactGitSnapshot(next.git);
2617
+ if (slimGit) next.git = slimGit;
2618
+ }
2619
+ if (Array.isArray(next.sessions)) {
2620
+ next.sessionSummary = summarizeNodeSessions(next.sessions);
2621
+ if (!includeSessions) delete next.sessions;
2622
+ }
2623
+ if (next.daemonBuild !== void 0) delete next.daemonBuild;
2624
+ return next;
2508
2625
  }) : results;
2509
2626
  const response = {
2510
2627
  meshId: mesh.id,
@@ -2520,6 +2637,12 @@ async function meshStatus(ctx, args = {}) {
2520
2637
  historicalEvidenceOnly: ["recoveryHints", "ledgerSummary"]
2521
2638
  },
2522
2639
  nodes: nodesForResponse,
2640
+ ...compact && Object.keys(daemonSessions).length > 0 ? { daemonSessions } : {},
2641
+ ...Object.keys(daemonBuilds).length > 0 ? { daemonBuilds } : {},
2642
+ ...staleDaemonBuilds.length > 0 ? {
2643
+ staleDaemonBuilds,
2644
+ staleDaemonBuildWarning: "One or more live daemons were built from a commit behind the workspace HEAD. Merged refinery/mesh-tool fixes are NOT live on those daemons until they are rebuilt/redeployed and restarted \u2014 a local daemon-core dist rebuild does not update a cloud daemon. Do not assume a just-merged fix is active."
2645
+ } : {},
2523
2646
  activeWork: activeWorkEvidence.activeWork,
2524
2647
  staleDirectWorkSummary,
2525
2648
  ...args.includeStaleDirectWorkDetails === true ? { staleDirectWork: activeWorkEvidence.staleDirectWork } : {},
@@ -2584,14 +2707,16 @@ async function meshStatus(ctx, args = {}) {
2584
2707
  }
2585
2708
  async function meshTaskHistory(ctx, args) {
2586
2709
  const { mesh } = ctx;
2710
+ const compact = args.verbose === true ? false : args.compact ?? true;
2587
2711
  const pendingEvents = await drainCoordinatorPendingEvents(ctx);
2588
- const tail = typeof args.tail === "number" && args.tail > 0 ? args.tail : 20;
2712
+ const requestedTail = typeof args.tail === "number" && args.tail > 0 ? Math.floor(args.tail) : 20;
2713
+ const tail = compact ? Math.min(requestedTail, 40) : Math.min(requestedTail, 200);
2589
2714
  const kind = typeof args.kind === "string" && args.kind.trim() ? [args.kind.trim()] : void 0;
2590
2715
  const rawEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail, kind });
2591
- const entries = rawEntries.map((e) => ({
2716
+ const entries = compact ? rawEntries.map((e) => ({
2592
2717
  ...e,
2593
2718
  payload: e.payload ? slimLedgerPayload(e.payload) : e.payload
2594
- }));
2719
+ })) : rawEntries;
2595
2720
  const summary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
2596
2721
  let taskStats;
2597
2722
  try {
@@ -2604,6 +2729,7 @@ async function meshTaskHistory(ctx, args) {
2604
2729
  }
2605
2730
  return JSON.stringify({
2606
2731
  meshId: mesh.id,
2732
+ payloadMode: compact ? "compact" : "full",
2607
2733
  entries,
2608
2734
  summary,
2609
2735
  ...taskStats ? { taskStats } : {},
@@ -2688,6 +2814,89 @@ async function meshReconcileLedger(ctx, args) {
2688
2814
  });
2689
2815
  return JSON.stringify({ success: true, evidence }, null, 2);
2690
2816
  }
2817
+ async function meshPruneStaleDirect(ctx, args = {}) {
2818
+ await refreshMeshFromDaemon(ctx);
2819
+ const execute = args.execute === true && args.dry_run !== true;
2820
+ const includeTerminal = args.include_terminal === true;
2821
+ const liveNodes = await collectMeshViewQueueNodesWithLiveSessions(ctx);
2822
+ const ledgerEntries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 500 });
2823
+ const directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
2824
+ const activeWorkEvidence = (0, import_daemon_core.buildMeshActiveWork)({
2825
+ meshId: ctx.mesh.id,
2826
+ queue: (0, import_daemon_core.getQueue)(ctx.mesh.id),
2827
+ ledgerEntries,
2828
+ directDispatches,
2829
+ nodes: liveNodes,
2830
+ includeTerminalDirect: includeTerminal
2831
+ });
2832
+ const candidates = [
2833
+ ...activeWorkEvidence.staleDirectWork,
2834
+ ...includeTerminal ? activeWorkEvidence.terminalDirectWork : []
2835
+ ];
2836
+ const storeTaskIds = new Set(directDispatches.map((d) => d.taskId));
2837
+ const prunable = [];
2838
+ const preservedUnacknowledged = [];
2839
+ const preservedLedgerOnly = [];
2840
+ const preservedNotOrphan = [];
2841
+ for (const record of candidates) {
2842
+ const classification = (0, import_daemon_core.classifyStaleDirectForPrune)(record, { includeTerminal });
2843
+ if (classification === "preserve_unacknowledged") {
2844
+ preservedUnacknowledged.push(record);
2845
+ continue;
2846
+ }
2847
+ if (classification === "preserve_active") {
2848
+ preservedNotOrphan.push(record);
2849
+ continue;
2850
+ }
2851
+ if (!storeTaskIds.has(record.taskId)) {
2852
+ preservedLedgerOnly.push(record);
2853
+ continue;
2854
+ }
2855
+ prunable.push(record);
2856
+ }
2857
+ const summarize = (records) => records.map((r) => ({
2858
+ taskId: r.taskId,
2859
+ nodeId: r.nodeId,
2860
+ sessionId: r.sessionId,
2861
+ status: r.status,
2862
+ terminal: r.terminal === true,
2863
+ staleReason: r.staleReason,
2864
+ taskTitle: r.taskTitle,
2865
+ createdAt: r.createdAt
2866
+ }));
2867
+ let prunedCount = 0;
2868
+ if (execute && prunable.length) {
2869
+ prunedCount = (0, import_daemon_core.deleteDirectDispatchesByTaskId)(ctx.mesh.id, prunable.map((r) => r.taskId));
2870
+ (0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
2871
+ kind: "direct_dispatch_pruned",
2872
+ payload: {
2873
+ source: "mesh_prune_stale_direct",
2874
+ prunedCount,
2875
+ taskIds: prunable.map((r) => r.taskId),
2876
+ reasons: Array.from(new Set(prunable.map((r) => r.staleReason || (r.terminal ? "terminal" : "unknown"))))
2877
+ }
2878
+ });
2879
+ }
2880
+ return JSON.stringify({
2881
+ success: true,
2882
+ mode: execute ? "execute" : "dry_run",
2883
+ meshId: ctx.mesh.id,
2884
+ includeTerminal,
2885
+ candidateCount: candidates.length,
2886
+ prunableCount: prunable.length,
2887
+ prunedCount,
2888
+ prunable: summarize(prunable),
2889
+ preserved: {
2890
+ unacknowledgedCount: preservedUnacknowledged.length,
2891
+ ledgerOnlyCount: preservedLedgerOnly.length,
2892
+ notOrphanCount: preservedNotOrphan.length,
2893
+ unacknowledged: summarize(preservedUnacknowledged),
2894
+ ledgerOnly: summarize(preservedLedgerOnly),
2895
+ notOrphan: summarize(preservedNotOrphan)
2896
+ },
2897
+ note: execute ? `Pruned ${prunedCount} orphaned direct dispatch record(s) from the active staleDirect surface. The append-only mesh ledger audit history is preserved; a direct_dispatch_pruned entry records this prune.` : "Dry run \u2014 nothing was deleted. Re-run with execute=true to prune the listed orphaned records. Fresh unacknowledged dispatch failures (node/session still live) and ledger-only audit entries are always preserved."
2898
+ }, null, 2);
2899
+ }
2691
2900
  async function meshListNodes(ctx) {
2692
2901
  await refreshMeshFromDaemon(ctx);
2693
2902
  const { mesh } = ctx;
@@ -3554,10 +3763,12 @@ async function meshFastForwardNode(ctx, args) {
3554
3763
  meshId: ctx.mesh.id,
3555
3764
  nodeId: node.id,
3556
3765
  workspace: node.workspace,
3766
+ mode: args.mode === "push" ? "push" : "merge",
3557
3767
  branch: typeof args.branch === "string" ? args.branch : void 0,
3558
3768
  execute: args.execute === true && args.dry_run !== true,
3559
3769
  dryRun,
3560
3770
  updateSubmodules: args.update_submodules === true,
3771
+ pushSubmodules: args.push_submodules === true,
3561
3772
  submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
3562
3773
  });
3563
3774
  return JSON.stringify(unwrapCommandPayload(result), null, 2);
@@ -3745,7 +3956,7 @@ async function meshRefineBatch(ctx, args = {}) {
3745
3956
  inlineMesh: ctx.mesh
3746
3957
  });
3747
3958
  const payload = unwrapCommandPayload(result) ?? result;
3748
- if (payload?.batch && payload?.dryRun === false && Array.isArray(payload?.results)) {
3959
+ if (payload?.batch && payload?.dryRun === false && payload?.async !== true && Array.isArray(payload?.results)) {
3749
3960
  for (const outcome of payload.results) {
3750
3961
  if (outcome?.convergence === "merged_to_main" || outcome?.convergence === "skipped_patch_equivalent") {
3751
3962
  const idx = ctx.mesh.nodes.findIndex((n) => n.id === outcome.nodeId);
@@ -4963,6 +5174,9 @@ async function startMcpServer(opts) {
4963
5174
  case "mesh_cleanup_sessions":
4964
5175
  text = await meshCleanupSessions(meshCtx, a);
4965
5176
  break;
5177
+ case "mesh_prune_stale_direct":
5178
+ text = await meshPruneStaleDirect(meshCtx, a);
5179
+ break;
4966
5180
  case "mesh_task_history":
4967
5181
  text = await meshTaskHistory(meshCtx, a);
4968
5182
  break;