@adhdev/daemon-standalone 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/index.js +585 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/vendor/mcp-server/index.js +2 -2
- package/vendor/mcp-server/index.js.map +1 -1
package/dist/index.js
CHANGED
|
@@ -58358,8 +58358,12 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
58358
58358
|
this.rows = opts.rows ?? import_session_host_core6.DEFAULT_SESSION_HOST_ROWS;
|
|
58359
58359
|
this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
|
|
58360
58360
|
this.tickIntervalMs = opts.tickIntervalMs ?? 0;
|
|
58361
|
-
|
|
58362
|
-
|
|
58361
|
+
if (opts.transportFactory) {
|
|
58362
|
+
this.factory = opts.transportFactory;
|
|
58363
|
+
} else {
|
|
58364
|
+
const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS2(pty_transport_exports));
|
|
58365
|
+
this.factory = new NodePtyTransportFactory2();
|
|
58366
|
+
}
|
|
58363
58367
|
this.screen = new TerminalScreen(this.rows, this.cols);
|
|
58364
58368
|
}
|
|
58365
58369
|
rows;
|
|
@@ -58514,6 +58518,10 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
58514
58518
|
specWatcher = null;
|
|
58515
58519
|
/** Last full FSM evaluation, kept for the debugger. */
|
|
58516
58520
|
lastFsmEval = null;
|
|
58521
|
+
/** Ring buffer (max 20) of the full FSM evaluation captured at each
|
|
58522
|
+
* transition — the rich pre-transition table that lastFsmEval only keeps
|
|
58523
|
+
* for the single most recent evaluation. Separate from stateHistory. */
|
|
58524
|
+
fsmSnapshotHistory = [];
|
|
58517
58525
|
subscribe(listener) {
|
|
58518
58526
|
this.listeners.add(listener);
|
|
58519
58527
|
return () => {
|
|
@@ -58604,6 +58612,9 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
58604
58612
|
getStateHistory() {
|
|
58605
58613
|
return this.stateHistory;
|
|
58606
58614
|
}
|
|
58615
|
+
getFsmSnapshotHistory() {
|
|
58616
|
+
return this.fsmSnapshotHistory;
|
|
58617
|
+
}
|
|
58607
58618
|
getSections() {
|
|
58608
58619
|
try {
|
|
58609
58620
|
const screen = this.adapter.snapshot();
|
|
@@ -58699,7 +58710,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
58699
58710
|
this.lastFsmEval = ev;
|
|
58700
58711
|
this.prevScreenLines = currentLines;
|
|
58701
58712
|
if (ev.fired) {
|
|
58702
|
-
this.commitTransition(ev.fired, now);
|
|
58713
|
+
this.commitTransition(ev.fired, now, ev);
|
|
58703
58714
|
this.emitStateChanged(forceEmit);
|
|
58704
58715
|
this.scheduleWakeForState();
|
|
58705
58716
|
return;
|
|
@@ -58708,8 +58719,9 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
58708
58719
|
this.scheduleWakeForState();
|
|
58709
58720
|
this.maybeMarkReady();
|
|
58710
58721
|
}
|
|
58711
|
-
commitTransition(fired, now) {
|
|
58722
|
+
commitTransition(fired, now, ev) {
|
|
58712
58723
|
const from = this.currentStateId;
|
|
58724
|
+
this.pushFsmSnapshot(from, fired, now, ev);
|
|
58713
58725
|
this.currentStateId = fired.to;
|
|
58714
58726
|
this.stateEnteredAt = now;
|
|
58715
58727
|
this.regionLastChangedAt.clear();
|
|
@@ -58720,6 +58732,22 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
58720
58732
|
});
|
|
58721
58733
|
LOG2.info("FsmDriver", `[${this.specTag()}] ${from} \u2192 ${fired.to} (${fired.label})`);
|
|
58722
58734
|
}
|
|
58735
|
+
/** Snapshot the full FSM evaluation that produced a transition into the
|
|
58736
|
+
* separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
|
|
58737
|
+
* table is captured by reference — it is freshly built per evaluation in
|
|
58738
|
+
* evaluateFsm and never mutated after, so no clone is needed. */
|
|
58739
|
+
pushFsmSnapshot(from, fired, now, ev) {
|
|
58740
|
+
this.fsmSnapshotHistory.push({
|
|
58741
|
+
stateFrom: from,
|
|
58742
|
+
stateTo: fired.to,
|
|
58743
|
+
at: now,
|
|
58744
|
+
firedTo: fired.to,
|
|
58745
|
+
firedLabel: fired.label,
|
|
58746
|
+
reason: summarizeTransition(fired),
|
|
58747
|
+
transitions: ev.transitions
|
|
58748
|
+
});
|
|
58749
|
+
if (this.fsmSnapshotHistory.length > 20) this.fsmSnapshotHistory.shift();
|
|
58750
|
+
}
|
|
58723
58751
|
/** Re-derive the visible modal + controls for the current state and emit a
|
|
58724
58752
|
* state_changed if anything differs from the last emit. */
|
|
58725
58753
|
emitStateChanged(forceEmit) {
|
|
@@ -59748,7 +59776,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
59748
59776
|
function stripAnsi3(text) {
|
|
59749
59777
|
return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
59750
59778
|
}
|
|
59751
|
-
var SpecCliAdapter = class {
|
|
59779
|
+
var SpecCliAdapter = class _SpecCliAdapter {
|
|
59752
59780
|
cliType;
|
|
59753
59781
|
cliName;
|
|
59754
59782
|
workingDir;
|
|
@@ -59773,6 +59801,23 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
59773
59801
|
activeInteractivePrompt = null;
|
|
59774
59802
|
interactivePromptTransport = null;
|
|
59775
59803
|
claudeTuiPromptCaptureInFlight = false;
|
|
59804
|
+
/**
|
|
59805
|
+
* Wall clock of the first frame on which a held interactive prompt was
|
|
59806
|
+
* observed to have left the screen. Mirrors the approval FSM's
|
|
59807
|
+
* `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
|
|
59808
|
+
* repaints the choice picker as several PTY chunks, so a single frame
|
|
59809
|
+
* with no "Enter to select" footer is not proof the prompt is gone — it
|
|
59810
|
+
* may just be mid-repaint. We only clear the held prompt once it has
|
|
59811
|
+
* been absent across a short grace window. Reset to null the moment the
|
|
59812
|
+
* prompt footer reappears.
|
|
59813
|
+
*
|
|
59814
|
+
* Without this, a choice prompt resolved *directly in the terminal* (the
|
|
59815
|
+
* user picked an option without going through ADHDev's
|
|
59816
|
+
* setInteractivePromptResponse) was never cleared from
|
|
59817
|
+
* `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
|
|
59818
|
+
* forever — the choice-resolve-stuck bug.
|
|
59819
|
+
*/
|
|
59820
|
+
interactivePromptLostAt = null;
|
|
59776
59821
|
jsonLineTail = "";
|
|
59777
59822
|
exited = false;
|
|
59778
59823
|
spawned = false;
|
|
@@ -60033,6 +60078,11 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60033
60078
|
// transition from the current state with its per-condition match
|
|
60034
60079
|
// result + countdown — the canonical "why isn't it moving" answer.
|
|
60035
60080
|
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
60081
|
+
// v4 FSM transition snapshot history (null for v3 specs). The full
|
|
60082
|
+
// pre-transition evaluation table captured at each transition —
|
|
60083
|
+
// answers "why did this rule fire" after the fact, unlike the live
|
|
60084
|
+
// `fsm` field which only reflects the current instant.
|
|
60085
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
60036
60086
|
// Extended fields
|
|
60037
60087
|
name: this.cliName,
|
|
60038
60088
|
status: this.getStatus().status,
|
|
@@ -60070,11 +60120,13 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60070
60120
|
if (ev.state.title) {
|
|
60071
60121
|
LOG2.debug("SpecAdapter", `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
|
|
60072
60122
|
}
|
|
60123
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
60073
60124
|
this.maybeCaptureClaudeTuiPrompt();
|
|
60074
60125
|
this.statusCallback?.();
|
|
60075
60126
|
return;
|
|
60076
60127
|
case "pty_data":
|
|
60077
60128
|
this.detectInteractivePromptFromPtyChunk(ev.chunk);
|
|
60129
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
60078
60130
|
this.maybeCaptureClaudeTuiPrompt();
|
|
60079
60131
|
try {
|
|
60080
60132
|
this.ptyDataCallback?.(ev.chunk);
|
|
@@ -60107,6 +60159,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60107
60159
|
if (!prompt) continue;
|
|
60108
60160
|
this.activeInteractivePrompt = prompt;
|
|
60109
60161
|
this.interactivePromptTransport = "stream-json";
|
|
60162
|
+
this.interactivePromptLostAt = null;
|
|
60110
60163
|
this.statusCallback?.();
|
|
60111
60164
|
} catch {
|
|
60112
60165
|
}
|
|
@@ -60160,6 +60213,47 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60160
60213
|
}
|
|
60161
60214
|
return messages;
|
|
60162
60215
|
}
|
|
60216
|
+
/**
|
|
60217
|
+
* Grace window a held interactive prompt must be absent from the screen
|
|
60218
|
+
* before we treat it as resolved-in-terminal and clear it. claude-cli
|
|
60219
|
+
* repaints the picker across multiple PTY chunks, so a single
|
|
60220
|
+
* footer-less frame is not proof the prompt is gone. Sized in the same
|
|
60221
|
+
* spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
|
|
60222
|
+
*/
|
|
60223
|
+
static INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
|
|
60224
|
+
/**
|
|
60225
|
+
* Clear a held interactive prompt once the user has resolved it directly
|
|
60226
|
+
* in the terminal (the choice picker leaves the screen without going
|
|
60227
|
+
* through setInteractivePromptResponse). The approval path already does
|
|
60228
|
+
* this via the FSM's modal-lost hysteresis; the interactive-prompt path
|
|
60229
|
+
* had no equivalent, so a terminal-side answer left activeInteractivePrompt
|
|
60230
|
+
* set and getStatus() re-emitted the same choice modal forever.
|
|
60231
|
+
*
|
|
60232
|
+
* Detection mirrors capture: the claude TUI picker is on-screen exactly
|
|
60233
|
+
* while its "Enter to select" footer is rendered. When the footer is gone
|
|
60234
|
+
* for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
|
|
60235
|
+
*/
|
|
60236
|
+
maybeClearResolvedClaudeTuiPrompt() {
|
|
60237
|
+
if (this.cliType !== "claude-cli" || !this.activeInteractivePrompt) return;
|
|
60238
|
+
let screenText = "";
|
|
60239
|
+
try {
|
|
60240
|
+
screenText = this.driver.snapshot();
|
|
60241
|
+
} catch {
|
|
60242
|
+
return;
|
|
60243
|
+
}
|
|
60244
|
+
const stillOnScreen = screenText.includes("Enter to select");
|
|
60245
|
+
if (stillOnScreen) {
|
|
60246
|
+
this.interactivePromptLostAt = null;
|
|
60247
|
+
return;
|
|
60248
|
+
}
|
|
60249
|
+
const lostAt = this.interactivePromptLostAt ?? Date.now();
|
|
60250
|
+
if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
|
|
60251
|
+
if (Date.now() - lostAt < _SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
|
|
60252
|
+
this.activeInteractivePrompt = null;
|
|
60253
|
+
this.interactivePromptTransport = null;
|
|
60254
|
+
this.interactivePromptLostAt = null;
|
|
60255
|
+
this.statusCallback?.();
|
|
60256
|
+
}
|
|
60163
60257
|
maybeCaptureClaudeTuiPrompt() {
|
|
60164
60258
|
if (this.cliType !== "claude-cli" || this.activeInteractivePrompt || this.claudeTuiPromptCaptureInFlight) return;
|
|
60165
60259
|
const screenText = this.driver.snapshot();
|
|
@@ -60173,6 +60267,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60173
60267
|
if (!prompt) return;
|
|
60174
60268
|
this.activeInteractivePrompt = prompt;
|
|
60175
60269
|
this.interactivePromptTransport = "tui";
|
|
60270
|
+
this.interactivePromptLostAt = null;
|
|
60176
60271
|
this.statusCallback?.();
|
|
60177
60272
|
return;
|
|
60178
60273
|
}
|
|
@@ -60209,6 +60304,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60209
60304
|
if (!prompt) return;
|
|
60210
60305
|
this.activeInteractivePrompt = prompt;
|
|
60211
60306
|
this.interactivePromptTransport = "tui";
|
|
60307
|
+
this.interactivePromptLostAt = null;
|
|
60212
60308
|
this.statusCallback?.();
|
|
60213
60309
|
}
|
|
60214
60310
|
getDebugState() {
|
|
@@ -60263,6 +60359,9 @@ ${formatManifestValidationIssues2(validation.issues)}`,
|
|
|
60263
60359
|
// and countdown. This is the canonical "why isn't it transitioning"
|
|
60264
60360
|
// answer — no screenshots needed.
|
|
60265
60361
|
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
60362
|
+
// v4 FSM transition snapshot history — the captured pre-transition
|
|
60363
|
+
// evaluation table at each transition (null for v3 specs).
|
|
60364
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
60266
60365
|
messages,
|
|
60267
60366
|
committedMessages: messages
|
|
60268
60367
|
};
|
|
@@ -70198,8 +70297,37 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
|
|
|
70198
70297
|
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
70199
70298
|
});
|
|
70200
70299
|
const mergeBase = git(["merge-base", baseHead, branchHead]).trim();
|
|
70201
|
-
|
|
70202
|
-
|
|
70300
|
+
let mergedTree = "";
|
|
70301
|
+
let mergeTreeStdout = "";
|
|
70302
|
+
let gitlinkTrivialFastForward;
|
|
70303
|
+
try {
|
|
70304
|
+
mergeTreeStdout = git(["merge-tree", "--write-tree", baseHead, branchHead]);
|
|
70305
|
+
mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || "";
|
|
70306
|
+
} catch (mergeTreeErr) {
|
|
70307
|
+
const output = `${mergeTreeErr?.message || ""}
|
|
70308
|
+
${mergeTreeErr?.stdout || ""}
|
|
70309
|
+
${mergeTreeErr?.stderr || ""}`;
|
|
70310
|
+
const isSubmoduleConflict = /(submodule|160000)/i.test(output) || /Recursive merging with submodules/i.test(output);
|
|
70311
|
+
if (!isSubmoduleConflict) throw mergeTreeErr;
|
|
70312
|
+
const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead);
|
|
70313
|
+
if (!evaluation.trivial) {
|
|
70314
|
+
return {
|
|
70315
|
+
status: "failed",
|
|
70316
|
+
equivalent: false,
|
|
70317
|
+
baseHead,
|
|
70318
|
+
branchHead,
|
|
70319
|
+
mergeBase: mergeBase || void 0,
|
|
70320
|
+
durationMs: Date.now() - startedAt,
|
|
70321
|
+
error: mergeTreeErr?.message || String(mergeTreeErr),
|
|
70322
|
+
stdout: truncateValidationOutput(mergeTreeErr?.stdout),
|
|
70323
|
+
stderr: truncateValidationOutput(mergeTreeErr?.stderr),
|
|
70324
|
+
gitlinkTrivialFastForward: { resolved: false, gitlinks: evaluation.gitlinks, reason: evaluation.reason },
|
|
70325
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output)
|
|
70326
|
+
};
|
|
70327
|
+
}
|
|
70328
|
+
mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, evaluation.gitlinks) || "";
|
|
70329
|
+
gitlinkTrivialFastForward = { resolved: true, gitlinks: evaluation.gitlinks };
|
|
70330
|
+
}
|
|
70203
70331
|
if (!mergeBase || !mergedTree) {
|
|
70204
70332
|
return {
|
|
70205
70333
|
status: "failed",
|
|
@@ -70210,7 +70338,8 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
|
|
|
70210
70338
|
mergedTree: mergedTree || void 0,
|
|
70211
70339
|
durationMs: Date.now() - startedAt,
|
|
70212
70340
|
error: "patch equivalence preflight could not resolve merge-base or synthetic merge tree",
|
|
70213
|
-
stdout: truncateValidationOutput(mergeTreeStdout)
|
|
70341
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
70342
|
+
gitlinkTrivialFastForward
|
|
70214
70343
|
};
|
|
70215
70344
|
}
|
|
70216
70345
|
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
@@ -70225,7 +70354,8 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
|
|
|
70225
70354
|
mergedTree,
|
|
70226
70355
|
expectedPatchId,
|
|
70227
70356
|
actualPatchId,
|
|
70228
|
-
durationMs: Date.now() - startedAt
|
|
70357
|
+
durationMs: Date.now() - startedAt,
|
|
70358
|
+
gitlinkTrivialFastForward
|
|
70229
70359
|
};
|
|
70230
70360
|
} catch (e) {
|
|
70231
70361
|
return {
|
|
@@ -70248,6 +70378,65 @@ ${e?.stderr || ""}`
|
|
|
70248
70378
|
};
|
|
70249
70379
|
}
|
|
70250
70380
|
}
|
|
70381
|
+
async function runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead) {
|
|
70382
|
+
const startedAt = Date.now();
|
|
70383
|
+
try {
|
|
70384
|
+
const { execFileSync: execFileSync6 } = await import("child_process");
|
|
70385
|
+
const git = (args, opts) => execFileSync6("git", args, {
|
|
70386
|
+
cwd: opts?.cwd || repoRoot,
|
|
70387
|
+
encoding: "utf8",
|
|
70388
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
70389
|
+
});
|
|
70390
|
+
const rawDiff = git(["diff", "--raw", baseHead, branchHead]).trim();
|
|
70391
|
+
if (rawDiff) {
|
|
70392
|
+
const changedPaths = rawDiff.split("\n").map((line) => line.split(" ").slice(1).join(" ").trim()).filter(Boolean).slice(0, 50);
|
|
70393
|
+
return {
|
|
70394
|
+
status: "passed",
|
|
70395
|
+
hasEffectiveDiff: true,
|
|
70396
|
+
baseHead,
|
|
70397
|
+
branchHead,
|
|
70398
|
+
changedPaths,
|
|
70399
|
+
durationMs: Date.now() - startedAt
|
|
70400
|
+
};
|
|
70401
|
+
}
|
|
70402
|
+
const submoduleHints = [];
|
|
70403
|
+
try {
|
|
70404
|
+
const status = git(["submodule", "status"]);
|
|
70405
|
+
for (const line of status.split("\n")) {
|
|
70406
|
+
const trimmed = line.trimEnd();
|
|
70407
|
+
if (!trimmed) continue;
|
|
70408
|
+
if (trimmed.startsWith("+")) {
|
|
70409
|
+
const parts = trimmed.slice(1).trim().split(/\s+/);
|
|
70410
|
+
const path39 = parts[1] || parts[0] || "(unknown)";
|
|
70411
|
+
submoduleHints.push({
|
|
70412
|
+
path: path39,
|
|
70413
|
+
reason: "submodule checked-out commit differs from the committed gitlink (pointer bump not committed on the root branch)"
|
|
70414
|
+
});
|
|
70415
|
+
}
|
|
70416
|
+
}
|
|
70417
|
+
} catch {
|
|
70418
|
+
}
|
|
70419
|
+
return {
|
|
70420
|
+
status: "failed",
|
|
70421
|
+
hasEffectiveDiff: false,
|
|
70422
|
+
baseHead,
|
|
70423
|
+
branchHead,
|
|
70424
|
+
...submoduleHints.length ? { submoduleHints } : {},
|
|
70425
|
+
durationMs: Date.now() - startedAt
|
|
70426
|
+
};
|
|
70427
|
+
} catch (e) {
|
|
70428
|
+
return {
|
|
70429
|
+
status: "skipped",
|
|
70430
|
+
hasEffectiveDiff: true,
|
|
70431
|
+
baseHead,
|
|
70432
|
+
branchHead,
|
|
70433
|
+
durationMs: Date.now() - startedAt,
|
|
70434
|
+
error: e?.message || String(e),
|
|
70435
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
70436
|
+
stderr: truncateValidationOutput(e?.stderr)
|
|
70437
|
+
};
|
|
70438
|
+
}
|
|
70439
|
+
}
|
|
70251
70440
|
function buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output) {
|
|
70252
70441
|
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return void 0;
|
|
70253
70442
|
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => ({
|
|
@@ -70304,6 +70493,135 @@ ${e?.stderr || ""}`
|
|
|
70304
70493
|
return void 0;
|
|
70305
70494
|
}
|
|
70306
70495
|
}
|
|
70496
|
+
function resolveGitDir(repoRoot) {
|
|
70497
|
+
const out = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "--absolute-git-dir"], {
|
|
70498
|
+
cwd: repoRoot,
|
|
70499
|
+
encoding: "utf8",
|
|
70500
|
+
maxBuffer: 1024 * 1024
|
|
70501
|
+
}).trim();
|
|
70502
|
+
return out;
|
|
70503
|
+
}
|
|
70504
|
+
function isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit) {
|
|
70505
|
+
if (!baseCommit || !branchCommit) return false;
|
|
70506
|
+
if (baseCommit === branchCommit) return true;
|
|
70507
|
+
try {
|
|
70508
|
+
if (!fs23.existsSync(submoduleRepoPath)) return false;
|
|
70509
|
+
(0, import_node_child_process6.execFileSync)("git", ["cat-file", "-e", `${baseCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
|
|
70510
|
+
(0, import_node_child_process6.execFileSync)("git", ["cat-file", "-e", `${branchCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
|
|
70511
|
+
(0, import_node_child_process6.execFileSync)("git", ["merge-base", "--is-ancestor", baseCommit, branchCommit], { cwd: submoduleRepoPath, stdio: "ignore" });
|
|
70512
|
+
return true;
|
|
70513
|
+
} catch {
|
|
70514
|
+
return false;
|
|
70515
|
+
}
|
|
70516
|
+
}
|
|
70517
|
+
function readChangedPathKinds(repoRoot, fromRef, toRef) {
|
|
70518
|
+
try {
|
|
70519
|
+
const output = (0, import_node_child_process6.execFileSync)("git", ["diff", "--raw", "--no-abbrev", fromRef, toRef], {
|
|
70520
|
+
cwd: repoRoot,
|
|
70521
|
+
encoding: "utf8",
|
|
70522
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
|
|
70523
|
+
});
|
|
70524
|
+
const result = [];
|
|
70525
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70526
|
+
for (const line of output.split("\n")) {
|
|
70527
|
+
if (!line.trim()) continue;
|
|
70528
|
+
const metaAndPath = line.split(" ");
|
|
70529
|
+
const meta3 = metaAndPath[0] || "";
|
|
70530
|
+
const path39 = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
70531
|
+
if (!path39 || seen.has(path39)) continue;
|
|
70532
|
+
seen.add(path39);
|
|
70533
|
+
const parts = meta3.split(/\s+/);
|
|
70534
|
+
const isGitlink = !!(parts[0]?.includes("160000") || parts[1]?.includes("160000"));
|
|
70535
|
+
result.push({ path: path39, isGitlink });
|
|
70536
|
+
}
|
|
70537
|
+
return result;
|
|
70538
|
+
} catch {
|
|
70539
|
+
return [];
|
|
70540
|
+
}
|
|
70541
|
+
}
|
|
70542
|
+
function evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead) {
|
|
70543
|
+
const changedGitlinks = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => {
|
|
70544
|
+
const baseCommit = readTreeObject(repoRoot, baseHead, path39);
|
|
70545
|
+
const branchCommit = readTreeObject(repoRoot, branchHead, path39);
|
|
70546
|
+
const submoduleRepoPath = (0, import_path10.resolve)(repoRoot, path39);
|
|
70547
|
+
const fastForward = !!baseCommit && !!branchCommit && isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit);
|
|
70548
|
+
return { path: path39, baseCommit, branchCommit, fastForward };
|
|
70549
|
+
});
|
|
70550
|
+
if (changedGitlinks.length === 0) {
|
|
70551
|
+
return { trivial: false, reason: "no_changed_gitlinks", gitlinks: changedGitlinks };
|
|
70552
|
+
}
|
|
70553
|
+
const nonFastForward = changedGitlinks.filter((entry) => !entry.fastForward);
|
|
70554
|
+
if (nonFastForward.length > 0) {
|
|
70555
|
+
return {
|
|
70556
|
+
trivial: false,
|
|
70557
|
+
reason: `diverged_gitlinks:${nonFastForward.map((entry) => entry.path).join(",")}`,
|
|
70558
|
+
gitlinks: changedGitlinks
|
|
70559
|
+
};
|
|
70560
|
+
}
|
|
70561
|
+
let mergeBase = "";
|
|
70562
|
+
try {
|
|
70563
|
+
mergeBase = (0, import_node_child_process6.execFileSync)("git", ["merge-base", baseHead, branchHead], {
|
|
70564
|
+
cwd: repoRoot,
|
|
70565
|
+
encoding: "utf8",
|
|
70566
|
+
maxBuffer: 1024 * 1024
|
|
70567
|
+
}).trim();
|
|
70568
|
+
} catch {
|
|
70569
|
+
return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
|
|
70570
|
+
}
|
|
70571
|
+
if (!mergeBase) {
|
|
70572
|
+
return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
|
|
70573
|
+
}
|
|
70574
|
+
const baseSideChanges = readChangedPathKinds(repoRoot, mergeBase, baseHead);
|
|
70575
|
+
const branchSideChanges = readChangedPathKinds(repoRoot, mergeBase, branchHead);
|
|
70576
|
+
const baseChangedPaths = new Map(baseSideChanges.map((entry) => [entry.path, entry]));
|
|
70577
|
+
const overlapping = branchSideChanges.filter((entry) => baseChangedPaths.has(entry.path));
|
|
70578
|
+
const nonGitlinkOverlap = overlapping.filter((entry) => {
|
|
70579
|
+
const baseEntry = baseChangedPaths.get(entry.path);
|
|
70580
|
+
return !(entry.isGitlink && baseEntry?.isGitlink);
|
|
70581
|
+
});
|
|
70582
|
+
if (nonGitlinkOverlap.length > 0) {
|
|
70583
|
+
return {
|
|
70584
|
+
trivial: false,
|
|
70585
|
+
reason: `non_gitlink_overlap:${nonGitlinkOverlap.map((entry) => entry.path).join(",")}`,
|
|
70586
|
+
gitlinks: changedGitlinks
|
|
70587
|
+
};
|
|
70588
|
+
}
|
|
70589
|
+
return { trivial: true, gitlinks: changedGitlinks };
|
|
70590
|
+
}
|
|
70591
|
+
function synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, gitlinks) {
|
|
70592
|
+
try {
|
|
70593
|
+
const baseTree = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", `${baseHead}^{tree}`], {
|
|
70594
|
+
cwd: repoRoot,
|
|
70595
|
+
encoding: "utf8",
|
|
70596
|
+
maxBuffer: 1024 * 1024
|
|
70597
|
+
}).trim();
|
|
70598
|
+
if (!baseTree) return void 0;
|
|
70599
|
+
const updates = gitlinks.filter((entry) => entry.branchCommit).map((entry) => `160000 commit ${entry.branchCommit} ${entry.path}`).join("\n");
|
|
70600
|
+
if (!updates) return baseTree;
|
|
70601
|
+
const tmpIndex = (0, import_path10.join)(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
|
|
70602
|
+
const env2 = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
70603
|
+
try {
|
|
70604
|
+
(0, import_node_child_process6.execFileSync)("git", ["read-tree", baseTree], { cwd: repoRoot, env: env2, stdio: "ignore" });
|
|
70605
|
+
(0, import_node_child_process6.execFileSync)("git", ["update-index", "--index-info"], {
|
|
70606
|
+
cwd: repoRoot,
|
|
70607
|
+
env: env2,
|
|
70608
|
+
input: `${updates}
|
|
70609
|
+
`,
|
|
70610
|
+
encoding: "utf8",
|
|
70611
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
70612
|
+
});
|
|
70613
|
+
const newTree = (0, import_node_child_process6.execFileSync)("git", ["write-tree"], { cwd: repoRoot, env: env2, encoding: "utf8" }).trim();
|
|
70614
|
+
return newTree || void 0;
|
|
70615
|
+
} finally {
|
|
70616
|
+
try {
|
|
70617
|
+
fs23.rmSync(tmpIndex, { force: true });
|
|
70618
|
+
} catch {
|
|
70619
|
+
}
|
|
70620
|
+
}
|
|
70621
|
+
} catch {
|
|
70622
|
+
return void 0;
|
|
70623
|
+
}
|
|
70624
|
+
}
|
|
70307
70625
|
async function alignRefinerySubmodulesAfterMerge(repoRoot, previousBaseHead, currentHead, options = {}) {
|
|
70308
70626
|
const startedAt = Date.now();
|
|
70309
70627
|
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead).filter((path39) => !(options.submoduleIgnorePaths || []).includes(path39));
|
|
@@ -70971,6 +71289,10 @@ ${e?.stderr || ""}`
|
|
|
70971
71289
|
runningRefineJobs = /* @__PURE__ */ new Map();
|
|
70972
71290
|
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
70973
71291
|
terminalRefineJobs = /* @__PURE__ */ new Map();
|
|
71292
|
+
/** In-memory async batch Refinery jobs keyed by meshId (one batch convergence per mesh at a time). */
|
|
71293
|
+
runningRefineBatchJobs = /* @__PURE__ */ new Map();
|
|
71294
|
+
/** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
|
|
71295
|
+
terminalRefineBatchJobs = /* @__PURE__ */ new Map();
|
|
70974
71296
|
constructor(deps) {
|
|
70975
71297
|
this.deps = deps;
|
|
70976
71298
|
}
|
|
@@ -72200,6 +72522,48 @@ ${tail}` : ""
|
|
|
72200
72522
|
}
|
|
72201
72523
|
};
|
|
72202
72524
|
}
|
|
72525
|
+
const effectiveDiffStarted = Date.now();
|
|
72526
|
+
const effectiveDiff = await runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead);
|
|
72527
|
+
recordMeshRefineStage(refineStages, "effective_diff", effectiveDiff.status, effectiveDiffStarted, {
|
|
72528
|
+
hasEffectiveDiff: effectiveDiff.hasEffectiveDiff,
|
|
72529
|
+
changedPaths: effectiveDiff.changedPaths,
|
|
72530
|
+
submoduleHints: effectiveDiff.submoduleHints,
|
|
72531
|
+
...effectiveDiff.error ? { error: effectiveDiff.error } : {}
|
|
72532
|
+
});
|
|
72533
|
+
if (effectiveDiff.status === "failed" && !effectiveDiff.hasEffectiveDiff) {
|
|
72534
|
+
const hintLines = (effectiveDiff.submoduleHints || []).map((h) => ` - ${h.path}: ${h.reason}`);
|
|
72535
|
+
const message = [
|
|
72536
|
+
`Refinery no-op guard: branch '${branch}' has no effective root-tree diff against '${baseBranch}' (${baseHead.slice(0, 12)}); nothing would merge.`,
|
|
72537
|
+
"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.",
|
|
72538
|
+
hintLines.length ? `Submodules with uncommitted pointer bumps:
|
|
72539
|
+
${hintLines.join("\n")}` : "",
|
|
72540
|
+
`Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`
|
|
72541
|
+
].filter(Boolean).join("\n");
|
|
72542
|
+
return {
|
|
72543
|
+
success: false,
|
|
72544
|
+
code: "no_effective_diff",
|
|
72545
|
+
convergenceStatus: "blocked_review",
|
|
72546
|
+
error: message,
|
|
72547
|
+
branch,
|
|
72548
|
+
into: baseBranch,
|
|
72549
|
+
validationSummary,
|
|
72550
|
+
patchEquivalence,
|
|
72551
|
+
effectiveDiff,
|
|
72552
|
+
refineStages,
|
|
72553
|
+
finalBranchConvergenceState: {
|
|
72554
|
+
branch,
|
|
72555
|
+
baseBranch,
|
|
72556
|
+
merged: false,
|
|
72557
|
+
removed: false,
|
|
72558
|
+
validation: "passed",
|
|
72559
|
+
patchEquivalence: "passed",
|
|
72560
|
+
effectiveDiff: "no_effective_diff",
|
|
72561
|
+
status: "blocked_review",
|
|
72562
|
+
reason: "no_effective_diff",
|
|
72563
|
+
...effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}
|
|
72564
|
+
}
|
|
72565
|
+
};
|
|
72566
|
+
}
|
|
72203
72567
|
let mergeResult;
|
|
72204
72568
|
const mergeStarted = Date.now();
|
|
72205
72569
|
try {
|
|
@@ -72541,6 +72905,17 @@ ${tail}` : ""
|
|
|
72541
72905
|
note: "Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order."
|
|
72542
72906
|
};
|
|
72543
72907
|
}
|
|
72908
|
+
return this.runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args);
|
|
72909
|
+
}
|
|
72910
|
+
/**
|
|
72911
|
+
* Convergence core shared by the synchronous batch entry and the async batch job.
|
|
72912
|
+
* Refines each node in order: the per-node refine pipeline fetches origin/<base>
|
|
72913
|
+
* fresh, so each merged sibling advances the base before the next node's auto-rebase
|
|
72914
|
+
* + patch-equivalence re-check. A blocked/failed node is isolated; the batch
|
|
72915
|
+
* continues with the remaining nodes. Does NOT touch the per-node merge logic — it
|
|
72916
|
+
* only sequences calls to executeMeshRefineNodeSynchronously and aggregates outcomes.
|
|
72917
|
+
*/
|
|
72918
|
+
async runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args) {
|
|
72544
72919
|
const results = [];
|
|
72545
72920
|
for (const node of orderedNodes) {
|
|
72546
72921
|
let result;
|
|
@@ -72595,6 +72970,204 @@ ${tail}` : ""
|
|
|
72595
72970
|
}
|
|
72596
72971
|
};
|
|
72597
72972
|
}
|
|
72973
|
+
buildRefineBatchJobKey(meshId) {
|
|
72974
|
+
return `${meshId}::batch`;
|
|
72975
|
+
}
|
|
72976
|
+
buildRefineBatchJobHandle(args) {
|
|
72977
|
+
return {
|
|
72978
|
+
success: true,
|
|
72979
|
+
async: true,
|
|
72980
|
+
batch: true,
|
|
72981
|
+
status: args.status || "accepted",
|
|
72982
|
+
jobId: args.jobId || `refine_batch_${createInteractionId()}`,
|
|
72983
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
72984
|
+
meshId: args.meshId,
|
|
72985
|
+
batchLabel: `batch:${args.nodeIds.length} node${args.nodeIds.length === 1 ? "" : "s"}`,
|
|
72986
|
+
nodeIds: args.nodeIds,
|
|
72987
|
+
nodeCount: args.nodeIds.length,
|
|
72988
|
+
order: args.order,
|
|
72989
|
+
startedAt: args.startedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
72990
|
+
...args.completedAt ? { completedAt: args.completedAt } : {},
|
|
72991
|
+
...args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {},
|
|
72992
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
72993
|
+
evidence: {
|
|
72994
|
+
pendingEventsCommand: "get_pending_mesh_events",
|
|
72995
|
+
ledgerCommand: "get_mesh_ledger_slice",
|
|
72996
|
+
taskHistoryKind: args.status === "completed" ? "task_completed" : args.status === "failed" ? "task_failed" : "task_dispatched"
|
|
72997
|
+
}
|
|
72998
|
+
};
|
|
72999
|
+
}
|
|
73000
|
+
/**
|
|
73001
|
+
* Emit a batch Refinery terminal/accepted event through the SAME pending-event +
|
|
73002
|
+
* forward mechanism single-node refine uses (queueRefineJobEvent), so the
|
|
73003
|
+
* coordinator's existing refine:accepted/completed/failed handling and message
|
|
73004
|
+
* renderer apply unchanged. The aggregate per-node results ride along in `result`.
|
|
73005
|
+
*/
|
|
73006
|
+
queueRefineBatchJobEvent(event, handle, result) {
|
|
73007
|
+
const metadataEvent = {
|
|
73008
|
+
source: "refine_mesh_node_async_job",
|
|
73009
|
+
batch: true,
|
|
73010
|
+
jobId: handle.jobId,
|
|
73011
|
+
interactionId: handle.interactionId,
|
|
73012
|
+
meshId: handle.meshId,
|
|
73013
|
+
nodeId: handle.batchLabel,
|
|
73014
|
+
nodeIds: handle.nodeIds,
|
|
73015
|
+
workspace: void 0,
|
|
73016
|
+
status: handle.status,
|
|
73017
|
+
startedAt: handle.startedAt,
|
|
73018
|
+
completedAt: handle.completedAt,
|
|
73019
|
+
order: handle.order,
|
|
73020
|
+
...result ? { result } : {}
|
|
73021
|
+
};
|
|
73022
|
+
const eventPayload = {
|
|
73023
|
+
event,
|
|
73024
|
+
meshId: handle.meshId,
|
|
73025
|
+
nodeLabel: handle.batchLabel,
|
|
73026
|
+
nodeId: handle.batchLabel,
|
|
73027
|
+
metadataEvent,
|
|
73028
|
+
queuedAt: Date.now(),
|
|
73029
|
+
...handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}
|
|
73030
|
+
};
|
|
73031
|
+
if (typeof this.deps.instanceManager?.getByCategory === "function") {
|
|
73032
|
+
const forwarded = handleMeshForwardEvent(
|
|
73033
|
+
{ instanceManager: this.deps.instanceManager },
|
|
73034
|
+
{
|
|
73035
|
+
event,
|
|
73036
|
+
meshId: handle.meshId,
|
|
73037
|
+
nodeId: handle.batchLabel,
|
|
73038
|
+
jobId: handle.jobId,
|
|
73039
|
+
interactionId: handle.interactionId,
|
|
73040
|
+
status: handle.status,
|
|
73041
|
+
startedAt: handle.startedAt,
|
|
73042
|
+
completedAt: handle.completedAt,
|
|
73043
|
+
...result ? { result } : {}
|
|
73044
|
+
}
|
|
73045
|
+
);
|
|
73046
|
+
if (forwarded?.success === true) return;
|
|
73047
|
+
LOG2.warn("Mesh", `[Refinery] Failed to forward async refine batch event ${event}: ${forwarded?.error || "unknown error"}`);
|
|
73048
|
+
}
|
|
73049
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
73050
|
+
}
|
|
73051
|
+
async appendRefineBatchJobLedger(kind, handle, result) {
|
|
73052
|
+
try {
|
|
73053
|
+
const { appendLedgerEntry: appendLedgerEntry2 } = await Promise.resolve().then(() => (init_mesh_ledger(), mesh_ledger_exports));
|
|
73054
|
+
appendLedgerEntry2(handle.meshId, {
|
|
73055
|
+
kind,
|
|
73056
|
+
nodeId: handle.batchLabel,
|
|
73057
|
+
payload: {
|
|
73058
|
+
source: "refine_mesh_node_async_job",
|
|
73059
|
+
refineJob: {
|
|
73060
|
+
batch: true,
|
|
73061
|
+
jobId: handle.jobId,
|
|
73062
|
+
interactionId: handle.interactionId,
|
|
73063
|
+
status: handle.status,
|
|
73064
|
+
meshId: handle.meshId,
|
|
73065
|
+
nodeIds: handle.nodeIds,
|
|
73066
|
+
order: handle.order,
|
|
73067
|
+
targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
73068
|
+
startedAt: handle.startedAt,
|
|
73069
|
+
completedAt: handle.completedAt
|
|
73070
|
+
},
|
|
73071
|
+
async: true,
|
|
73072
|
+
batch: true,
|
|
73073
|
+
...result ? {
|
|
73074
|
+
success: result.success === true,
|
|
73075
|
+
result
|
|
73076
|
+
} : {}
|
|
73077
|
+
}
|
|
73078
|
+
});
|
|
73079
|
+
} catch (e) {
|
|
73080
|
+
LOG2.warn("Mesh", `[Refinery] Failed to append async refine batch ledger entry: ${e?.message || e}`);
|
|
73081
|
+
}
|
|
73082
|
+
}
|
|
73083
|
+
async finishMeshRefineBatchJob(handle, orderedNodes, ordering, args) {
|
|
73084
|
+
const key = this.buildRefineBatchJobKey(handle.meshId);
|
|
73085
|
+
let result;
|
|
73086
|
+
try {
|
|
73087
|
+
result = await this.runMeshRefineBatchConvergence(handle.meshId, orderedNodes, ordering, args);
|
|
73088
|
+
} catch (e) {
|
|
73089
|
+
result = { success: false, error: e?.message || String(e), batch: true };
|
|
73090
|
+
}
|
|
73091
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
73092
|
+
const summary = result.summary && typeof result.summary === "object" ? result.summary : void 0;
|
|
73093
|
+
const allConverged = result.allConverged === true;
|
|
73094
|
+
const isTerminalSuccess = result.success === true && allConverged;
|
|
73095
|
+
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.";
|
|
73096
|
+
const normalizedResult = {
|
|
73097
|
+
...result,
|
|
73098
|
+
batch: true,
|
|
73099
|
+
nextStep,
|
|
73100
|
+
...summary ? {
|
|
73101
|
+
convergenceStatus: allConverged ? "all_converged" : "partial"
|
|
73102
|
+
} : {}
|
|
73103
|
+
};
|
|
73104
|
+
const terminalHandle = this.buildRefineBatchJobHandle({
|
|
73105
|
+
meshId: handle.meshId,
|
|
73106
|
+
nodeIds: handle.nodeIds,
|
|
73107
|
+
order: handle.order,
|
|
73108
|
+
status: isTerminalSuccess ? "completed" : "failed",
|
|
73109
|
+
startedAt: handle.startedAt,
|
|
73110
|
+
completedAt,
|
|
73111
|
+
jobId: handle.jobId,
|
|
73112
|
+
interactionId: handle.interactionId,
|
|
73113
|
+
coordinatorDaemonId: handle.targetCoordinatorDaemonId
|
|
73114
|
+
});
|
|
73115
|
+
const terminal = { ...terminalHandle, result: normalizedResult };
|
|
73116
|
+
this.terminalRefineBatchJobs.set(key, terminal);
|
|
73117
|
+
this.runningRefineBatchJobs.delete(key);
|
|
73118
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
73119
|
+
await this.appendRefineBatchJobLedger(isTerminalSuccess ? "task_completed" : "task_failed", terminalHandle, normalizedResult);
|
|
73120
|
+
this.queueRefineBatchJobEvent(isTerminalSuccess ? "refine:completed" : "refine:failed", terminalHandle, normalizedResult);
|
|
73121
|
+
}
|
|
73122
|
+
/**
|
|
73123
|
+
* Async entry for the batch Refinery execute path. Mirrors startMeshRefineJob:
|
|
73124
|
+
* resolves the plan synchronously (so target/ordering errors and the dry-run shape
|
|
73125
|
+
* stay synchronous), then for execute=true registers an in-flight batch job, returns
|
|
73126
|
+
* {async:true, status:'accepted', batch:true, ...plan} immediately, and runs the
|
|
73127
|
+
* convergence loop in the background — emitting the same terminal refine event.
|
|
73128
|
+
* Idempotent: a batch already in flight for this mesh returns the running handle
|
|
73129
|
+
* with duplicate:true rather than spawning a second background job.
|
|
73130
|
+
*/
|
|
73131
|
+
async startMeshRefineBatchJob(meshId, requestedNodeIds, args) {
|
|
73132
|
+
const plan = await this.batchRefineMeshNodes(meshId, requestedNodeIds, { ...args, dryRun: true, execute: false });
|
|
73133
|
+
const planRecord = plan;
|
|
73134
|
+
if (planRecord.success !== true) return plan;
|
|
73135
|
+
if (args?.dryRun === true && args?.execute !== true) return plan;
|
|
73136
|
+
const order = Array.isArray(planRecord.order) ? planRecord.order.filter((v) => typeof v === "string") : [];
|
|
73137
|
+
const nodeIds = order.slice();
|
|
73138
|
+
if (nodeIds.length === 0) {
|
|
73139
|
+
return { ...planRecord, success: true, batch: true, dryRun: false, async: false };
|
|
73140
|
+
}
|
|
73141
|
+
const key = this.buildRefineBatchJobKey(meshId);
|
|
73142
|
+
const running = this.runningRefineBatchJobs.get(key);
|
|
73143
|
+
if (running) return { ...running, duplicate: true };
|
|
73144
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
73145
|
+
const mesh = meshRecord?.mesh;
|
|
73146
|
+
const allNodes = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
|
|
73147
|
+
const orderedNodes = nodeIds.map((id) => allNodes.find((n) => n.id === id || n.nodeId === id)).filter((n) => !!n);
|
|
73148
|
+
if (orderedNodes.length === 0) {
|
|
73149
|
+
return { success: false, error: "Batch nodes no longer resolvable in mesh", batch: true };
|
|
73150
|
+
}
|
|
73151
|
+
const ordering = {
|
|
73152
|
+
order,
|
|
73153
|
+
rationale: planRecord.orderingRationale
|
|
73154
|
+
};
|
|
73155
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === "string" && args.coordinatorDaemonId.trim() ? args.coordinatorDaemonId.trim() : this.deps.statusInstanceId || void 0;
|
|
73156
|
+
const handle = this.buildRefineBatchJobHandle({ meshId, nodeIds, order, coordinatorDaemonId });
|
|
73157
|
+
this.runningRefineBatchJobs.set(key, handle);
|
|
73158
|
+
await this.appendRefineBatchJobLedger("task_dispatched", handle);
|
|
73159
|
+
this.queueRefineBatchJobEvent("refine:accepted", handle);
|
|
73160
|
+
setImmediate(() => {
|
|
73161
|
+
void this.finishMeshRefineBatchJob(handle, orderedNodes, ordering, args);
|
|
73162
|
+
});
|
|
73163
|
+
return {
|
|
73164
|
+
...handle,
|
|
73165
|
+
order,
|
|
73166
|
+
orderingRationale: planRecord.orderingRationale,
|
|
73167
|
+
plan: planRecord.plan,
|
|
73168
|
+
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."
|
|
73169
|
+
};
|
|
73170
|
+
}
|
|
72598
73171
|
async finishMeshRefineJob(handle, args) {
|
|
72599
73172
|
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
72600
73173
|
let result;
|
|
@@ -74142,7 +74715,9 @@ ${tail}` : ""
|
|
|
74142
74715
|
const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
|
|
74143
74716
|
if (!meshId) return { success: false, error: "meshId required" };
|
|
74144
74717
|
const requestedNodeIds = Array.isArray(args?.nodeIds) ? args.nodeIds.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
|
|
74145
|
-
|
|
74718
|
+
const isDryRun = args?.dryRun !== false && args?.execute !== true;
|
|
74719
|
+
if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
|
|
74720
|
+
return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
|
|
74146
74721
|
}
|
|
74147
74722
|
case "remove_mesh_node": {
|
|
74148
74723
|
const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
|