@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.mjs
CHANGED
|
@@ -28408,8 +28408,12 @@ var TerminalAdapter = class {
|
|
|
28408
28408
|
this.rows = opts.rows ?? DEFAULT_SESSION_HOST_ROWS5;
|
|
28409
28409
|
this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
|
|
28410
28410
|
this.tickIntervalMs = opts.tickIntervalMs ?? 0;
|
|
28411
|
-
|
|
28412
|
-
|
|
28411
|
+
if (opts.transportFactory) {
|
|
28412
|
+
this.factory = opts.transportFactory;
|
|
28413
|
+
} else {
|
|
28414
|
+
const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS(pty_transport_exports));
|
|
28415
|
+
this.factory = new NodePtyTransportFactory2();
|
|
28416
|
+
}
|
|
28413
28417
|
this.screen = new TerminalScreen(this.rows, this.cols);
|
|
28414
28418
|
}
|
|
28415
28419
|
rows;
|
|
@@ -28566,6 +28570,10 @@ var FsmDriver = class {
|
|
|
28566
28570
|
specWatcher = null;
|
|
28567
28571
|
/** Last full FSM evaluation, kept for the debugger. */
|
|
28568
28572
|
lastFsmEval = null;
|
|
28573
|
+
/** Ring buffer (max 20) of the full FSM evaluation captured at each
|
|
28574
|
+
* transition — the rich pre-transition table that lastFsmEval only keeps
|
|
28575
|
+
* for the single most recent evaluation. Separate from stateHistory. */
|
|
28576
|
+
fsmSnapshotHistory = [];
|
|
28569
28577
|
subscribe(listener) {
|
|
28570
28578
|
this.listeners.add(listener);
|
|
28571
28579
|
return () => {
|
|
@@ -28656,6 +28664,9 @@ var FsmDriver = class {
|
|
|
28656
28664
|
getStateHistory() {
|
|
28657
28665
|
return this.stateHistory;
|
|
28658
28666
|
}
|
|
28667
|
+
getFsmSnapshotHistory() {
|
|
28668
|
+
return this.fsmSnapshotHistory;
|
|
28669
|
+
}
|
|
28659
28670
|
getSections() {
|
|
28660
28671
|
try {
|
|
28661
28672
|
const screen = this.adapter.snapshot();
|
|
@@ -28751,7 +28762,7 @@ var FsmDriver = class {
|
|
|
28751
28762
|
this.lastFsmEval = ev;
|
|
28752
28763
|
this.prevScreenLines = currentLines;
|
|
28753
28764
|
if (ev.fired) {
|
|
28754
|
-
this.commitTransition(ev.fired, now);
|
|
28765
|
+
this.commitTransition(ev.fired, now, ev);
|
|
28755
28766
|
this.emitStateChanged(forceEmit);
|
|
28756
28767
|
this.scheduleWakeForState();
|
|
28757
28768
|
return;
|
|
@@ -28760,8 +28771,9 @@ var FsmDriver = class {
|
|
|
28760
28771
|
this.scheduleWakeForState();
|
|
28761
28772
|
this.maybeMarkReady();
|
|
28762
28773
|
}
|
|
28763
|
-
commitTransition(fired, now) {
|
|
28774
|
+
commitTransition(fired, now, ev) {
|
|
28764
28775
|
const from = this.currentStateId;
|
|
28776
|
+
this.pushFsmSnapshot(from, fired, now, ev);
|
|
28765
28777
|
this.currentStateId = fired.to;
|
|
28766
28778
|
this.stateEnteredAt = now;
|
|
28767
28779
|
this.regionLastChangedAt.clear();
|
|
@@ -28772,6 +28784,22 @@ var FsmDriver = class {
|
|
|
28772
28784
|
});
|
|
28773
28785
|
LOG.info("FsmDriver", `[${this.specTag()}] ${from} \u2192 ${fired.to} (${fired.label})`);
|
|
28774
28786
|
}
|
|
28787
|
+
/** Snapshot the full FSM evaluation that produced a transition into the
|
|
28788
|
+
* separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
|
|
28789
|
+
* table is captured by reference — it is freshly built per evaluation in
|
|
28790
|
+
* evaluateFsm and never mutated after, so no clone is needed. */
|
|
28791
|
+
pushFsmSnapshot(from, fired, now, ev) {
|
|
28792
|
+
this.fsmSnapshotHistory.push({
|
|
28793
|
+
stateFrom: from,
|
|
28794
|
+
stateTo: fired.to,
|
|
28795
|
+
at: now,
|
|
28796
|
+
firedTo: fired.to,
|
|
28797
|
+
firedLabel: fired.label,
|
|
28798
|
+
reason: summarizeTransition(fired),
|
|
28799
|
+
transitions: ev.transitions
|
|
28800
|
+
});
|
|
28801
|
+
if (this.fsmSnapshotHistory.length > 20) this.fsmSnapshotHistory.shift();
|
|
28802
|
+
}
|
|
28775
28803
|
/** Re-derive the visible modal + controls for the current state and emit a
|
|
28776
28804
|
* state_changed if anything differs from the last emit. */
|
|
28777
28805
|
emitStateChanged(forceEmit) {
|
|
@@ -29804,7 +29832,7 @@ import * as fs12 from "fs";
|
|
|
29804
29832
|
function stripAnsi3(text) {
|
|
29805
29833
|
return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
29806
29834
|
}
|
|
29807
|
-
var SpecCliAdapter = class {
|
|
29835
|
+
var SpecCliAdapter = class _SpecCliAdapter {
|
|
29808
29836
|
cliType;
|
|
29809
29837
|
cliName;
|
|
29810
29838
|
workingDir;
|
|
@@ -29829,6 +29857,23 @@ var SpecCliAdapter = class {
|
|
|
29829
29857
|
activeInteractivePrompt = null;
|
|
29830
29858
|
interactivePromptTransport = null;
|
|
29831
29859
|
claudeTuiPromptCaptureInFlight = false;
|
|
29860
|
+
/**
|
|
29861
|
+
* Wall clock of the first frame on which a held interactive prompt was
|
|
29862
|
+
* observed to have left the screen. Mirrors the approval FSM's
|
|
29863
|
+
* `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
|
|
29864
|
+
* repaints the choice picker as several PTY chunks, so a single frame
|
|
29865
|
+
* with no "Enter to select" footer is not proof the prompt is gone — it
|
|
29866
|
+
* may just be mid-repaint. We only clear the held prompt once it has
|
|
29867
|
+
* been absent across a short grace window. Reset to null the moment the
|
|
29868
|
+
* prompt footer reappears.
|
|
29869
|
+
*
|
|
29870
|
+
* Without this, a choice prompt resolved *directly in the terminal* (the
|
|
29871
|
+
* user picked an option without going through ADHDev's
|
|
29872
|
+
* setInteractivePromptResponse) was never cleared from
|
|
29873
|
+
* `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
|
|
29874
|
+
* forever — the choice-resolve-stuck bug.
|
|
29875
|
+
*/
|
|
29876
|
+
interactivePromptLostAt = null;
|
|
29832
29877
|
jsonLineTail = "";
|
|
29833
29878
|
exited = false;
|
|
29834
29879
|
spawned = false;
|
|
@@ -30089,6 +30134,11 @@ var SpecCliAdapter = class {
|
|
|
30089
30134
|
// transition from the current state with its per-condition match
|
|
30090
30135
|
// result + countdown — the canonical "why isn't it moving" answer.
|
|
30091
30136
|
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
30137
|
+
// v4 FSM transition snapshot history (null for v3 specs). The full
|
|
30138
|
+
// pre-transition evaluation table captured at each transition —
|
|
30139
|
+
// answers "why did this rule fire" after the fact, unlike the live
|
|
30140
|
+
// `fsm` field which only reflects the current instant.
|
|
30141
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
30092
30142
|
// Extended fields
|
|
30093
30143
|
name: this.cliName,
|
|
30094
30144
|
status: this.getStatus().status,
|
|
@@ -30126,11 +30176,13 @@ var SpecCliAdapter = class {
|
|
|
30126
30176
|
if (ev.state.title) {
|
|
30127
30177
|
LOG.debug("SpecAdapter", `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
|
|
30128
30178
|
}
|
|
30179
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
30129
30180
|
this.maybeCaptureClaudeTuiPrompt();
|
|
30130
30181
|
this.statusCallback?.();
|
|
30131
30182
|
return;
|
|
30132
30183
|
case "pty_data":
|
|
30133
30184
|
this.detectInteractivePromptFromPtyChunk(ev.chunk);
|
|
30185
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
30134
30186
|
this.maybeCaptureClaudeTuiPrompt();
|
|
30135
30187
|
try {
|
|
30136
30188
|
this.ptyDataCallback?.(ev.chunk);
|
|
@@ -30163,6 +30215,7 @@ var SpecCliAdapter = class {
|
|
|
30163
30215
|
if (!prompt) continue;
|
|
30164
30216
|
this.activeInteractivePrompt = prompt;
|
|
30165
30217
|
this.interactivePromptTransport = "stream-json";
|
|
30218
|
+
this.interactivePromptLostAt = null;
|
|
30166
30219
|
this.statusCallback?.();
|
|
30167
30220
|
} catch {
|
|
30168
30221
|
}
|
|
@@ -30216,6 +30269,47 @@ var SpecCliAdapter = class {
|
|
|
30216
30269
|
}
|
|
30217
30270
|
return messages;
|
|
30218
30271
|
}
|
|
30272
|
+
/**
|
|
30273
|
+
* Grace window a held interactive prompt must be absent from the screen
|
|
30274
|
+
* before we treat it as resolved-in-terminal and clear it. claude-cli
|
|
30275
|
+
* repaints the picker across multiple PTY chunks, so a single
|
|
30276
|
+
* footer-less frame is not proof the prompt is gone. Sized in the same
|
|
30277
|
+
* spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
|
|
30278
|
+
*/
|
|
30279
|
+
static INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
|
|
30280
|
+
/**
|
|
30281
|
+
* Clear a held interactive prompt once the user has resolved it directly
|
|
30282
|
+
* in the terminal (the choice picker leaves the screen without going
|
|
30283
|
+
* through setInteractivePromptResponse). The approval path already does
|
|
30284
|
+
* this via the FSM's modal-lost hysteresis; the interactive-prompt path
|
|
30285
|
+
* had no equivalent, so a terminal-side answer left activeInteractivePrompt
|
|
30286
|
+
* set and getStatus() re-emitted the same choice modal forever.
|
|
30287
|
+
*
|
|
30288
|
+
* Detection mirrors capture: the claude TUI picker is on-screen exactly
|
|
30289
|
+
* while its "Enter to select" footer is rendered. When the footer is gone
|
|
30290
|
+
* for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
|
|
30291
|
+
*/
|
|
30292
|
+
maybeClearResolvedClaudeTuiPrompt() {
|
|
30293
|
+
if (this.cliType !== "claude-cli" || !this.activeInteractivePrompt) return;
|
|
30294
|
+
let screenText = "";
|
|
30295
|
+
try {
|
|
30296
|
+
screenText = this.driver.snapshot();
|
|
30297
|
+
} catch {
|
|
30298
|
+
return;
|
|
30299
|
+
}
|
|
30300
|
+
const stillOnScreen = screenText.includes("Enter to select");
|
|
30301
|
+
if (stillOnScreen) {
|
|
30302
|
+
this.interactivePromptLostAt = null;
|
|
30303
|
+
return;
|
|
30304
|
+
}
|
|
30305
|
+
const lostAt = this.interactivePromptLostAt ?? Date.now();
|
|
30306
|
+
if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
|
|
30307
|
+
if (Date.now() - lostAt < _SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
|
|
30308
|
+
this.activeInteractivePrompt = null;
|
|
30309
|
+
this.interactivePromptTransport = null;
|
|
30310
|
+
this.interactivePromptLostAt = null;
|
|
30311
|
+
this.statusCallback?.();
|
|
30312
|
+
}
|
|
30219
30313
|
maybeCaptureClaudeTuiPrompt() {
|
|
30220
30314
|
if (this.cliType !== "claude-cli" || this.activeInteractivePrompt || this.claudeTuiPromptCaptureInFlight) return;
|
|
30221
30315
|
const screenText = this.driver.snapshot();
|
|
@@ -30229,6 +30323,7 @@ var SpecCliAdapter = class {
|
|
|
30229
30323
|
if (!prompt) return;
|
|
30230
30324
|
this.activeInteractivePrompt = prompt;
|
|
30231
30325
|
this.interactivePromptTransport = "tui";
|
|
30326
|
+
this.interactivePromptLostAt = null;
|
|
30232
30327
|
this.statusCallback?.();
|
|
30233
30328
|
return;
|
|
30234
30329
|
}
|
|
@@ -30265,6 +30360,7 @@ var SpecCliAdapter = class {
|
|
|
30265
30360
|
if (!prompt) return;
|
|
30266
30361
|
this.activeInteractivePrompt = prompt;
|
|
30267
30362
|
this.interactivePromptTransport = "tui";
|
|
30363
|
+
this.interactivePromptLostAt = null;
|
|
30268
30364
|
this.statusCallback?.();
|
|
30269
30365
|
}
|
|
30270
30366
|
getDebugState() {
|
|
@@ -30319,6 +30415,9 @@ var SpecCliAdapter = class {
|
|
|
30319
30415
|
// and countdown. This is the canonical "why isn't it transitioning"
|
|
30320
30416
|
// answer — no screenshots needed.
|
|
30321
30417
|
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
30418
|
+
// v4 FSM transition snapshot history — the captured pre-transition
|
|
30419
|
+
// evaluation table at each transition (null for v3 specs).
|
|
30420
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
30322
30421
|
messages,
|
|
30323
30422
|
committedMessages: messages
|
|
30324
30423
|
};
|
|
@@ -40319,8 +40418,37 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
|
|
|
40319
40418
|
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
40320
40419
|
});
|
|
40321
40420
|
const mergeBase = git(["merge-base", baseHead, branchHead]).trim();
|
|
40322
|
-
|
|
40323
|
-
|
|
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
|
+
}
|
|
40324
40452
|
if (!mergeBase || !mergedTree) {
|
|
40325
40453
|
return {
|
|
40326
40454
|
status: "failed",
|
|
@@ -40331,7 +40459,8 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
|
|
|
40331
40459
|
mergedTree: mergedTree || void 0,
|
|
40332
40460
|
durationMs: Date.now() - startedAt,
|
|
40333
40461
|
error: "patch equivalence preflight could not resolve merge-base or synthetic merge tree",
|
|
40334
|
-
stdout: truncateValidationOutput(mergeTreeStdout)
|
|
40462
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
40463
|
+
gitlinkTrivialFastForward
|
|
40335
40464
|
};
|
|
40336
40465
|
}
|
|
40337
40466
|
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
@@ -40346,7 +40475,8 @@ async function runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead)
|
|
|
40346
40475
|
mergedTree,
|
|
40347
40476
|
expectedPatchId,
|
|
40348
40477
|
actualPatchId,
|
|
40349
|
-
durationMs: Date.now() - startedAt
|
|
40478
|
+
durationMs: Date.now() - startedAt,
|
|
40479
|
+
gitlinkTrivialFastForward
|
|
40350
40480
|
};
|
|
40351
40481
|
} catch (e) {
|
|
40352
40482
|
return {
|
|
@@ -40369,6 +40499,65 @@ ${e?.stderr || ""}`
|
|
|
40369
40499
|
};
|
|
40370
40500
|
}
|
|
40371
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
|
+
}
|
|
40372
40561
|
function buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output) {
|
|
40373
40562
|
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return void 0;
|
|
40374
40563
|
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => ({
|
|
@@ -40425,6 +40614,135 @@ function readTreeObject(repoRoot, ref, path39) {
|
|
|
40425
40614
|
return void 0;
|
|
40426
40615
|
}
|
|
40427
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
|
+
}
|
|
40428
40746
|
async function alignRefinerySubmodulesAfterMerge(repoRoot, previousBaseHead, currentHead, options = {}) {
|
|
40429
40747
|
const startedAt = Date.now();
|
|
40430
40748
|
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead).filter((path39) => !(options.submoduleIgnorePaths || []).includes(path39));
|
|
@@ -41092,6 +41410,10 @@ var DaemonCommandRouter = class {
|
|
|
41092
41410
|
runningRefineJobs = /* @__PURE__ */ new Map();
|
|
41093
41411
|
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
41094
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();
|
|
41095
41417
|
constructor(deps) {
|
|
41096
41418
|
this.deps = deps;
|
|
41097
41419
|
}
|
|
@@ -42321,6 +42643,48 @@ ${tail}` : ""
|
|
|
42321
42643
|
}
|
|
42322
42644
|
};
|
|
42323
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
|
+
}
|
|
42324
42688
|
let mergeResult;
|
|
42325
42689
|
const mergeStarted = Date.now();
|
|
42326
42690
|
try {
|
|
@@ -42662,6 +43026,17 @@ ${tail}` : ""
|
|
|
42662
43026
|
note: "Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order."
|
|
42663
43027
|
};
|
|
42664
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) {
|
|
42665
43040
|
const results = [];
|
|
42666
43041
|
for (const node of orderedNodes) {
|
|
42667
43042
|
let result;
|
|
@@ -42716,6 +43091,204 @@ ${tail}` : ""
|
|
|
42716
43091
|
}
|
|
42717
43092
|
};
|
|
42718
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
|
+
}
|
|
42719
43292
|
async finishMeshRefineJob(handle, args) {
|
|
42720
43293
|
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
42721
43294
|
let result;
|
|
@@ -44263,7 +44836,9 @@ ${tail}` : ""
|
|
|
44263
44836
|
const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
|
|
44264
44837
|
if (!meshId) return { success: false, error: "meshId required" };
|
|
44265
44838
|
const requestedNodeIds = Array.isArray(args?.nodeIds) ? args.nodeIds.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
|
|
44266
|
-
|
|
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);
|
|
44267
44842
|
}
|
|
44268
44843
|
case "remove_mesh_node": {
|
|
44269
44844
|
const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
|