@adhdev/daemon-core 0.9.82-rc.260 → 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/commands/router.d.ts +116 -0
- package/dist/index.js +585 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +585 -10
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/cli-adapter.d.ts +38 -0
- package/dist/providers/spec/fsm-driver.d.ts +41 -0
- package/package.json +1 -1
- package/src/commands/router.ts +756 -7
- package/src/providers/spec/adapter.ts +10 -3
- package/src/providers/spec/cli-adapter.ts +80 -0
- package/src/providers/spec/fsm-driver.ts +60 -3
package/dist/index.js
CHANGED
|
@@ -28744,8 +28744,12 @@ var TerminalAdapter = class {
|
|
|
28744
28744
|
this.rows = opts.rows ?? import_session_host_core6.DEFAULT_SESSION_HOST_ROWS;
|
|
28745
28745
|
this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
|
|
28746
28746
|
this.tickIntervalMs = opts.tickIntervalMs ?? 0;
|
|
28747
|
-
|
|
28748
|
-
|
|
28747
|
+
if (opts.transportFactory) {
|
|
28748
|
+
this.factory = opts.transportFactory;
|
|
28749
|
+
} else {
|
|
28750
|
+
const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS(pty_transport_exports));
|
|
28751
|
+
this.factory = new NodePtyTransportFactory2();
|
|
28752
|
+
}
|
|
28749
28753
|
this.screen = new TerminalScreen(this.rows, this.cols);
|
|
28750
28754
|
}
|
|
28751
28755
|
rows;
|
|
@@ -28902,6 +28906,10 @@ var FsmDriver = class {
|
|
|
28902
28906
|
specWatcher = null;
|
|
28903
28907
|
/** Last full FSM evaluation, kept for the debugger. */
|
|
28904
28908
|
lastFsmEval = null;
|
|
28909
|
+
/** Ring buffer (max 20) of the full FSM evaluation captured at each
|
|
28910
|
+
* transition — the rich pre-transition table that lastFsmEval only keeps
|
|
28911
|
+
* for the single most recent evaluation. Separate from stateHistory. */
|
|
28912
|
+
fsmSnapshotHistory = [];
|
|
28905
28913
|
subscribe(listener) {
|
|
28906
28914
|
this.listeners.add(listener);
|
|
28907
28915
|
return () => {
|
|
@@ -28992,6 +29000,9 @@ var FsmDriver = class {
|
|
|
28992
29000
|
getStateHistory() {
|
|
28993
29001
|
return this.stateHistory;
|
|
28994
29002
|
}
|
|
29003
|
+
getFsmSnapshotHistory() {
|
|
29004
|
+
return this.fsmSnapshotHistory;
|
|
29005
|
+
}
|
|
28995
29006
|
getSections() {
|
|
28996
29007
|
try {
|
|
28997
29008
|
const screen = this.adapter.snapshot();
|
|
@@ -29087,7 +29098,7 @@ var FsmDriver = class {
|
|
|
29087
29098
|
this.lastFsmEval = ev;
|
|
29088
29099
|
this.prevScreenLines = currentLines;
|
|
29089
29100
|
if (ev.fired) {
|
|
29090
|
-
this.commitTransition(ev.fired, now);
|
|
29101
|
+
this.commitTransition(ev.fired, now, ev);
|
|
29091
29102
|
this.emitStateChanged(forceEmit);
|
|
29092
29103
|
this.scheduleWakeForState();
|
|
29093
29104
|
return;
|
|
@@ -29096,8 +29107,9 @@ var FsmDriver = class {
|
|
|
29096
29107
|
this.scheduleWakeForState();
|
|
29097
29108
|
this.maybeMarkReady();
|
|
29098
29109
|
}
|
|
29099
|
-
commitTransition(fired, now) {
|
|
29110
|
+
commitTransition(fired, now, ev) {
|
|
29100
29111
|
const from = this.currentStateId;
|
|
29112
|
+
this.pushFsmSnapshot(from, fired, now, ev);
|
|
29101
29113
|
this.currentStateId = fired.to;
|
|
29102
29114
|
this.stateEnteredAt = now;
|
|
29103
29115
|
this.regionLastChangedAt.clear();
|
|
@@ -29108,6 +29120,22 @@ var FsmDriver = class {
|
|
|
29108
29120
|
});
|
|
29109
29121
|
LOG.info("FsmDriver", `[${this.specTag()}] ${from} \u2192 ${fired.to} (${fired.label})`);
|
|
29110
29122
|
}
|
|
29123
|
+
/** Snapshot the full FSM evaluation that produced a transition into the
|
|
29124
|
+
* separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
|
|
29125
|
+
* table is captured by reference — it is freshly built per evaluation in
|
|
29126
|
+
* evaluateFsm and never mutated after, so no clone is needed. */
|
|
29127
|
+
pushFsmSnapshot(from, fired, now, ev) {
|
|
29128
|
+
this.fsmSnapshotHistory.push({
|
|
29129
|
+
stateFrom: from,
|
|
29130
|
+
stateTo: fired.to,
|
|
29131
|
+
at: now,
|
|
29132
|
+
firedTo: fired.to,
|
|
29133
|
+
firedLabel: fired.label,
|
|
29134
|
+
reason: summarizeTransition(fired),
|
|
29135
|
+
transitions: ev.transitions
|
|
29136
|
+
});
|
|
29137
|
+
if (this.fsmSnapshotHistory.length > 20) this.fsmSnapshotHistory.shift();
|
|
29138
|
+
}
|
|
29111
29139
|
/** Re-derive the visible modal + controls for the current state and emit a
|
|
29112
29140
|
* state_changed if anything differs from the last emit. */
|
|
29113
29141
|
emitStateChanged(forceEmit) {
|
|
@@ -30140,7 +30168,7 @@ init_logger();
|
|
|
30140
30168
|
function stripAnsi3(text) {
|
|
30141
30169
|
return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
30142
30170
|
}
|
|
30143
|
-
var SpecCliAdapter = class {
|
|
30171
|
+
var SpecCliAdapter = class _SpecCliAdapter {
|
|
30144
30172
|
cliType;
|
|
30145
30173
|
cliName;
|
|
30146
30174
|
workingDir;
|
|
@@ -30165,6 +30193,23 @@ var SpecCliAdapter = class {
|
|
|
30165
30193
|
activeInteractivePrompt = null;
|
|
30166
30194
|
interactivePromptTransport = null;
|
|
30167
30195
|
claudeTuiPromptCaptureInFlight = false;
|
|
30196
|
+
/**
|
|
30197
|
+
* Wall clock of the first frame on which a held interactive prompt was
|
|
30198
|
+
* observed to have left the screen. Mirrors the approval FSM's
|
|
30199
|
+
* `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
|
|
30200
|
+
* repaints the choice picker as several PTY chunks, so a single frame
|
|
30201
|
+
* with no "Enter to select" footer is not proof the prompt is gone — it
|
|
30202
|
+
* may just be mid-repaint. We only clear the held prompt once it has
|
|
30203
|
+
* been absent across a short grace window. Reset to null the moment the
|
|
30204
|
+
* prompt footer reappears.
|
|
30205
|
+
*
|
|
30206
|
+
* Without this, a choice prompt resolved *directly in the terminal* (the
|
|
30207
|
+
* user picked an option without going through ADHDev's
|
|
30208
|
+
* setInteractivePromptResponse) was never cleared from
|
|
30209
|
+
* `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
|
|
30210
|
+
* forever — the choice-resolve-stuck bug.
|
|
30211
|
+
*/
|
|
30212
|
+
interactivePromptLostAt = null;
|
|
30168
30213
|
jsonLineTail = "";
|
|
30169
30214
|
exited = false;
|
|
30170
30215
|
spawned = false;
|
|
@@ -30425,6 +30470,11 @@ var SpecCliAdapter = class {
|
|
|
30425
30470
|
// transition from the current state with its per-condition match
|
|
30426
30471
|
// result + countdown — the canonical "why isn't it moving" answer.
|
|
30427
30472
|
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
30473
|
+
// v4 FSM transition snapshot history (null for v3 specs). The full
|
|
30474
|
+
// pre-transition evaluation table captured at each transition —
|
|
30475
|
+
// answers "why did this rule fire" after the fact, unlike the live
|
|
30476
|
+
// `fsm` field which only reflects the current instant.
|
|
30477
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
30428
30478
|
// Extended fields
|
|
30429
30479
|
name: this.cliName,
|
|
30430
30480
|
status: this.getStatus().status,
|
|
@@ -30462,11 +30512,13 @@ var SpecCliAdapter = class {
|
|
|
30462
30512
|
if (ev.state.title) {
|
|
30463
30513
|
LOG.debug("SpecAdapter", `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
|
|
30464
30514
|
}
|
|
30515
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
30465
30516
|
this.maybeCaptureClaudeTuiPrompt();
|
|
30466
30517
|
this.statusCallback?.();
|
|
30467
30518
|
return;
|
|
30468
30519
|
case "pty_data":
|
|
30469
30520
|
this.detectInteractivePromptFromPtyChunk(ev.chunk);
|
|
30521
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
30470
30522
|
this.maybeCaptureClaudeTuiPrompt();
|
|
30471
30523
|
try {
|
|
30472
30524
|
this.ptyDataCallback?.(ev.chunk);
|
|
@@ -30499,6 +30551,7 @@ var SpecCliAdapter = class {
|
|
|
30499
30551
|
if (!prompt) continue;
|
|
30500
30552
|
this.activeInteractivePrompt = prompt;
|
|
30501
30553
|
this.interactivePromptTransport = "stream-json";
|
|
30554
|
+
this.interactivePromptLostAt = null;
|
|
30502
30555
|
this.statusCallback?.();
|
|
30503
30556
|
} catch {
|
|
30504
30557
|
}
|
|
@@ -30552,6 +30605,47 @@ var SpecCliAdapter = class {
|
|
|
30552
30605
|
}
|
|
30553
30606
|
return messages;
|
|
30554
30607
|
}
|
|
30608
|
+
/**
|
|
30609
|
+
* Grace window a held interactive prompt must be absent from the screen
|
|
30610
|
+
* before we treat it as resolved-in-terminal and clear it. claude-cli
|
|
30611
|
+
* repaints the picker across multiple PTY chunks, so a single
|
|
30612
|
+
* footer-less frame is not proof the prompt is gone. Sized in the same
|
|
30613
|
+
* spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
|
|
30614
|
+
*/
|
|
30615
|
+
static INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
|
|
30616
|
+
/**
|
|
30617
|
+
* Clear a held interactive prompt once the user has resolved it directly
|
|
30618
|
+
* in the terminal (the choice picker leaves the screen without going
|
|
30619
|
+
* through setInteractivePromptResponse). The approval path already does
|
|
30620
|
+
* this via the FSM's modal-lost hysteresis; the interactive-prompt path
|
|
30621
|
+
* had no equivalent, so a terminal-side answer left activeInteractivePrompt
|
|
30622
|
+
* set and getStatus() re-emitted the same choice modal forever.
|
|
30623
|
+
*
|
|
30624
|
+
* Detection mirrors capture: the claude TUI picker is on-screen exactly
|
|
30625
|
+
* while its "Enter to select" footer is rendered. When the footer is gone
|
|
30626
|
+
* for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
|
|
30627
|
+
*/
|
|
30628
|
+
maybeClearResolvedClaudeTuiPrompt() {
|
|
30629
|
+
if (this.cliType !== "claude-cli" || !this.activeInteractivePrompt) return;
|
|
30630
|
+
let screenText = "";
|
|
30631
|
+
try {
|
|
30632
|
+
screenText = this.driver.snapshot();
|
|
30633
|
+
} catch {
|
|
30634
|
+
return;
|
|
30635
|
+
}
|
|
30636
|
+
const stillOnScreen = screenText.includes("Enter to select");
|
|
30637
|
+
if (stillOnScreen) {
|
|
30638
|
+
this.interactivePromptLostAt = null;
|
|
30639
|
+
return;
|
|
30640
|
+
}
|
|
30641
|
+
const lostAt = this.interactivePromptLostAt ?? Date.now();
|
|
30642
|
+
if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
|
|
30643
|
+
if (Date.now() - lostAt < _SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
|
|
30644
|
+
this.activeInteractivePrompt = null;
|
|
30645
|
+
this.interactivePromptTransport = null;
|
|
30646
|
+
this.interactivePromptLostAt = null;
|
|
30647
|
+
this.statusCallback?.();
|
|
30648
|
+
}
|
|
30555
30649
|
maybeCaptureClaudeTuiPrompt() {
|
|
30556
30650
|
if (this.cliType !== "claude-cli" || this.activeInteractivePrompt || this.claudeTuiPromptCaptureInFlight) return;
|
|
30557
30651
|
const screenText = this.driver.snapshot();
|
|
@@ -30565,6 +30659,7 @@ var SpecCliAdapter = class {
|
|
|
30565
30659
|
if (!prompt) return;
|
|
30566
30660
|
this.activeInteractivePrompt = prompt;
|
|
30567
30661
|
this.interactivePromptTransport = "tui";
|
|
30662
|
+
this.interactivePromptLostAt = null;
|
|
30568
30663
|
this.statusCallback?.();
|
|
30569
30664
|
return;
|
|
30570
30665
|
}
|
|
@@ -30601,6 +30696,7 @@ var SpecCliAdapter = class {
|
|
|
30601
30696
|
if (!prompt) return;
|
|
30602
30697
|
this.activeInteractivePrompt = prompt;
|
|
30603
30698
|
this.interactivePromptTransport = "tui";
|
|
30699
|
+
this.interactivePromptLostAt = null;
|
|
30604
30700
|
this.statusCallback?.();
|
|
30605
30701
|
}
|
|
30606
30702
|
getDebugState() {
|
|
@@ -30655,6 +30751,9 @@ var SpecCliAdapter = class {
|
|
|
30655
30751
|
// and countdown. This is the canonical "why isn't it transitioning"
|
|
30656
30752
|
// answer — no screenshots needed.
|
|
30657
30753
|
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
30754
|
+
// v4 FSM transition snapshot history — the captured pre-transition
|
|
30755
|
+
// evaluation table at each transition (null for v3 specs).
|
|
30756
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
30658
30757
|
messages,
|
|
30659
30758
|
committedMessages: messages
|
|
30660
30759
|
};
|
|
@@ -40650,8 +40749,37 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
|
|
|
40650
40749
|
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
40651
40750
|
});
|
|
40652
40751
|
const mergeBase = git(["merge-base", baseHead, branchHead]).trim();
|
|
40653
|
-
|
|
40654
|
-
|
|
40752
|
+
let mergedTree = "";
|
|
40753
|
+
let mergeTreeStdout = "";
|
|
40754
|
+
let gitlinkTrivialFastForward;
|
|
40755
|
+
try {
|
|
40756
|
+
mergeTreeStdout = git(["merge-tree", "--write-tree", baseHead, branchHead]);
|
|
40757
|
+
mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || "";
|
|
40758
|
+
} catch (mergeTreeErr) {
|
|
40759
|
+
const output = `${mergeTreeErr?.message || ""}
|
|
40760
|
+
${mergeTreeErr?.stdout || ""}
|
|
40761
|
+
${mergeTreeErr?.stderr || ""}`;
|
|
40762
|
+
const isSubmoduleConflict = /(submodule|160000)/i.test(output) || /Recursive merging with submodules/i.test(output);
|
|
40763
|
+
if (!isSubmoduleConflict) throw mergeTreeErr;
|
|
40764
|
+
const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead);
|
|
40765
|
+
if (!evaluation.trivial) {
|
|
40766
|
+
return {
|
|
40767
|
+
status: "failed",
|
|
40768
|
+
equivalent: false,
|
|
40769
|
+
baseHead,
|
|
40770
|
+
branchHead,
|
|
40771
|
+
mergeBase: mergeBase || void 0,
|
|
40772
|
+
durationMs: Date.now() - startedAt,
|
|
40773
|
+
error: mergeTreeErr?.message || String(mergeTreeErr),
|
|
40774
|
+
stdout: truncateValidationOutput(mergeTreeErr?.stdout),
|
|
40775
|
+
stderr: truncateValidationOutput(mergeTreeErr?.stderr),
|
|
40776
|
+
gitlinkTrivialFastForward: { resolved: false, gitlinks: evaluation.gitlinks, reason: evaluation.reason },
|
|
40777
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output)
|
|
40778
|
+
};
|
|
40779
|
+
}
|
|
40780
|
+
mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, evaluation.gitlinks) || "";
|
|
40781
|
+
gitlinkTrivialFastForward = { resolved: true, gitlinks: evaluation.gitlinks };
|
|
40782
|
+
}
|
|
40655
40783
|
if (!mergeBase || !mergedTree) {
|
|
40656
40784
|
return {
|
|
40657
40785
|
status: "failed",
|
|
@@ -40662,7 +40790,8 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
|
|
|
40662
40790
|
mergedTree: mergedTree || void 0,
|
|
40663
40791
|
durationMs: Date.now() - startedAt,
|
|
40664
40792
|
error: "patch equivalence preflight could not resolve merge-base or synthetic merge tree",
|
|
40665
|
-
stdout: truncateValidationOutput(mergeTreeStdout)
|
|
40793
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
40794
|
+
gitlinkTrivialFastForward
|
|
40666
40795
|
};
|
|
40667
40796
|
}
|
|
40668
40797
|
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
@@ -40677,7 +40806,8 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
|
|
|
40677
40806
|
mergedTree,
|
|
40678
40807
|
expectedPatchId,
|
|
40679
40808
|
actualPatchId,
|
|
40680
|
-
durationMs: Date.now() - startedAt
|
|
40809
|
+
durationMs: Date.now() - startedAt,
|
|
40810
|
+
gitlinkTrivialFastForward
|
|
40681
40811
|
};
|
|
40682
40812
|
} catch (e) {
|
|
40683
40813
|
return {
|
|
@@ -40700,6 +40830,65 @@ ${e?.stderr || ""}`
|
|
|
40700
40830
|
};
|
|
40701
40831
|
}
|
|
40702
40832
|
}
|
|
40833
|
+
async function runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead) {
|
|
40834
|
+
const startedAt = Date.now();
|
|
40835
|
+
try {
|
|
40836
|
+
const { execFileSync: execFileSync6 } = await import("child_process");
|
|
40837
|
+
const git = (args, opts) => execFileSync6("git", args, {
|
|
40838
|
+
cwd: opts?.cwd || repoRoot,
|
|
40839
|
+
encoding: "utf8",
|
|
40840
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
40841
|
+
});
|
|
40842
|
+
const rawDiff = git(["diff", "--raw", baseHead, branchHead]).trim();
|
|
40843
|
+
if (rawDiff) {
|
|
40844
|
+
const changedPaths = rawDiff.split("\n").map((line) => line.split(" ").slice(1).join(" ").trim()).filter(Boolean).slice(0, 50);
|
|
40845
|
+
return {
|
|
40846
|
+
status: "passed",
|
|
40847
|
+
hasEffectiveDiff: true,
|
|
40848
|
+
baseHead,
|
|
40849
|
+
branchHead,
|
|
40850
|
+
changedPaths,
|
|
40851
|
+
durationMs: Date.now() - startedAt
|
|
40852
|
+
};
|
|
40853
|
+
}
|
|
40854
|
+
const submoduleHints = [];
|
|
40855
|
+
try {
|
|
40856
|
+
const status = git(["submodule", "status"]);
|
|
40857
|
+
for (const line of status.split("\n")) {
|
|
40858
|
+
const trimmed = line.trimEnd();
|
|
40859
|
+
if (!trimmed) continue;
|
|
40860
|
+
if (trimmed.startsWith("+")) {
|
|
40861
|
+
const parts = trimmed.slice(1).trim().split(/\s+/);
|
|
40862
|
+
const path39 = parts[1] || parts[0] || "(unknown)";
|
|
40863
|
+
submoduleHints.push({
|
|
40864
|
+
path: path39,
|
|
40865
|
+
reason: "submodule checked-out commit differs from the committed gitlink (pointer bump not committed on the root branch)"
|
|
40866
|
+
});
|
|
40867
|
+
}
|
|
40868
|
+
}
|
|
40869
|
+
} catch {
|
|
40870
|
+
}
|
|
40871
|
+
return {
|
|
40872
|
+
status: "failed",
|
|
40873
|
+
hasEffectiveDiff: false,
|
|
40874
|
+
baseHead,
|
|
40875
|
+
branchHead,
|
|
40876
|
+
...submoduleHints.length ? { submoduleHints } : {},
|
|
40877
|
+
durationMs: Date.now() - startedAt
|
|
40878
|
+
};
|
|
40879
|
+
} catch (e) {
|
|
40880
|
+
return {
|
|
40881
|
+
status: "skipped",
|
|
40882
|
+
hasEffectiveDiff: true,
|
|
40883
|
+
baseHead,
|
|
40884
|
+
branchHead,
|
|
40885
|
+
durationMs: Date.now() - startedAt,
|
|
40886
|
+
error: e?.message || String(e),
|
|
40887
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
40888
|
+
stderr: truncateValidationOutput(e?.stderr)
|
|
40889
|
+
};
|
|
40890
|
+
}
|
|
40891
|
+
}
|
|
40703
40892
|
function buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output) {
|
|
40704
40893
|
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return void 0;
|
|
40705
40894
|
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => ({
|
|
@@ -40756,6 +40945,135 @@ function readTreeObject(repoRoot, ref, path39) {
|
|
|
40756
40945
|
return void 0;
|
|
40757
40946
|
}
|
|
40758
40947
|
}
|
|
40948
|
+
function resolveGitDir(repoRoot) {
|
|
40949
|
+
const out = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "--absolute-git-dir"], {
|
|
40950
|
+
cwd: repoRoot,
|
|
40951
|
+
encoding: "utf8",
|
|
40952
|
+
maxBuffer: 1024 * 1024
|
|
40953
|
+
}).trim();
|
|
40954
|
+
return out;
|
|
40955
|
+
}
|
|
40956
|
+
function isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit) {
|
|
40957
|
+
if (!baseCommit || !branchCommit) return false;
|
|
40958
|
+
if (baseCommit === branchCommit) return true;
|
|
40959
|
+
try {
|
|
40960
|
+
if (!fs23.existsSync(submoduleRepoPath)) return false;
|
|
40961
|
+
(0, import_node_child_process6.execFileSync)("git", ["cat-file", "-e", `${baseCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
|
|
40962
|
+
(0, import_node_child_process6.execFileSync)("git", ["cat-file", "-e", `${branchCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
|
|
40963
|
+
(0, import_node_child_process6.execFileSync)("git", ["merge-base", "--is-ancestor", baseCommit, branchCommit], { cwd: submoduleRepoPath, stdio: "ignore" });
|
|
40964
|
+
return true;
|
|
40965
|
+
} catch {
|
|
40966
|
+
return false;
|
|
40967
|
+
}
|
|
40968
|
+
}
|
|
40969
|
+
function readChangedPathKinds(repoRoot, fromRef, toRef) {
|
|
40970
|
+
try {
|
|
40971
|
+
const output = (0, import_node_child_process6.execFileSync)("git", ["diff", "--raw", "--no-abbrev", fromRef, toRef], {
|
|
40972
|
+
cwd: repoRoot,
|
|
40973
|
+
encoding: "utf8",
|
|
40974
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
40975
|
+
});
|
|
40976
|
+
const result = [];
|
|
40977
|
+
const seen = /* @__PURE__ */ new Set();
|
|
40978
|
+
for (const line of output.split("\n")) {
|
|
40979
|
+
if (!line.trim()) continue;
|
|
40980
|
+
const metaAndPath = line.split(" ");
|
|
40981
|
+
const meta = metaAndPath[0] || "";
|
|
40982
|
+
const path39 = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
40983
|
+
if (!path39 || seen.has(path39)) continue;
|
|
40984
|
+
seen.add(path39);
|
|
40985
|
+
const parts = meta.split(/\s+/);
|
|
40986
|
+
const isGitlink = !!(parts[0]?.includes("160000") || parts[1]?.includes("160000"));
|
|
40987
|
+
result.push({ path: path39, isGitlink });
|
|
40988
|
+
}
|
|
40989
|
+
return result;
|
|
40990
|
+
} catch {
|
|
40991
|
+
return [];
|
|
40992
|
+
}
|
|
40993
|
+
}
|
|
40994
|
+
function evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead) {
|
|
40995
|
+
const changedGitlinks = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => {
|
|
40996
|
+
const baseCommit = readTreeObject(repoRoot, baseHead, path39);
|
|
40997
|
+
const branchCommit = readTreeObject(repoRoot, branchHead, path39);
|
|
40998
|
+
const submoduleRepoPath = (0, import_path10.resolve)(repoRoot, path39);
|
|
40999
|
+
const fastForward = !!baseCommit && !!branchCommit && isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit);
|
|
41000
|
+
return { path: path39, baseCommit, branchCommit, fastForward };
|
|
41001
|
+
});
|
|
41002
|
+
if (changedGitlinks.length === 0) {
|
|
41003
|
+
return { trivial: false, reason: "no_changed_gitlinks", gitlinks: changedGitlinks };
|
|
41004
|
+
}
|
|
41005
|
+
const nonFastForward = changedGitlinks.filter((entry) => !entry.fastForward);
|
|
41006
|
+
if (nonFastForward.length > 0) {
|
|
41007
|
+
return {
|
|
41008
|
+
trivial: false,
|
|
41009
|
+
reason: `diverged_gitlinks:${nonFastForward.map((entry) => entry.path).join(",")}`,
|
|
41010
|
+
gitlinks: changedGitlinks
|
|
41011
|
+
};
|
|
41012
|
+
}
|
|
41013
|
+
let mergeBase = "";
|
|
41014
|
+
try {
|
|
41015
|
+
mergeBase = (0, import_node_child_process6.execFileSync)("git", ["merge-base", baseHead, branchHead], {
|
|
41016
|
+
cwd: repoRoot,
|
|
41017
|
+
encoding: "utf8",
|
|
41018
|
+
maxBuffer: 1024 * 1024
|
|
41019
|
+
}).trim();
|
|
41020
|
+
} catch {
|
|
41021
|
+
return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
|
|
41022
|
+
}
|
|
41023
|
+
if (!mergeBase) {
|
|
41024
|
+
return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
|
|
41025
|
+
}
|
|
41026
|
+
const baseSideChanges = readChangedPathKinds(repoRoot, mergeBase, baseHead);
|
|
41027
|
+
const branchSideChanges = readChangedPathKinds(repoRoot, mergeBase, branchHead);
|
|
41028
|
+
const baseChangedPaths = new Map(baseSideChanges.map((entry) => [entry.path, entry]));
|
|
41029
|
+
const overlapping = branchSideChanges.filter((entry) => baseChangedPaths.has(entry.path));
|
|
41030
|
+
const nonGitlinkOverlap = overlapping.filter((entry) => {
|
|
41031
|
+
const baseEntry = baseChangedPaths.get(entry.path);
|
|
41032
|
+
return !(entry.isGitlink && baseEntry?.isGitlink);
|
|
41033
|
+
});
|
|
41034
|
+
if (nonGitlinkOverlap.length > 0) {
|
|
41035
|
+
return {
|
|
41036
|
+
trivial: false,
|
|
41037
|
+
reason: `non_gitlink_overlap:${nonGitlinkOverlap.map((entry) => entry.path).join(",")}`,
|
|
41038
|
+
gitlinks: changedGitlinks
|
|
41039
|
+
};
|
|
41040
|
+
}
|
|
41041
|
+
return { trivial: true, gitlinks: changedGitlinks };
|
|
41042
|
+
}
|
|
41043
|
+
function synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, gitlinks) {
|
|
41044
|
+
try {
|
|
41045
|
+
const baseTree = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", `${baseHead}^{tree}`], {
|
|
41046
|
+
cwd: repoRoot,
|
|
41047
|
+
encoding: "utf8",
|
|
41048
|
+
maxBuffer: 1024 * 1024
|
|
41049
|
+
}).trim();
|
|
41050
|
+
if (!baseTree) return void 0;
|
|
41051
|
+
const updates = gitlinks.filter((entry) => entry.branchCommit).map((entry) => `160000 commit ${entry.branchCommit} ${entry.path}`).join("\n");
|
|
41052
|
+
if (!updates) return baseTree;
|
|
41053
|
+
const tmpIndex = (0, import_path10.join)(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
|
|
41054
|
+
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
41055
|
+
try {
|
|
41056
|
+
(0, import_node_child_process6.execFileSync)("git", ["read-tree", baseTree], { cwd: repoRoot, env, stdio: "ignore" });
|
|
41057
|
+
(0, import_node_child_process6.execFileSync)("git", ["update-index", "--index-info"], {
|
|
41058
|
+
cwd: repoRoot,
|
|
41059
|
+
env,
|
|
41060
|
+
input: `${updates}
|
|
41061
|
+
`,
|
|
41062
|
+
encoding: "utf8",
|
|
41063
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
41064
|
+
});
|
|
41065
|
+
const newTree = (0, import_node_child_process6.execFileSync)("git", ["write-tree"], { cwd: repoRoot, env, encoding: "utf8" }).trim();
|
|
41066
|
+
return newTree || void 0;
|
|
41067
|
+
} finally {
|
|
41068
|
+
try {
|
|
41069
|
+
fs23.rmSync(tmpIndex, { force: true });
|
|
41070
|
+
} catch {
|
|
41071
|
+
}
|
|
41072
|
+
}
|
|
41073
|
+
} catch {
|
|
41074
|
+
return void 0;
|
|
41075
|
+
}
|
|
41076
|
+
}
|
|
40759
41077
|
async function alignRefinerySubmodulesAfterMerge(repoRoot, previousBaseHead, currentHead, options = {}) {
|
|
40760
41078
|
const startedAt = Date.now();
|
|
40761
41079
|
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead).filter((path39) => !(options.submoduleIgnorePaths || []).includes(path39));
|
|
@@ -41423,6 +41741,10 @@ var DaemonCommandRouter = class {
|
|
|
41423
41741
|
runningRefineJobs = /* @__PURE__ */ new Map();
|
|
41424
41742
|
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
41425
41743
|
terminalRefineJobs = /* @__PURE__ */ new Map();
|
|
41744
|
+
/** In-memory async batch Refinery jobs keyed by meshId (one batch convergence per mesh at a time). */
|
|
41745
|
+
runningRefineBatchJobs = /* @__PURE__ */ new Map();
|
|
41746
|
+
/** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
|
|
41747
|
+
terminalRefineBatchJobs = /* @__PURE__ */ new Map();
|
|
41426
41748
|
constructor(deps) {
|
|
41427
41749
|
this.deps = deps;
|
|
41428
41750
|
}
|
|
@@ -42652,6 +42974,48 @@ ${tail}` : ""
|
|
|
42652
42974
|
}
|
|
42653
42975
|
};
|
|
42654
42976
|
}
|
|
42977
|
+
const effectiveDiffStarted = Date.now();
|
|
42978
|
+
const effectiveDiff = await runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead);
|
|
42979
|
+
recordMeshRefineStage(refineStages, "effective_diff", effectiveDiff.status, effectiveDiffStarted, {
|
|
42980
|
+
hasEffectiveDiff: effectiveDiff.hasEffectiveDiff,
|
|
42981
|
+
changedPaths: effectiveDiff.changedPaths,
|
|
42982
|
+
submoduleHints: effectiveDiff.submoduleHints,
|
|
42983
|
+
...effectiveDiff.error ? { error: effectiveDiff.error } : {}
|
|
42984
|
+
});
|
|
42985
|
+
if (effectiveDiff.status === "failed" && !effectiveDiff.hasEffectiveDiff) {
|
|
42986
|
+
const hintLines = (effectiveDiff.submoduleHints || []).map((h) => ` - ${h.path}: ${h.reason}`);
|
|
42987
|
+
const message = [
|
|
42988
|
+
`Refinery no-op guard: branch '${branch}' has no effective root-tree diff against '${baseBranch}' (${baseHead.slice(0, 12)}); nothing would merge.`,
|
|
42989
|
+
"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.",
|
|
42990
|
+
hintLines.length ? `Submodules with uncommitted pointer bumps:
|
|
42991
|
+
${hintLines.join("\n")}` : "",
|
|
42992
|
+
`Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`
|
|
42993
|
+
].filter(Boolean).join("\n");
|
|
42994
|
+
return {
|
|
42995
|
+
success: false,
|
|
42996
|
+
code: "no_effective_diff",
|
|
42997
|
+
convergenceStatus: "blocked_review",
|
|
42998
|
+
error: message,
|
|
42999
|
+
branch,
|
|
43000
|
+
into: baseBranch,
|
|
43001
|
+
validationSummary,
|
|
43002
|
+
patchEquivalence,
|
|
43003
|
+
effectiveDiff,
|
|
43004
|
+
refineStages,
|
|
43005
|
+
finalBranchConvergenceState: {
|
|
43006
|
+
branch,
|
|
43007
|
+
baseBranch,
|
|
43008
|
+
merged: false,
|
|
43009
|
+
removed: false,
|
|
43010
|
+
validation: "passed",
|
|
43011
|
+
patchEquivalence: "passed",
|
|
43012
|
+
effectiveDiff: "no_effective_diff",
|
|
43013
|
+
status: "blocked_review",
|
|
43014
|
+
reason: "no_effective_diff",
|
|
43015
|
+
...effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}
|
|
43016
|
+
}
|
|
43017
|
+
};
|
|
43018
|
+
}
|
|
42655
43019
|
let mergeResult;
|
|
42656
43020
|
const mergeStarted = Date.now();
|
|
42657
43021
|
try {
|
|
@@ -42993,6 +43357,17 @@ ${tail}` : ""
|
|
|
42993
43357
|
note: "Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order."
|
|
42994
43358
|
};
|
|
42995
43359
|
}
|
|
43360
|
+
return this.runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args);
|
|
43361
|
+
}
|
|
43362
|
+
/**
|
|
43363
|
+
* Convergence core shared by the synchronous batch entry and the async batch job.
|
|
43364
|
+
* Refines each node in order: the per-node refine pipeline fetches origin/<base>
|
|
43365
|
+
* fresh, so each merged sibling advances the base before the next node's auto-rebase
|
|
43366
|
+
* + patch-equivalence re-check. A blocked/failed node is isolated; the batch
|
|
43367
|
+
* continues with the remaining nodes. Does NOT touch the per-node merge logic — it
|
|
43368
|
+
* only sequences calls to executeMeshRefineNodeSynchronously and aggregates outcomes.
|
|
43369
|
+
*/
|
|
43370
|
+
async runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args) {
|
|
42996
43371
|
const results = [];
|
|
42997
43372
|
for (const node of orderedNodes) {
|
|
42998
43373
|
let result;
|
|
@@ -43047,6 +43422,204 @@ ${tail}` : ""
|
|
|
43047
43422
|
}
|
|
43048
43423
|
};
|
|
43049
43424
|
}
|
|
43425
|
+
buildRefineBatchJobKey(meshId) {
|
|
43426
|
+
return `${meshId}::batch`;
|
|
43427
|
+
}
|
|
43428
|
+
buildRefineBatchJobHandle(args) {
|
|
43429
|
+
return {
|
|
43430
|
+
success: true,
|
|
43431
|
+
async: true,
|
|
43432
|
+
batch: true,
|
|
43433
|
+
status: args.status || "accepted",
|
|
43434
|
+
jobId: args.jobId || `refine_batch_${createInteractionId()}`,
|
|
43435
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
43436
|
+
meshId: args.meshId,
|
|
43437
|
+
batchLabel: `batch:${args.nodeIds.length} node${args.nodeIds.length === 1 ? "" : "s"}`,
|
|
43438
|
+
nodeIds: args.nodeIds,
|
|
43439
|
+
nodeCount: args.nodeIds.length,
|
|
43440
|
+
order: args.order,
|
|
43441
|
+
startedAt: args.startedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
43442
|
+
...args.completedAt ? { completedAt: args.completedAt } : {},
|
|
43443
|
+
...args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {},
|
|
43444
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
43445
|
+
evidence: {
|
|
43446
|
+
pendingEventsCommand: "get_pending_mesh_events",
|
|
43447
|
+
ledgerCommand: "get_mesh_ledger_slice",
|
|
43448
|
+
taskHistoryKind: args.status === "completed" ? "task_completed" : args.status === "failed" ? "task_failed" : "task_dispatched"
|
|
43449
|
+
}
|
|
43450
|
+
};
|
|
43451
|
+
}
|
|
43452
|
+
/**
|
|
43453
|
+
* Emit a batch Refinery terminal/accepted event through the SAME pending-event +
|
|
43454
|
+
* forward mechanism single-node refine uses (queueRefineJobEvent), so the
|
|
43455
|
+
* coordinator's existing refine:accepted/completed/failed handling and message
|
|
43456
|
+
* renderer apply unchanged. The aggregate per-node results ride along in `result`.
|
|
43457
|
+
*/
|
|
43458
|
+
queueRefineBatchJobEvent(event, handle, result) {
|
|
43459
|
+
const metadataEvent = {
|
|
43460
|
+
source: "refine_mesh_node_async_job",
|
|
43461
|
+
batch: true,
|
|
43462
|
+
jobId: handle.jobId,
|
|
43463
|
+
interactionId: handle.interactionId,
|
|
43464
|
+
meshId: handle.meshId,
|
|
43465
|
+
nodeId: handle.batchLabel,
|
|
43466
|
+
nodeIds: handle.nodeIds,
|
|
43467
|
+
workspace: void 0,
|
|
43468
|
+
status: handle.status,
|
|
43469
|
+
startedAt: handle.startedAt,
|
|
43470
|
+
completedAt: handle.completedAt,
|
|
43471
|
+
order: handle.order,
|
|
43472
|
+
...result ? { result } : {}
|
|
43473
|
+
};
|
|
43474
|
+
const eventPayload = {
|
|
43475
|
+
event,
|
|
43476
|
+
meshId: handle.meshId,
|
|
43477
|
+
nodeLabel: handle.batchLabel,
|
|
43478
|
+
nodeId: handle.batchLabel,
|
|
43479
|
+
metadataEvent,
|
|
43480
|
+
queuedAt: Date.now(),
|
|
43481
|
+
...handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}
|
|
43482
|
+
};
|
|
43483
|
+
if (typeof this.deps.instanceManager?.getByCategory === "function") {
|
|
43484
|
+
const forwarded = handleMeshForwardEvent(
|
|
43485
|
+
{ instanceManager: this.deps.instanceManager },
|
|
43486
|
+
{
|
|
43487
|
+
event,
|
|
43488
|
+
meshId: handle.meshId,
|
|
43489
|
+
nodeId: handle.batchLabel,
|
|
43490
|
+
jobId: handle.jobId,
|
|
43491
|
+
interactionId: handle.interactionId,
|
|
43492
|
+
status: handle.status,
|
|
43493
|
+
startedAt: handle.startedAt,
|
|
43494
|
+
completedAt: handle.completedAt,
|
|
43495
|
+
...result ? { result } : {}
|
|
43496
|
+
}
|
|
43497
|
+
);
|
|
43498
|
+
if (forwarded?.success === true) return;
|
|
43499
|
+
LOG.warn("Mesh", `[Refinery] Failed to forward async refine batch event ${event}: ${forwarded?.error || "unknown error"}`);
|
|
43500
|
+
}
|
|
43501
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
43502
|
+
}
|
|
43503
|
+
async appendRefineBatchJobLedger(kind, handle, result) {
|
|
43504
|
+
try {
|
|
43505
|
+
const { appendLedgerEntry: appendLedgerEntry2 } = await Promise.resolve().then(() => (init_mesh_ledger(), mesh_ledger_exports));
|
|
43506
|
+
appendLedgerEntry2(handle.meshId, {
|
|
43507
|
+
kind,
|
|
43508
|
+
nodeId: handle.batchLabel,
|
|
43509
|
+
payload: {
|
|
43510
|
+
source: "refine_mesh_node_async_job",
|
|
43511
|
+
refineJob: {
|
|
43512
|
+
batch: true,
|
|
43513
|
+
jobId: handle.jobId,
|
|
43514
|
+
interactionId: handle.interactionId,
|
|
43515
|
+
status: handle.status,
|
|
43516
|
+
meshId: handle.meshId,
|
|
43517
|
+
nodeIds: handle.nodeIds,
|
|
43518
|
+
order: handle.order,
|
|
43519
|
+
targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
43520
|
+
startedAt: handle.startedAt,
|
|
43521
|
+
completedAt: handle.completedAt
|
|
43522
|
+
},
|
|
43523
|
+
async: true,
|
|
43524
|
+
batch: true,
|
|
43525
|
+
...result ? {
|
|
43526
|
+
success: result.success === true,
|
|
43527
|
+
result
|
|
43528
|
+
} : {}
|
|
43529
|
+
}
|
|
43530
|
+
});
|
|
43531
|
+
} catch (e) {
|
|
43532
|
+
LOG.warn("Mesh", `[Refinery] Failed to append async refine batch ledger entry: ${e?.message || e}`);
|
|
43533
|
+
}
|
|
43534
|
+
}
|
|
43535
|
+
async finishMeshRefineBatchJob(handle, orderedNodes, ordering, args) {
|
|
43536
|
+
const key = this.buildRefineBatchJobKey(handle.meshId);
|
|
43537
|
+
let result;
|
|
43538
|
+
try {
|
|
43539
|
+
result = await this.runMeshRefineBatchConvergence(handle.meshId, orderedNodes, ordering, args);
|
|
43540
|
+
} catch (e) {
|
|
43541
|
+
result = { success: false, error: e?.message || String(e), batch: true };
|
|
43542
|
+
}
|
|
43543
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
43544
|
+
const summary = result.summary && typeof result.summary === "object" ? result.summary : void 0;
|
|
43545
|
+
const allConverged = result.allConverged === true;
|
|
43546
|
+
const isTerminalSuccess = result.success === true && allConverged;
|
|
43547
|
+
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.";
|
|
43548
|
+
const normalizedResult = {
|
|
43549
|
+
...result,
|
|
43550
|
+
batch: true,
|
|
43551
|
+
nextStep,
|
|
43552
|
+
...summary ? {
|
|
43553
|
+
convergenceStatus: allConverged ? "all_converged" : "partial"
|
|
43554
|
+
} : {}
|
|
43555
|
+
};
|
|
43556
|
+
const terminalHandle = this.buildRefineBatchJobHandle({
|
|
43557
|
+
meshId: handle.meshId,
|
|
43558
|
+
nodeIds: handle.nodeIds,
|
|
43559
|
+
order: handle.order,
|
|
43560
|
+
status: isTerminalSuccess ? "completed" : "failed",
|
|
43561
|
+
startedAt: handle.startedAt,
|
|
43562
|
+
completedAt,
|
|
43563
|
+
jobId: handle.jobId,
|
|
43564
|
+
interactionId: handle.interactionId,
|
|
43565
|
+
coordinatorDaemonId: handle.targetCoordinatorDaemonId
|
|
43566
|
+
});
|
|
43567
|
+
const terminal = { ...terminalHandle, result: normalizedResult };
|
|
43568
|
+
this.terminalRefineBatchJobs.set(key, terminal);
|
|
43569
|
+
this.runningRefineBatchJobs.delete(key);
|
|
43570
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
43571
|
+
await this.appendRefineBatchJobLedger(isTerminalSuccess ? "task_completed" : "task_failed", terminalHandle, normalizedResult);
|
|
43572
|
+
this.queueRefineBatchJobEvent(isTerminalSuccess ? "refine:completed" : "refine:failed", terminalHandle, normalizedResult);
|
|
43573
|
+
}
|
|
43574
|
+
/**
|
|
43575
|
+
* Async entry for the batch Refinery execute path. Mirrors startMeshRefineJob:
|
|
43576
|
+
* resolves the plan synchronously (so target/ordering errors and the dry-run shape
|
|
43577
|
+
* stay synchronous), then for execute=true registers an in-flight batch job, returns
|
|
43578
|
+
* {async:true, status:'accepted', batch:true, ...plan} immediately, and runs the
|
|
43579
|
+
* convergence loop in the background — emitting the same terminal refine event.
|
|
43580
|
+
* Idempotent: a batch already in flight for this mesh returns the running handle
|
|
43581
|
+
* with duplicate:true rather than spawning a second background job.
|
|
43582
|
+
*/
|
|
43583
|
+
async startMeshRefineBatchJob(meshId, requestedNodeIds, args) {
|
|
43584
|
+
const plan = await this.batchRefineMeshNodes(meshId, requestedNodeIds, { ...args, dryRun: true, execute: false });
|
|
43585
|
+
const planRecord = plan;
|
|
43586
|
+
if (planRecord.success !== true) return plan;
|
|
43587
|
+
if (args?.dryRun === true && args?.execute !== true) return plan;
|
|
43588
|
+
const order = Array.isArray(planRecord.order) ? planRecord.order.filter((v) => typeof v === "string") : [];
|
|
43589
|
+
const nodeIds = order.slice();
|
|
43590
|
+
if (nodeIds.length === 0) {
|
|
43591
|
+
return { ...planRecord, success: true, batch: true, dryRun: false, async: false };
|
|
43592
|
+
}
|
|
43593
|
+
const key = this.buildRefineBatchJobKey(meshId);
|
|
43594
|
+
const running = this.runningRefineBatchJobs.get(key);
|
|
43595
|
+
if (running) return { ...running, duplicate: true };
|
|
43596
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
43597
|
+
const mesh = meshRecord?.mesh;
|
|
43598
|
+
const allNodes = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
|
|
43599
|
+
const orderedNodes = nodeIds.map((id) => allNodes.find((n) => n.id === id || n.nodeId === id)).filter((n) => !!n);
|
|
43600
|
+
if (orderedNodes.length === 0) {
|
|
43601
|
+
return { success: false, error: "Batch nodes no longer resolvable in mesh", batch: true };
|
|
43602
|
+
}
|
|
43603
|
+
const ordering = {
|
|
43604
|
+
order,
|
|
43605
|
+
rationale: planRecord.orderingRationale
|
|
43606
|
+
};
|
|
43607
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === "string" && args.coordinatorDaemonId.trim() ? args.coordinatorDaemonId.trim() : this.deps.statusInstanceId || void 0;
|
|
43608
|
+
const handle = this.buildRefineBatchJobHandle({ meshId, nodeIds, order, coordinatorDaemonId });
|
|
43609
|
+
this.runningRefineBatchJobs.set(key, handle);
|
|
43610
|
+
await this.appendRefineBatchJobLedger("task_dispatched", handle);
|
|
43611
|
+
this.queueRefineBatchJobEvent("refine:accepted", handle);
|
|
43612
|
+
setImmediate(() => {
|
|
43613
|
+
void this.finishMeshRefineBatchJob(handle, orderedNodes, ordering, args);
|
|
43614
|
+
});
|
|
43615
|
+
return {
|
|
43616
|
+
...handle,
|
|
43617
|
+
order,
|
|
43618
|
+
orderingRationale: planRecord.orderingRationale,
|
|
43619
|
+
plan: planRecord.plan,
|
|
43620
|
+
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."
|
|
43621
|
+
};
|
|
43622
|
+
}
|
|
43050
43623
|
async finishMeshRefineJob(handle, args) {
|
|
43051
43624
|
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
43052
43625
|
let result;
|
|
@@ -44594,7 +45167,9 @@ ${tail}` : ""
|
|
|
44594
45167
|
const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
|
|
44595
45168
|
if (!meshId) return { success: false, error: "meshId required" };
|
|
44596
45169
|
const requestedNodeIds = Array.isArray(args?.nodeIds) ? args.nodeIds.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
|
|
44597
|
-
|
|
45170
|
+
const isDryRun = args?.dryRun !== false && args?.execute !== true;
|
|
45171
|
+
if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
|
|
45172
|
+
return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
|
|
44598
45173
|
}
|
|
44599
45174
|
case "remove_mesh_node": {
|
|
44600
45175
|
const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
|