@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.
@@ -0,0 +1,8 @@
1
+ export declare const CLONE_BOOTSTRAP_GRACE_MS: number;
2
+ /** Record that a worktree node id was just cloned, opening its transient grace window. */
3
+ export declare function noteRecentlyClonedNode(nodeId: string | undefined | null, nowMs?: number): void;
4
+ /** True when nodeId was cloned within the grace window (and the entry has not expired). */
5
+ export declare function isWithinCloneBootstrapGrace(nodeId: string | undefined | null, nowMs?: number): boolean;
6
+ /** Drop a node's grace entry (e.g. once it has fully resolved into the mesh view). */
7
+ export declare function clearCloneBootstrapGrace(nodeId: string | undefined | null): void;
8
+ export declare function __resetCloneBootstrapGraceForTests(): void;
@@ -44,6 +44,20 @@ export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoord
44
44
  export declare function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>, opts?: {
45
45
  onlyEvents?: ReadonlySet<string>;
46
46
  }): PendingMeshCoordinatorEvent[];
47
+ /**
48
+ * FALSE-BLOCKER-CLONE-QUEUE: retract any still-UNDELIVERED `mesh:dispatch_blocked`
49
+ * actionable-skip event for a task whose blocker has since resolved (the task was
50
+ * claimed, or its skip transitioned to a self-resolving transient reason). Without
51
+ * this, a `target_node_id_unmatched` blocker paged during the brief clone/bootstrap
52
+ * propagation window would linger in the coordinator's pending queue and surface as a
53
+ * false "actionable blocker — will NOT clear on its own" even after the task dispatched.
54
+ *
55
+ * Only removes events that have NOT yet been drained/delivered to the coordinator — an
56
+ * already-delivered message cannot be unsent, but de-dup re-arm (caller side) plus this
57
+ * retraction guarantee no NEW stale blocker accumulates. Best-effort across both the
58
+ * SQLite inbox and the JSONL legacy files (scoped + shared). Returns rows removed.
59
+ */
60
+ export declare function retractPendingDispatchBlockedEvent(meshId: string | undefined, taskId: string | undefined, coordinatorDaemonId?: string): number;
47
61
  /** Peek at pending coordinator events without draining (non-destructive). */
48
62
  export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>): readonly PendingMeshCoordinatorEvent[];
49
63
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.419",
3
+ "version": "0.9.82-rc.420",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.419",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.420",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -18,6 +18,7 @@ import {
18
18
  } from '../../mesh/worktree-bootstrap-config.js';
19
19
  import { loadRepoSettings } from '../../config/repo-settings.js';
20
20
  import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../../mesh/mesh-events.js';
21
+ import { noteRecentlyClonedNode } from '../../mesh/mesh-clone-grace.js';
21
22
  import { loadConfig } from '../../config/config.js';
