@adhdev/daemon-core 0.9.82-rc.261 → 0.9.82-rc.262

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.mjs CHANGED
@@ -40418,8 +40418,37 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
40418
40418
  maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
40419
40419
  });
40420
40420
  const mergeBase = git(["merge-base", baseHead, branchHead]).trim();
40421
- const mergeTreeStdout = git(["merge-tree", "--write-tree", baseHead, branchHead]);
40422
- const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || "";
40421
+ let mergedTree = "";
40422
+ let mergeTreeStdout = "";
40423
+ let gitlinkTrivialFastForward;
40424
+ try {
40425
+ mergeTreeStdout = git(["merge-tree", "--write-tree", baseHead, branchHead]);
40426
+ mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || "";
40427
+ } catch (mergeTreeErr) {
40428
+ const output = `${mergeTreeErr?.message || ""}
40429
+ ${mergeTreeErr?.stdout || ""}
40430
+ ${mergeTreeErr?.stderr || ""}`;
40431
+ const isSubmoduleConflict = /(submodule|160000)/i.test(output) || /Recursive merging with submodules/i.test(output);
40432
+ if (!isSubmoduleConflict) throw mergeTreeErr;
40433
+ const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead);
40434
+ if (!evaluation.trivial) {
40435
+ return {
40436
+ status: "failed",
40437
+ equivalent: false,
40438
+ baseHead,
40439
+ branchHead,
40440
+ mergeBase: mergeBase || void 0,
40441
+ durationMs: Date.now() - startedAt,
40442
+ error: mergeTreeErr?.message || String(mergeTreeErr),
40443
+ stdout: truncateValidationOutput(mergeTreeErr?.stdout),
40444
+ stderr: truncateValidationOutput(mergeTreeErr?.stderr),
40445
+ gitlinkTrivialFastForward: { resolved: false, gitlinks: evaluation.gitlinks, reason: evaluation.reason },
40446
+ actionableHint: buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output)
40447
+ };
40448
+ }
40449
+ mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, evaluation.gitlinks) || "";
40450
+ gitlinkTrivialFastForward = { resolved: true, gitlinks: evaluation.gitlinks };
40451
+ }
40423
40452
  if (!mergeBase || !mergedTree) {
40424
40453
  return {
40425
40454
  status: "failed",
@@ -40430,7 +40459,8 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
40430
40459
  mergedTree: mergedTree || void 0,
40431
40460
  durationMs: Date.now() - startedAt,
40432
40461
  error: "patch equivalence preflight could not resolve merge-base or synthetic merge tree",
40433
- stdout: truncateValidationOutput(mergeTreeStdout)
40462
+ stdout: truncateValidationOutput(mergeTreeStdout),
40463
+ gitlinkTrivialFastForward
40434
40464
  };
40435
40465
  }
40436
40466
  const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
@@ -40445,7 +40475,8 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
40445
40475
  mergedTree,
40446
40476
  expectedPatchId,
40447
40477
  actualPatchId,
40448
- durationMs: Date.now() - startedAt
40478
+ durationMs: Date.now() - startedAt,
40479
+ gitlinkTrivialFastForward
40449
40480
  };
