@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/dist/index.js +57 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +57 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +22 -0
- package/src/mesh/mesh-events-coordinator.ts +102 -21
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.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.
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
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
|
}
|