22
23
  import {
23
24
  hydrateInlineMeshDirectTruth,
@@ -417,6 +418,39 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
417
418
  const explicitSessionIds = Array.isArray(args?.sessionIds)
418
419
  ? (args.sessionIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
419
420
  : undefined;
421
+ // Precheck-first: for a LOCAL worktree removal, validate removability
422
+ // with a purely non-destructive precheck BEFORE touching the session.
423
+ // The session cleanup below is destructive and irreversible
424
+ // (stop_and_delete), so a refusal that fires only AFTER it — as the old
425
+ // ordering did when removeWorktree rejected a dirty worktree — orphaned
426
+ // the delegated session. Running the precheck here means a refusal
427
+ // (dirty worktree, missing/mismatched metadata, etc.) returns with the
428
+ // session left fully intact. Remote-forwarded worktrees are prechecked
429
+ // on the owning daemon (it runs this same handler), so we only gate the
430
+ // local case here. Success/skip cases return ok:true and fall through to
431
+ // the unchanged normal flow.
432
+ if (node?.isLocalWorktree) {
433
+ const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
434
+ const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, ctx.deps.statusInstanceId) && ctx.deps.dispatchMeshCommand
435
+ && !args?._meshDirectDispatch;
436
+ if (!isRemoteWorktree) {
437
+ const precheck = await ctx.precheckLocalWorktreeRemovable({ mesh, node, nodeId, force: args?.force === true });
438
+ if (precheck.ok === false) {
439
+ return {
440
+ success: false,
441
+ removed: false,
442
+ code: precheck.code,
443
+ error: precheck.error,
444
+ recoveryHint: precheck.recoveryHint,
445
+ // No sessionCleanup key: the session was deliberately NOT
446
+ // touched. worktreeCleanup mirrors the destructive path's
447
+ // refusal shape so existing callers see the same code.
448
+ worktreeCleanup: { success: false, code: precheck.code, error: precheck.error, recoveryHint: precheck.recoveryHint },
449
+ };
450
+ }
451
+ }
452
+ }
453
+
420
454
  let sessionCleanup: Record<string, unknown> | undefined;
421
455
  if (node && sessionCleanupMode !== 'preserve') {
422
456
  sessionCleanup = await ctx.cleanupMeshSessions({
@@ -597,6 +631,13 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
597
631
  ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
598
632
  _meshDirectDispatch: true,
599
633
  });
634
+ // FALSE-BLOCKER-CLONE-QUEUE: open the transient grace window for the freshly
635
+ // cloned node on THIS coordinator daemon. The clone ran (and wrote the inline-
636
+ // cache node) on the remote source daemon; its inline entry has not propagated
637
+ // here yet, so a queue task pinned to it would transiently look like a permanent
638
+ // 'target_node_id_unmatched'. Recording its id marks the unmatch as transient.
639
+ const forwardedNodeId = (forwarded as { node?: { id?: unknown } } | null | undefined)?.node?.id;
640
+ if (typeof forwardedNodeId === 'string' && forwardedNodeId) noteRecentlyClonedNode(forwardedNodeId);
600
641
  return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
601
642
  }
602
643
 
@@ -654,6 +695,12 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
654
695
  ctx.invalidateAggregateMeshStatus(meshId);
655
696
  }
656
697
 
698
+ // FALSE-BLOCKER-CLONE-QUEUE: open the transient grace window for the freshly cloned
699
+ // node. A queue task pinned to it (target_node pin) enqueued before bootstrap
700
+ // completes / the inline-cache entry fully settles must be classified as a transient
701
+ // skip, not a permanent 'target_node_id_unmatched' actionable blocker.
702
+ if (typeof node?.id === 'string' && node.id) noteRecentlyClonedNode(node.id);
703
+
657
704
  const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
658
705
  node.worktreeBootstrap = bootstrapState;
659
706
  if (meshRecord.inline) {
@@ -31,6 +31,16 @@ export type CleanupLocalWorktreeNodeResult =
31
31
  | { success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown>; recovered?: boolean; residue?: boolean; residueWarning?: string; residueError?: string }
32
32
  | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> };
33
33
 
34
+ /**
35
+ * Result of the non-destructive local-worktree removability precheck. `ok:false`
36
+ * carries the same refusal `code`/`error`/`recoveryHint` that the destructive
37
+ * cleanup would have returned, so callers can refuse a removal BEFORE performing
38
+ * any irreversible step (e.g. stopping/deleting delegated sessions).
39
+ */
40
+ export type WorktreeRemovalPrecheckResult =
41
+ | { ok: true }
42
+ | { ok: false; code: string; error: string; recoveryHint: string };
43
+
34
44
  /**
35
45
  * Router-private collaborators injected at dispatch. Each is a bound method or
36
46
  * field of DaemonCommandRouter; handlers that don't need a given collaborator
@@ -85,6 +95,18 @@ export interface MedFamilyContext {
85
95
  force?: boolean;
86
96
  }) => Promise<CleanupLocalWorktreeNodeResult>;
87
97
 
98
+ /**
99
+ * Bound `DaemonCommandRouter.precheckLocalWorktreeRemovable` — purely
100
+ * non-destructive validation of whether a local worktree node can be removed.
101
+ * Called BEFORE session cleanup so a refusal does not orphan the session.
102
+ */
103
+ precheckLocalWorktreeRemovable: (args: {
104
+ mesh: any;
105
+ node: any;
106
+ nodeId: string;
107
+ force?: boolean;
108
+ }) => Promise<WorktreeRemovalPrecheckResult>;
109
+
88
110
  /** Bound `DaemonCommandRouter.startMeshRefineJob` (async execute path). */