40450
40481
  } catch (e) {
40451
40482
  return {
@@ -40468,6 +40499,65 @@ ${e?.stderr || ""}`
40468
40499
  };
40469
40500
  }
40470
40501
  }
40502
+ async function runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead) {
40503
+ const startedAt = Date.now();
40504
+ try {
40505
+ const { execFileSync: execFileSync6 } = await import("child_process");
40506
+ const git = (args, opts) => execFileSync6("git", args, {
40507
+ cwd: opts?.cwd || repoRoot,
40508
+ encoding: "utf8",
40509
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
40510
+ });
40511
+ const rawDiff = git(["diff", "--raw", baseHead, branchHead]).trim();
40512
+ if (rawDiff) {
40513
+ const changedPaths = rawDiff.split("\n").map((line) => line.split(" ").slice(1).join(" ").trim()).filter(Boolean).slice(0, 50);
40514
+ return {
40515
+ status: "passed",
40516
+ hasEffectiveDiff: true,
40517
+ baseHead,
40518
+ branchHead,
40519
+ changedPaths,
40520
+ durationMs: Date.now() - startedAt
40521
+ };
40522
+ }
40523
+ const submoduleHints = [];
40524
+ try {
40525
+ const status = git(["submodule", "status"]);
40526
+ for (const line of status.split("\n")) {
40527
+ const trimmed = line.trimEnd();
40528
+ if (!trimmed) continue;
40529
+ if (trimmed.startsWith("+")) {
40530
+ const parts = trimmed.slice(1).trim().split(/\s+/);
40531
+ const path39 = parts[1] || parts[0] || "(unknown)";
40532
+ submoduleHints.push({
40533
+ path: path39,
40534
+ reason: "submodule checked-out commit differs from the committed gitlink (pointer bump not committed on the root branch)"
40535
+ });
40536
+ }
40537
+ }
40538
+ } catch {
40539
+ }
40540
+ return {
40541
+ status: "failed",
40542
+ hasEffectiveDiff: false,
40543
+ baseHead,
40544
+ branchHead,
40545
+ ...submoduleHints.length ? { submoduleHints } : {},
40546
+ durationMs: Date.now() - startedAt
40547
+ };
40548
+ } catch (e) {
40549
+ return {
40550
+ status: "skipped",
40551
+ hasEffectiveDiff: true,
40552
+ baseHead,
40553
+ branchHead,
40554
+ durationMs: Date.now() - startedAt,
40555
+ error: e?.message || String(e),
40556
+ stdout: truncateValidationOutput(e?.stdout),
40557
+ stderr: truncateValidationOutput(e?.stderr)
40558
+ };
40559
+ }
40560
+ }
40471
40561
  function buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output) {
40472
40562
  if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return void 0;
40473
40563
  const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => ({
@@ -40524,6 +40614,135 @@ function readTreeObject(repoRoot, ref, path39) {
40524
40614
  return void 0;
40525
40615
  }
40526
40616
  }
40617
+ function resolveGitDir(repoRoot) {
40618
+ const out = execFileSync5("git", ["rev-parse", "--absolute-git-dir"], {
40619
+ cwd: repoRoot,
40620
+ encoding: "utf8",
40621
+ maxBuffer: 1024 * 1024
40622
+ }).trim();
40623
+ return out;
40624
+ }
40625
+ function isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit) {
40626
+ if (!baseCommit || !branchCommit) return false;
40627
+ if (baseCommit === branchCommit) return true;
40628
+ try {
40629
+ if (!fs23.existsSync(submoduleRepoPath)) return false;
40630
+ execFileSync5("git", ["cat-file", "-e", `${baseCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
40631
+ execFileSync5("git", ["cat-file", "-e", `${branchCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
40632
+ execFileSync5("git", ["merge-base", "--is-ancestor", baseCommit, branchCommit], { cwd: submoduleRepoPath, stdio: "ignore" });
40633
+ return true;
40634
+ } catch {
40635
+ return false;
40636
+ }
40637
+ }
40638
+ function readChangedPathKinds(repoRoot, fromRef, toRef) {
40639
+ try {
40640
+ const output = execFileSync5("git", ["diff", "--raw", "--no-abbrev", fromRef, toRef], {
40641
+ cwd: repoRoot,
40642
+ encoding: "utf8",
40643
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
40644
+ });
40645
+ const result = [];
40646
+ const seen = /* @__PURE__ */ new Set();
40647
+ for (const line of output.split("\n")) {
40648
+ if (!line.trim()) continue;
40649
+ const metaAndPath = line.split(" ");
40650
+ const meta = metaAndPath[0] || "";
40651
+ const path39 = metaAndPath[metaAndPath.length - 1]?.trim();
40652
+ if (!path39 || seen.has(path39)) continue;
40653
+ seen.add(path39);
40654
+ const parts = meta.split(/\s+/);
40655
+ const isGitlink = !!(parts[0]?.includes("160000") || parts[1]?.includes("160000"));
40656
+ result.push({ path: path39, isGitlink });
40657
+ }
40658
+ return result;
40659
+ } catch {
40660
+ return [];
40661
+ }
40662
+ }
40663
+ function evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead) {
40664
+ const changedGitlinks = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => {
40665
+ const baseCommit = readTreeObject(repoRoot, baseHead, path39);
40666
+ const branchCommit = readTreeObject(repoRoot, branchHead, path39);
40667
+ const submoduleRepoPath = pathResolve2(repoRoot, path39);
40668
+ const fastForward = !!baseCommit && !!branchCommit && isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit);
40669
+ return { path: path39, baseCommit, branchCommit, fastForward };
40670
+ });
40671
+ if (changedGitlinks.length === 0) {
40672
+ return { trivial: false, reason: "no_changed_gitlinks", gitlinks: changedGitlinks };
40673
+ }
40674
+ const nonFastForward = changedGitlinks.filter((entry) => !entry.fastForward);
40675
+ if (nonFastForward.length > 0) {
40676
+ return {
40677
+ trivial: false,
40678
+ reason: `diverged_gitlinks:${nonFastForward.map((entry) => entry.path).join(",")}`,
40679
+ gitlinks: changedGitlinks
40680
+ };
40681
+ }
40682
+ let mergeBase = "";
40683
+ try {
40684
+ mergeBase = execFileSync5("git", ["merge-base", baseHead, branchHead], {
40685
+ cwd: repoRoot,
40686
+ encoding: "utf8",
40687
+ maxBuffer: 1024 * 1024
40688
+ }).trim();
40689
+ } catch {
40690
+ return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
40691
+ }
40692
+ if (!mergeBase) {
40693
+ return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
40694
+ }
40695
+ const baseSideChanges = readChangedPathKinds(repoRoot, mergeBase, baseHead);
40696
+ const branchSideChanges = readChangedPathKinds(repoRoot, mergeBase, branchHead);
40697
+ const baseChangedPaths = new Map(baseSideChanges.map((entry) => [entry.path, entry]));
40698
+ const overlapping = branchSideChanges.filter((entry) => baseChangedPaths.has(entry.path));
40699
+ const nonGitlinkOverlap = overlapping.filter((entry) => {
40700
+ const baseEntry = baseChangedPaths.get(entry.path);
40701
+ return !(entry.isGitlink && baseEntry?.isGitlink);
40702
+ });
40703
+ if (nonGitlinkOverlap.length > 0) {
40704
+ return {
40705
+ trivial: false,
40706
+ reason: `non_gitlink_overlap:${nonGitlinkOverlap.map((entry) => entry.path).join(",")}`,
40707
+ gitlinks: changedGitlinks
40708
+ };
40709
+ }
40710
+ return { trivial: true, gitlinks: changedGitlinks };
40711
+ }
40712
+ function synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, gitlinks) {
40713
+ try {
40714
+ const baseTree = execFileSync5("git", ["rev-parse", `${baseHead}^{tree}`], {
40715
+ cwd: repoRoot,
40716
+ encoding: "utf8",
40717
+ maxBuffer: 1024 * 1024
40718
+ }).trim();
40719
+ if (!baseTree) return void 0;
40720
+ const updates = gitlinks.filter((entry) => entry.branchCommit).map((entry) => `160000 commit ${entry.branchCommit} ${entry.path}`).join("\n");
40721
+ if (!updates) return baseTree;
40722
+ const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
40723
+ const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
40724
+ try {
40725
+ execFileSync5("git", ["read-tree", baseTree], { cwd: repoRoot, env, stdio: "ignore" });
40726
+ execFileSync5("git", ["update-index", "--index-info"], {
40727
+ cwd: repoRoot,
40728
+ env,
40729
+ input: `${updates}
40730
+ `,
40731
+ encoding: "utf8",
40732
+ stdio: ["pipe", "ignore", "ignore"]
40733
+ });
40734
+ const newTree = execFileSync5("git", ["write-tree"], { cwd: repoRoot, env, encoding: "utf8" }).trim();
40735
+ return newTree || void 0;
40736
+ } finally {
40737
+ try {
40738
+ fs23.rmSync(tmpIndex, { force: true });
40739
+ } catch {
40740
+ }
40741
+ }
40742
+ } catch {
40743
+ return void 0;
40744
+ }
40745
+ }
40527
40746
  async function alignRefinerySubmodulesAfterMerge(repoRoot, previousBaseHead, currentHead, options = {}) {
40528
40747
  const startedAt = Date.now();
40529
40748
  const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead).filter((path39) => !(options.submoduleIgnorePaths || []).includes(path39));
@@ -41191,6 +41410,10 @@ var DaemonCommandRouter = class {
41191
41410
  runningRefineJobs = /* @__PURE__ */ new Map();
41192
41411
  /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
41193
41412
  terminalRefineJobs = /* @__PURE__ */ new Map();
41413
+ /** In-memory async batch Refinery jobs keyed by meshId (one batch convergence per mesh at a time). */
41414
+ runningRefineBatchJobs = /* @__PURE__ */ new Map();
41415
+ /** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
41416
+ terminalRefineBatchJobs = /* @__PURE__ */ new Map();
41194
41417
  constructor(deps) {
41195
41418
  this.deps = deps;
41196
41419
  }
@@ -42420,6 +42643,48 @@ ${tail}` : ""
42420
42643
  }
42421
42644
  };
42422
42645
  }
42646
+ const effectiveDiffStarted = Date.now();
42647
+ const effectiveDiff = await runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead);
42648
+ recordMeshRefineStage(refineStages, "effective_diff", effectiveDiff.status, effectiveDiffStarted, {
42649
+ hasEffectiveDiff: effectiveDiff.hasEffectiveDiff,
42650
+ changedPaths: effectiveDiff.changedPaths,
42651
+ submoduleHints: effectiveDiff.submoduleHints,
42652
+ ...effectiveDiff.error ? { error: effectiveDiff.error } : {}
42653
+ });
42654
+ if (effectiveDiff.status === "failed" && !effectiveDiff.hasEffectiveDiff) {
42655
+ const hintLines = (effectiveDiff.submoduleHints || []).map((h) => ` - ${h.path}: ${h.reason}`);
42656
+ const message = [
42657
+ `Refinery no-op guard: branch '${branch}' has no effective root-tree diff against '${baseBranch}' (${baseHead.slice(0, 12)}); nothing would merge.`,
42658
+ "This usually means a submodule (e.g. oss) has commits but the root branch never committed the gitlink (pointer) bump, so the merge would be a silent no-op while the real change never reaches main.",
42659
+ hintLines.length ? `Submodules with uncommitted pointer bumps:
42660
+ ${hintLines.join("\n")}` : "",
42661
+ `Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`
42662
+ ].filter(Boolean).join("\n");
42663
+ return {
42664
+ success: false,
42665
+ code: "no_effective_diff",
42666
+ convergenceStatus: "blocked_review",
42667
+ error: message,
42668
+ branch,
42669
+ into: baseBranch,
42670
+ validationSummary,
42671
+ patchEquivalence,
42672
+ effectiveDiff,
42673
+ refineStages,
42674
+ finalBranchConvergenceState: {
42675
+ branch,
42676
+ baseBranch,
42677
+ merged: false,
42678
+ removed: false,
42679
+ validation: "passed",
42680
+ patchEquivalence: "passed",
42681
+ effectiveDiff: "no_effective_diff",
42682
+ status: "blocked_review",
42683
+ reason: "no_effective_diff",
42684
+ ...effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}
42685
+ }
42686
+ };
42687
+ }
42423
42688
  let mergeResult;
