@adhdev/daemon-core 0.9.82-rc.286 → 0.9.82-rc.288

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.286",
3
+ "version": "0.9.82-rc.288",
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.286",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.288",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -12,7 +12,7 @@ import { createSessionDelivery, markSessionDeliveriesTerminal, updateSessionDeli
12
12
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
13
13
  import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents } from './mesh-events-pending.js';
14
14
  import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
15
- import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent } from './mesh-routing.js';
15
+ import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
16
16
  import { resolveDelegatedWorkerAutoApprove } from '../repo-mesh-types.js';
17
17
  import {
18
18
  findRecentTerminalLedgerEvidence,
@@ -1446,11 +1446,16 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1446
1446
  if (!isMeshCoordinatorEvent(eventName)) {
1447
1447
  return { success: false, error: 'unsupported mesh event' };
1448
1448
  }
1449
- const meshId = readNonEmptyString(payload.meshId);
1450
- if (!meshId) return { success: false, error: 'meshId required' };
1451
-
1452
1449
  const nodeId = readNonEmptyString(payload.nodeId);
1453
1450
  const workspace = readNonEmptyString(payload.workspace);
1451
+
1452
+ // The fallback worker-forward path (forwardUnresolvedDelegateEvent) cannot resolve a
1453
+ // mesh id locally on the remote worker, so it forwards the event with workspace only.
1454
+ // The coordinator hosting the mesh CAN resolve it: recover the mesh id by workspace
1455
+ // when the payload doesn't carry one.
1456
+ const meshId = readNonEmptyString(payload.meshId)
1457
+ || (workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '');
1458
+ if (!meshId) return { success: false, error: 'meshId required' };
1454
1459
  const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
1455
1460
  const relayModalMessage = readNonEmptyString(payload.modalMessage);
1456
1461
  const relayModalButtons = Array.isArray(payload.modalButtons)
@@ -1493,6 +1498,67 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1493
1498
  });
1494
1499
  }
1495
1500
 
