@adhdev/daemon-core 0.9.82-rc.284 → 0.9.82-rc.286
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 +45 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/mesh/mesh-reconcile-loop.ts +108 -47
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.286",
|
|
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.286",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
43
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
44
|
+
import type { LocalMeshEntry } from '../repo-mesh-types.js';
|
|
44
45
|
import { loadConfig } from '../config/config.js';
|
|
45
46
|
import { listMeshes } from '../config/mesh-config.js';
|
|
46
47
|
import { LOG } from '../logging/logger.js';
|
|
@@ -87,6 +88,30 @@ function resolveCoordinatorDaemonIds(components: DaemonComponents): string[] {
|
|
|
87
88
|
return [...ids];
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// Whether THIS daemon is the coordinator/host for a mesh — i.e. the daemon that
|
|
92
|
+
// owns coordinator ownership and must collect every worker node's completion
|
|
93
|
+
// events into its local queue. This is true regardless of whether a *live CLI*
|
|
94
|
+
// coordinator session currently exists: the coordinator is frequently a pure
|
|
95
|
+
// stdio MCP LLM (no live CLI session to inject into), and that LLM only sees the
|
|
96
|
+
// queue when it next calls a mesh tool. For it to see remote worker completions
|
|
97
|
+
// at all, the daemon must have already pulled them into the local queue on the
|
|
98
|
+
// timer — which is exactly what this predicate gates.
|
|
99
|
+
//
|
|
100
|
+
// Rule: this daemon hosts the mesh when meshHost.role is 'host' (the default for
|
|
101
|
+
// standalone-compat meshes with no host metadata) AND, when a hostDaemonId is
|
|
102
|
+
// pinned, it resolves to one of this daemon's ids. Member-only daemons return
|
|
103
|
+
// false — their own queue is pulled BY the host, not the other way around.
|
|
104
|
+
function daemonHostsMesh(mesh: LocalMeshEntry, daemonIds: string[]): boolean {
|
|
105
|
+
const host = mesh.meshHost;
|
|
106
|
+
// No metadata → default host (standalone compatibility, see createDefaultMeshHostMetadata).
|
|
107
|
+
if (!host) return true;
|
|
108
|
+
if (host.role && host.role !== 'host') return false;
|
|
109
|
+
const hostDaemonId = readNonEmptyString(host.hostDaemonId);
|
|
110
|
+
// Host role but no pinned hostDaemonId → treat as host (single-daemon / legacy).
|
|
111
|
+
if (!hostDaemonId) return true;
|
|
112
|
+
return daemonIds.includes(hostDaemonId);
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
// Find live CLI coordinator instances on THIS daemon, keyed by mesh.
|
|
91
116
|
function findLiveCoordinators(components: DaemonComponents): LiveCoordinator[] {
|
|
92
117
|
const out: LiveCoordinator[] = [];
|
|
@@ -118,12 +143,57 @@ function injectPendingIntoCoordinator(
|
|
|
118
143
|
});
|
|
119
144
|
}
|
|
120
145
|
|
|
121
|
-
// One reconcile tick
|
|
146
|
+
// One reconcile tick. Two independent phases:
|
|
147
|
+
//
|
|
148
|
+
// PHASE 1 — Remote queue pull (the fix for remote worktree completions never
|
|
149
|
+
// reaching an MCP/LLM coordinator). For EVERY mesh this daemon hosts/
|
|
150
|
+
// coordinates, pull each remote worker node's pending-events queue over P2P
|
|
151
|
+
// into THIS daemon's local queue. This runs *regardless of whether a live CLI
|
|
152
|
+
// coordinator exists* — the coordinator is usually a pure stdio MCP LLM with
|
|
153
|
+
// no live CLI session, and it can only observe a remote worker's completion
|
|
154
|
+
// once that event has been pulled into the local queue (which it then drains
|
|
155
|
+
// on its next mesh tool call). Previously this pull was gated behind a live
|
|
156
|
+
// CLI coordinator and so never ran for MCP/LLM coordinators — remote
|
|
157
|
+
// completions sat on the remote node's queue until the LLM happened to call
|
|
158
|
+
// mesh_read_chat, which triggered the MCP-side pull. The daemon now does it
|
|
159
|
+
// autonomously on the timer. Standalone (no dispatchMeshCommand) skips this
|
|
160
|
+
// phase entirely — there are no remote nodes to pull from.
|
|
161
|
+
//
|
|
162
|
+
// PHASE 2 — Live CLI inject. For each mesh that has a live CLI coordinator on
|
|
163
|
+
// THIS daemon, drain the local queue and inject pending events into the PTY.
|
|
164
|
+
// Unchanged from before.
|
|
122
165
|
export async function runMeshReconcileTick(components: DaemonComponents): Promise<void> {
|
|
166
|
+
const localDaemonId = readNonEmptyString(loadConfig().machineId) || undefined;
|
|
167
|
+
// The id-set used to scope the local queue drain (status id + machineId). See
|
|
168
|
+
// resolveCoordinatorDaemonIds — the status id is what the MCP layer stamps and
|
|
169
|
+
// is mandatory here for a generating CLI coordinator to self-receive completions.
|
|
170
|
+
const drainDaemonIds = resolveCoordinatorDaemonIds(components);
|
|
171
|
+
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
172
|
+
const store = (() => {
|
|
173
|
+
try { return MeshRuntimeStore.getInstance(); } catch { return undefined; }
|
|
174
|
+
})();
|
|
175
|
+
|
|
176
|
+
// ── PHASE 1: pull remote node queues for every mesh this daemon hosts ──────
|
|
177
|
+
// Cloud-only (dispatchMeshCommand present). Runs whether or not a live CLI
|
|
178
|
+
// coordinator exists — this is what lets an MCP/LLM coordinator ever see a
|
|
179
|
+
// remote worker's completion.
|
|
180
|
+
if (dispatchMeshCommand) {
|
|
181
|
+
for (const mesh of listMeshes()) {
|
|
182
|
+
if (!daemonHostsMesh(mesh, drainDaemonIds)) continue;
|
|
183
|
+
try {
|
|
184
|
+
await pullRemoteNodeQueues(components, mesh, localDaemonId, drainDaemonIds);
|
|
185
|
+
} catch (e: any) {
|
|
186
|
+
LOG.warn('MeshReconcile', `Remote node pull failed for mesh ${mesh.id}: ${e?.message || e}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── PHASE 2: inject into live CLI coordinators on this daemon ──────────────
|
|
123
192
|
const coordinators = findLiveCoordinators(components);
|
|
124
193
|
if (coordinators.length === 0) {
|
|
125
194
|
// No live CLI coordinator on this daemon — nothing to inject into.
|
|
126
|
-
// (MCP-only LLM coordinators drain the queue via their own tool
|
|
195
|
+
// (MCP-only LLM coordinators drain the local queue via their own tool
|
|
196
|
+
// calls; PHASE 1 above has already populated it from remote nodes.)
|
|
127
197
|
return;
|
|
128
198
|
}
|
|
129
199
|
|
|
@@ -136,31 +206,8 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
|
|
|
136
206
|
else byMesh.set(c.meshId, [c]);
|
|
137
207
|
}
|
|
138
208
|
|
|
139
|
-
const localDaemonId = readNonEmptyString(loadConfig().machineId) || undefined;
|
|
140
|
-
// The id-set used to scope the local queue drain (status id + machineId). See
|
|
141
|
-
// resolveCoordinatorDaemonIds — the status id is what the MCP layer stamps and
|
|
142
|
-
// is mandatory here for a generating CLI coordinator to self-receive completions.
|
|
143
|
-
const drainDaemonIds = resolveCoordinatorDaemonIds(components);
|
|
144
|
-
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
145
|
-
const store = (() => {
|
|
146
|
-
try { return MeshRuntimeStore.getInstance(); } catch { return undefined; }
|
|
147
|
-
})();
|
|
148
|
-
|
|
149
209
|
for (const [meshId, meshCoordinators] of byMesh) {
|
|
150
|
-
//
|
|
151
|
-
// re-inject locally. This is the same cross-daemon pull the MCP
|
|
152
|
-
// drainCoordinatorPendingEvents performs, lifted to the daemon timer.
|
|
153
|
-
// On standalone (no dispatchMeshCommand) this whole block is skipped,
|
|
154
|
-
// keeping cloud/standalone identical for the local case.
|
|
155
|
-
if (dispatchMeshCommand) {
|
|
156
|
-
try {
|
|
157
|
-
await pullRemoteNodeQueues(components, meshId, localDaemonId);
|
|
158
|
-
} catch (e: any) {
|
|
159
|
-
LOG.warn('MeshReconcile', `Remote node pull failed for mesh ${meshId}: ${e?.message || e}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// (b) Drain the local queue scoped to this coordinator daemon and inject.
|
|
210
|
+
// Drain the local queue scoped to this coordinator daemon and inject.
|
|
164
211
|
// - If an idle coordinator exists, FULL-drain and deliver every event to it
|
|
165
212
|
// (it can receive non-force progress events without deadlocking).
|
|
166
213
|
// - If only GENERATING coordinators exist, force-drain ONLY the force-inject
|
|
@@ -207,42 +254,56 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
|
|
|
207
254
|
// Cloud-only: poll each remote worker node daemon for pending coordinator events
|
|
208
255
|
// and re-inject them locally via handleMeshForwardEvent (which re-queues +
|
|
209
256
|
// surfaces to the live coordinator on the next tick / immediately if idle).
|
|
257
|
+
//
|
|
258
|
+
// Scoping: the remote handler (get_pending_mesh_events) drains its queue filtered
|
|
259
|
+
// by coordinatorDaemonId — returning events targeted at that id OR unscoped, and
|
|
260
|
+
// leaving events targeted at a *different* coordinator. A remote worker stamps the
|
|
261
|
+
// coordinator id in one of two forms (the canonical status id `standalone_`/
|
|
262
|
+
// `daemon_<machineId>` stamped by the MCP layer, or the bare machineId stamped by
|
|
263
|
+
// the local queue path). We therefore pull ONCE PER candidate coordinator id
|
|
264
|
+
// (drainDaemonIds = both forms) so a completion stamped with either form is
|
|
265
|
+
// recovered. The remote drain is atomic (drained=1), so issuing both pulls cannot
|
|
266
|
+
// double-deliver — the first pull that matches consumes the event; the second sees
|
|
267
|
+
// nothing. When no ids resolve we fall back to a single unscoped pull.
|
|
210
268
|
async function pullRemoteNodeQueues(
|
|
211
269
|
components: DaemonComponents,
|
|
212
|
-
|
|
270
|
+
mesh: LocalMeshEntry,
|
|
213
271
|
localDaemonId: string | undefined,
|
|
272
|
+
drainDaemonIds: string[],
|
|
214
273
|
): Promise<void> {
|
|
215
274
|
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
216
275
|
if (!dispatchMeshCommand) return;
|
|
217
|
-
const
|
|
218
|
-
if (!mesh) return;
|
|
276
|
+
const meshId = mesh.id;
|
|
219
277
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
278
|
+
// One args object per coordinator-id form (status id + bare machineId), or a
|
|
279
|
+
// single unscoped pull when neither resolves.
|
|
280
|
+
const pulls: Array<Record<string, unknown>> = drainDaemonIds.length > 0
|
|
281
|
+
? drainDaemonIds.map(id => ({ meshId, coordinatorDaemonId: id }))
|
|
282
|
+
: [{ meshId }];
|
|
224
283
|
|
|
225
284
|
for (const node of mesh.nodes) {
|
|
226
285
|
const nodeDaemonId = readNonEmptyString(node.daemonId);
|
|
227
286
|
// Skip nodes without a daemon, and nodes on THIS daemon (their events are
|
|
228
|
-
// already in the local queue drained in
|
|
287
|
+
// already in the local queue drained in PHASE 2).
|
|
229
288
|
if (!nodeDaemonId) continue;
|
|
230
289
|
if (localDaemonId && nodeDaemonId === localDaemonId) continue;
|
|
231
290
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
events = await dispatchMeshCommand(nodeDaemonId, 'get_pending_mesh_events', pendingEventArgs);
|
|
235
|
-
} catch {
|
|
236
|
-
// Remote pull is best-effort; the node may be offline. Retry next tick.
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
const list = extractPendingEvents(events).filter(e => readNonEmptyString(e?.meshId) === meshId);
|
|
240
|
-
for (const event of list) {
|
|
241
|
-
const payload = buildForwardPayloadFromPending(event);
|
|
242
|
-
if (!payload.event || !payload.meshId) continue;
|
|
291
|
+
for (const pendingEventArgs of pulls) {
|
|
292
|
+
let events: unknown;
|
|
243
293
|
try {
|
|
244
|
-
|
|
245
|
-
} catch {
|
|
294
|
+
events = await dispatchMeshCommand(nodeDaemonId, 'get_pending_mesh_events', pendingEventArgs);
|
|
295
|
+
} catch {
|
|
296
|
+
// Remote pull is best-effort; the node may be offline. Retry next tick.
|
|
297
|
+
break; // node unreachable — don't bother with the other id form this tick.
|
|
298
|
+
}
|
|
299
|
+
const list = extractPendingEvents(events).filter(e => readNonEmptyString(e?.meshId) === meshId);
|
|
300
|
+
for (const event of list) {
|
|
301
|
+
const payload = buildForwardPayloadFromPending(event);
|
|
302
|
+
if (!payload.event || !payload.meshId) continue;
|
|
303
|
+
try {
|
|
304
|
+
handleMeshForwardEvent(components, payload);
|
|
305
|
+
} catch { /* best-effort re-inject */ }
|
|
306
|
+
}
|
|
246
307
|
}
|
|
247
308
|
}
|
|
248
309
|
}
|