@adhdev/daemon-core 0.9.82-rc.285 → 0.9.82-rc.287

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.285",
3
+ "version": "0.9.82-rc.287",
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.285",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.287",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -101,6 +101,13 @@ function resolveCoordinatorDaemonIds(components: DaemonComponents): string[] {
101
101
  // standalone-compat meshes with no host metadata) AND, when a hostDaemonId is
102
102
  // pinned, it resolves to one of this daemon's ids. Member-only daemons return
103
103
  // false — their own queue is pulled BY the host, not the other way around.
104
+ //
105
+ // `daemonIds` here is the EXPANDED self-identity set (runtime drain ids ∪ this
106
+ // daemon's mesh-config node id forms) — see resolveCoordinatorSelfIds. The
107
+ // pinned hostDaemonId is itself a config-form id and frequently does NOT equal a
108
+ // runtime id (bare machineId / status id), so gating on the runtime ids alone
109
+ // would wrongly classify the real host as a non-host and skip the remote pull
110
+ // entirely.
104
111
  function daemonHostsMesh(mesh: LocalMeshEntry, daemonIds: string[]): boolean {
105
112
  const host = mesh.meshHost;
106
113
  // No metadata → default host (standalone compatibility, see createDefaultMeshHostMetadata).
@@ -112,6 +119,39 @@ function daemonHostsMesh(mesh: LocalMeshEntry, daemonIds: string[]): boolean {
112
119
  return daemonIds.includes(hostDaemonId);
113
120
  }
114
121
 
122
+ // Resolve EVERY id-form this daemon answers to FOR A GIVEN MESH: the runtime drain
123
+ // ids (status id + bare machineId) unioned with this daemon's mesh-config identity
124
+ // forms — the self node's daemonId/machineId (the node whose daemonId/machineId
125
+ // matches a runtime id) and the pinned meshHost.hostDaemonId WHEN it is provably
126
+ // ours. This is the single source of truth for "is this id me?" across both the
127
+ // host gate and the remote pull filter; the worker's meshCoordinatorDaemonId stamp
128
+ // is guaranteed to be one of these forms (it comes from resolveCoordinatorDaemonId,
129
+ // which prefers the coordinator node's config-form daemonId over the runtime status id).
130
+ function resolveCoordinatorSelfIds(mesh: LocalMeshEntry, drainDaemonIds: string[]): string[] {
131
+ const ids = new Set<string>(drainDaemonIds);
132
+ // Expand with the config-form id(s) of the self node — the mesh node whose
133
+ // daemonId/machineId matches a runtime id. Its config-form daemonId is exactly
134
+ // what resolveCoordinatorNode()→resolveCoordinatorDaemonId() stamps onto a worker.
135
+ for (const node of mesh.nodes) {
136
+ const nodeDaemonId = readNonEmptyString(node.daemonId);
137
+ const nodeMachineId = readNonEmptyString(node.machineId);
138
+ const isSelf = (nodeDaemonId && drainDaemonIds.includes(nodeDaemonId))
139
+ || (nodeMachineId && drainDaemonIds.includes(nodeMachineId));
140
+ if (!isSelf) continue;
141
+ if (nodeDaemonId) ids.add(nodeDaemonId);
142
+ if (nodeMachineId) ids.add(nodeMachineId);
143
+ }
144
+ // The pinned host id is included ONLY when it is provably one of THIS daemon's ids
145
+ // (it already matches a runtime id or a resolved self-node id). A hostDaemonId that
146
+ // names a DIFFERENT daemon must NOT be claimed — that would make a member-only
147
+ // daemon believe it is the host and pull queues it does not own. Having a node on
148
+ // this daemon does not make this daemon the host; daemonHostsMesh still honours a
149
+ // foreign hostDaemonId and rejects ownership.
150
+ const hostDaemonId = readNonEmptyString(mesh.meshHost?.hostDaemonId);
151
+ if (hostDaemonId && ids.has(hostDaemonId)) ids.add(hostDaemonId);
152
+ return [...ids];
153
+ }
154
+
115
155
  // Find live CLI coordinator instances on THIS daemon, keyed by mesh.
116
156
  function findLiveCoordinators(components: DaemonComponents): LiveCoordinator[] {
117
157
  const out: LiveCoordinator[] = [];
@@ -179,9 +219,13 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
179
219
  // remote worker's completion.
180
220
  if (dispatchMeshCommand) {
181
221
  for (const mesh of listMeshes()) {
182
- if (!daemonHostsMesh(mesh, drainDaemonIds)) continue;
222
+ // Expand to every id-form this daemon answers to for this mesh (runtime
223
+ // drain ids ∪ config-form node/host ids) and use it for BOTH the host gate
224
+ // and the remote pull filter, so a worker stamp in any form is recovered.
225
+ const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
226
+ if (!daemonHostsMesh(mesh, selfIds)) continue;
183
227
  try {
184
- await pullRemoteNodeQueues(components, mesh, localDaemonId, drainDaemonIds);
228
+ await pullRemoteNodeQueues(components, mesh, localDaemonId, selfIds);
185
229
  } catch (e: any) {
186
230
  LOG.warn('MeshReconcile', `Remote node pull failed for mesh ${mesh.id}: ${e?.message || e}`);
187
231
  }
@@ -258,35 +302,43 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
258
302
  // Scoping: the remote handler (get_pending_mesh_events) drains its queue filtered
259
303
  // by coordinatorDaemonId — returning events targeted at that id OR unscoped, and
260
304
  // 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.
305
+ // coordinator id in one of SEVERAL forms (the canonical status id `standalone_`/
306
+ // `daemon_<machineId>` stamped by the MCP layer, the bare machineId stamped by the
307
+ // local queue path, OR most commonly for remote launches — the coordinator mesh
308
+ // node's config-form `daemonId`, which resolveCoordinatorDaemonId prefers and which
309
+ // is NOT canonicalised). `candidateDaemonIds` is the already-expanded self-identity
310
+ // set (resolveCoordinatorSelfIds: runtime drain ids this daemon's mesh-config node/
311
+ // host id forms), so we pull ONCE PER candidate id and a completion stamped with any
312
+ // of them is recovered. The remote drain is atomic (drained=1), so issuing multiple
313
+ // pulls cannot double-deliver — the first pull that matches consumes the event; the
314
+ // rest see nothing. When no ids resolve we fall back to a single unscoped pull.
268
315
  async function pullRemoteNodeQueues(
269
316
  components: DaemonComponents,
270
317
  mesh: LocalMeshEntry,
271
318
  localDaemonId: string | undefined,
272
- drainDaemonIds: string[],
319
+ candidateDaemonIds: string[],
273
320
  ): Promise<void> {
274
321
  const dispatchMeshCommand = components.dispatchMeshCommand;
275
322
  if (!dispatchMeshCommand) return;
276
323
  const meshId = mesh.id;
277
324
 
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 }))
325
+ // One args object per candidate coordinator-id form, or a single unscoped pull
326
+ // when none resolve.
327
+ const pulls: Array<Record<string, unknown>> = candidateDaemonIds.length > 0
328
+ ? candidateDaemonIds.map(id => ({ meshId, coordinatorDaemonId: id }))
282
329
  : [{ meshId }];
283
330
 
284
331
  for (const node of mesh.nodes) {
285
332
  const nodeDaemonId = readNonEmptyString(node.daemonId);
286
333
  // Skip nodes without a daemon, and nodes on THIS daemon (their events are
287
- // already in the local queue drained in PHASE 2).
334
+ // already in the local queue drained in PHASE 2). "This daemon" is matched
335
+ // against the full self-identity set (candidateDaemonIds), not just the bare
336
+ // localDaemonId — a self node can be registered under the config-form daemonId
337
+ // (`daemon_<machineId>`) which would NOT equal bare localDaemonId, and pulling
338
+ // from ourselves over P2P is both wasteful and a self-dispatch hazard.
288
339
  if (!nodeDaemonId) continue;
289
340
  if (localDaemonId && nodeDaemonId === localDaemonId) continue;
341
+ if (candidateDaemonIds.includes(nodeDaemonId)) continue;
290
342
 
291
343
  for (const pendingEventArgs of pulls) {
292
344
  let events: unknown;