@adhdev/daemon-core 0.9.82-rc.419 → 0.9.82-rc.420
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/med-family/types.d.ts +25 -0
- package/dist/commands/router.d.ts +20 -0
- package/dist/index.js +233 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +233 -8
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-events-pending.d.ts +14 -0
- package/package.json +2 -2
- package/src/commands/med-family/mesh-crud.ts +47 -0
- package/src/commands/med-family/types.ts +22 -0
- package/src/commands/router.ts +128 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-events-pending.ts +51 -0
- package/src/mesh/mesh-queue-assignment.ts +97 -4
|
@@ -45,6 +45,20 @@ export type CleanupLocalWorktreeNodeResult = {
|
|
|
45
45
|
recoveryHint: string;
|
|
46
46
|
convergence?: Record<string, unknown>;
|
|
47
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Result of the non-destructive local-worktree removability precheck. `ok:false`
|
|
50
|
+
* carries the same refusal `code`/`error`/`recoveryHint` that the destructive
|
|
51
|
+
* cleanup would have returned, so callers can refuse a removal BEFORE performing
|
|
52
|
+
* any irreversible step (e.g. stopping/deleting delegated sessions).
|
|
53
|
+
*/
|
|
54
|
+
export type WorktreeRemovalPrecheckResult = {
|
|
55
|
+
ok: true;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
code: string;
|
|
59
|
+
error: string;
|
|
60
|
+
recoveryHint: string;
|
|
61
|
+
};
|
|
48
62
|
/**
|
|
49
63
|
* Router-private collaborators injected at dispatch. Each is a bound method or
|
|
50
64
|
* field of DaemonCommandRouter; handlers that don't need a given collaborator
|
|
@@ -90,6 +104,17 @@ export interface MedFamilyContext {
|
|
|
90
104
|
nodeId: string;
|
|
91
105
|
force?: boolean;
|
|
92
106
|
}) => Promise<CleanupLocalWorktreeNodeResult>;
|
|
107
|
+
/**
|
|
108
|
+
* Bound `DaemonCommandRouter.precheckLocalWorktreeRemovable` — purely
|
|
109
|
+
* non-destructive validation of whether a local worktree node can be removed.
|
|
110
|
+
* Called BEFORE session cleanup so a refusal does not orphan the session.
|
|
111
|
+
*/
|
|
112
|
+
precheckLocalWorktreeRemovable: (args: {
|
|
113
|
+
mesh: any;
|
|
114
|
+
node: any;
|
|
115
|
+
nodeId: string;
|
|
116
|
+
force?: boolean;
|
|
117
|
+
}) => Promise<WorktreeRemovalPrecheckResult>;
|
|
93
118
|
/** Bound `DaemonCommandRouter.startMeshRefineJob` (async execute path). */
|
|
94
119
|
startMeshRefineJob: (meshId: string, nodeId: string, args: any) => Promise<CommandRouterResult>;
|
|
95
120
|
/** Bound `DaemonCommandRouter.batchRefineMeshNodes` (dry-run batch plan). */
|
|
@@ -248,6 +248,26 @@ export declare class DaemonCommandRouter {
|
|
|
248
248
|
* to give handles time to release, and reports whether residue remains.
|
|
249
249
|
*/
|
|
250
250
|
private bestEffortRemoveWorktreeDir;
|
|
251
|
+
/**
|
|
252
|
+
* Non-destructive precheck mirroring every REFUSAL condition in
|
|
253
|
+
* {@link cleanupLocalWorktreeNode} — missing workspace / source-repo / branch
|
|
254
|
+
* metadata, unexpected (non-managed) path, branch mismatch — PLUS the
|
|
255
|
+
* dirty-worktree guard that `removeWorktree(requireClean)` enforces
|
|
256
|
+
* (`git status --porcelain`). It performs ZERO destructive actions: no
|
|
257
|
+
* `git worktree remove`, no `git worktree prune`, no directory deletion.
|
|
258
|
+
*
|
|
259
|
+
* remove_mesh_node calls this BEFORE any session cleanup so that a refusal
|
|
260
|
+
* (the common one being a dirty worktree) does not first stop/delete the
|
|
261
|
+
* delegated session and orphan it — the original ordering bug. Success/skip
|
|
262
|
+
* cases that the real cleanup handles idempotently (worktree path already
|
|
263
|
+
* gone, git-de-registered residue) are NOT refusals and return `{ ok: true }`.
|
|
264
|
+
*
|
|
265
|
+
* `force:true` skips the dirty guard, preserving `removeWorktree`'s
|
|
266
|
+
* `requireClean: !force` semantics. This is a read-only superset check; the
|
|
267
|
+
* authoritative `requireClean` guard inside `removeWorktree` is intentionally
|
|
268
|
+
* kept as a second line of defense against a precheck→execute race.
|
|
269
|
+
*/
|
|
270
|
+
private precheckLocalWorktreeRemovable;
|
|
251
271
|
private cleanupLocalWorktreeNode;
|
|
252
272
|
private getWorktreeForceCleanupConvergence;
|
|
253
273
|
private isCompletedHostedSession;
|
package/dist/index.js
CHANGED
|
@@ -389,10 +389,10 @@ function readInjected(value) {
|
|
|
389
389
|
}
|
|
390
390
|
function getDaemonBuildInfo() {
|
|
391
391
|
if (cached) return cached;
|
|
392
|
-
const commit = readInjected(true ? "
|
|
393
|
-
const commitShort = readInjected(true ? "
|
|
394
|
-
const version = readInjected(true ? "0.9.82-rc.
|
|
395
|
-
const builtAt = readInjected(true ? "2026-06-
|
|
392
|
+
const commit = readInjected(true ? "bd19cb0fbe1e16db1d8d2fcd8c2c871aae3ce33b" : void 0) ?? "unknown";
|
|
393
|
+
const commitShort = readInjected(true ? "bd19cb0f" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
|
|
394
|
+
const version = readInjected(true ? "0.9.82-rc.420" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
|
|
395
|
+
const builtAt = readInjected(true ? "2026-06-28T19:41:31.696Z" : void 0);
|
|
396
396
|
cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
|
|
397
397
|
return cached;
|
|
398
398
|
}
|
|
@@ -10913,6 +10913,35 @@ function drainPendingMeshCoordinatorEvents(meshId, coordinatorDaemonId, opts) {
|
|
|
10913
10913
|
if (merged.length === 0) return [];
|
|
10914
10914
|
return reconcilePendingMeshCoordinatorEvents(meshId, merged);
|
|
10915
10915
|
}
|
|
10916
|
+
function retractPendingDispatchBlockedEvent(meshId, taskId, coordinatorDaemonId) {
|
|
10917
|
+
if (!meshId || !taskId) return 0;
|
|
10918
|
+
let removed = 0;
|
|
10919
|
+
const matchesTask = (event) => {
|
|
10920
|
+
if (!event || event.event !== "mesh:dispatch_blocked") return false;
|
|
10921
|
+
const rowTaskId = readNonEmptyString2(event.metadataEvent?.taskId);
|
|
10922
|
+
return rowTaskId === taskId;
|
|
10923
|
+
};
|
|
10924
|
+
try {
|
|
10925
|
+
const store = MeshRuntimeStore.getInstance();
|
|
10926
|
+
const ids = [];
|
|
10927
|
+
for (const row of store.peekPendingEvents(meshId)) {
|
|
10928
|
+
if (row.event !== "mesh:dispatch_blocked") continue;
|
|
10929
|
+
if (matchesTask(row.payload)) ids.push(row.id);
|
|
10930
|
+
}
|
|
10931
|
+
if (ids.length) removed += store.deletePendingEventsById(ids);
|
|
10932
|
+
} catch {
|
|
10933
|
+
}
|
|
10934
|
+
const daemonIds = normalizeCoordinatorDaemonIds(coordinatorDaemonId);
|
|
10935
|
+
const primaryDaemonId = daemonIds[0];
|
|
10936
|
+
const paths = primaryDaemonId ? [getPendingEventsPath(meshId, primaryDaemonId), getPendingEventsPath(meshId)] : [getPendingEventsPath(meshId)];
|
|
10937
|
+
for (const path43 of paths) {
|
|
10938
|
+
try {
|
|
10939
|
+
removed += selectiveDrainFile(path43, matchesTask).length;
|
|
10940
|
+
} catch {
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10943
|
+
return removed;
|
|
10944
|
+
}
|
|
10916
10945
|
function getPendingMeshCoordinatorEvents(meshId, coordinatorDaemonId) {
|
|
10917
10946
|
if (!meshId) return [];
|
|
10918
10947
|
const daemonIds = normalizeCoordinatorDaemonIds(coordinatorDaemonId);
|
|
@@ -11965,6 +11994,41 @@ var init_mesh_warmup_deadline = __esm({
|
|
|
11965
11994
|
}
|
|
11966
11995
|
});
|
|
11967
11996
|
|
|
11997
|
+
// src/mesh/mesh-clone-grace.ts
|
|
11998
|
+
function normalizeNodeIdKey(nodeId) {
|
|
11999
|
+
return normalizeMeshNodeId({ id: nodeId ?? void 0 }) ?? "";
|
|
12000
|
+
}
|
|
12001
|
+
function noteRecentlyClonedNode(nodeId, nowMs = Date.now()) {
|
|
12002
|
+
const key2 = normalizeNodeIdKey(nodeId);
|
|
12003
|
+
if (!key2) return;
|
|
12004
|
+
recentlyClonedNodeExpiry.set(key2, nowMs + CLONE_BOOTSTRAP_GRACE_MS);
|
|
12005
|
+
if (recentlyClonedNodeExpiry.size > MAX_TRACKED_CLONED_NODES) {
|
|
12006
|
+
const oldest = recentlyClonedNodeExpiry.keys().next().value;
|
|
12007
|
+
if (oldest !== void 0) recentlyClonedNodeExpiry.delete(oldest);
|
|
12008
|
+
}
|
|
12009
|
+
}
|
|
12010
|
+
function isWithinCloneBootstrapGrace(nodeId, nowMs = Date.now()) {
|
|
12011
|
+
const key2 = normalizeNodeIdKey(nodeId);
|
|
12012
|
+
if (!key2) return false;
|
|
12013
|
+
const expiry = recentlyClonedNodeExpiry.get(key2);
|
|
12014
|
+
if (expiry === void 0) return false;
|
|
12015
|
+
if (nowMs >= expiry) {
|
|
12016
|
+
recentlyClonedNodeExpiry.delete(key2);
|
|
12017
|
+
return false;
|
|
12018
|
+
}
|
|
12019
|
+
return true;
|
|
12020
|
+
}
|
|
12021
|
+
var CLONE_BOOTSTRAP_GRACE_MS, recentlyClonedNodeExpiry, MAX_TRACKED_CLONED_NODES;
|
|
12022
|
+
var init_mesh_clone_grace = __esm({
|
|
12023
|
+
"src/mesh/mesh-clone-grace.ts"() {
|
|
12024
|
+
"use strict";
|
|
12025
|
+
init_dist();
|
|
12026
|
+
CLONE_BOOTSTRAP_GRACE_MS = 10 * 60 * 1e3;
|
|
12027
|
+
recentlyClonedNodeExpiry = /* @__PURE__ */ new Map();
|
|
12028
|
+
MAX_TRACKED_CLONED_NODES = 512;
|
|
12029
|
+
}
|
|
12030
|
+
});
|
|
12031
|
+
|
|
11968
12032
|
// src/mesh/mesh-queue-assignment.ts
|
|
11969
12033
|
function localCoordinatorDaemonId() {
|
|
11970
12034
|
return canonicalDaemonId(readNonEmptyString2(loadConfig().machineId));
|
|
@@ -12128,6 +12192,7 @@ function tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType)
|
|
|
12128
12192
|
return false;
|
|
12129
12193
|
}
|
|
12130
12194
|
LOG.info("MeshQueue", `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
|
|
12195
|
+
retractActionableSkipIfPreviouslyNotified(meshId, task.id);
|
|
12131
12196
|
beginTaskDispatchInFlight(meshId, task.id);
|
|
12132
12197
|
if (node?.daemonId && components.dispatchMeshCommand) {
|
|
12133
12198
|
const isLocalNode = components.cliManager.adapters.has(sessionId);
|
|
@@ -12217,6 +12282,28 @@ function isActionableSkipReason(reason) {
|
|
|
12217
12282
|
if (!reason) return false;
|
|
12218
12283
|
return ACTIONABLE_SKIP_REASON_PREFIXES.some((prefix) => reason === prefix || reason.startsWith(prefix));
|
|
12219
12284
|
}
|
|
12285
|
+
function isTargetNodeTransientlyUnresolved(mesh, task) {
|
|
12286
|
+
const targetNodeId = readNonEmptyString2(task.targetNodeId);
|
|
12287
|
+
if (!targetNodeId) return false;
|
|
12288
|
+
const node = Array.isArray(mesh?.nodes) ? mesh.nodes.find((n) => meshNodeIdMatches(n, targetNodeId)) : void 0;
|
|
12289
|
+
if (node && node.worktreeBootstrap?.status === "running" && !isWorktreeBootstrapStaleRunning(node)) {
|
|
12290
|
+
return true;
|
|
12291
|
+
}
|
|
12292
|
+
return isWithinCloneBootstrapGrace(targetNodeId);
|
|
12293
|
+
}
|
|
12294
|
+
function retractActionableSkipIfPreviouslyNotified(meshId, taskId) {
|
|
12295
|
+
const dedupKey = `${meshId}:${taskId}`;
|
|
12296
|
+
if (!lastActionableSkipNotified.delete(dedupKey)) return;
|
|
12297
|
+
try {
|
|
12298
|
+
const coordinatorDaemonId = readNonEmptyString2(loadConfig().machineId) || void 0;
|
|
12299
|
+
const removed = retractPendingDispatchBlockedEvent(meshId, taskId, coordinatorDaemonId);
|
|
12300
|
+
if (removed > 0) {
|
|
12301
|
+
LOG.info("MeshQueue", `Retracted ${removed} stale dispatch-blocked event(s) for task ${taskId} (mesh ${meshId}) \u2014 its blocker resolved`);
|
|
12302
|
+
}
|
|
12303
|
+
} catch (e) {
|
|
12304
|
+
LOG.warn("MeshQueue", `Failed to retract stale dispatch-blocked event for task ${taskId} (mesh ${meshId}): ${e?.message || e}`);
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
12220
12307
|
function actionableSkipGuidance(reason) {
|
|
12221
12308
|
if (reason === "target_node_id_unmatched") return {
|
|
12222
12309
|
summary: "it is pinned to a target node id that matches no node in the mesh (the node may have been removed, or its id form does not resolve)",
|
|
@@ -12249,6 +12336,7 @@ function actionableSkipGuidance(reason) {
|
|
|
12249
12336
|
}
|
|
12250
12337
|
function notifyCoordinatorOfActionableSkip(meshId, taskId, reason, nodeId) {
|
|
12251
12338
|
if (!isActionableSkipReason(reason)) return;
|
|
12339
|
+
if (reason === "target_node_id_unmatched" && isWithinCloneBootstrapGrace(readNonEmptyString2(nodeId))) return;
|
|
12252
12340
|
const dedupKey = `${meshId}:${taskId}`;
|
|
12253
12341
|
if (lastActionableSkipNotified.get(dedupKey) === reason) return;
|
|
12254
12342
|
lastActionableSkipNotified.set(dedupKey, reason);
|
|
@@ -12486,9 +12574,13 @@ function markAutoLaunch(meshId, taskId, args) {
|
|
|
12486
12574
|
error: args.error
|
|
12487
12575
|
});
|
|
12488
12576
|
if (args.status === "skipped") {
|
|
12489
|
-
|
|
12577
|
+
if (isActionableSkipReason(args.reason)) {
|
|
12578
|
+
notifyCoordinatorOfActionableSkip(meshId, taskId, args.reason, args.nodeId);
|
|
12579
|
+
} else if (args.reason === TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON) {
|
|
12580
|
+
retractActionableSkipIfPreviouslyNotified(meshId, taskId);
|
|
12581
|
+
}
|
|
12490
12582
|
} else {
|
|
12491
|
-
|
|
12583
|
+
retractActionableSkipIfPreviouslyNotified(meshId, taskId);
|
|
12492
12584
|
}
|
|
12493
12585
|
}
|
|
12494
12586
|
async function resolveUsableProvider(components, nodeId, node, requiredTags) {
|
|
@@ -12576,9 +12668,10 @@ async function maybeAutoLaunchOneQueueSession(components, meshId, mesh) {
|
|
|
12576
12668
|
const matched = mesh.nodes.filter((n) => !task.targetNodeId || meshNodeIdMatches(n, task.targetNodeId));
|
|
12577
12669
|
return matched.length > 0 && matched.every((n) => n?.isLocalWorktree === true);
|
|
12578
12670
|
})();
|
|
12671
|
+
const targetTransientlyUnresolved = targetPinUnmatched && isTargetNodeTransientlyUnresolved(mesh, task);
|
|
12579
12672
|
markAutoLaunch(meshId, task.id, {
|
|
12580
12673
|
status: "skipped",
|
|
12581
|
-
reason: convergenceOntoWorktree ? "mesh_convergence_target_is_worktree" : targetPinUnmatched ? "target_node_id_unmatched" : "no_node_satisfies_required_tags",
|
|
12674
|
+
reason: convergenceOntoWorktree ? "mesh_convergence_target_is_worktree" : targetTransientlyUnresolved ? TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON : targetPinUnmatched ? "target_node_id_unmatched" : "no_node_satisfies_required_tags",
|
|
12582
12675
|
nodeId: task.targetNodeId
|
|
12583
12676
|
});
|
|
12584
12677
|
continue;
|
|
@@ -12931,7 +13024,7 @@ function runIdleMaintenanceThenAssignQueue(components, args) {
|
|
|
12931
13024
|
});
|
|
12932
13025
|
});
|
|
12933
13026
|
}
|
|
12934
|
-
var import_fs13, IDLE_AUTO_FAST_FORWARD_THROTTLE_MS, idleAutoFastForwardLastAttempt, DISPATCH_CONFIRM_TIMEOUT_MS, DISPATCH_CONNECT_TIMEOUT_MS, dispatchWarmupGetterMissingWarned, LOCAL_LAUNCH_READY_TIMEOUT_MS, LOCAL_LAUNCH_READY_POLL_MS, autoLaunchInProgress, autoLaunchCooldownUntil, AUTO_LAUNCH_COOLDOWN_MS, AUTO_LAUNCH_AWAIT_CLAIM_MS, lastAutoLaunchLedgerKey, AUTO_LAUNCH_LEDGER_DEDUP_MAX, ACTIONABLE_SKIP_REASON_PREFIXES, lastActionableSkipNotified;
|
|
13027
|
+
var import_fs13, IDLE_AUTO_FAST_FORWARD_THROTTLE_MS, idleAutoFastForwardLastAttempt, DISPATCH_CONFIRM_TIMEOUT_MS, DISPATCH_CONNECT_TIMEOUT_MS, dispatchWarmupGetterMissingWarned, LOCAL_LAUNCH_READY_TIMEOUT_MS, LOCAL_LAUNCH_READY_POLL_MS, autoLaunchInProgress, autoLaunchCooldownUntil, AUTO_LAUNCH_COOLDOWN_MS, AUTO_LAUNCH_AWAIT_CLAIM_MS, lastAutoLaunchLedgerKey, AUTO_LAUNCH_LEDGER_DEDUP_MAX, ACTIONABLE_SKIP_REASON_PREFIXES, TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON, lastActionableSkipNotified;
|
|
12935
13028
|
var init_mesh_queue_assignment = __esm({
|
|
12936
13029
|
"src/mesh/mesh-queue-assignment.ts"() {
|
|
12937
13030
|
"use strict";
|
|
@@ -12954,6 +13047,7 @@ var init_mesh_queue_assignment = __esm({
|
|
|
12954
13047
|
init_mesh_events_utils();
|
|
12955
13048
|
init_mesh_events_pending();
|
|
12956
13049
|
init_worktree_bootstrap_config();
|
|
13050
|
+
init_mesh_clone_grace();
|
|
12957
13051
|
init_mesh_task_inflight();
|
|
12958
13052
|
IDLE_AUTO_FAST_FORWARD_THROTTLE_MS = 30 * 60 * 1e3;
|
|
12959
13053
|
idleAutoFastForwardLastAttempt = /* @__PURE__ */ new Map();
|
|
@@ -12980,6 +13074,7 @@ var init_mesh_queue_assignment = __esm({
|
|
|
12980
13074
|
"provider_unusable",
|
|
12981
13075
|
"dirty_workspace"
|
|
12982
13076
|
];
|
|
13077
|
+
TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON = "target_node_bootstrap_pending";
|
|
12983
13078
|
lastActionableSkipNotified = /* @__PURE__ */ new Map();
|
|
12984
13079
|
}
|
|
12985
13080
|
});
|
|
@@ -48986,6 +49081,7 @@ init_dist();
|
|
|
48986
49081
|
init_mesh_host_ownership();
|
|
48987
49082
|
init_worktree_bootstrap_config();
|
|
48988
49083
|
init_mesh_events();
|
|
49084
|
+
init_mesh_clone_grace();
|
|
48989
49085
|
init_config();
|
|
48990
49086
|
async function decideOssCloneSync(ossCtx, worktreeOssSha, sourceSha, rg) {
|
|
48991
49087
|
if (!worktreeOssSha || !sourceSha || worktreeOssSha === sourceSha) return "noop";
|
|
@@ -49267,6 +49363,26 @@ var meshCrudHandlers = {
|
|
|
49267
49363
|
explicitCleanupMode ?? (node?.isLocalWorktree === true ? "stop_and_delete" : void 0) ?? mesh?.policy?.sessionCleanupOnNodeRemove
|
|
49268
49364
|
);
|
|
49269
49365
|
const explicitSessionIds = Array.isArray(args?.sessionIds) ? args.sessionIds.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
|
|
49366
|
+
if (node?.isLocalWorktree) {
|
|
49367
|
+
const nodeDaemonId = typeof node.daemonId === "string" ? node.daemonId.trim() : void 0;
|
|
49368
|
+
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, ctx.deps.statusInstanceId) && ctx.deps.dispatchMeshCommand && !args?._meshDirectDispatch;
|
|
49369
|
+
if (!isRemoteWorktree) {
|
|
49370
|
+
const precheck = await ctx.precheckLocalWorktreeRemovable({ mesh, node, nodeId, force: args?.force === true });
|
|
49371
|
+
if (precheck.ok === false) {
|
|
49372
|
+
return {
|
|
49373
|
+
success: false,
|
|
49374
|
+
removed: false,
|
|
49375
|
+
code: precheck.code,
|
|
49376
|
+
error: precheck.error,
|
|
49377
|
+
recoveryHint: precheck.recoveryHint,
|
|
49378
|
+
// No sessionCleanup key: the session was deliberately NOT
|
|
49379
|
+
// touched. worktreeCleanup mirrors the destructive path's
|
|
49380
|
+
// refusal shape so existing callers see the same code.
|
|
49381
|
+
worktreeCleanup: { success: false, code: precheck.code, error: precheck.error, recoveryHint: precheck.recoveryHint }
|
|
49382
|
+
};
|
|
49383
|
+
}
|
|
49384
|
+
}
|
|
49385
|
+
}
|
|
49270
49386
|
let sessionCleanup;
|
|
49271
49387
|
if (node && sessionCleanupMode !== "preserve") {
|
|
49272
49388
|
sessionCleanup = await ctx.cleanupMeshSessions({
|
|
@@ -49377,6 +49493,8 @@ var meshCrudHandlers = {
|
|
|
49377
49493
|
...typeof args === "object" && args !== null ? args : {},
|
|
49378
49494
|
_meshDirectDispatch: true
|
|
49379
49495
|
});
|
|
49496
|
+
const forwardedNodeId = forwarded?.node?.id;
|
|
49497
|
+
if (typeof forwardedNodeId === "string" && forwardedNodeId) noteRecentlyClonedNode(forwardedNodeId);
|
|
49380
49498
|
return forwarded ?? { success: false, error: "no response from remote node" };
|
|
49381
49499
|
}
|
|
49382
49500
|
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
@@ -49426,6 +49544,7 @@ var meshCrudHandlers = {
|
|
|
49426
49544
|
if (inlineForReconcile) ctx.updateInlineMeshNode(meshId, inlineForReconcile, node);
|
|
49427
49545
|
ctx.invalidateAggregateMeshStatus(meshId);
|
|
49428
49546
|
}
|
|
49547
|
+
if (typeof node?.id === "string" && node.id) noteRecentlyClonedNode(node.id);
|
|
49429
49548
|
const persistWorktreeSetupState = async (bootstrapState2) => {
|
|
49430
49549
|
node.worktreeBootstrap = bootstrapState2;
|
|
49431
49550
|
if (meshRecord.inline) {
|
|
@@ -54451,6 +54570,7 @@ var DaemonCommandRouter = class {
|
|
|
54451
54570
|
normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
|
|
54452
54571
|
cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
|
|
54453
54572
|
cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
|
|
54573
|
+
precheckLocalWorktreeRemovable: this.precheckLocalWorktreeRemovable.bind(this),
|
|
54454
54574
|
startMeshRefineJob: this.startMeshRefineJob.bind(this),
|
|
54455
54575
|
batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
|
|
54456
54576
|
startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
|
|
@@ -54667,6 +54787,111 @@ var DaemonCommandRouter = class {
|
|
|
54667
54787
|
}
|
|
54668
54788
|
return fs32.existsSync(dir) ? { removed: false, residue: true, error: String(lastErr?.message || lastErr || "unknown rm error") } : { removed: true, residue: false };
|
|
54669
54789
|
}
|
|
54790
|
+
/**
|
|
54791
|
+
* Non-destructive precheck mirroring every REFUSAL condition in
|
|
54792
|
+
* {@link cleanupLocalWorktreeNode} — missing workspace / source-repo / branch
|
|
54793
|
+
* metadata, unexpected (non-managed) path, branch mismatch — PLUS the
|
|
54794
|
+
* dirty-worktree guard that `removeWorktree(requireClean)` enforces
|
|
54795
|
+
* (`git status --porcelain`). It performs ZERO destructive actions: no
|
|
54796
|
+
* `git worktree remove`, no `git worktree prune`, no directory deletion.
|
|
54797
|
+
*
|
|
54798
|
+
* remove_mesh_node calls this BEFORE any session cleanup so that a refusal
|
|
54799
|
+
* (the common one being a dirty worktree) does not first stop/delete the
|
|
54800
|
+
* delegated session and orphan it — the original ordering bug. Success/skip
|
|
54801
|
+
* cases that the real cleanup handles idempotently (worktree path already
|
|
54802
|
+
* gone, git-de-registered residue) are NOT refusals and return `{ ok: true }`.
|
|
54803
|
+
*
|
|
54804
|
+
* `force:true` skips the dirty guard, preserving `removeWorktree`'s
|
|
54805
|
+
* `requireClean: !force` semantics. This is a read-only superset check; the
|
|
54806
|
+
* authoritative `requireClean` guard inside `removeWorktree` is intentionally
|
|
54807
|
+
* kept as a second line of defense against a precheck→execute race.
|
|
54808
|
+
*/
|
|
54809
|
+
async precheckLocalWorktreeRemovable(args) {
|
|
54810
|
+
const sessionPreservedNote = " The delegated session was left running (not stopped) \u2014 resolve the issue and retry mesh_remove_node.";
|
|
54811
|
+
const workspace = typeof args.node?.workspace === "string" ? args.node.workspace.trim() : "";
|
|
54812
|
+
if (!workspace) {
|
|
54813
|
+
return {
|
|
54814
|
+
ok: false,
|
|
54815
|
+
code: "mesh_worktree_cleanup_missing_workspace",
|
|
54816
|
+
error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
|
|
54817
|
+
recoveryHint: "Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains." + sessionPreservedNote
|
|
54818
|
+
};
|
|
54819
|
+
}
|
|
54820
|
+
if (!fs32.existsSync(workspace)) return { ok: true };
|
|
54821
|
+
const sourceNode = args.node?.clonedFromNodeId ? args.mesh?.nodes?.find((n) => meshNodeIdMatches(n, args.node.clonedFromNodeId)) : args.mesh?.nodes?.find((n) => !n.isLocalWorktree);
|
|
54822
|
+
const repoRoot = typeof sourceNode?.repoRoot === "string" && sourceNode.repoRoot.trim() ? sourceNode.repoRoot.trim() : typeof sourceNode?.workspace === "string" && sourceNode.workspace.trim() ? sourceNode.workspace.trim() : "";
|
|
54823
|
+
if (!repoRoot || !fs32.existsSync(repoRoot)) {
|
|
54824
|
+
return {
|
|
54825
|
+
ok: false,
|
|
54826
|
+
code: "mesh_worktree_cleanup_missing_source_repo",
|
|
54827
|
+
error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
|
|
54828
|
+
recoveryHint: "Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying." + sessionPreservedNote
|
|
54829
|
+
};
|
|
54830
|
+
}
|
|
54831
|
+
if (typeof args.node?.worktreeBranch !== "string" || !args.node.worktreeBranch.trim()) {
|
|
54832
|
+
return {
|
|
54833
|
+
ok: false,
|
|
54834
|
+
code: "mesh_worktree_cleanup_missing_branch",
|
|
54835
|
+
error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
|
|
54836
|
+
recoveryHint: "Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata." + sessionPreservedNote
|
|
54837
|
+
};
|
|
54838
|
+
}
|
|
54839
|
+
const { resolveWorktreePath: resolveWorktreePath2, listWorktrees: listWorktrees2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
|
|
54840
|
+
const normalizePath = (value) => {
|
|
54841
|
+
const resolved = (0, import_path16.resolve)(value);
|
|
54842
|
+
try {
|
|
54843
|
+
return fs32.realpathSync(resolved);
|
|
54844
|
+
} catch {
|
|
54845
|
+
return resolved;
|
|
54846
|
+
}
|
|
54847
|
+
};
|
|
54848
|
+
const expectedPath = normalizePath(resolveWorktreePath2(repoRoot, String(args.mesh?.name || args.mesh?.id || "mesh"), args.node.worktreeBranch));
|
|
54849
|
+
const actualPath = normalizePath(workspace);
|
|
54850
|
+
if (actualPath !== expectedPath) {
|
|
54851
|
+
return {
|
|
54852
|
+
ok: false,
|
|
54853
|
+
code: "mesh_worktree_cleanup_unexpected_path",
|
|
54854
|
+
error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
|
|
54855
|
+
recoveryHint: "Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree." + sessionPreservedNote
|
|
54856
|
+
};
|
|
54857
|
+
}
|
|
54858
|
+
const entries = await listWorktrees2(repoRoot);
|
|
54859
|
+
const managedEntry = entries.find((entry) => normalizePath(entry.path) === actualPath);
|
|
54860
|
+
if (!managedEntry) return { ok: true };
|
|
54861
|
+
if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
|
|
54862
|
+
return {
|
|
54863
|
+
ok: false,
|
|
54864
|
+
code: "mesh_worktree_cleanup_branch_mismatch",
|
|
54865
|
+
error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
|
|
54866
|
+
recoveryHint: "Inspect the worktree branch and mesh metadata before retrying cleanup." + sessionPreservedNote
|
|
54867
|
+
};
|
|
54868
|
+
}
|
|
54869
|
+
if (args.force !== true) {
|
|
54870
|
+
const { execFile: execFile5 } = await import("child_process");
|
|
54871
|
+
const { promisify: promisify8 } = await import("util");
|
|
54872
|
+
const execFileAsync4 = promisify8(execFile5);
|
|
54873
|
+
try {
|
|
54874
|
+
const { stdout } = await execFileAsync4("git", ["status", "--porcelain"], {
|
|
54875
|
+
cwd: workspace,
|
|
54876
|
+
encoding: "utf8",
|
|
54877
|
+
timeout: 3e4,
|
|
54878
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
54879
|
+
windowsHide: true
|
|
54880
|
+
});
|
|
54881
|
+
if (stdout.trim()) {
|
|
54882
|
+
return {
|
|
54883
|
+
ok: false,
|
|
54884
|
+
code: "mesh_worktree_cleanup_dirty",
|
|
54885
|
+
error: `Refusing to remove dirty worktree: ${workspace}`,
|
|
54886
|
+
recoveryHint: "Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe." + sessionPreservedNote
|
|
54887
|
+
};
|
|
54888
|
+
}
|
|
54889
|
+
} catch {
|
|
54890
|
+
return { ok: true };
|
|
54891
|
+
}
|
|
54892
|
+
}
|
|
54893
|
+
return { ok: true };
|
|
54894
|
+
}
|
|
54670
54895
|
async cleanupLocalWorktreeNode(args) {
|
|
54671
54896
|
const workspace = typeof args.node?.workspace === "string" ? args.node.workspace.trim() : "";
|
|
54672
54897
|
if (!workspace) {
|