@adhdev/daemon-core 0.9.82-rc.371 → 0.9.82-rc.373
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/router.d.ts +28 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +218 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +221 -68
- package/dist/index.mjs.map +1 -1
- package/dist/logging/log-tail-reader.d.ts +40 -5
- package/dist/mesh/mesh-events-coordinator.d.ts +1 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +1 -0
- package/dist/mesh/mesh-runtime-store.d.ts +1 -0
- package/dist/mesh/mesh-work-queue.d.ts +1 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +11 -2
- package/src/commands/low-family/mesh-node-logs.ts +6 -0
- package/src/commands/router.ts +45 -0
- package/src/index.ts +1 -1
- package/src/logging/log-tail-reader.ts +187 -66
- package/src/mesh/mesh-events-coordinator.ts +67 -3
- package/src/mesh/mesh-reconcile-loop.ts +58 -6
- package/src/mesh/mesh-runtime-store.ts +26 -1
- package/src/mesh/mesh-work-queue.ts +1 -1
|
@@ -49,7 +49,7 @@ import { drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, bui
|
|
|
49
49
|
import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
50
50
|
import { appendLedgerEntry } from './mesh-ledger.js';
|
|
51
51
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
52
|
-
import { handleMeshForwardEvent, shouldForceInjectMeshEvent, MESH_FORCE_INJECT_EVENTS, triggerMeshQueue } from './mesh-events-coordinator.js';
|
|
52
|
+
import { handleMeshForwardEvent, shouldForceInjectMeshEvent, MESH_FORCE_INJECT_EVENTS, triggerMeshQueue, resolveForwardEventMeshId } from './mesh-events-coordinator.js';
|
|
53
53
|
import {
|
|
54
54
|
peekUnresolvedDelegateForwards,
|
|
55
55
|
ackUnresolvedDelegateForward,
|
|
@@ -724,6 +724,22 @@ function holdOrExpireStrictUnmatchedEvent(
|
|
|
724
724
|
// Stale entries (coordinator unreachable past the max age) are expired first so the
|
|
725
725
|
// outbox can't grow without bound. The coordinator dedups duplicate deliveries on its
|
|
726
726
|
// own fingerprint, so a retry that races the original immediate push is harmless.
|
|
727
|
+
// RECONCILE-MESHID-DROP: per-entry count of consecutive HARD rejections (the coordinator
|
|
728
|
+
// returned success:false, e.g. "meshId required"). A rejection means the push was delivered
|
|
729
|
+
// and deterministically refused — retrying the identical payload every 4s can never succeed,
|
|
730
|
+
// so it would loop until the 30-minute age expiry, spamming the log the whole time. After
|
|
731
|
+
// MAX_FORWARD_REJECTIONS such rejections we drop the entry (drain it) with ONE fail-loud
|
|
732
|
+
// warning. Transient transport failures (the dispatch throws — coordinator momentarily
|
|
733
|
+
// unreachable) do NOT count here; those legitimately retry until the age expiry. In-memory
|
|
734
|
+
// (keyed by the durable outbox row id) is sufficient: a daemon restart re-arms the loop, and
|
|
735
|
+
// the age expiry remains the durable backstop. Cleared whenever an entry is delivered/drained.
|
|
736
|
+
const unresolvedForwardRejectionCounts = new Map<string, number>();
|
|
737
|
+
const MAX_FORWARD_REJECTIONS = 5;
|
|
738
|
+
|
|
739
|
+
export function __resetUnresolvedForwardRejectionCountsForTests(): void {
|
|
740
|
+
unresolvedForwardRejectionCounts.clear();
|
|
741
|
+
}
|
|
742
|
+
|
|
727
743
|
async function retryUnresolvedDelegateForwards(components: DaemonComponents): Promise<void> {
|
|
728
744
|
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
729
745
|
if (!dispatchMeshCommand) return;
|
|
@@ -732,7 +748,11 @@ async function retryUnresolvedDelegateForwards(components: DaemonComponents): Pr
|
|
|
732
748
|
expireStaleUnresolvedDelegateForwards();
|
|
733
749
|
|
|
734
750
|
const entries = peekUnresolvedDelegateForwards();
|
|
735
|
-
if (entries.length === 0)
|
|
751
|
+
if (entries.length === 0) {
|
|
752
|
+
// Nothing queued — clear any stale per-entry rejection counters so the map can't grow.
|
|
753
|
+
if (unresolvedForwardRejectionCounts.size > 0) unresolvedForwardRejectionCounts.clear();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
736
756
|
|
|
737
757
|
// Every id-form THIS daemon answers to. A self-addressed outbox entry (coordinator
|
|
738
758
|
// == this daemon) must never be cross-dialled — see the self-route branch below.
|
|
@@ -771,6 +791,7 @@ async function retryUnresolvedDelegateForwards(components: DaemonComponents): Pr
|
|
|
771
791
|
LOG.warn('MeshReconcile', `Local route of self-addressed forward to ${entry.coordinatorDaemonId} threw: ${e?.message || e} — draining anyway to break the retry loop`);
|
|
772
792
|
}
|
|
773
793
|
ackUnresolvedDelegateForward(entry.id);
|
|
794
|
+
unresolvedForwardRejectionCounts.delete(entry.id);
|
|
774
795
|
if (localResult && localResult.success === false) {
|
|
775
796
|
LOG.warn('MeshReconcile', `Self-addressed unresolved-delegate ${readNonEmptyString(entry.payload.event)} rejected by local router (${readNonEmptyString(localResult.error) || 'no reason'}) — drained to break the self-forward retry loop`);
|
|
776
797
|
traceMeshEventDrop('self_forward_local_rejected', entryTraceCtx, readNonEmptyString(localResult.error) || 'no reason');
|
|
@@ -780,23 +801,54 @@ async function retryUnresolvedDelegateForwards(components: DaemonComponents): Pr
|
|
|
780
801
|
continue;
|
|
781
802
|
}
|
|
782
803
|
|
|
804
|
+
// RECONCILE-MESHID-DROP: the stored forward payload was built when the worker
|
|
805
|
+
// "couldn't resolve" its meshId, so the coordinator rejects it "meshId required"
|
|
806
|
+
// when its own workspace/nodeId recovery misses. The worker can usually resolve it
|
|
807
|
+
// now (member node membership / live-session meshNodeFor) — stamp it on so the
|
|
808
|
+
// coordinator accepts. Covers entries persisted before this fix AND late-bound
|
|
809
|
+
// sessions. No-op when the payload already carries a meshId or none is resolvable.
|
|
810
|
+
let pushPayload = entry.payload;
|
|
811
|
+
if (!readNonEmptyString(pushPayload.meshId)) {
|
|
812
|
+
const recoveredMeshId = resolveForwardEventMeshId(components, pushPayload);
|
|
813
|
+
if (recoveredMeshId) {
|
|
814
|
+
pushPayload = { ...pushPayload, meshId: recoveredMeshId };
|
|
815
|
+
traceMeshEventStage('forward_meshid_recovered', entryTraceCtx, `meshId=${recoveredMeshId}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
783
819
|
let result: any;
|
|
784
820
|
try {
|
|
785
821
|
traceMeshEventStage('forward_send', entryTraceCtx, `retry → ${entry.coordinatorDaemonId}`);
|
|
786
|
-
result = await dispatchMeshCommand(entry.coordinatorDaemonId, 'mesh_forward_event',
|
|
822
|
+
result = await dispatchMeshCommand(entry.coordinatorDaemonId, 'mesh_forward_event', pushPayload);
|
|
787
823
|
} catch (e: any) {
|
|
788
|
-
// Coordinator unreachable — keep the entry queued and try again
|
|
824
|
+
// Coordinator unreachable (transport threw) — keep the entry queued and try again
|
|
825
|
+
// next tick. This is NOT a hard rejection, so it does not count toward the cap;
|
|
826
|
+
// the age expiry bounds a permanently-offline coordinator.
|
|
789
827
|
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} failed: ${e?.message || e} — left queued`);
|
|
790
828
|
traceMeshEventDrop('retry_forward_failed', entryTraceCtx, e?.message || String(e));
|
|
791
829
|
continue;
|
|
792
830
|
}
|
|
793
831
|
if (result && result.success === false) {
|
|
794
|
-
|
|
795
|
-
|
|
832
|
+
// Hard rejection: the push was delivered and deterministically refused. Retrying
|
|
833
|
+
// the identical payload can never succeed, so bound it — after MAX_FORWARD_REJECTIONS
|
|
834
|
+
// drop (drain) the entry with one fail-loud warning instead of re-spamming every tick.
|
|
835
|
+
const rejections = (unresolvedForwardRejectionCounts.get(entry.id) || 0) + 1;
|
|
836
|
+
unresolvedForwardRejectionCounts.set(entry.id, rejections);
|
|
837
|
+
const reason = readNonEmptyString(result.error) || 'no reason';
|
|
838
|
+
if (rejections >= MAX_FORWARD_REJECTIONS) {
|
|
839
|
+
ackUnresolvedDelegateForward(entry.id);
|
|
840
|
+
unresolvedForwardRejectionCounts.delete(entry.id);
|
|
841
|
+
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} rejected ${rejections}x (${reason}) — dropping unresolved-delegate ${readNonEmptyString(entry.payload.event)} (sess=${readNonEmptyString(entry.payload.targetSessionId) || readNonEmptyString(entry.payload.sessionId) || '-'}) to stop the retry loop`);
|
|
842
|
+
traceMeshEventDrop('retry_forward_exhausted', entryTraceCtx, `${reason} (${rejections} rejections)`);
|
|
843
|
+
} else {
|
|
844
|
+
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} rejected (${reason}) — left queued (attempt ${rejections}/${MAX_FORWARD_REJECTIONS})`);
|
|
845
|
+
traceMeshEventDrop('retry_forward_rejected', entryTraceCtx, reason);
|
|
846
|
+
}
|
|
796
847
|
continue;
|
|
797
848
|
}
|
|
798
849
|
// Acked — mark the durable copy delivered.
|
|
799
850
|
ackUnresolvedDelegateForward(entry.id);
|
|
851
|
+
unresolvedForwardRejectionCounts.delete(entry.id);
|
|
800
852
|
LOG.info('MeshReconcile', `Retried+delivered unresolved-delegate ${readNonEmptyString(entry.payload.event)} to coordinator ${entry.coordinatorDaemonId}`);
|
|
801
853
|
}
|
|
802
854
|
}
|
|
@@ -609,7 +609,7 @@ export class MeshRuntimeStore {
|
|
|
609
609
|
nodeId: string,
|
|
610
610
|
sessionId: string,
|
|
611
611
|
capabilityTags: string[] = [],
|
|
612
|
-
opts?: { providerType?: string; providerMaxParallel?: number },
|
|
612
|
+
opts?: { providerType?: string; providerMaxParallel?: number; nodeIsWorktree?: boolean },
|
|
613
613
|
): MeshWorkQueueEntry | null {
|
|
614
614
|
return this.transaction(() => {
|
|
615
615
|
this.ensureLegacyQueueMigrated(meshId);
|
|
@@ -690,9 +690,34 @@ export class MeshRuntimeStore {
|
|
|
690
690
|
return !nodeBusy;
|
|
691
691
|
};
|
|
692
692
|
|
|
693
|
+
// WTDISPATCH-FANOUT: a `convergence` task lands its work onto base (merge →
|
|
694
|
+
// push → cleanup against the real checkout). It must NEVER be claimed by a
|
|
695
|
+
// co-located worktree-clone session — N sibling worktree sessions on one daemon
|
|
696
|
+
// each claiming the same convergence intent is the 4-way push/deploy fan-out the
|
|
697
|
+
// live repro hit. Base-only, fail-closed: when the claiming node is a worktree
|
|
698
|
+
// (nodeIsWorktree), exclude every convergence candidate so it stays pending for
|
|
699
|
+
// the base node to pull.
|
|
700
|
+
const nodeIsWorktree = opts?.nodeIsWorktree === true;
|
|
701
|
+
const convergenceAllows = (candidate: MeshWorkQueueEntry): boolean =>
|
|
702
|
+
candidate.taskMode !== 'convergence' || !nodeIsWorktree;
|
|
703
|
+
|
|
704
|
+
// WTDISPATCH-FANOUT: defensive exact-target gate. The prioritized SQL above
|
|
705
|
+
// already segregates session/node-pinned rows, but a future query change (or a
|
|
706
|
+
// candidate row whose stored target drifted from its column) must never let a
|
|
707
|
+
// sibling worktree session on the same daemon absorb another node's/session's
|
|
708
|
+
// pinned task. When a task carries an explicit target, require an exact match
|
|
709
|
+
// here too — fail-closed.
|
|
710
|
+
const targetMatches = (candidate: MeshWorkQueueEntry): boolean => {
|
|
711
|
+
if (candidate.targetSessionId && candidate.targetSessionId !== sessionId) return false;
|
|
712
|
+
if (candidate.targetNodeId && candidate.targetNodeId !== nodeId) return false;
|
|
713
|
+
return true;
|
|
714
|
+
};
|
|
715
|
+
|
|
693
716
|
const entry = candidates.find(candidate =>
|
|
694
717
|
nodeSatisfiesRequiredTags(candidate.requiredTags, capabilityTags)
|
|
695
718
|
&& dependenciesSatisfied(candidate)
|
|
719
|
+
&& convergenceAllows(candidate)
|
|
720
|
+
&& targetMatches(candidate)
|
|
696
721
|
&& nodeConflictAllows(candidate));
|
|
697
722
|
if (!entry) return null;
|
|
698
723
|
|
|
@@ -721,7 +721,7 @@ export function claimNextTask(
|
|
|
721
721
|
nodeId: string,
|
|
722
722
|
sessionId: string,
|
|
723
723
|
capabilityTags?: string[],
|
|
724
|
-
opts?: { providerType?: string; providerMaxParallel?: number },
|
|
724
|
+
opts?: { providerType?: string; providerMaxParallel?: number; nodeIsWorktree?: boolean },
|
|
725
725
|
): MeshWorkQueueEntry | null {
|
|
726
726
|
return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags, opts);
|
|
727
727
|
}
|