1501
+ // ---------------------------------------------------------------------------
1502
+ // Worker-side fallback forward for unresolved-mesh delegates.
1503
+ //
1504
+ // A REMOTE worker daemon that is being P2P-remote-controlled by a coordinator is
1505
+ // NOT a member of the coordinator's mesh — it has no local mesh record. So when its
1506
+ // completion event reaches the forwarder, resolveWorkerDelegateRouting() resolves the
1507
+ // coordinator anchor (meshCoordinatorDaemonId) from the worker envelope but cannot
1508
+ // resolve the mesh id (neither meshNodeFor nor a workspace→mesh lookup yields one) and
1509
+ // returns isDelegate=false / mesh_unresolved. Before this fallback the event was dropped
1510
+ // (delivery_unroutable) and only recovered later when the coordinator happened to pull
1511
+ // the worker's queue — which it can't, because the worker never queued an unroutable
1512
+ // event. Live symptom: `WARN [MeshEvents] delivery_unroutable: ... mesh unresolved`.
1513
+ //
1514
+ // The fix: the routing object still carries coordinatorDaemonId. Forward the raw event
1515
+ // straight to that coordinator daemon over P2P (mesh_forward_event). The coordinator
1516
+ // hosts the mesh, so it recovers the mesh id by workspace in handleMeshForwardEvent and
1517
+ // injects/queues it normally. meshId is intentionally omitted from the payload (the
1518
+ // worker has none); workspace is the routing anchor the coordinator resolves from.
1519
+ //
1520
+ // No loop / no double-delivery:
1521
+ // - This only fires on the WORKER (the coordinator-own session is rejected by the
1522
+ // resolver before reaching here), and the coordinator merely injects — it does not
1523
+ // re-enter this forwarder for the relayed event.
1524
+ // - It fires only when the normal queue path did NOT run (isDelegate=false), so the
1525
+ // event is never both queued locally and forwarded.
1526
+ //
1527
+ // Returns true when the event was handed off to the coordinator daemon (so the caller
1528
+ // skips the delivery_unroutable diagnostic); false when no fallback was possible.
1529
+ function forwardUnresolvedDelegateEvent(
1530
+ components: DaemonComponents,
1531
+ routing: ReturnType<typeof resolveWorkerDelegateRouting>,
1532
+ event: Record<string, unknown>,
1533
+ ): boolean {
1534
+ const coordinatorDaemonId = readNonEmptyString(routing.coordinatorDaemonId);
1535
+ if (!coordinatorDaemonId) return false;
1536
+ if (!components.dispatchMeshCommand) return false;
1537
+
1538
+ const eventName = readNonEmptyString(event.event);
1539
+ if (!eventName) return false;
1540
+
1541
+ // Flat payload mirroring buildForwardPayloadFromPending / what handleMeshForwardEvent
1542
+ // reads. meshId is omitted on purpose — the worker can't resolve it; the coordinator
1543
+ // recovers it from workspace. nodeId/workspace come from the worker envelope so the
1544
+ // coordinator can name and locate the node.
1545
+ const payload: Record<string, unknown> = {
1546
+ ...event,
1547
+ event: eventName,
1548
+ nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId) || undefined,
1549
+ workspace: readNonEmptyString(routing.workspace) || readNonEmptyString(event.workspace) || undefined,
1550
+ };
1551
+
1552
+ Promise.resolve(components.dispatchMeshCommand(coordinatorDaemonId, 'mesh_forward_event', payload))
1553
+ .catch((e: any) => {
1554
+ // The coordinator may be momentarily unreachable; the diagnostic was already
1555
+ // skipped, so leave a trace here so an operator can see the relay attempt failed.
1556
+ LOG.warn('MeshEvents', `Fallback forward of ${eventName} to coordinator ${coordinatorDaemonId} failed: ${e?.message || e}`);
1557
+ });
1558
+ LOG.info('MeshEvents', `Fallback-forwarded ${eventName} for unresolved-mesh worker at ${routing.workspace || '(no workspace)'} to coordinator daemon ${coordinatorDaemonId}`);
1559
+ return true;
1560
+ }
1561
+
1496
1562
  export function setupMeshEventForwarding(components: DaemonComponents) {
1497
1563
  components.instanceManager.onEvent((event) => {
1498
1564
  // --- Coordinator idle auto-flush (fast path) ---
@@ -1572,10 +1638,19 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
1572
1638
  getMeshByWorkspace: (workspace) => getCachedMeshByWorkspace(workspace),
1573
1639
  });
1574
1640
  if (!routing.isDelegate) {
1575
- // R4: a worker that presented a valid envelope but resolved to no mesh used to be
1576
- // dropped silently. Leave a fail-loud diagnostic so the missing completion is
1577
- // traceable. Benign non-delegate rejections (not_cli / no_workspace / etc.) are
1578
- // no-ops inside recordUnroutableDelegateEvent.
1641
+ // Fallback: a REMOTE worker that isn't a member of the coordinator's mesh can't
1642
+ // resolve a mesh id locally (mesh_unresolved), but it still carries the coordinator
1643
+ // daemon anchor. Forward the event straight to that coordinator over P2P instead of
1644
+ // dropping it — the coordinator hosts the mesh and recovers the id by workspace.
1645
+ if (isUnroutableDelegateRejection(routing)
1646
+ && forwardUnresolvedDelegateEvent(components, routing, event)) {
1647
+ return;
1648
+ }
1649
+ // R4: a worker that presented a valid envelope but resolved to no mesh (and could
1650
+ // not be fallback-forwarded — e.g. no coordinator anchor) used to be dropped
1651
+ // silently. Leave a fail-loud diagnostic so the missing completion is traceable.
1652
+ // Benign non-delegate rejections (not_cli / no_workspace / etc.) are no-ops inside
1653
+ // recordUnroutableDelegateEvent.
1579
1654
  recordUnroutableDelegateEvent(routing, event.event);
1580
1655
  return;
1581
1656
  }
@@ -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;
@@ -82,10 +82,13 @@ export function resolveWorkerDelegateRouting(
82
82
  const sessionId = readNonEmptyString(instanceId);
83
83
  let workspace = '';
84
84
  let coordinatorDaemonId = '';
85
+ // Runtime node-id stamp, surfaced even on rejection so the unresolved-mesh fallback
86
+ // forward can name the worker node for the coordinator.
87
+ let runtimeNodeId = '';
85
88
  const reject = (rejectionReason: WorkerDelegateRejectionReason): WorkerDelegateRouting => ({
86
89
  isDelegate: false,
87
90
  meshId: '',
88
- nodeId: '',
91
+ nodeId: runtimeNodeId,
89
92
  nodeLabel: '',
90
93
  coordinatorDaemonId,
91
94
  workspace,
@@ -102,6 +105,7 @@ export function resolveWorkerDelegateRouting(
102
105
 
103
106
  const settings = readSettings(state);
104
107
  coordinatorDaemonId = readNonEmptyString(settings.meshCoordinatorDaemonId);
108
+ runtimeNodeId = readNonEmptyString(settings.meshNodeId);
105
109
 
106
110
  // A coordinator session (meshCoordinatorFor set) is only treated as a worker delegate
107
111
  // when it is itself the target of an active direct dispatch — otherwise its own events
@@ -138,7 +142,6 @@ export function resolveWorkerDelegateRouting(
138
142
  if (!meshId) return reject('mesh_unresolved');
139
143
 
140
144
  const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
141
- const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
142
145
  const nodeId = readNonEmptyString(targetNode?.id) || runtimeNodeId;
143
146
  const nodeLabel = targetNode
144
147
  ? `Node '${targetNode.id}'`