@adhdev/daemon-core 0.9.82-rc.381 → 0.9.82-rc.382

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.
@@ -1,4 +1,5 @@
1
1
  import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
2
+ export declare function recoverMeshIdByCoordinatorAndNode(coordinatorDaemonId: string, nodeId: string): string;
2
3
  export declare function resolveForwardEventMeshId(components: DaemonComponents, payload: Record<string, unknown>): string;
3
4
  export declare function __resetMeshWorkspaceCacheForTests(): void;
4
5
  export declare function buildRelayMetadataEvent(payload: Record<string, unknown>): Record<string, unknown>;
@@ -150,6 +150,24 @@ export declare class CliProviderInstance implements ProviderInstance {
150
150
  * terminal state. Leaving meshNodeFor pinned would route this session's
151
151
  * subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
152
152
  * coordinator as if they were task completions.
153
+ *
154
+ * MESHID-DROP-ON-DETACH (Fix C): a coordinator-LAUNCHED worker session
155
+ * (launchedByCoordinator) holds its mesh membership (meshNodeFor / meshNodeId /
156
+ * meshCoordinatorDaemonId) at the SESSION level — set once at launch
157
+ * (mesh_launch_session / queue auto-launch), independent of any single task.
158
+ * The original detach wiped meshNodeFor + meshNodeId together with the
159
+ * task-level meshActiveTaskId, so the FIRST task completion stripped the
160
+ * membership and EVERY subsequent completion forwarded with meshId absent —
161
+ * resolveWorkerDelegateRouting fell to mesh_unresolved and the coordinator
162
+ * rejected the forward "meshId required". For a launched member we therefore
163
+ * clear ONLY the task-level marker (meshActiveTaskId) and preserve the
164
+ * session-level membership so its next task's completion still resolves.
165
+ * A task-less ad-hoc turn on a preserved-membership session is NOT misrouted:
166
+ * its completion carries no taskId and the session holds no active assignment,
167
+ * so the forwarder's WARMUPGAP guard skips the dispatch-row flip (it only
168
+ * injects a benign task-less notification). A NON-launched session (a plain CLI
169
+ * session adopted by mesh_send_task --direct, launchedByCoordinator falsy)
170
+ * keeps the original full clear so an ad-hoc session is never left pinned.
153
171
  */
154
172
  detachMeshAssignment(): void;
155
173
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.381",
3
+ "version": "0.9.82-rc.382",
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.381",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.382",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -107,6 +107,19 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
107
107
  const queue = getQueue(meshId);
108
108
  const queueSummary = getMeshQueueStats(meshId);
109
109
 
110
+ // Scheduling-runtime projection — the load-balancer's live view (tie-break
111
+ // strategy, global parallel caps + consumption, per-node load/priority/provider
112
+ // caps). Built from the SAME helper + args the MCP `mesh_status` tool uses
113
+ // (mesh-tools-status.ts: buildMeshSchedulingRuntime(mesh, getQueue(mesh.id)))
114
+ // so the dashboard surface and the coordinator MCP surface render an identical
115
+ // runtime. Without this the dashboard Status/Runtime tab showed the
116
+ // "not reported by this daemon (older build)" fallback (RepoMeshStatus.scheduling
117
+ // was never populated by this daemon-command producer). Computed once so each
118
+ // node entry can attach its slice and statusResult can carry the mesh rollup.
119
+ const { buildMeshSchedulingRuntime } = await import('../../mesh/mesh-scheduling-runtime.js');
120
+ const schedulingRuntime = buildMeshSchedulingRuntime(mesh, queue);
121
+ const schedulingByNode = new Map(schedulingRuntime.nodes.map(n => [n.nodeId, n]));
122
+
110
123
  const { readLedgerEntries, getLedgerSummary } = await import('../../mesh/mesh-ledger.js');
111
124
  const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
112
125
  const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
@@ -260,6 +273,16 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
260
273
  activeSessionDetails: [],
261
274
  launchReady: false,
262
275
  };
