@adhdev/daemon-core 0.9.82-rc.319 → 0.9.82-rc.320
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 +25 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +25 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/mesh/mesh-events-coordinator.ts +56 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.320",
|
|
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.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.320",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -379,6 +379,17 @@ export function tryAssignQueueTask(
|
|
|
379
379
|
const autoLaunchInProgress = new Set<string>();
|
|
380
380
|
const autoLaunchCooldownUntil = new Map<string, number>();
|
|
381
381
|
const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
|
|
382
|
+
// A remote auto-launch (launch_cli forward) is fire-and-async: the worker session
|
|
383
|
+
// spawns, reaches idle, emits agent:ready, that ready is queued on the worker, pulled
|
|
384
|
+
// by this coordinator (reconcile PHASE 1), and only THEN claims the task. That round
|
|
385
|
+
// trip routinely exceeds the 5s per-(mesh,node) cooldown, so cooldown alone lets the
|
|
386
|
+
// reconcile loop fire a SECOND launch for the same still-pending task before the first
|
|
387
|
+
// session's claim lands — every tick spawns yet another orphan session (observed live:
|
|
388
|
+
// 26 sessions for one task). This is a per-TASK await-claim window: once a task has a
|
|
389
|
+
// successfully-launched session whose claim we are still waiting on, do not launch it
|
|
390
|
+
// again until the window lapses. It is generous (a slow remote spawn can take tens of
|
|
391
|
+
// seconds) but bounded so a launch that silently never reaches idle is eventually retried.
|
|
392
|
+
const AUTO_LAUNCH_AWAIT_CLAIM_MS = 90_000;
|
|
382
393
|
|
|
383
394
|
// De-dup for repeated `skipped` ledger noise: the reconcile loop re-runs the queue
|
|
384
395
|
// trigger every 4s, so a task that can't be claimed (e.g. a remote node with no
|
|
@@ -818,6 +829,25 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
818
829
|
continue;
|
|
819
830
|
}
|
|
820
831
|
|
|
832
|
+
// Per-task await-claim guard. A prior auto-launch already spawned a session for
|
|
833
|
+
// this task and we are waiting for that session's idle→claim to land (remote
|
|
834
|
+
// claims arrive via the worker→coordinator agent:ready pull, which can lag well
|
|
835
|
+
// past the per-node cooldown). Re-launching now would spawn a duplicate orphan
|
|
836
|
+
// session that never gets work. The task leaves `pending` the instant the claim
|
|
837
|
+
// succeeds, so this guard only suppresses the in-flight window; if the launched
|
|
838
|
+
// session never reaches idle within the window, a later tick retries.
|
|
839
|
+
if (task.autoLaunch?.status === 'completed' && task.autoLaunch.sessionId) {
|
|
840
|
+
const launchedAtMs = Date.parse(task.autoLaunch.updatedAt);
|
|
841
|
+
if (Number.isFinite(launchedAtMs) && Date.now() - launchedAtMs < AUTO_LAUNCH_AWAIT_CLAIM_MS) {
|
|
842
|
+
// Record the skip in the ledger ONLY (dedup'd). Do NOT call markAutoLaunch
|
|
843
|
+
// here: recordTaskAutoLaunch overwrites task.autoLaunch wholesale, which would
|
|
844
|
+
// erase the very `completed` record (status + sessionId + updatedAt) this guard
|
|
845
|
+
// reads on the next tick, reopening the duplicate-launch hole it closes.
|
|
846
|
+
recordAutoLaunchEvent(meshId, { phase: 'skipped', taskId: task.id, reason: 'awaiting_launched_session_claim', nodeId: task.autoLaunch.nodeId, sessionId: task.autoLaunch.sessionId });
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
821
851
|
const candidateNodes = Array.isArray(mesh?.nodes)
|
|
822
852
|
? mesh.nodes.filter((node: any) => {
|
|
823
853
|
if (task.targetNodeId && readMeshNodeId(node) !== task.targetNodeId) return false;
|
|
@@ -1738,7 +1768,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1738
1768
|
metadataEvent: args.metadataEvent,
|
|
1739
1769
|
recoveryContext,
|
|
1740
1770
|
});
|
|
1741
|
-
if (!messageText)
|
|
1771
|
+
if (!messageText) {
|
|
1772
|
+
// Lifecycle events that carry no coordinator-facing message (agent:ready /
|
|
1773
|
+
// agent:generating_started) still drive the remote-claim state machine: the
|
|
1774
|
+
// coordinator's agent:ready branch above runs setRemoteIdleSession +
|
|
1775
|
+
// tryAssignQueueTask, and agent:generating_started clears the remote-idle entry.
|
|
1776
|
+
// For a LOCAL worker whose coordinator is a REMOTE daemon those side effects ran
|
|
1777
|
+
// on the wrong daemon (this worker's empty queue / store), so the coordinator never
|
|
1778
|
+
// learns the auto-launched session went idle and re-auto-launches it forever
|
|
1779
|
+
// (queue task stuck pending). Queue the silent event so the coordinator pulls it
|
|
1780
|
+
// (PHASE 1 pullRemoteNodeQueues → handleMeshForwardEvent) and re-runs the claim on
|
|
1781
|
+
// the daemon that actually owns the queue. Gate strictly on a present, REMOTE
|
|
1782
|
+
// coordinator daemon id: a co-located worker already ran the claim on the right
|
|
1783
|
+
// daemon, and a coordinator processing a *pulled* event has no sourceSession so
|
|
1784
|
+
// workerCoordinatorDaemonId is empty — neither re-queues, so there is no loop.
|
|
1785
|
+
const isSilentClaimRelevantEvent = args.event === 'agent:ready' || args.event === 'agent:generating_started';
|
|
1786
|
+
const coordinatorIsRemote = !!workerCoordinatorDaemonId
|
|
1787
|
+
&& !resolveCoordinatorDrainDaemonIds(components).includes(workerCoordinatorDaemonId);
|
|
1788
|
+
if (!(isSilentClaimRelevantEvent && coordinatorIsRemote)) {
|
|
1789
|
+
return { success: false, error: 'unsupported mesh event' };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1742
1792
|
|
|
1743
1793
|
// ── Queue-only delivery (single-model: queue + periodic poll) ──────────────
|
|
1744
1794
|
// Every mesh coordinator event — terminal or not, local-coordinator or
|
|
@@ -1768,7 +1818,11 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1768
1818
|
...args.metadataEvent,
|
|
1769
1819
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
1770
1820
|
},
|
|
1771
|
-
|
|
1821
|
+
// Silent lifecycle events (agent:ready / agent:generating_started) carry no
|
|
1822
|
+
// coordinator message; they are queued only so the coordinator re-runs the
|
|
1823
|
+
// remote-claim state machine on pull. injectPendingIntoCoordinator skips
|
|
1824
|
+
// entries without a coordinatorMessage, so a live CLI coordinator is not spammed.
|
|
1825
|
+
...(messageText ? { coordinatorMessage: messageText } : {}),
|
|
1772
1826
|
queuedAt: Date.now(),
|
|
1773
1827
|
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
1774
1828
|
};
|