@adhdev/daemon-core 0.9.82-rc.364 → 0.9.82-rc.365
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/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +2 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/router.d.ts +83 -0
- package/dist/index.js +1590 -1513
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1593 -1517
- package/dist/index.mjs.map +1 -1
- package/dist/system/hash.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +2 -1
- package/src/commands/med-family/cli-agent.ts +218 -0
- package/src/commands/med-family/fast-forward.ts +198 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +35 -0
- package/src/commands/med-family/mesh-crud.ts +788 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +131 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +2 -2
- package/src/commands/router.ts +57 -1602
- package/src/config/mesh-config.ts +3 -2
- package/src/mesh/mesh-active-work.ts +59 -81
- package/src/system/hash.ts +23 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER MED family — Mesh Host manual-pairing commands.
|
|
3
|
+
*
|
|
4
|
+
* get_mesh_host_pairing, configure_mesh_host_pairing,
|
|
5
|
+
* create_mesh_host_pairing_token, apply_mesh_host_join and
|
|
6
|
+
* join_mesh_host_pairing. These read/mutate the mesh host pairing metadata and,
|
|
7
|
+
* for join, apply the request to the host over mesh-command dispatch or a
|
|
8
|
+
* standalone HTTP command. Extracted verbatim from executeDaemonCommand; pairing
|
|
9
|
+
* helpers are imported from router.js, identical to the original references.
|
|
10
|
+
*/
|
|
11
|
+
import { resolveMeshHostStatus } from '../../mesh/mesh-host-ownership.js';
|
|
12
|
+
import { buildMemberJoinNode, normalizeStandaloneHostCommandUrl } from '../router.js';
|
|
13
|
+
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
14
|
+
|
|
15
|
+
export const meshHostPairingHandlers: Record<string, MedFamilyHandler> = {
|
|
16
|
+
get_mesh_host_pairing: async (ctx: MedFamilyContext, args: any) => {
|
|
17
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
18
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
19
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
20
|
+
const mesh = meshRecord?.mesh;
|
|
21
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
22
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
23
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
27
|
+
meshId,
|
|
28
|
+
hostAddress: meshHost.hostAddress,
|
|
29
|
+
meshHost,
|
|
30
|
+
manualPairing: {
|
|
31
|
+
status: pairingStatus,
|
|
32
|
+
joinImplemented: true,
|
|
33
|
+
protocol: 'standalone_command_direct_v1',
|
|
34
|
+
description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
configure_mesh_host_pairing: async (ctx: MedFamilyContext, args: any) => {
|
|
40
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
41
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
42
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
43
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
44
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
45
|
+
try {
|
|
46
|
+
const { configureMeshHostPairing } = await import('../../config/mesh-config.js');
|
|
47
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
48
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
49
|
+
ctx.inlineMeshCache.set(meshId, configured.mesh);
|
|
50
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
code: 'mesh_host_pairing_pending',
|
|
54
|
+
meshId,
|
|
55
|
+
hostAddress: configured.hostAddress,
|
|
56
|
+
meshHost,
|
|
57
|
+
manualPairing: {
|
|
58
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
59
|
+
joinImplemented: true,
|
|
60
|
+
protocol: 'standalone_command_direct_v1',
|
|
61
|
+
description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
create_mesh_host_pairing_token: async (ctx: MedFamilyContext, args: any) => {
|
|
70
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
71
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
72
|
+
try {
|
|
73
|
+
const { createMeshHostPairingToken } = await import('../../config/mesh-config.js');
|
|
74
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
75
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
76
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
77
|
+
});
|
|
78
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
79
|
+
ctx.inlineMeshCache.set(meshId, created.mesh);
|
|
80
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
code: 'mesh_host_pairing_token_created',
|
|
84
|
+
meshId,
|
|
85
|
+
token: created.token,
|
|
86
|
+
tokenId: created.tokenId,
|
|
87
|
+
expiresAt: created.expiresAt,
|
|
88
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
89
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
90
|
+
};
|
|
91
|
+
} catch (e: any) {
|
|
92
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
apply_mesh_host_join: async (ctx: MedFamilyContext, args: any) => {
|
|
97
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
98
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
99
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
100
|
+
? args.memberNode
|
|
101
|
+
: null;
|
|
102
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
103
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
104
|
+
try {
|
|
105
|
+
const { applyMeshHostJoinRequest } = await import('../../config/mesh-config.js');
|
|
106
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
107
|
+
token,
|
|
108
|
+
memberNode: memberNode as any,
|
|
109
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
110
|
+
});
|
|
111
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
112
|
+
if (!applied.accepted) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
code: 'mesh_host_join_rejected',
|
|
116
|
+
meshId,
|
|
117
|
+
tokenId: applied.tokenId,
|
|
118
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
119
|
+
error: applied.reason,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
ctx.inlineMeshCache.set(meshId, applied.mesh);
|
|
123
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
124
|
+
try {
|
|
125
|
+
const { appendLedgerEntry } = await import('../../mesh/mesh-ledger.js');
|
|
126
|
+
appendLedgerEntry(meshId, {
|
|
127
|
+
kind: 'node_joined',
|
|
128
|
+
nodeId: applied.node.id,
|
|
129
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
130
|
+
});
|
|
131
|
+
} catch { /* ledger append is best-effort */ }
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
code: 'mesh_host_join_accepted',
|
|
135
|
+
meshId,
|
|
136
|
+
node: applied.node,
|
|
137
|
+
tokenId: applied.tokenId,
|
|
138
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
139
|
+
};
|
|
140
|
+
} catch (e: any) {
|
|
141
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
join_mesh_host_pairing: async (ctx: MedFamilyContext, args: any) => {
|
|
146
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
147
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
148
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
149
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
150
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
151
|
+
const mesh = meshRecord?.mesh;
|
|
152
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
153
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
154
|
+
if (meshHost.role !== 'member') {
|
|
155
|
+
return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../../config/mesh-config.js');
|
|
159
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
160
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
161
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
162
|
+
}
|
|
163
|
+
const memberNode = buildMemberJoinNode(mesh, args, ctx.deps.statusInstanceId);
|
|
164
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
165
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
166
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
167
|
+
? args.hostDaemonId.trim()
|
|
168
|
+
: meshHost.hostDaemonId;
|
|
169
|
+
let hostResult: any;
|
|
170
|
+
let transport: string;
|
|
171
|
+
if (hostDaemonId && ctx.deps.dispatchMeshCommand) {
|
|
172
|
+
transport = 'mesh_command_dispatch';
|
|
173
|
+
hostResult = await ctx.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
174
|
+
meshId: hostMeshId,
|
|
175
|
+
token,
|
|
176
|
+
memberMeshId: meshId,
|
|
177
|
+
memberNode,
|
|
178
|
+
});
|
|
179
|
+
} else if (meshHost.hostAddress) {
|
|
180
|
+
transport = 'standalone_http_command';
|
|
181
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
182
|
+
const response = await fetch(commandUrl, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
186
|
+
});
|
|
187
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
188
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
189
|
+
} else {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
193
|
+
meshId,
|
|
194
|
+
meshHost,
|
|
195
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (!hostResult?.success) {
|
|
199
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
200
|
+
}
|
|
201
|
+
const joined = meshRecord.inline
|
|
202
|
+
? null
|
|
203
|
+
: markMeshHostPairingJoined(meshId, {
|
|
204
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
205
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
206
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
207
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
208
|
+
});
|
|
209
|
+
if (joined) {
|
|
210
|
+
ctx.inlineMeshCache.set(meshId, joined.mesh);
|
|
211
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
code: 'mesh_host_join_applied',
|
|
216
|
+
meshId,
|
|
217
|
+
hostMeshId,
|
|
218
|
+
transport,
|
|
219
|
+
node: hostResult.node,
|
|
220
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
221
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
222
|
+
hostResult,
|
|
223
|
+
manualPairing: {
|
|
224
|
+
status: 'paired',
|
|
225
|
+
joinImplemented: true,
|
|
226
|
+
protocol: 'standalone_command_direct_v1',
|
|
227
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
} catch (e: any) {
|
|
231
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER MED family — mesh work-queue commands.
|
|
3
|
+
*
|
|
4
|
+
* get_mesh_queue (view with dependency annotation), cancel_mesh_queue_task,
|
|
5
|
+
* requeue_mesh_queue_task and trigger_mesh_queue. The mutating commands gate on
|
|
6
|
+
* the Mesh Host owner check (ctx.requireMeshHostMutationOwner). trigger preflights
|
|
7
|
+
* a preferred-node claim before the round-robin trigger. Extracted verbatim from
|
|
8
|
+
* executeDaemonCommand.
|
|
9
|
+
*/
|
|
10
|
+
import { readStringValue } from '../router.js';
|
|
11
|
+
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
12
|
+
|
|
13
|
+
export const meshQueueHandlers: Record<string, MedFamilyHandler> = {
|
|
14
|
+
get_mesh_queue: async (_ctx: MedFamilyContext, args: any) => {
|
|
15
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
16
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
17
|
+
try {
|
|
18
|
+
const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../../mesh/mesh-work-queue.js');
|
|
19
|
+
const status = Array.isArray(args?.status)
|
|
20
|
+
? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
|
21
|
+
: undefined;
|
|
22
|
+
const rawQueue = getQueue(meshId, { status: status as any });
|
|
23
|
+
// M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
|
|
24
|
+
const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
|
|
25
|
+
const queue = rawQueue.map(task =>
|
|
26
|
+
Array.isArray(task.dependsOn) && task.dependsOn.length > 0
|
|
27
|
+
? { ...task, ...describeTaskDependencyState(task, statusById) }
|
|
28
|
+
: task);
|
|
29
|
+
const summary = getMeshQueueStats(meshId);
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
queue,
|
|
33
|
+
summary,
|
|
34
|
+
sourceOfTruth: {
|
|
35
|
+
kind: 'mesh_work_queue_file',
|
|
36
|
+
activeStatuses: ['pending', 'assigned'],
|
|
37
|
+
historicalStatuses: ['completed', 'failed', 'cancelled'],
|
|
38
|
+
notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
} catch (e: any) {
|
|
42
|
+
return { success: false, error: e.message };
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
cancel_mesh_queue_task: async (ctx: MedFamilyContext, args: any) => {
|
|
47
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
48
|
+
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
49
|
+
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
50
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
51
|
+
if (ownerFailure) return ownerFailure;
|
|
52
|
+
try {
|
|
53
|
+
const { cancelTask } = await import('../../mesh/mesh-work-queue.js');
|
|
54
|
+
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
55
|
+
const task = cancelTask(meshId, taskId, { reason });
|
|
56
|
+
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
57
|
+
return { success: true, task };
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
return { success: false, error: e.message };
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
requeue_mesh_queue_task: async (ctx: MedFamilyContext, args: any) => {
|
|
64
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
65
|
+
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
66
|
+
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
67
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
68
|
+
if (ownerFailure) return ownerFailure;
|
|
69
|
+
try {
|
|
70
|
+
const { requeueTask } = await import('../../mesh/mesh-work-queue.js');
|
|
71
|
+
const task = requeueTask(meshId, taskId, {
|
|
72
|
+
reason: typeof args?.reason === 'string' ? args.reason : undefined,
|
|
73
|
+
targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
|
|
74
|
+
targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
|
|
75
|
+
clearTargetNode: args?.clearTargetNode === true,
|
|
76
|
+
clearTargetSession: args?.clearTargetSession !== false,
|
|
77
|
+
});
|
|
78
|
+
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
79
|
+
return { success: true, task };
|
|
80
|
+
} catch (e: any) {
|
|
81
|
+
return { success: false, error: e.message };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
trigger_mesh_queue: async (ctx: MedFamilyContext, args: any) => {
|
|
86
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
87
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
88
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
89
|
+
if (ownerFailure) return ownerFailure;
|
|
90
|
+
try {
|
|
91
|
+
const { triggerMeshQueue, tryAssignQueueTask } = await import('../../mesh/mesh-events.js');
|
|
92
|
+
|
|
93
|
+
// Bug A fix: when preferredNodeId is provided, attempt to claim a pending
|
|
94
|
+
// task for the preferred node's idle session first, before the general
|
|
95
|
+
// round-robin trigger picks a different node.
|
|
96
|
+
const preferredNodeId = typeof args?.preferredNodeId === 'string' ? args.preferredNodeId.trim() : '';
|
|
97
|
+
if (preferredNodeId) {
|
|
98
|
+
const cliInstances = ctx.deps.instanceManager.getByCategory('cli');
|
|
99
|
+
// Sort: preferred node's sessions first, others after
|
|
100
|
+
const sorted = [...cliInstances].sort((a, b) => {
|
|
101
|
+
const aSettings = a.getState().settings as Record<string, unknown> || {};
|
|
102
|
+
const bSettings = b.getState().settings as Record<string, unknown> || {};
|
|
103
|
+
const aNode = readStringValue(aSettings.meshNodeId, aSettings.nodeId);
|
|
104
|
+
const bNode = readStringValue(bSettings.meshNodeId, bSettings.nodeId);
|
|
105
|
+
return (aNode === preferredNodeId ? -1 : 0) - (bNode === preferredNodeId ? -1 : 0);
|
|
106
|
+
});
|
|
107
|
+
for (const inst of sorted) {
|
|
108
|
+
const state = inst.getState();
|
|
109
|
+
const settings = state.settings as Record<string, unknown> || {};
|
|
110
|
+
const nodeId = readStringValue(settings.meshNodeId, settings.nodeId);
|
|
111
|
+
if (!nodeId || nodeId !== preferredNodeId) continue;
|
|
112
|
+
const meshNodeFor = readStringValue(settings.meshNodeFor);
|
|
113
|
+
if (meshNodeFor !== meshId) continue;
|
|
114
|
+
const status = (readStringValue(state.status) || '').toLowerCase();
|
|
115
|
+
if (status !== 'idle') continue;
|
|
116
|
+
const sessionId = typeof state.instanceId === 'string' ? state.instanceId : '';
|
|
117
|
+
const providerType = readStringValue(state.type, settings.providerType) || '';
|
|
118
|
+
if (sessionId && providerType) {
|
|
119
|
+
tryAssignQueueTask(ctx.deps as any, meshId, nodeId, sessionId, providerType);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const trigger = await triggerMeshQueue(ctx.deps as any, meshId);
|
|
126
|
+
return { success: true, trigger };
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
return { success: false, error: e.message };
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER MED family — shared types for the extracted medium-coupling command
|
|
3
|
+
* handlers. Like the LOW family, each handler is a function of (context, args)
|
|
4
|
+
* that returns the exact CommandRouterResult the original `executeDaemonCommand`
|
|
5
|
+
* switch case returned, so the router facade is unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the LOW family, MED handlers need a handful of router-private
|
|
8
|
+
* collaborators (mesh resolution, owner gating, inline-cache mutation, worktree /
|
|
9
|
+
* session cleanup, refine job starters, IDE launch). The router binds these onto
|
|
10
|
+
* MedFamilyContext at dispatch; they are NOT reachable from `deps`. The IDE family
|
|
11
|
+
* also needs `launchIde` to break the original `launch_ide`/`restart_*`
|
|
12
|
+
* self-recursion through executeDaemonCommand.
|
|
13
|
+
*
|
|
14
|
+
* Registry dispatch: DaemonCommandRouter.executeDaemonCommand looks up the cmd in
|
|
15
|
+
* medFamilyRegistry BEFORE its switch; a hit returns the handler result, a miss
|
|
16
|
+
* falls through to the remaining switch (and ultimately CommandHandler delegation).
|
|
17
|
+
*/
|
|
18
|
+
import type { CommandRouterDeps, CommandRouterResult, MeshGitProbeCache } from '../router.js';
|
|
19
|
+
import type { RepoMeshSessionCleanupMode } from '../../repo-mesh-types.js';
|
|
20
|
+
import type { WorktreeBootstrapState } from '../../mesh/worktree-bootstrap-config.js';
|
|
21
|
+
|
|
22
|
+
/** Mesh record resolved from the router's inline-mesh cache + local config. */
|
|
23
|
+
export type ResolvedMeshForCommand = {
|
|
24
|
+
mesh: any;
|
|
25
|
+
inline: boolean;
|
|
26
|
+
source: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
27
|
+
} | null;
|
|
28
|
+
|
|
29
|
+
/** Result of the router's local worktree-node cleanup. */
|
|
30
|
+
export type CleanupLocalWorktreeNodeResult =
|
|
31
|
+
| { success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown>; recovered?: boolean; residue?: boolean; residueWarning?: string; residueError?: string }
|
|
32
|
+
| { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Router-private collaborators injected at dispatch. Each is a bound method or
|
|
36
|
+
* field of DaemonCommandRouter; handlers that don't need a given collaborator
|
|
37
|
+
* simply ignore it. The router owns this instance state (inline-mesh cache,
|
|
38
|
+
* aggregate-status cache, session/worktree cleanup, refine jobs), so it cannot be
|
|
39
|
+
* read from `deps` — the registry receives bound references instead.
|
|
40
|
+
*/
|
|
41
|
+
export interface MedFamilyContext {
|
|
42
|
+
deps: CommandRouterDeps;
|
|
43
|
+
|
|
44
|
+
/** Bound `DaemonCommandRouter.getMeshForCommand`. */
|
|
45
|
+
getMeshForCommand: (
|
|
46
|
+
meshId: string,
|
|
47
|
+
inlineMesh?: unknown,
|
|
48
|
+
options?: { preferInline?: boolean },
|
|
49
|
+
) => Promise<ResolvedMeshForCommand>;
|
|
50
|
+
|
|
51
|
+
/** Bound `DaemonCommandRouter.getCachedInlineMesh`. */
|
|
52
|
+
getCachedInlineMesh: (meshId: string, inlineMesh?: unknown) => any | undefined;
|
|
53
|
+
|
|
54
|
+
/** Bound `DaemonCommandRouter.requireMeshHostMutationOwner` (owner gate). */
|
|
55
|
+
requireMeshHostMutationOwner: (meshId: string, inlineMesh: unknown, operation: string) => Promise<CommandRouterResult | null>;
|
|
56
|
+
|
|
57
|
+
/** Bound `DaemonCommandRouter.invalidateAggregateMeshStatus`. */
|
|
58
|
+
invalidateAggregateMeshStatus: (meshId: string) => void;
|
|
59
|
+
|
|
60
|
+
/** Bound `DaemonCommandRouter.updateInlineMeshNode`. */
|
|
61
|
+
updateInlineMeshNode: (meshId: string, mesh: any, node: any) => void;
|
|
62
|
+
|
|
63
|
+
/** Bound `DaemonCommandRouter.removeInlineMeshNode`. */
|
|
64
|
+
removeInlineMeshNode: (meshId: string, mesh: any, nodeId: string) => boolean;
|
|
65
|
+
|
|
66
|
+
/** Bound `DaemonCommandRouter.normalizeMeshSessionCleanupMode`. */
|
|
67
|
+
normalizeMeshSessionCleanupMode: (value: unknown) => RepoMeshSessionCleanupMode;
|
|
68
|
+
|
|
69
|
+
/** Bound `DaemonCommandRouter.cleanupMeshSessions`. */
|
|
70
|
+
cleanupMeshSessions: (args: {
|
|
71
|
+
meshId: string;
|
|
72
|
+
nodeId: string;
|
|
73
|
+
node: any;
|
|
74
|
+
mode: RepoMeshSessionCleanupMode;
|
|
75
|
+
sessionIds?: string[];
|
|
76
|
+
dryRun?: boolean;
|
|
77
|
+
source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
|
|
78
|
+
}) => Promise<{ success: boolean; [key: string]: unknown }>;
|
|
79
|
+
|
|
80
|
+
/** Bound `DaemonCommandRouter.cleanupLocalWorktreeNode`. */
|
|
81
|
+
cleanupLocalWorktreeNode: (args: {
|
|
82
|
+
mesh: any;
|
|
83
|
+
node: any;
|
|
84
|
+
nodeId: string;
|
|
85
|
+
force?: boolean;
|
|
86
|
+
}) => Promise<CleanupLocalWorktreeNodeResult>;
|
|
87
|
+
|
|
88
|
+
/** Bound `DaemonCommandRouter.startMeshRefineJob` (async execute path). */
|
|
89
|
+
startMeshRefineJob: (meshId: string, nodeId: string, args: any) => Promise<CommandRouterResult>;
|
|
90
|
+
|
|
91
|
+
/** Bound `DaemonCommandRouter.batchRefineMeshNodes` (dry-run batch plan). */
|
|
92
|
+
batchRefineMeshNodes: (meshId: string, requestedNodeIds: string[] | undefined, args: any) => Promise<CommandRouterResult>;
|
|
93
|
+
|
|
94
|
+
/** Bound `DaemonCommandRouter.startMeshRefineBatchJob` (async batch execute). */
|
|
95
|
+
startMeshRefineBatchJob: (meshId: string, requestedNodeIds: string[] | undefined, args: any) => Promise<CommandRouterResult>;
|
|
96
|
+
|
|
97
|
+
/** Bound `DaemonCommandRouter.stopIde` (CDP disconnect + cleanup + optional kill). */
|
|
98
|
+
stopIde: (ideType: string, killProcess?: boolean) => Promise<void>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Module-level `launchIde` helper bound to this context. The original
|
|
102
|
+
* `launch_ide` case body, lifted into a free function so `restart_session` /
|
|
103
|
+
* `restart_ide` can invoke the IDE launch directly instead of recursing
|
|
104
|
+
* through `executeDaemonCommand('launch_ide')` (which would re-enter the
|
|
105
|
+
* registry). Byte-identical to the original case body.
|
|
106
|
+
*/
|
|
107
|
+
launchIde: (args: any) => Promise<CommandRouterResult>;
|
|
108
|
+
|
|
109
|
+
/** Router's inline-mesh cache (read/write of resolved mesh records). */
|
|
110
|
+
inlineMeshCache: Map<string, any>;
|
|
111
|
+
|
|
112
|
+
/** Router's mesh git-probe cache (reused direct-truth probes for get_mesh). */
|
|
113
|
+
meshGitProbeCache: MeshGitProbeCache;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type MedFamilyHandler = (ctx: MedFamilyContext, args: any) => Promise<CommandRouterResult | null>;
|
|
117
|
+
|
|
118
|
+
export type MedFamilyRegistry = Map<string, MedFamilyHandler>;
|
|
119
|
+
|
|
120
|
+
export type { WorktreeBootstrapState };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
2
|
import * as os from 'node:os'
|
|
4
3
|
import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core'
|
|
5
4
|
import { basename, isAbsolute, join, resolve } from 'node:path'
|
|
6
5
|
import { LOG } from '../logging/logger.js'
|
|
6
|
+
import { shortHash } from '../system/hash.js'
|
|
7
7
|
import type {
|
|
8
8
|
MeshCoordinatorMcpConfigFormat,
|
|
9
9
|
MeshCoordinatorSystemPromptInjection,
|
|
@@ -241,7 +241,7 @@ function replaceLegacyCliCommandMcpArgs(command: string, args: string[]): string
|
|
|
241
241
|
|
|
242
242
|
function resolveHermesCoordinatorHome(meshId: string, workspace: string): string {
|
|
243
243
|
const key = `${meshId || 'mesh'}\n${resolve(workspace || os.tmpdir())}`
|
|
244
|
-
const hash =
|
|
244
|
+
const hash = shortHash(key)
|
|
245
245
|
return join(os.tmpdir(), `adhdev-hermes-mesh-coordinator-${hash}`)
|
|
246
246
|
}
|
|
247
247
|
|