276
+ // Per-node scheduling slice (load / priority / provider caps / claim-block
277
+ // reasons) read by the dashboard's MeshNodeSchedulingBadges. Full shape —
278
+ // unlike the MCP compact path this is a dashboard surface, so it keeps the
279
+ // whole RepoMeshNodeSchedulingStatus. Redundant nodeId dropped (the entry
280
+ // already carries it).
281
+ const nodeScheduling = schedulingByNode.get(nodeId);
282
+ if (nodeScheduling) {
283
+ const { nodeId: _omitNodeId, ...nodeSchedulingRest } = nodeScheduling;
284
+ status.scheduling = nodeSchedulingRest;
285
+ }
263
286
  if (isSelfNode) {
264
287
  status.connection = {
265
288
  perspective: 'selected_coordinator',
@@ -511,6 +534,19 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
511
534
  branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
512
535
  ...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
513
536
  nodes: nodeStatuses,
537
+ // Mesh-level scheduling rollup (strategy + global cap consumption). Mirrors
538
+ // the MCP `mesh_status` tool's `scheduling` block field-for-field so both
539
+ // surfaces read the same runtime; per-node detail lives on each
540
+ // nodes[].scheduling above.
541
+ scheduling: {
542
+ strategy: schedulingRuntime.strategy,
543
+ maxParallelTasks: schedulingRuntime.maxParallelTasks,
544
+ maxReadonlyParallelTasks: schedulingRuntime.maxReadonlyParallelTasks,
545
+ activeWriteAssigned: schedulingRuntime.activeWriteAssigned,
546
+ activeReadonlyAssigned: schedulingRuntime.activeReadonlyAssigned,
547
+ globalWriteCapReached: schedulingRuntime.globalWriteCapReached,
548
+ globalReadonlyCapReached: schedulingRuntime.globalReadonlyCapReached,
549
+ },
514
550
  queue: { tasks: queue, summary: queueSummary },
515
551
  ledger: { entries: ledgerEntries, summary: ledgerSummary },
516
552
  ...(missions.length > 0 ? { missions } : {}),
@@ -9,6 +9,7 @@ import { markSessionDeliveriesTerminal, updateSessionDeliveryStatus, recordCompl
9
9
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
10
10
  import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents } from './mesh-events-pending.js';
11
11
  import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
12
+ import { resolveMeshHostStatus } from './mesh-host-ownership.js';
12
13
  import { enqueueUnresolvedDelegateForward, peekUnresolvedDelegateForwards, ackUnresolvedDelegateForward } from './mesh-unresolved-forward-outbox.js';
13
14
  import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
14
15
  import { getLastDisplayMessage } from '../status/snapshot.js';
@@ -92,6 +93,38 @@ function recoverMeshIdByNodeId(nodeId: string): string {
92
93
  return '';
93
94
  }
94
95
 
96
+ // MESHID-DROP coordinator-anchor recovery (Fix B): the last-resort meshId recovery for an
97
+ // unresolved-delegate forward whose payload carries the worker's coordinator anchor
98
+ // (meshCoordinatorDaemonId) but no resolvable meshId — neither workspace nor nodeId scan
99
+ // matched (a freshly-cloned worktree node not yet registered under the forwarded id form, or
100
+ // an empty payload nodeId). The receiving daemon IS the coordinator/host, so scope to the
101
+ // meshes IT hosts whose host daemon matches the anchor (daemonIdsEquivalent — never a raw
102
+ // compare, so daemon_mach_/mach_/standalone_ forms all match the same machine). Among those:
103
+ // • a nodeId → the mesh whose nodes contain it (meshNodeIdMatches 3-form) wins;
104
+ // • no nodeId → fall back to the anchor's SINGLE hosted mesh only. Ambiguity (the anchor
105
+ // hosts >1 mesh and no node disambiguates) returns '' rather than guess — a wrong meshId
106
+ // would inject the completion into an unrelated mesh's ledger, worse than the retry-cap drop.
107
+ export function recoverMeshIdByCoordinatorAndNode(coordinatorDaemonId: string, nodeId: string): string {
108
+ if (!coordinatorDaemonId) return '';
109
+ const hosted = listMeshes().filter(mesh => {
110
+ const host = resolveMeshHostStatus(mesh);
111
+ return host.role === 'host'
112
+ && (!host.hostDaemonId || daemonIdsEquivalent(host.hostDaemonId, coordinatorDaemonId));
113
+ });
114
+ if (hosted.length === 0) return '';
115
+ if (nodeId) {
116
+ const byNode = hosted.find(mesh =>
117
+ Array.isArray(mesh.nodes) && mesh.nodes.some((n: any) => meshNodeIdMatches(n, nodeId)));
118
+ if (byNode) return readNonEmptyString(byNode.id);
119
+ // nodeId present but matched no hosted mesh — do NOT fall through to the single-mesh
120
+ // guess; the node belongs to a mesh we don't host (or under a different id), and
121
+ // guessing would misroute. Stay unresolved.
122
+ return '';
123
+ }
124
+ // No nodeId to disambiguate: only safe when the anchor hosts exactly one mesh.
125
+ return hosted.length === 1 ? readNonEmptyString(hosted[0].id) : '';
126
+ }
127
+
95
128
  // RECONCILE-MESHID-DROP: WORKER-side meshId resolution for an unresolved-delegate
96
129
  // forward payload. forwardUnresolvedDelegateEvent omits meshId by design (the worker
97
130
  // "can't resolve it") and relies on the COORDINATOR recovering it from workspace/nodeId.
@@ -1234,7 +1267,14 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1234
1267
  // The nodeId is a stable coordinator-side fact and resolves timing-independently.
1235
1268
  const meshId = readNonEmptyString(payload.meshId)
1236
1269
  || (workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '')
1237
- || recoverMeshIdByNodeId(nodeId);
1270
+ || recoverMeshIdByNodeId(nodeId)
1271
+ // Fix B last resort: workspace + nodeId scan both missed, but the worker stamped
1272
+ // its coordinator anchor onto the forward (forwardUnresolvedDelegateEvent). Recover
1273
+ // via the hosted mesh that anchor owns (+ node disambiguation), guarding ambiguity.
1274
+ || recoverMeshIdByCoordinatorAndNode(
1275
+ readNonEmptyString(payload.meshCoordinatorDaemonId) || readNonEmptyString(payload.coordinatorDaemonId),
1276
+ nodeId,
1277
+ );
1238
1278
  if (!meshId) {
1239
1279
  // EVTTRACE: forwarded event rejected at receive — no meshId could be resolved
1240
1280
  // (no payload.meshId, no workspace→mesh, no nodeId→mesh). Observation only.
@@ -1364,6 +1404,11 @@ function forwardUnresolvedDelegateEvent(
1364
1404
  event: eventName,
1365
1405
  nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId) || undefined,
1366
1406
  workspace: readNonEmptyString(routing.workspace) || readNonEmptyString(event.workspace) || undefined,
1407
+ // Fix B: carry the resolved coordinator anchor so the coordinator's receive-side
1408
+ // recovery (recoverMeshIdByCoordinatorAndNode) can match this forward to one of the
1409
+ // meshes it hosts when workspace + nodeId both miss. routing.coordinatorDaemonId is
1410
+ // the same anchor this forward is addressed to (coordinatorDaemonId below).
1411
+ meshCoordinatorDaemonId: coordinatorDaemonId,
1367
1412
  };
1368
1413
  // RECONCILE-MESHID-DROP: stamp meshId when the WORKER can resolve it (member node /
1369
1414
  // live-session meshNodeFor). Historically omitted "because the worker can't resolve
@@ -972,9 +972,36 @@ export class CliProviderInstance implements ProviderInstance {
972
972
  * terminal state. Leaving meshNodeFor pinned would route this session's
973
973
  * subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
974
974
  * coordinator as if they were task completions.
975
+ *
976
+ * MESHID-DROP-ON-DETACH (Fix C): a coordinator-LAUNCHED worker session
977
+ * (launchedByCoordinator) holds its mesh membership (meshNodeFor / meshNodeId /
978
+ * meshCoordinatorDaemonId) at the SESSION level — set once at launch
979
+ * (mesh_launch_session / queue auto-launch), independent of any single task.
980
+ * The original detach wiped meshNodeFor + meshNodeId together with the
981
+ * task-level meshActiveTaskId, so the FIRST task completion stripped the
982
+ * membership and EVERY subsequent completion forwarded with meshId absent —
983
+ * resolveWorkerDelegateRouting fell to mesh_unresolved and the coordinator
984
+ * rejected the forward "meshId required". For a launched member we therefore
985
+ * clear ONLY the task-level marker (meshActiveTaskId) and preserve the
986
+ * session-level membership so its next task's completion still resolves.
987
+ * A task-less ad-hoc turn on a preserved-membership session is NOT misrouted:
988
+ * its completion carries no taskId and the session holds no active assignment,
989
+ * so the forwarder's WARMUPGAP guard skips the dispatch-row flip (it only
990
+ * injects a benign task-less notification). A NON-launched session (a plain CLI
991
+ * session adopted by mesh_send_task --direct, launchedByCoordinator falsy)
992
+ * keeps the original full clear so an ad-hoc session is never left pinned.
975
993
  */
976
994
  detachMeshAssignment(): void {
977
995
  if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
996
+ // Session-level member: keep membership, drop only the task-level marker.
997
+ if (this.settings.launchedByCoordinator === true) {
998
+ if (!this.settings.meshActiveTaskId) return;
999
+ const { meshActiveTaskId, ...rest } = this.settings;
1000
+ void meshActiveTaskId;
1001
+ this.settings = rest;
1002
+ this.adapter.updateRuntimeSettings?.(this.settings);
1003
+ return;
1004
+ }
978
1005
  const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
979
1006
  void meshNodeFor; void meshActiveTaskId;
980
1007
  // WTCLAIM (A): clear the active binding but PRESERVE the last bound node id