@adhdev/daemon-core 0.9.82-rc.365 → 0.9.82-rc.367
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/chat-commands.d.ts +27 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +60 -0
- package/dist/commands/router.d.ts +242 -0
- package/dist/index.js +2100 -1769
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2085 -1754
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/chat-commands.ts +96 -2
- package/src/commands/cli-manager.ts +28 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
- package/src/commands/high-family/mesh-events.ts +47 -0
- package/src/commands/high-family/mesh-status.ts +639 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/router.ts +407 -1254
- package/src/mesh/mesh-events-coordinator.ts +78 -13
- package/src/mesh/mesh-reconcile-loop.ts +38 -0
|
@@ -19,7 +19,7 @@ import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
|
|
|
19
19
|
import { getLastDisplayMessage } from '../status/snapshot.js';
|
|
20
20
|
import { resolveDelegatedWorkerAutoApprove, resolveProviderMaxParallel, resolveNodeSchedulingPriority, normalizeMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
21
21
|
import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
22
|
-
import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, type MeshNodeIdentified } from '@adhdev/mesh-shared';
|
|
22
|
+
import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, normalizeMeshWorkspaceForCompare, type MeshNodeIdentified } from '@adhdev/mesh-shared';
|
|
23
23
|
import {
|
|
24
24
|
findRecentTerminalLedgerEvidence,
|
|
25
25
|
hasDispatchAfterTerminal,
|
|
@@ -457,16 +457,10 @@ function deliverTaskToSession(dispatchThunk: () => Promise<unknown>, ctx: Delive
|
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
// WTCLAIM:
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// told apart. Kept local (the cli-manager copy is module-private) so the comparison rule
|
|
465
|
-
// stays identical to the one fix-B already uses on the worker side.
|
|
466
|
-
function normalizeMeshWorkspaceForCompare(dir?: string): string {
|
|
467
|
-
if (typeof dir !== 'string') return '';
|
|
468
|
-
return dir.trim().replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
469
|
-
}
|
|
460
|
+
// WTCLAIM: workspace normalization for base-vs-worktree comparison now lives in
|
|
461
|
+
// @adhdev/mesh-shared (normalizeMeshWorkspaceForCompare) so the enqueue→claim path,
|
|
462
|
+
// the mesh_status per-node session filter, and the read_chat node scope guard all
|
|
463
|
+
// share one comparison rule instead of drifting module-private copies.
|
|
470
464
|
|
|
471
465
|
export function tryAssignQueueTask(
|
|
472
466
|
components: DaemonComponents,
|
|
@@ -1325,6 +1319,14 @@ export interface MeshQueueTriggerResult {
|
|
|
1325
1319
|
status?: string;
|
|
1326
1320
|
}>;
|
|
1327
1321
|
autoLaunchStarted: boolean;
|
|
1322
|
+
/**
|
|
1323
|
+
* True when a worker session is already on its way to claim a still-pending task —
|
|
1324
|
+
* either launched this tick (autoLaunchStarted) or launched on a prior tick and still
|
|
1325
|
+
* booting/awaiting-claim. Callers MUST treat this as "wait, do not launch another
|
|
1326
|
+
* session": a second launch double-edits the worktree. Mutually informative with
|
|
1327
|
+
* `noIdleMeshSessionAvailable`, which is suppressed whenever this is true.
|
|
1328
|
+
*/
|
|
1329
|
+
autoLaunchPending?: boolean;
|
|
1328
1330
|
noIdleMeshSessionAvailable?: boolean;
|
|
1329
1331
|
}
|
|
1330
1332
|
|
|
@@ -1485,6 +1487,28 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
1485
1487
|
nodeId: task.assignedNodeId,
|
|
1486
1488
|
sessionId: task.assignedSessionId,
|
|
1487
1489
|
}));
|
|
1490
|
+
|
|
1491
|
+
// An auto-launch is "pending" when the coordinator has already spun a session up
|
|
1492
|
+
// for a still-pending task and is waiting on that session's idle→claim. This covers
|
|
1493
|
+
// two ticks:
|
|
1494
|
+
// - THIS tick fired the launch (autoLaunchStarted), or
|
|
1495
|
+
// - a PRIOR tick launched a session that is still booting/awaiting-claim — the
|
|
1496
|
+
// per-task await-claim guard (maybeAutoLaunchOneQueueSession) deliberately
|
|
1497
|
+
// declines to launch again, so autoLaunchStarted is false even though a session
|
|
1498
|
+
// is on its way to claim this task.
|
|
1499
|
+
// Without this signal, the second tick reports `noIdleMeshSessionAvailable` and the
|
|
1500
|
+
// MCP guidance tells the coordinator to launch ANOTHER worker — producing a duplicate
|
|
1501
|
+
// session that double-edits the worktree. The claim itself is fine; only the wording
|
|
1502
|
+
// was wrong, so we surface `autoLaunchPending` to suppress the bad "launch one more"
|
|
1503
|
+
// advice while the just-launched session converges.
|
|
1504
|
+
const autoLaunchPending = autoLaunchStarted || afterQueue.some(task => {
|
|
1505
|
+
if (task.status !== 'pending') return false;
|
|
1506
|
+
const al = task.autoLaunch;
|
|
1507
|
+
if (!al || (al.status !== 'started' && al.status !== 'completed')) return false;
|
|
1508
|
+
const launchedAtMs = Date.parse(al.updatedAt);
|
|
1509
|
+
return Number.isFinite(launchedAtMs) && Date.now() - launchedAtMs < AUTO_LAUNCH_AWAIT_CLAIM_MS;
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1488
1512
|
return {
|
|
1489
1513
|
success: true,
|
|
1490
1514
|
meshId,
|
|
@@ -1498,7 +1522,11 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
1498
1522
|
remoteIdleSessionsChecked,
|
|
1499
1523
|
skippedSessions,
|
|
1500
1524
|
autoLaunchStarted,
|
|
1501
|
-
...(
|
|
1525
|
+
...(autoLaunchPending ? { autoLaunchPending: true } : {}),
|
|
1526
|
+
// Only report "no idle session, go launch one" when nothing is already on its way.
|
|
1527
|
+
// A pending auto-launch (this tick or a prior still-converging one) means a session
|
|
1528
|
+
// WILL claim shortly, so it is not a no-session-available situation.
|
|
1529
|
+
...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchPending
|
|
1502
1530
|
? { noIdleMeshSessionAvailable: true }
|
|
1503
1531
|
: {}),
|
|
1504
1532
|
};
|
|
@@ -1810,7 +1838,26 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1810
1838
|
// re-attributed to the latest task (the normal task_completed path below).
|
|
1811
1839
|
const supersedesWeakTerminal = isWeakTerminalLedgerPayload(terminal.payload)
|
|
1812
1840
|
&& isGenuineCompletionEvidence(args.metadataEvent);
|
|
1813
|
-
|
|
1841
|
+
// CANON-B (direct-dispatch completion race): a FAST direct dispatch (mesh_send_task)
|
|
1842
|
+
// to an already-idle, previously-used session can have its genuine completion reach
|
|
1843
|
+
// this coordinator handler BEFORE the dispatching side records the new task's dispatch
|
|
1844
|
+
// row / task_dispatched ledger entry — insertDirectDispatch + appendLedgerEntry both run
|
|
1845
|
+
// AFTER the agent_command await resolves, while the worker may already be done. In that
|
|
1846
|
+
// window sessionHasActiveAssignment is false (no active dispatch row, no unterminal
|
|
1847
|
+
// ledger entry yet), so this prior-terminal dedup engages; and because providerSessionId
|
|
1848
|
+
// is STABLE across a reused session's turns, the providerSessionId/finalSummary match
|
|
1849
|
+
// below would suppress the NEW task's completion as a duplicate of the PRIOR task —
|
|
1850
|
+
// silently losing it (the observed intermittent miss; fresh enqueue/autoLaunch is immune
|
|
1851
|
+
// because a fresh session has no prior same-providerSessionId terminal and the queue row
|
|
1852
|
+
// is claimed atomically before dispatch). The echoed taskId is the authoritative
|
|
1853
|
+
// discriminator: when the completion names a DIFFERENT task than the recorded terminal,
|
|
1854
|
+
// it is a genuinely new task's completion, never a duplicate — let it through so it is
|
|
1855
|
+
// attributed to its own taskId. A same-task re-arrival (taskId equal) or a taskId-less
|
|
1856
|
+
// legacy event still falls through to the providerSessionId/finalSummary dedup.
|
|
1857
|
+
const terminalTaskId = readNonEmptyString(terminal.payload.taskId);
|
|
1858
|
+
const eventTaskId = readNonEmptyString(args.metadataEvent.taskId);
|
|
1859
|
+
const distinctTaskCompletion = !!eventTaskId && !!terminalTaskId && eventTaskId !== terminalTaskId;
|
|
1860
|
+
if (!newDispatchAfterTerminal && !supersedesWeakTerminal && !distinctTaskCompletion) {
|
|
1814
1861
|
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
1815
1862
|
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
1816
1863
|
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
@@ -2433,6 +2480,24 @@ function forwardUnresolvedDelegateEvent(
|
|
|
2433
2480
|
workspace: readNonEmptyString(routing.workspace) || readNonEmptyString(event.workspace) || undefined,
|
|
2434
2481
|
};
|
|
2435
2482
|
|
|
2483
|
+
// Self-addressed fallback: the resolved coordinator IS this daemon (a self-
|
|
2484
|
+
// coordinating / single-node mesh, or a delegate whose coordinator anchor resolved
|
|
2485
|
+
// to our own id). A cross-daemon mesh_forward_event to our own id is REFUSED by the
|
|
2486
|
+
// dispatch self-dial guard ("route via the local router instead"), so persisting it
|
|
2487
|
+
// to the outbox would only loop forever in PHASE 0's retry, never acked. Honour the
|
|
2488
|
+
// guard's advice: route the event straight through the local receiver — the exact
|
|
2489
|
+
// path the coordinator runs on receiving a remote push — and skip the outbox entirely.
|
|
2490
|
+
const selfDaemonIds = resolveCoordinatorDrainDaemonIds(components);
|
|
2491
|
+
if (selfDaemonIds.some(self => daemonIdsEquivalent(self, coordinatorDaemonId))) {
|
|
2492
|
+
try {
|
|
2493
|
+
handleMeshForwardEvent(components, payload);
|
|
2494
|
+
LOG.info('MeshEvents', `Self-addressed unresolved-delegate ${eventName} routed via local router (coordinator ${coordinatorDaemonId} is self) — outbox skipped`);
|
|
2495
|
+
} catch (e: any) {
|
|
2496
|
+
LOG.warn('MeshEvents', `Local route of self-addressed unresolved-delegate ${eventName} failed: ${e?.message || e}`);
|
|
2497
|
+
}
|
|
2498
|
+
return true;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2436
2501
|
// 1) Persist durably FIRST. Idempotent on fingerprint, so a re-fired completion
|
|
2437
2502
|
// does not duplicate the outbox row. If persistence fails we still attempt the
|
|
2438
2503
|
// push below (degrades to the old at-most-once behaviour rather than dropping
|
|
@@ -734,6 +734,12 @@ async function retryUnresolvedDelegateForwards(components: DaemonComponents): Pr
|
|
|
734
734
|
const entries = peekUnresolvedDelegateForwards();
|
|
735
735
|
if (entries.length === 0) return;
|
|
736
736
|
|
|
737
|
+
// Every id-form THIS daemon answers to. A self-addressed outbox entry (coordinator
|
|
738
|
+
// == this daemon) must never be cross-dialled — see the self-route branch below.
|
|
739
|
+
const selfIds = resolveCoordinatorDaemonIds(components);
|
|
740
|
+
const isSelfCoordinatorId = (id: string): boolean =>
|
|
741
|
+
selfIds.some(self => daemonIdsEquivalent(self, id));
|
|
742
|
+
|
|
737
743
|
for (const entry of entries) {
|
|
738
744
|
// EVTTRACE correlation context for this outbox entry's retry.
|
|
739
745
|
const entryTraceCtx = {
|
|
@@ -742,6 +748,38 @@ async function retryUnresolvedDelegateForwards(components: DaemonComponents): Pr
|
|
|
742
748
|
nodeId: readNonEmptyString(entry.payload.nodeId),
|
|
743
749
|
event: readNonEmptyString(entry.payload.event),
|
|
744
750
|
};
|
|
751
|
+
|
|
752
|
+
// Self-addressed forward: the coordinator daemon this entry targets IS this
|
|
753
|
+
// daemon (a self-coordinating / single-node mesh, or a delegate whose coordinator
|
|
754
|
+
// anchor resolved to our own id). A cross-daemon mesh_forward_event to our own id
|
|
755
|
+
// is REFUSED by the dispatch self-dial guard ("Refusing to send ... to this
|
|
756
|
+
// daemon's own id; route via the local router instead") on every retry, so the
|
|
757
|
+
// entry can never be acked and loops forever (~every tick), spamming the log and
|
|
758
|
+
// pinning the outbox row permanently undrained. Honour the guard's own advice:
|
|
759
|
+
// route the event straight through the local receiver (handleMeshForwardEvent —
|
|
760
|
+
// the same path the coordinator runs on receiving a remote push), then ack it.
|
|
761
|
+
// We drain regardless of the local result: a cross-daemon dispatch could not have
|
|
762
|
+
// resolved it either (the guard rejects before the receiver ever runs), so leaving
|
|
763
|
+
// it queued only re-spams. handleMeshForwardEvent has the BEST recovery chance —
|
|
764
|
+
// this daemon hosts the mesh, so its workspace/nodeId → meshId recovery applies.
|
|
765
|
+
if (isSelfCoordinatorId(entry.coordinatorDaemonId)) {
|
|
766
|
+
let localResult: any;
|
|
767
|
+
try {
|
|
768
|
+
traceMeshEventStage('forward_send', entryTraceCtx, `self → local router (${entry.coordinatorDaemonId})`);
|
|
769
|
+
localResult = handleMeshForwardEvent(components, entry.payload);
|
|
770
|
+
} catch (e: any) {
|
|
771
|
+
LOG.warn('MeshReconcile', `Local route of self-addressed forward to ${entry.coordinatorDaemonId} threw: ${e?.message || e} — draining anyway to break the retry loop`);
|
|
772
|
+
}
|
|
773
|
+
ackUnresolvedDelegateForward(entry.id);
|
|
774
|
+
if (localResult && localResult.success === false) {
|
|
775
|
+
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
|
+
traceMeshEventDrop('self_forward_local_rejected', entryTraceCtx, readNonEmptyString(localResult.error) || 'no reason');
|
|
777
|
+
} else {
|
|
778
|
+
LOG.info('MeshReconcile', `Self-addressed unresolved-delegate ${readNonEmptyString(entry.payload.event)} routed via local router (coordinator ${entry.coordinatorDaemonId} is self) — drained`);
|
|
779
|
+
}
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
|
|
745
783
|
let result: any;
|
|
746
784
|
try {
|
|
747
785
|
traceMeshEventStage('forward_send', entryTraceCtx, `retry → ${entry.coordinatorDaemonId}`);
|