@adhdev/daemon-core 0.9.82-rc.367 → 0.9.82-rc.368

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.367",
3
+ "version": "0.9.82-rc.368",
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.367",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.368",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -706,6 +706,28 @@ export class DaemonCliManager {
706
706
  }
707
707
 
708
708
  this.adapters.set(key, cliInstance.getAdapter());
709
+
710
+ // WTDISPATCH (no_node_binding): a coordinator-launched worker carries its mesh node
711
+ // binding on the CLI-instance settings, but the session-host RECORD meta was never
712
+ // stamped with it — `updateRuntimeSettings` only mutates in-memory runtime settings and
713
+ // `updateRuntimeMeta` was only ever called with providerSessionId. So mesh_cleanup_sessions
714
+ // matched these worker sessions to a node by workspace ALONE
715
+ // (`live_session_matched_by_workspace_only_no_node_binding`), which on a daemon hosting
716
+ // sibling worktree nodes cannot tell two co-located clones apart. Push the launch-time node
717
+ // binding to the record meta so the record is 1:1 bound to its node (the spawned pty exists
718
+ // by now, so updateMeta reaches the session-host store). Best-effort; guarded.
719
+ const launchMeshNodeId = typeof settings?.meshNodeId === 'string' ? settings.meshNodeId.trim() : '';
720
+ const launchMeshNodeFor = typeof settings?.meshNodeFor === 'string' ? settings.meshNodeFor.trim() : '';
721
+ if (launchMeshNodeId || launchMeshNodeFor) {
722
+ try {
723
+ cliInstance.getAdapter().updateRuntimeMeta?.({
724
+ ...(launchMeshNodeId ? { meshNodeId: launchMeshNodeId } : {}),
725
+ ...(launchMeshNodeFor ? { meshNodeFor: launchMeshNodeFor } : {}),
726
+ ...(settings?.launchedByCoordinator === true ? { launchedByCoordinator: true } : {}),
727
+ });
728
+ } catch { /* best-effort — record-meta stamp is cleanup hygiene, not on the dispatch path */ }
729
+ }
730
+
709
731
  this.startCliExitMonitor(key, cliType);
710
732
  }
711
733
 