42424
42689
  const mergeStarted = Date.now();
42425
42690
  try {
@@ -42761,6 +43026,17 @@ ${tail}` : ""
42761
43026
  note: "Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order."
42762
43027
  };
42763
43028
  }
43029
+ return this.runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args);
43030
+ }
43031
+ /**
43032
+ * Convergence core shared by the synchronous batch entry and the async batch job.
43033
+ * Refines each node in order: the per-node refine pipeline fetches origin/<base>
43034
+ * fresh, so each merged sibling advances the base before the next node's auto-rebase
43035
+ * + patch-equivalence re-check. A blocked/failed node is isolated; the batch
43036
+ * continues with the remaining nodes. Does NOT touch the per-node merge logic — it
43037
+ * only sequences calls to executeMeshRefineNodeSynchronously and aggregates outcomes.
43038
+ */
43039
+ async runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args) {
42764
43040
  const results = [];
42765
43041
  for (const node of orderedNodes) {
42766
43042
  let result;
@@ -42815,6 +43091,204 @@ ${tail}` : ""
42815
43091
  }
42816
43092
  };
42817
43093
  }
43094
+ buildRefineBatchJobKey(meshId) {
43095
+ return `${meshId}::batch`;
43096
+ }
43097
+ buildRefineBatchJobHandle(args) {
43098
+ return {
43099
+ success: true,
43100
+ async: true,
43101
+ batch: true,
43102
+ status: args.status || "accepted",
43103
+ jobId: args.jobId || `refine_batch_${createInteractionId()}`,
43104
+ interactionId: args.interactionId || createInteractionId(),
43105
+ meshId: args.meshId,
43106
+ batchLabel: `batch:${args.nodeIds.length} node${args.nodeIds.length === 1 ? "" : "s"}`,
43107
+ nodeIds: args.nodeIds,
43108
+ nodeCount: args.nodeIds.length,
43109
+ order: args.order,
43110
+ startedAt: args.startedAt || (/* @__PURE__ */ new Date()).toISOString(),
43111
+ ...args.completedAt ? { completedAt: args.completedAt } : {},
43112
+ ...args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {},
43113
+ eventDelivery: { pendingEvents: true, ledger: true },
43114
+ evidence: {
43115
+ pendingEventsCommand: "get_pending_mesh_events",
43116
+ ledgerCommand: "get_mesh_ledger_slice",
43117
+ taskHistoryKind: args.status === "completed" ? "task_completed" : args.status === "failed" ? "task_failed" : "task_dispatched"
43118
+ }
43119
+ };
43120
+ }
43121
+ /**
43122
+ * Emit a batch Refinery terminal/accepted event through the SAME pending-event +
43123
+ * forward mechanism single-node refine uses (queueRefineJobEvent), so the
43124
+ * coordinator's existing refine:accepted/completed/failed handling and message
43125
+ * renderer apply unchanged. The aggregate per-node results ride along in `result`.
43126
+ */
43127
+ queueRefineBatchJobEvent(event, handle, result) {
43128
+ const metadataEvent = {
43129
+ source: "refine_mesh_node_async_job",
43130
+ batch: true,
43131
+ jobId: handle.jobId,
43132
+ interactionId: handle.interactionId,
43133
+ meshId: handle.meshId,
43134
+ nodeId: handle.batchLabel,
43135
+ nodeIds: handle.nodeIds,
43136
+ workspace: void 0,
43137
+ status: handle.status,
43138
+ startedAt: handle.startedAt,
43139
+ completedAt: handle.completedAt,
43140
+ order: handle.order,
43141
+ ...result ? { result } : {}
43142
+ };
43143
+ const eventPayload = {
43144
+ event,
43145
+ meshId: handle.meshId,
43146
+ nodeLabel: handle.batchLabel,
43147
+ nodeId: handle.batchLabel,
43148
+ metadataEvent,
43149
+ queuedAt: Date.now(),
43150
+ ...handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}
43151
+ };
43152
+ if (typeof this.deps.instanceManager?.getByCategory === "function") {
43153
+ const forwarded = handleMeshForwardEvent(
43154
+ { instanceManager: this.deps.instanceManager },
43155
+ {
43156
+ event,
43157
+ meshId: handle.meshId,
43158
+ nodeId: handle.batchLabel,
43159
+ jobId: handle.jobId,
43160
+ interactionId: handle.interactionId,
43161
+ status: handle.status,
43162
+ startedAt: handle.startedAt,
43163
+ completedAt: handle.completedAt,
43164
+ ...result ? { result } : {}
43165
+ }
43166
+ );
43167
+ if (forwarded?.success === true) return;
43168
+ LOG.warn("Mesh", `[Refinery] Failed to forward async refine batch event ${event}: ${forwarded?.error || "unknown error"}`);
43169
+ }
43170
+ queuePendingMeshCoordinatorEvent(eventPayload);
43171
+ }
43172
+ async appendRefineBatchJobLedger(kind, handle, result) {
43173
+ try {
43174
+ const { appendLedgerEntry: appendLedgerEntry2 } = await Promise.resolve().then(() => (init_mesh_ledger(), mesh_ledger_exports));
43175
+ appendLedgerEntry2(handle.meshId, {
43176
+ kind,
43177
+ nodeId: handle.batchLabel,
43178
+ payload: {
43179
+ source: "refine_mesh_node_async_job",
43180
+ refineJob: {
43181
+ batch: true,
43182
+ jobId: handle.jobId,
43183
+ interactionId: handle.interactionId,
43184
+ status: handle.status,
43185
+ meshId: handle.meshId,
43186
+ nodeIds: handle.nodeIds,
43187
+ order: handle.order,
43188
+ targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
43189
+ startedAt: handle.startedAt,
43190
+ completedAt: handle.completedAt
43191
+ },
43192
+ async: true,
43193
+ batch: true,
43194
+ ...result ? {
43195
+ success: result.success === true,
43196
+ result
43197
+ } : {}
43198
+ }
43199
+ });
43200
+ } catch (e) {
43201
+ LOG.warn("Mesh", `[Refinery] Failed to append async refine batch ledger entry: ${e?.message || e}`);
43202
+ }
43203
+ }
43204
+ async finishMeshRefineBatchJob(handle, orderedNodes, ordering, args) {
43205
+ const key = this.buildRefineBatchJobKey(handle.meshId);
43206
+ let result;
43207
+ try {
43208
+ result = await this.runMeshRefineBatchConvergence(handle.meshId, orderedNodes, ordering, args);
43209
+ } catch (e) {
43210
+ result = { success: false, error: e?.message || String(e), batch: true };
43211
+ }
43212
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
43213
+ const summary = result.summary && typeof result.summary === "object" ? result.summary : void 0;
43214
+ const allConverged = result.allConverged === true;
43215
+ const isTerminalSuccess = result.success === true && allConverged;
43216
+ const nextStep = typeof result.nextStep === "string" && result.nextStep ? result.nextStep : isTerminalSuccess ? "All batched nodes converged onto base. Continue from the updated mesh state." : "Resolve blocked_review / not_mergeable nodes (see per-node code/stage/error in result.results), then re-run mesh_refine_batch for the remaining nodes.";
43217
+ const normalizedResult = {
43218
+ ...result,
43219
+ batch: true,
43220
+ nextStep,
43221
+ ...summary ? {
43222
+ convergenceStatus: allConverged ? "all_converged" : "partial"
43223
+ } : {}
43224
+ };
43225
+ const terminalHandle = this.buildRefineBatchJobHandle({
43226
+ meshId: handle.meshId,
43227
+ nodeIds: handle.nodeIds,
43228
+ order: handle.order,
43229
+ status: isTerminalSuccess ? "completed" : "failed",
43230
+ startedAt: handle.startedAt,
43231
+ completedAt,
43232
+ jobId: handle.jobId,
43233
+ interactionId: handle.interactionId,
43234
+ coordinatorDaemonId: handle.targetCoordinatorDaemonId
43235
+ });
43236
+ const terminal = { ...terminalHandle, result: normalizedResult };
43237
+ this.terminalRefineBatchJobs.set(key, terminal);
43238
+ this.runningRefineBatchJobs.delete(key);
43239
+ this.invalidateAggregateMeshStatus(handle.meshId);
43240
+ await this.appendRefineBatchJobLedger(isTerminalSuccess ? "task_completed" : "task_failed", terminalHandle, normalizedResult);
43241
+ this.queueRefineBatchJobEvent(isTerminalSuccess ? "refine:completed" : "refine:failed", terminalHandle, normalizedResult);
43242
+ }
43243
+ /**
43244
+ * Async entry for the batch Refinery execute path. Mirrors startMeshRefineJob:
43245
+ * resolves the plan synchronously (so target/ordering errors and the dry-run shape
43246
+ * stay synchronous), then for execute=true registers an in-flight batch job, returns
43247
+ * {async:true, status:'accepted', batch:true, ...plan} immediately, and runs the
43248
+ * convergence loop in the background — emitting the same terminal refine event.
43249
+ * Idempotent: a batch already in flight for this mesh returns the running handle
43250
+ * with duplicate:true rather than spawning a second background job.
43251
+ */
43252
+ async startMeshRefineBatchJob(meshId, requestedNodeIds, args) {
43253
+ const plan = await this.batchRefineMeshNodes(meshId, requestedNodeIds, { ...args, dryRun: true, execute: false });
43254
+ const planRecord = plan;
43255
+ if (planRecord.success !== true) return plan;
43256
+ if (args?.dryRun === true && args?.execute !== true) return plan;
43257
+ const order = Array.isArray(planRecord.order) ? planRecord.order.filter((v) => typeof v === "string") : [];
43258
+ const nodeIds = order.slice();
43259
+ if (nodeIds.length === 0) {
43260
+ return { ...planRecord, success: true, batch: true, dryRun: false, async: false };
43261
+ }
43262
+ const key = this.buildRefineBatchJobKey(meshId);
43263
+ const running = this.runningRefineBatchJobs.get(key);
43264
+ if (running) return { ...running, duplicate: true };
43265
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
43266
+ const mesh = meshRecord?.mesh;
43267
+ const allNodes = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
43268
+ const orderedNodes = nodeIds.map((id) => allNodes.find((n) => n.id === id || n.nodeId === id)).filter((n) => !!n);
43269
+ if (orderedNodes.length === 0) {
43270
+ return { success: false, error: "Batch nodes no longer resolvable in mesh", batch: true };
43271
+ }
43272
+ const ordering = {
43273
+ order,
43274
+ rationale: planRecord.orderingRationale
43275
+ };
43276
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === "string" && args.coordinatorDaemonId.trim() ? args.coordinatorDaemonId.trim() : this.deps.statusInstanceId || void 0;
43277
+ const handle = this.buildRefineBatchJobHandle({ meshId, nodeIds, order, coordinatorDaemonId });
43278
+ this.runningRefineBatchJobs.set(key, handle);
43279
+ await this.appendRefineBatchJobLedger("task_dispatched", handle);
43280
+ this.queueRefineBatchJobEvent("refine:accepted", handle);
43281
+ setImmediate(() => {
43282
+ void this.finishMeshRefineBatchJob(handle, orderedNodes, ordering, args);
43283
+ });
43284
+ return {
43285
+ ...handle,
43286
+ order,
43287
+ orderingRationale: planRecord.orderingRationale,
43288
+ plan: planRecord.plan,
43289
+ note: "Batch convergence accepted and running in the background. Completion/failure (with per-node results) will be delivered as a terminal refine event; do not poll repeatedly."
43290
+ };
43291
+ }
42818
43292
  async finishMeshRefineJob(handle, args) {
42819
43293
  const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
42820
43294
  let result;
@@ -44362,7 +44836,9 @@ ${tail}` : ""
44362
44836
  const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
44363
44837
  if (!meshId) return { success: false, error: "meshId required" };
44364
44838
  const requestedNodeIds = Array.isArray(args?.nodeIds) ? args.nodeIds.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
44365
- return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
44839
+ const isDryRun = args?.dryRun !== false && args?.execute !== true;
44840
+ if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
44841
+ return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
44366
44842
  }
44367
44843
  case "remove_mesh_node": {
44368
44844
  const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";