@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.
- package/dist/index.js +180 -102
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -102
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-event-forwarding.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +18 -0
- package/package.json +2 -2
- package/src/commands/high-family/mesh-status.ts +36 -0
- package/src/mesh/mesh-event-forwarding.ts +46 -1
- package/src/providers/cli-provider-instance.ts +27 -0
|
@@ -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.
|
|
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.
|
|
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
|