@@ -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, normalizeMeshWorkspaceForCompare, type MeshNodeIdentified } from '@adhdev/mesh-shared';
22
+ import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, normalizeMeshWorkspaceForCompare, meshWorkspacesEquivalent, type MeshNodeIdentified } from '@adhdev/mesh-shared';
23
23
  import {
24
24
  findRecentTerminalLedgerEvidence,
25
25
  hasDispatchAfterTerminal,
@@ -487,14 +487,52 @@ export function tryAssignQueueTask(
487
487
  // getRemoteIdleSessions). Conservative by design: when either workspace is unknown we do NOT
488
488
  // skip, so a node with no declared workspace keeps its prior behavior and no legitimate claim
489
489
  // is starved.
490
+ // WTDISPATCH (residual of WTCLAIM): the cross-node claim guard must reach EVERY claiming
491
+ // session this daemon can observe — not only those whose adapter happens to be in
492
+ // cliManager.adapters. An auto-launched worker session can carry its node binding on the
493
+ // CLI-instance settings while its session-host record shows no_node_binding, and the
494
+ // event-driven / remote-idle drain (agent:ready → setRemoteIdleSession → tryAssignQueueTask)
495
+ // can pass a nodeId that does NOT belong to the claiming session — a sibling worktree node
496
+ // on the SAME daemon. The adapter-only WTCLAIM check (rc.361/4c5b30b1) never engaged for a
497
+ // session observed solely via instanceManager, so session A could pull node B's task and
498
+ // node A's task was left with no session to claim it (no task_dispatched — it never dispatches).
499
+ //
500
+ // Resolve the claiming session's REAL identity from the adapter workingDir, then fall back to
501
+ // the live CLI instance's workspace + its stamped meshNodeId, and refuse a claim that
502
+ // contradicts EITHER (fail-closed). Reuses the shared meshWorkspacesEquivalent / meshNodeIdMatches
503
+ // comparators — no new comparison logic. Conservative: when neither the workspace NOR the stamp
504
+ // is resolvable we do NOT refuse, so a node with no declared workspace keeps prior behavior and
505
+ // a genuinely remote (cross-daemon) candidate stays nodeId-matched from getRemoteIdleSessions.
490
506
  const localClaimAdapter = components.cliManager?.adapters?.get(sessionId) as { workingDir?: string } | undefined;
491
- if (localClaimAdapter) {
492
- const sessionWorkspace = normalizeMeshWorkspaceForCompare(localClaimAdapter.workingDir);
493
- const nodeWorkspace = normalizeMeshWorkspaceForCompare(readNonEmptyString(node?.workspace));
494
- if (sessionWorkspace && nodeWorkspace && sessionWorkspace !== nodeWorkspace) {
495
- LOG.info('MeshQueue', `WTCLAIM: refusing claim for node ${nodeId} (${sessionId}) — session workspace "${sessionWorkspace}" ≠ node workspace "${nodeWorkspace}" (cross-workspace dispatch blocked)`);
507
+ let claimInstanceWorkspace = '';
508
+ let claimStampedNodeId = '';
509
+ try {
510
+ const claimState = components.instanceManager?.getInstance?.(sessionId)?.getState?.();
511
+ claimInstanceWorkspace = readNonEmptyString(claimState?.workspace);
512
+ const claimSettings = (claimState?.settings as Record<string, unknown>) || {};
513
+ claimStampedNodeId = readNonEmptyString(claimSettings.meshNodeId);
514
+ } catch { /* best-effort — fall through to the conservative (no refuse) path */ }
515
+
516
+ const nodeWorkspaceRaw = readNonEmptyString(node?.workspace);
517
+ const sessionWorkspaceRaw = readNonEmptyString(localClaimAdapter?.workingDir) || claimInstanceWorkspace;
518
+
519
+ if (claimStampedNodeId && nodeId) {
520
+ // The session carries its OWN meshNodeId stamp — its authoritative node identity, set when
521
+ // the coordinator launched/dispatched it (mesh-routing trusts this stamp FIRST). When it
522
+ // matches the claim target the session genuinely belongs to this node, so the stamp settles
523
+ // it and the workspace heuristic is skipped (a base/worktree pair can legitimately share a
524
+ // workspace). When it does NOT match, the claim is a cross-node leak — refuse, fail-closed.
525
+ if (!meshNodeIdMatches({ id: claimStampedNodeId } as MeshNodeIdentified, nodeId)) {
526
+ LOG.info('MeshQueue', `WTDISPATCH: refusing claim for node ${nodeId} (${sessionId}) — session is bound to node "${claimStampedNodeId}" (cross-node claim blocked)`);
496
527
  return false;
497
528
  }
529
+ } else if (sessionWorkspaceRaw && nodeWorkspaceRaw && !meshWorkspacesEquivalent(sessionWorkspaceRaw, nodeWorkspaceRaw)) {
530
+ // No stamp (the no_node_binding worker) — fall back to the workspace to tell two co-located
531
+ // sibling worktree sessions apart. WTCLAIM, now reaching instanceManager-observable sessions
532
+ // too. Conservative: unknown workspace on either side → do NOT refuse (no legitimate claim
533
+ // starved; a genuinely remote cross-daemon candidate stays nodeId-matched as before).
534
+ LOG.info('MeshQueue', `WTCLAIM: refusing claim for node ${nodeId} (${sessionId}) — session workspace "${normalizeMeshWorkspaceForCompare(sessionWorkspaceRaw)}" ≠ node workspace "${normalizeMeshWorkspaceForCompare(nodeWorkspaceRaw)}" (cross-workspace dispatch blocked)`);
535
+ return false;
498
536
  }
499
537
 
500
538
  const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
@@ -2421,6 +2459,43 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
2421
2459
  });
2422
2460
  }
2423
2461
 
2462
+ // ---------------------------------------------------------------------------
2463
+ // Per-coordinator forward serialization (P2P send-backpressure relief).
2464
+ //
2465
+ // When several workers finish at once, each completion runs forwardUnresolvedDelegate
2466
+ // Event and fires its own `mesh_forward_event` push. Firing the whole burst
2467
+ // concurrently dumps it into the single per-peer P2P DataChannel buffer in one tick,
2468
+ // which starves the rpc_ack/rpc_res replies the same channel must carry — a
2469
+ // coordinator's inbound `git_status` then times out even though the worker's own
2470
+ // forward acks return in ~1s. To cap the concurrent burst we serialize the immediate
2471
+ // pushes per coordinator: at most one push is in flight to a given coordinator at a
2472
+ // time, the rest run in arrival order behind it. A lone event (idle lane) still
2473
+ // dispatches immediately — only a genuine burst is paced. Durability is unchanged:
2474
+ // every event is already persisted to the outbox before the push runs, so serializing
2475
+ // only delays the best-effort fast path; PHASE 0 retry still covers any gap. This pairs
2476
+ // with the DataChannel send-buffer gate in daemon-cloud's mesh manager (writeRequest),
2477
+ // which is the hard guarantee; this throttle keeps the burst from piling up there.
2478
+ interface CoordinatorForwardLane { tail: Promise<unknown>; depth: number; }
2479
+ const coordinatorForwardLanes = new Map<string, CoordinatorForwardLane>();
2480
+ function enqueueCoordinatorForwardPush(coordinatorDaemonId: string, run: () => Promise<unknown>): void {
2481
+ let lane = coordinatorForwardLanes.get(coordinatorDaemonId);
2482
+ if (!lane) { lane = { tail: Promise.resolve(), depth: 0 }; coordinatorForwardLanes.set(coordinatorDaemonId, lane); }
2483
+ const wasIdle = lane.depth === 0;
2484
+ lane.depth += 1;
2485
+ const dec = (): void => { lane!.depth -= 1; };
2486
+ if (wasIdle) {
2487
+ // Idle lane → dispatch synchronously, so a lone completion (the common case) has
2488
+ // ZERO added latency and the push call happens in-line. Only a genuine burst —
2489
+ // events arriving while a push is still in flight — is paced (else branch).
2490
+ lane.tail = Promise.resolve(run()).catch(() => {}).then(dec, dec);
2491
+ } else {
2492
+ // Burst: queue behind the in-flight push(es) in arrival order so the whole burst
2493
+ // is not dumped into the shared DataChannel buffer at once. The tail is guarded
2494
+ // so one rejecting push never wedges the lane for the next.
2495
+ lane.tail = lane.tail.then(() => run()).catch(() => {}).then(dec, dec);
2496
+ }
2497
+ }
2498
+
2424
2499
  // ---------------------------------------------------------------------------
2425
2500
  // Worker-side fallback forward for unresolved-mesh delegates.
2426
2501
  //
@@ -2516,21 +2591,27 @@ function forwardUnresolvedDelegateEvent(
2516
2591
  // 2) Best-effort immediate push for low latency. On success, ack the outbox row so
2517
2592
  // the retry loop won't re-send it. On failure, leave it queued — PHASE 0 retries.
2518
2593
  traceMeshEventStage('forward_send', fwdTraceCtx, 'immediate push');
2519
- Promise.resolve(components.dispatchMeshCommand(coordinatorDaemonId, 'mesh_forward_event', payload))
2520
- .then((result: any) => {
2521
- if (result && result.success === false) {
2522
- LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} rejected (${readNonEmptyString(result.error) || 'no reason'}) — left queued for retry`);
2523
- traceMeshEventDrop('immediate_forward_rejected', fwdTraceCtx, readNonEmptyString(result.error) || 'no reason');
2524
- return;
2525
- }
2526
- // Acked. Mark the durable copy delivered so the retry loop skips it.
2527
- if (persisted) ackUnresolvedDelegateForwardByFingerprint(coordinatorDaemonId, eventName, payload);
2528
- })
2529
- .catch((e: any) => {
2530
- // Coordinator momentarily unreachable; the durable row stays queued and the
2531
- // reconcile loop retries it. Trace so the relay attempt is visible.
2532
- LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} failed: ${e?.message || e} — left queued for retry`);
2533
- });
2594
+ // Serialize per coordinator so a multi-worker completion burst is paced rather than
2595
+ // dumped concurrently into the shared P2P DataChannel buffer (see coordinator
2596
+ // ForwardLanes). dispatchMeshCommand was null-checked above; capture it for the
2597
+ // deferred closure.
2598
+ const dispatchMeshCommand = components.dispatchMeshCommand;
2599
+ enqueueCoordinatorForwardPush(coordinatorDaemonId, () =>
2600
+ Promise.resolve(dispatchMeshCommand(coordinatorDaemonId, 'mesh_forward_event', payload))
2601
+ .then((result: any) => {
2602
+ if (result && result.success === false) {
2603
+ LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} rejected (${readNonEmptyString(result.error) || 'no reason'}) — left queued for retry`);
2604
+ traceMeshEventDrop('immediate_forward_rejected', fwdTraceCtx, readNonEmptyString(result.error) || 'no reason');
2605
+ return;
2606
+ }
2607
+ // Acked. Mark the durable copy delivered so the retry loop skips it.
2608
+ if (persisted) ackUnresolvedDelegateForwardByFingerprint(coordinatorDaemonId, eventName, payload);
2609
+ })
2610
+ .catch((e: any) => {
2611
+ // Coordinator momentarily unreachable; the durable row stays queued and the
2612
+ // reconcile loop retries it. Trace so the relay attempt is visible.
2613
+ LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} failed: ${e?.message || e} — left queued for retry`);
2614
+ }));
2534
2615
  LOG.info('MeshEvents', `Durably forwarded ${eventName} for unresolved-mesh worker at ${routing.workspace || '(no workspace)'} to coordinator daemon ${coordinatorDaemonId}`);
2535
2616
  return true;
2536
2617
  }