89
111
  startMeshRefineJob: (meshId: string, nodeId: string, args: any) => Promise<CommandRouterResult>;
90
112
 
@@ -647,6 +647,7 @@ export class DaemonCommandRouter {
647
647
  normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
648
648
  cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
649
649
  cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
650
+ precheckLocalWorktreeRemovable: this.precheckLocalWorktreeRemovable.bind(this),
650
651
  startMeshRefineJob: this.startMeshRefineJob.bind(this),
651
652
  batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
652
653
  startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
@@ -916,6 +917,133 @@ export class DaemonCommandRouter {
916
917
  : { removed: true, residue: false };
917
918
  }
918
919
 
920
+ /**
921
+ * Non-destructive precheck mirroring every REFUSAL condition in
922
+ * {@link cleanupLocalWorktreeNode} — missing workspace / source-repo / branch
923
+ * metadata, unexpected (non-managed) path, branch mismatch — PLUS the
924
+ * dirty-worktree guard that `removeWorktree(requireClean)` enforces
925
+ * (`git status --porcelain`). It performs ZERO destructive actions: no
926
+ * `git worktree remove`, no `git worktree prune`, no directory deletion.
927
+ *
928
+ * remove_mesh_node calls this BEFORE any session cleanup so that a refusal
929
+ * (the common one being a dirty worktree) does not first stop/delete the
930
+ * delegated session and orphan it — the original ordering bug. Success/skip
931
+ * cases that the real cleanup handles idempotently (worktree path already
932
+ * gone, git-de-registered residue) are NOT refusals and return `{ ok: true }`.
933
+ *
934
+ * `force:true` skips the dirty guard, preserving `removeWorktree`'s
935
+ * `requireClean: !force` semantics. This is a read-only superset check; the
936
+ * authoritative `requireClean` guard inside `removeWorktree` is intentionally
937
+ * kept as a second line of defense against a precheck→execute race.
938
+ */
939
+ private async precheckLocalWorktreeRemovable(args: {
940
+ mesh: any;
941
+ node: any;
942
+ nodeId: string;
943
+ force?: boolean;
944
+ }): Promise<{ ok: true } | { ok: false; code: string; error: string; recoveryHint: string }> {
945
+ const sessionPreservedNote = ' The delegated session was left running (not stopped) — resolve the issue and retry mesh_remove_node.';
946
+ const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
947
+ if (!workspace) {
948
+ return {
949
+ ok: false,
950
+ code: 'mesh_worktree_cleanup_missing_workspace',
951
+ error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
952
+ recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.' + sessionPreservedNote,
953
+ };
954
+ }
955
+
956
+ // Worktree path already gone → not a refusal; the real cleanup returns a
957
+ // skipped:true success and the node is dropped from the registry.
958
+ if (!fs.existsSync(workspace)) return { ok: true };
959
+
960
+ const sourceNode = args.node?.clonedFromNodeId
961
+ ? args.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, args.node.clonedFromNodeId))
962
+ : args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
963
+ const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
964
+ ? sourceNode.repoRoot.trim()
965
+ : typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
966
+ ? sourceNode.workspace.trim()
967
+ : '';
968
+ if (!repoRoot || !fs.existsSync(repoRoot)) {
969
+ return {
970
+ ok: false,
971
+ code: 'mesh_worktree_cleanup_missing_source_repo',
972
+ error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
973
+ recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.' + sessionPreservedNote,
974
+ };
975
+ }
976
+ if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
977
+ return {
978
+ ok: false,
979
+ code: 'mesh_worktree_cleanup_missing_branch',
980
+ error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
981
+ recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.' + sessionPreservedNote,
982
+ };
983
+ }
984
+
985
+ const { resolveWorktreePath, listWorktrees } = await import('../git/git-worktree.js');
986
+ const normalizePath = (value: string) => {
987
+ const resolved = pathResolve(value);
988
+ try { return fs.realpathSync(resolved); } catch { return resolved; }
989
+ };
990
+ const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
991
+ const actualPath = normalizePath(workspace);
992
+ if (actualPath !== expectedPath) {
993
+ return {
994
+ ok: false,
995
+ code: 'mesh_worktree_cleanup_unexpected_path',
996
+ error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
997
+ 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,
998
+ };
999
+ }
1000
+
1001
+ const entries = await listWorktrees(repoRoot);
1002
+ const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
1003
+ // De-registered residue (git no longer lists it as a worktree) is an
1004
+ // idempotent recovery path in the real cleanup, NOT a refusal — neither the
1005
+ // branch-mismatch nor the dirty check applies, so let the removal proceed.
1006
+ if (!managedEntry) return { ok: true };
1007
+
1008
+ if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
1009
+ return {
1010
+ ok: false,
1011
+ code: 'mesh_worktree_cleanup_branch_mismatch',
1012
+ error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
1013
+ recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.' + sessionPreservedNote,
1014
+ };
1015
+ }
1016
+
1017
+ // Dirty-worktree guard — a read-only mirror of removeWorktree(requireClean)
1018
+ // (`git status --porcelain` run inside the worktree). `force:true` skips it,
1019
+ // preserving the requireClean:!force semantics.
1020
+ if (args.force !== true) {
1021
+ const { execFile } = await import('node:child_process');
1022
+ const { promisify } = await import('node:util');
1023
+ const execFileAsync = promisify(execFile);
1024
+ try {
1025
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
1026
+ cwd: workspace, encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
1027
+ });
1028
+ if (stdout.trim()) {
1029
+ return {
1030
+ ok: false,
1031
+ code: 'mesh_worktree_cleanup_dirty',
1032
+ error: `Refusing to remove dirty worktree: ${workspace}`,
1033
+ 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,
1034
+ };
1035
+ }
1036
+ } catch {
1037
+ // A status probe failure is not itself proof of dirtiness; defer to
1038
+ // the authoritative removeWorktree(requireClean) guard rather than
1039
+ // refusing here (which would block an otherwise-clean removal).
1040
+ return { ok: true };
1041
+ }
1042
+ }
1043
+
1044
+ return { ok: true };
1045
+ }
1046
+
919
1047
  private async cleanupLocalWorktreeNode(args: {
920
1048
  mesh: any;
921
1049
  node: any;
@@ -0,0 +1,68 @@
1
+ import { normalizeMeshNodeId, type MeshNodeIdentified } from '@adhdev/mesh-shared';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Recently-cloned worktree node grace window
5
+ // ---------------------------------------------------------------------------
6
+ // FALSE-BLOCKER-CLONE-QUEUE: right after mesh_clone_node registers a worktree node, a
7
+ // queue task pinned to that node id (target_node pin) can transiently find NO matching
8
+ // node in the coordinator-owning daemon's mesh view, because:
9
+ // - the clone wrote the node ONLY into the inline mesh cache (med-family clone branch),
10
+ // and that inline entry has not yet PROPAGATED to the coordinator daemon (the clone
11
+ // may have run on the source node's machine and forwarded), and/or
12
+ // - the node's worktree bootstrap (npm install / native-addon repair) is still running,
13
+ // so the claim gate defers anyway.
14
+ // In BOTH cases the unmatch SELF-RESOLVES within ~seconds (observed bootstrap ~2m8s) — it
15
+ // is NOT the permanent `target_node_id_unmatched` routing miss (a removed/dead node) that
16
+ // the actionable-blocker coordinator notification exists for. This registry lets the skip
17
+ // classifier tell the transient case apart from the permanent one: a node id recorded here
18
+ // (within the grace TTL) is a freshly cloned worktree whose unmatch should be reported as a
19
+ // transient, NON-actionable skip rather than paging the coordinator with a false
20
+ // "will NOT clear on its own" blocker.
21
+ //
22
+ // The window is floored well above the observed clone + bootstrap + inline-cache
23
+ // propagation latency so a slow-but-live clone is never misclassified as a permanent
24
+ // unmatch. A genuinely dead/removed node is never recorded here (or its entry has long
25
+ // expired), so its `target_node_id_unmatched` stays correctly actionable.
26
+ export const CLONE_BOOTSTRAP_GRACE_MS = 10 * 60 * 1000;
27
+
28
+ const recentlyClonedNodeExpiry = new Map<string, number>(); // normalized nodeId -> expiry epoch ms
29
+ const MAX_TRACKED_CLONED_NODES = 512;
30
+
31
+ function normalizeNodeIdKey(nodeId: string | undefined | null): string {
32
+ return normalizeMeshNodeId({ id: nodeId ?? undefined } as MeshNodeIdentified) ?? '';
33
+ }
34
+
35
+ /** Record that a worktree node id was just cloned, opening its transient grace window. */
36
+ export function noteRecentlyClonedNode(nodeId: string | undefined | null, nowMs: number = Date.now()): void {
37
+ const key = normalizeNodeIdKey(nodeId);
38
+ if (!key) return;
39
+ recentlyClonedNodeExpiry.set(key, nowMs + CLONE_BOOTSTRAP_GRACE_MS);
40
+ if (recentlyClonedNodeExpiry.size > MAX_TRACKED_CLONED_NODES) {
41
+ // Map iteration is insertion-ordered; the first key is the oldest-inserted entry.
42
+ const oldest = recentlyClonedNodeExpiry.keys().next().value;
43
+ if (oldest !== undefined) recentlyClonedNodeExpiry.delete(oldest);
44
+ }
45
+ }
46
+
47
+ /** True when nodeId was cloned within the grace window (and the entry has not expired). */
48
+ export function isWithinCloneBootstrapGrace(nodeId: string | undefined | null, nowMs: number = Date.now()): boolean {
49
+ const key = normalizeNodeIdKey(nodeId);
50
+ if (!key) return false;
51
+ const expiry = recentlyClonedNodeExpiry.get(key);
52
+ if (expiry === undefined) return false;
53
+ if (nowMs >= expiry) {
54
+ recentlyClonedNodeExpiry.delete(key);
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+
60
+ /** Drop a node's grace entry (e.g. once it has fully resolved into the mesh view). */
61
+ export function clearCloneBootstrapGrace(nodeId: string | undefined | null): void {
62
+ const key = normalizeNodeIdKey(nodeId);
63
+ if (key) recentlyClonedNodeExpiry.delete(key);
64
+ }
65
+
66
+ export function __resetCloneBootstrapGraceForTests(): void {
67
+ recentlyClonedNodeExpiry.clear();
68
+ }
@@ -541,6 +541,57 @@ export function drainPendingMeshCoordinatorEvents(
541
541
  return reconcilePendingMeshCoordinatorEvents(meshId, merged);
542
542
  }
543
543
 
544
+ /**
545
+ * FALSE-BLOCKER-CLONE-QUEUE: retract any still-UNDELIVERED `mesh:dispatch_blocked`
546
+ * actionable-skip event for a task whose blocker has since resolved (the task was
547
+ * claimed, or its skip transitioned to a self-resolving transient reason). Without
548
+ * this, a `target_node_id_unmatched` blocker paged during the brief clone/bootstrap
549
+ * propagation window would linger in the coordinator's pending queue and surface as a
550
+ * false "actionable blocker — will NOT clear on its own" even after the task dispatched.
551
+ *
552
+ * Only removes events that have NOT yet been drained/delivered to the coordinator — an
553
+ * already-delivered message cannot be unsent, but de-dup re-arm (caller side) plus this
554
+ * retraction guarantee no NEW stale blocker accumulates. Best-effort across both the
555
+ * SQLite inbox and the JSONL legacy files (scoped + shared). Returns rows removed.
556
+ */
557
+ export function retractPendingDispatchBlockedEvent(
558
+ meshId: string | undefined,
559
+ taskId: string | undefined,
560
+ coordinatorDaemonId?: string,
561
+ ): number {
562
+ if (!meshId || !taskId) return 0;
563
+ let removed = 0;
564
+ const matchesTask = (event: PendingMeshCoordinatorEvent | undefined): boolean => {
565
+ if (!event || event.event !== 'mesh:dispatch_blocked') return false;
566
+ const rowTaskId = readNonEmptyString((event.metadataEvent as Record<string, unknown> | undefined)?.taskId);
567
+ return rowTaskId === taskId;
568
+ };
569
+
570
+ // SQLite inbox: peek undrained rows for the mesh, hard-delete the matching ones by id.
571
+ try {
572
+ const store = MeshRuntimeStore.getInstance();
573
+ const ids: string[] = [];
574
+ for (const row of store.peekPendingEvents(meshId)) {
575
+ if (row.event !== 'mesh:dispatch_blocked') continue;
576
+ if (matchesTask(row.payload as PendingMeshCoordinatorEvent)) ids.push(row.id);
577
+ }
578
+ if (ids.length) removed += store.deletePendingEventsById(ids);
579
+ } catch { /* best-effort — JSONL retraction below still runs */ }
580
+
581
+ // JSONL legacy files: selectively drop matching lines (rewrites the rest back).
582
+ const daemonIds = normalizeCoordinatorDaemonIds(coordinatorDaemonId);
583
+ const primaryDaemonId = daemonIds[0];
584
+ const paths = primaryDaemonId
585
+ ? [getPendingEventsPath(meshId, primaryDaemonId), getPendingEventsPath(meshId)]
586
+ : [getPendingEventsPath(meshId)];
587
+ for (const path of paths) {
588
+ try {
589
+ removed += selectiveDrainFile(path, matchesTask).length;
590
+ } catch { /* best-effort */ }
591
+ }
592
+ return removed;
593
+ }
594
+
544
595
  /** Peek at pending coordinator events without draining (non-destructive). */
545
596
  export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>): readonly PendingMeshCoordinatorEvent[] {
546
597
  if (!meshId) return [];
@@ -18,8 +18,9 @@ import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
18
18
  import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, canonicalDaemonId, normalizeMeshWorkspaceForCompare, meshWorkspacesEquivalent, type MeshNodeIdentified } from '@adhdev/mesh-shared';
19
19
  import { findTerminalLedgerEvidenceForTask, hasUnterminalDirectDispatchLedgerEntry } from './mesh-events-stale.js';
20
20
  import { readNonEmptyString } from './mesh-events-utils.js';
21
- import { queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
21
+ import { queuePendingMeshCoordinatorEvent, retractPendingDispatchBlockedEvent } from './mesh-events-pending.js';
22
22
  import { isWorktreeBootstrapStaleRunning } from './worktree-bootstrap-config.js';
23
+ import { isWithinCloneBootstrapGrace } from './mesh-clone-grace.js';
23
24
  import { beginTaskDispatchInFlight, endTaskDispatchInFlight } from './mesh-task-inflight.js';
24
25
 
25
26
  /**
@@ -434,6 +435,13 @@ export function tryAssignQueueTask(
434
435
 
435
436
  LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
436
437
 
438
+ // FALSE-BLOCKER-CLONE-QUEUE (stale-event clear): the task just claimed and will dispatch,
439
+ // so any actionable blocker previously paged for it (e.g. a 'target_node_id_unmatched'
440
+ // emitted during the clone/bootstrap propagation window before the node became
441
+ // claimable) is now stale — re-arm the de-dup ledger and retract any undelivered
442
+ // dispatch_blocked event so the coordinator does not keep seeing a resolved blocker.
443
+ retractActionableSkipIfPreviouslyNotified(meshId, task.id);
444
+
437
445
  // CANON-IDENTITY single-flight: mark the just-claimed task in-flight the moment it
438
446
  // is handed to a transport. The atomic claim already prevents a concurrent claim,
439
447
  // but this lets requeueTask distinguish a genuinely-generating task (refuse the
@@ -606,6 +614,16 @@ const ACTIONABLE_SKIP_REASON_PREFIXES = [
606
614
  'dirty_workspace',
607
615
  ];
608
616
 
617
+ // FALSE-BLOCKER-CLONE-QUEUE: the TRANSIENT counterpart of 'target_node_id_unmatched'. A
618
+ // queue task pinned to a freshly cloned worktree node can transiently find no matching node
619
+ // (its inline-cache entry has not propagated to this coordinator daemon yet, and/or its
620
+ // worktree bootstrap is still running). That unmatch SELF-RESOLVES — it is not the permanent
621
+ // routing miss the actionable blocker exists for — so it is deliberately NOT listed in
622
+ // ACTIONABLE_SKIP_REASON_PREFIXES: isActionableSkipReason() returns false for it, so no
623
+ // "actionable blocker — will NOT clear on its own" coordinator page is emitted. The skip is
624
+ // still recorded to task.autoLaunch + the ledger for diagnosability.
625
+ const TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON = 'target_node_bootstrap_pending';
626
+
609
627
  // De-dup actionable-skip coordinator notifications: emit once per (mesh, task) until the
610
628
  // reason CHANGES, so the 4s reconcile loop re-marking the same skip does not re-notify. A
611
629
  // non-skip transition (or a genuine reason change) re-arms it. In-memory only — a daemon
@@ -617,6 +635,54 @@ function isActionableSkipReason(reason?: string): boolean {
617
635
  return ACTIONABLE_SKIP_REASON_PREFIXES.some(prefix => reason === prefix || reason.startsWith(prefix));
618
636
  }
619
637
 
638
+ /**
639
+ * FALSE-BLOCKER-CLONE-QUEUE: a target pin is TRANSIENTLY (not permanently) unresolved when
640
+ * the pinned node is a freshly cloned worktree that will auto-claim once its bootstrap
641
+ * completes / its inline-cache entry propagates — as opposed to a removed/dead node whose
642
+ * unmatch is a permanent, actionable routing miss. Two signals, either suffices:
643
+ * (a) the node IS visible in the (cache-merged) mesh view but its worktree bootstrap is
644
+ * still 'running' (and not stuck past the stale backstop), or
645
+ * (b) the node is NOT visible here yet, but a clone for its id was issued within the grace
646
+ * window (propagation/bootstrap latency) — see mesh-clone-grace.
647
+ * Conservative: a node neither bootstrap-running nor recently cloned → returns false, so a
648
+ * genuinely dead node keeps its permanent, actionable 'target_node_id_unmatched'.
649
+ */
650
+ function isTargetNodeTransientlyUnresolved(mesh: any, task: MeshWorkQueueEntry): boolean {
651
+ const targetNodeId = readNonEmptyString(task.targetNodeId);
652
+ if (!targetNodeId) return false;
653
+ const node = Array.isArray(mesh?.nodes)
654
+ ? mesh.nodes.find((n: any) => meshNodeIdMatches(n, targetNodeId))
655
+ : undefined;
656
+ if (node
657
+ && (node as { worktreeBootstrap?: { status?: string } }).worktreeBootstrap?.status === 'running'
658
+ && !isWorktreeBootstrapStaleRunning(node)) {
659
+ return true;
660
+ }
661
+ return isWithinCloneBootstrapGrace(targetNodeId);
662
+ }
663
+
664
+ /**
665
+ * FALSE-BLOCKER-CLONE-QUEUE (stale-event clear): once a task whose actionable blocker we
666
+ * previously paged either gets claimed or transitions to a self-resolving state, re-arm the
667
+ * de-dup ledger (so a later genuine blocker re-notifies) AND retract any still-undelivered
668
+ * dispatch_blocked pending event, so the coordinator's pending queue no longer carries the
669
+ * stale "will NOT clear on its own" warning. Cheap: only touches the pending store when this
670
+ * (mesh, task) actually had a prior actionable notification recorded.
671
+ */
672
+ function retractActionableSkipIfPreviouslyNotified(meshId: string, taskId: string): void {
673
+ const dedupKey = `${meshId}:${taskId}`;
674
+ if (!lastActionableSkipNotified.delete(dedupKey)) return; // nothing was paged → nothing to retract
675
+ try {
676
+ const coordinatorDaemonId = readNonEmptyString(loadConfig().machineId) || undefined;
677
+ const removed = retractPendingDispatchBlockedEvent(meshId, taskId, coordinatorDaemonId);
678
+ if (removed > 0) {
679
+ LOG.info('MeshQueue', `Retracted ${removed} stale dispatch-blocked event(s) for task ${taskId} (mesh ${meshId}) — its blocker resolved`);
680
+ }
681
+ } catch (e: any) {
682
+ LOG.warn('MeshQueue', `Failed to retract stale dispatch-blocked event for task ${taskId} (mesh ${meshId}): ${e?.message || e}`);
683
+ }
684
+ }
685
+
620
686
  function actionableSkipGuidance(reason: string): { summary: string; nextAction: string } {
621
687
  if (reason === 'target_node_id_unmatched') return {
622
688
  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)',
@@ -652,6 +718,12 @@ function actionableSkipGuidance(reason: string): { summary: string; nextAction:
652
718
  * event (so it is delivered actively, not only on poll). De-duped per (mesh, task, reason). */
653
719
  function notifyCoordinatorOfActionableSkip(meshId: string, taskId: string, reason: string | undefined, nodeId?: string): void {
654
720
  if (!isActionableSkipReason(reason)) return;
721
+ // FALSE-BLOCKER-CLONE-QUEUE chokepoint defense: a 'target_node_id_unmatched' skip whose
722
+ // node was cloned within the grace window is a TRANSIENT propagation/bootstrap gap that
723
+ // auto-clears, not a permanent routing miss — never page the coordinator for it (the
724
+ // reason classifier upstream already routes the common case to the transient reason; this
725
+ // is the single-funnel backstop for any path that still labels it as the permanent reason).
726
+ if (reason === 'target_node_id_unmatched' && isWithinCloneBootstrapGrace(readNonEmptyString(nodeId))) return;
655
727
  const dedupKey = `${meshId}:${taskId}`;
656
728
  if (lastActionableSkipNotified.get(dedupKey) === reason) return;
657
729
  lastActionableSkipNotified.set(dedupKey, reason!);
@@ -1095,9 +1167,19 @@ function markAutoLaunch(meshId: string, taskId: string, args: {
1095
1167
  // notification on any non-skip transition (started/completed) so a later genuine skip
1096
1168
  // re-notifies.
1097
1169
  if (args.status === 'skipped') {
1098
- notifyCoordinatorOfActionableSkip(meshId, taskId, args.reason, args.nodeId);
1170
+ if (isActionableSkipReason(args.reason)) {
1171
+ notifyCoordinatorOfActionableSkip(meshId, taskId, args.reason, args.nodeId);
1172
+ } else if (args.reason === TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON) {
1173
+ // FALSE-BLOCKER-CLONE-QUEUE (stale-event clear): the unmatch is now known to be a
1174
+ // self-resolving clone/bootstrap window — retract any earlier actionable blocker we
1175
+ // paged for this same task. Other transient/back-pressure reasons (cooldown, caps)
1176
+ // intentionally do NOT retract: they can mask a still-standing real blocker.
1177
+ retractActionableSkipIfPreviouslyNotified(meshId, taskId);
1178
+ }
1099
1179
  } else {
1100
- lastActionableSkipNotified.delete(`${meshId}:${taskId}`);
1180
+ // started/completed: the task is progressing — re-arm the de-dup ledger and retract
1181
+ // any still-undelivered stale blocker for it.
1182
+ retractActionableSkipIfPreviouslyNotified(meshId, taskId);
1101
1183
  }
1102
1184
  }
1103
1185
 
@@ -1264,11 +1346,22 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1264
1346
  !task.targetNodeId || meshNodeIdMatches(n, task.targetNodeId));
1265
1347
  return matched.length > 0 && matched.every((n: any) => n?.isLocalWorktree === true);
1266
1348
  })();
1349
+ // FALSE-BLOCKER-CLONE-QUEUE: an unmatched target pin is only a PERMANENT routing
1350
+ // miss when the node is genuinely absent — a freshly cloned worktree whose
1351
+ // inline-cache entry has not propagated here yet (or whose bootstrap is still
1352
+ // running) is TRANSIENTLY unresolved and auto-claims shortly. Report that as the
1353
+ // transient (non-actionable) reason so the coordinator is not paged with a false
1354
+ // "actionable blocker — will NOT clear on its own". A genuinely dead node is neither
1355
+ // bootstrap-running nor inside the clone grace window → stays 'target_node_id_unmatched'.
1356
+ const targetTransientlyUnresolved = targetPinUnmatched
1357
+ && isTargetNodeTransientlyUnresolved(mesh, task);
1267
1358
  markAutoLaunch(meshId, task.id, {
1268
1359
  status: 'skipped',
1269
1360
  reason: convergenceOntoWorktree
1270
1361
  ? 'mesh_convergence_target_is_worktree'
1271
- : (targetPinUnmatched ? 'target_node_id_unmatched' : 'no_node_satisfies_required_tags'),
1362
+ : targetTransientlyUnresolved
1363
+ ? TRANSIENT_TARGET_NODE_BOOTSTRAP_PENDING_REASON
1364
+ : (targetPinUnmatched ? 'target_node_id_unmatched' : 'no_node_satisfies_required_tags'),
1272
1365
  nodeId: task.targetNodeId,
1273
1366
  });
1274
1367
  continue;