@adhdev/daemon-core 0.9.82-rc.384 → 0.9.82-rc.385
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 +36 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -5
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +1 -1
- package/dist/mesh/mesh-reconcile-loop.d.ts +56 -0
- package/package.json +2 -2
- package/src/commands/high-family/mesh-events.ts +13 -1
- package/src/mesh/mesh-events.ts +2 -0
- package/src/mesh/mesh-reconcile-loop.ts +84 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
2
2
|
export { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, } from './mesh-events-pending.js';
|
|
3
3
|
export { reconcileDirectDispatchCompletionFromTranscript, } from './mesh-events-stale.js';
|
|
4
|
-
export { setupMeshReconcileLoop, runMeshReconcileTick, } from './mesh-reconcile-loop.js';
|
|
4
|
+
export { setupMeshReconcileLoop, runMeshReconcileTick, resolveCoordinatorDrainDeliverability, shouldHoldPendingDrainForBusyLocalCoordinator, } from './mesh-reconcile-loop.js';
|
|
5
5
|
export type { MeshQueueTriggerResult } from './mesh-events-coordinator.js';
|
|
6
6
|
export { tryAssignQueueTask, triggerMeshQueue, handleMeshForwardEvent, setupMeshEventForwarding, isMeshCoordinatorEvent, __resetIdleAutoFastForwardForTests, __resetMeshWorkspaceCacheForTests, } from './mesh-events-coordinator.js';
|
|
@@ -1,4 +1,60 @@
|
|
|
1
1
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
|
+
/**
|
|
3
|
+
* DRAIN-WITHOUT-INJECT guard. Classify, for a mesh on THIS daemon, whether a
|
|
4
|
+
* queue-drain caller (the MCP `get_pending_mesh_events` poll) may safely consume
|
|
5
|
+
* pending coordinator events — i.e. whether there is a surface that will actually
|
|
6
|
+
* deliver them.
|
|
7
|
+
*
|
|
8
|
+
* Root cause being guarded: `get_pending_mesh_events` marks rows drained=1
|
|
9
|
+
* atomically and unconditionally. When the live CLI coordinator for the mesh is
|
|
10
|
+
* GENERATING (or modal-parked), the reconcile loop correctly HOLDS its terminal
|
|
11
|
+
* events (drained=0) for the coordinator's next idle tick — but a concurrent MCP
|
|
12
|
+
* poll draining the SAME queue consumes those held rows into a tool result that
|
|
13
|
+
* the busy coordinator never surfaces as a turn, so the completion is lost
|
|
14
|
+
* forever (drained=1, never re-queued). The reconcile loop is the authoritative
|
|
15
|
+
* delivery path for a live CLI coordinator; the MCP poll must defer to it.
|
|
16
|
+
*
|
|
17
|
+
* Returns:
|
|
18
|
+
* - hasLiveCliCoordinator: a CLI session with meshCoordinatorFor === meshId
|
|
19
|
+
* exists on this daemon (the reconcile loop owns its delivery).
|
|
20
|
+
* - deliverableNow: there is an IDLE live CLI coordinator (reconcile would
|
|
21
|
+
* full-drain into it) — draining now is safe and equivalent.
|
|
22
|
+
* - holdForReconcile: a live CLI coordinator exists but is non-idle
|
|
23
|
+
* (generating / modal-parked). The MCP poll MUST NOT drain; the reconcile
|
|
24
|
+
* loop holds the events undrained and injects them on the next idle tick.
|
|
25
|
+
*
|
|
26
|
+
* A mesh with NO live CLI coordinator on this daemon is a pure stdio MCP / LLM
|
|
27
|
+
* coordinator: the MCP tool result IS the only surface, so the poll legitimately
|
|
28
|
+
* drains (holdForReconcile=false). No regression to that path.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveCoordinatorDrainDeliverability(components: Pick<DaemonComponents, 'instanceManager'>, meshId: string): {
|
|
31
|
+
hasLiveCliCoordinator: boolean;
|
|
32
|
+
deliverableNow: boolean;
|
|
33
|
+
holdForReconcile: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* DRAIN-WITHOUT-INJECT guard for the `get_pending_mesh_events` daemon handler.
|
|
37
|
+
*
|
|
38
|
+
* Decides whether an incoming pending-events DRAIN must be held (return nothing,
|
|
39
|
+
* leave rows drained=0) because the only surface for those events is a LOCAL live
|
|
40
|
+
* CLI coordinator that is currently busy (generating / modal-parked) — in which
|
|
41
|
+
* case the reconcile loop owns delivery on the coordinator's next idle tick, and
|
|
42
|
+
* the poll draining them now would lose them.
|
|
43
|
+
*
|
|
44
|
+
* The hold applies ONLY when BOTH:
|
|
45
|
+
* 1) a live CLI coordinator for this mesh on THIS daemon is non-idle, AND
|
|
46
|
+
* 2) the drain is targeted at THIS daemon (the requested coordinatorDaemonId is
|
|
47
|
+
* empty/broadcast, or matches one of this daemon's id forms).
|
|
48
|
+
*
|
|
49
|
+
* A REMOTE coordinator pulling our worker's events passes its own (remote)
|
|
50
|
+
* coordinatorDaemonId — condition (2) is false — so the drain proceeds and the
|
|
51
|
+
* remote pull is never blocked by our local coordinator's busy state. A pure
|
|
52
|
+
* stdio MCP coordinator (no live CLI session) never satisfies (1), so its tool
|
|
53
|
+
* result remains the surface and the drain proceeds. No regression to either.
|
|
54
|
+
*/
|
|
55
|
+
export declare function shouldHoldPendingDrainForBusyLocalCoordinator(components: Pick<DaemonComponents, 'instanceManager'> & {
|
|
56
|
+
statusInstanceId?: string;
|
|
57
|
+
}, meshId: string, requestedCoordinatorDaemonId?: string | null): boolean;
|
|
2
58
|
export declare function runMeshReconcileTick(components: DaemonComponents): Promise<void>;
|
|
3
59
|
export declare function __resetUnresolvedForwardRejectionCountsForTests(): void;
|
|
4
60
|
interface ReconcileLoopHandle {
|
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.385",
|
|
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.385",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
handleMeshForwardEvent,
|
|
12
12
|
drainPendingMeshCoordinatorEvents,
|
|
13
|
+
shouldHoldPendingDrainForBusyLocalCoordinator,
|
|
13
14
|
} from '../../mesh/mesh-events.js';
|
|
14
15
|
import { normalizeInteractivePromptResponse } from '../../providers/types/interactive-prompt.js';
|
|
15
16
|
import type { HighFamilyContext, HighFamilyHandler } from './types.js';
|
|
@@ -19,7 +20,7 @@ export const meshEventsHandlers: Record<string, HighFamilyHandler> = {
|
|
|
19
20
|
return handleMeshForwardEvent({ instanceManager: ctx.deps.instanceManager } as any, args as Record<string, unknown>);
|
|
20
21
|
},
|
|
21
22
|
|
|
22
|
-
get_pending_mesh_events: async (
|
|
23
|
+
get_pending_mesh_events: async (ctx: HighFamilyContext, args: any) => {
|
|
23
24
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
24
25
|
// (B3) Respect coordinatorDaemonId when the caller declares it
|
|
25
26
|
// so unicast events route to the right coordinator instead of
|
|
@@ -27,6 +28,17 @@ export const meshEventsHandlers: Record<string, HighFamilyHandler> = {
|
|
|
27
28
|
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
28
29
|
? args.coordinatorDaemonId.trim()
|
|
29
30
|
: undefined;
|
|
31
|
+
// DRAIN-WITHOUT-INJECT guard: when a LOCAL live CLI coordinator for this mesh is
|
|
32
|
+
// busy (generating / modal-parked), the reconcile loop is HOLDING its terminal
|
|
33
|
+
// events (drained=0) for the coordinator's next idle tick. Draining here would
|
|
34
|
+
// consume those held rows (drained=1) into an MCP tool result the busy coordinator
|
|
35
|
+
// never surfaces as a turn — losing the completion forever. Defer to the reconcile
|
|
36
|
+
// loop: return nothing, leaving the rows undrained for its idle-tick delivery. A
|
|
37
|
+
// remote pull (foreign coordinatorDaemonId) or a pure stdio MCP coordinator (no live
|
|
38
|
+
// CLI session) is NOT held — see shouldHoldPendingDrainForBusyLocalCoordinator.
|
|
39
|
+
if (meshId && shouldHoldPendingDrainForBusyLocalCoordinator(ctx.deps, meshId, coordinatorDaemonId)) {
|
|
40
|
+
return { success: true, events: [], heldForBusyLocalCoordinator: true };
|
|
41
|
+
}
|
|
30
42
|
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
31
43
|
return { success: true, events };
|
|
32
44
|
},
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -20,6 +20,8 @@ export {
|
|
|
20
20
|
export {
|
|
21
21
|
setupMeshReconcileLoop,
|
|
22
22
|
runMeshReconcileTick,
|
|
23
|
+
resolveCoordinatorDrainDeliverability,
|
|
24
|
+
shouldHoldPendingDrainForBusyLocalCoordinator,
|
|
23
25
|
} from './mesh-reconcile-loop.js';
|
|
24
26
|
|
|
25
27
|
export type { MeshQueueTriggerResult } from './mesh-events-coordinator.js';
|
|
@@ -248,6 +248,90 @@ function findLiveCoordinators(components: DaemonComponents): LiveCoordinator[] {
|
|
|
248
248
|
return out;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* DRAIN-WITHOUT-INJECT guard. Classify, for a mesh on THIS daemon, whether a
|
|
253
|
+
* queue-drain caller (the MCP `get_pending_mesh_events` poll) may safely consume
|
|
254
|
+
* pending coordinator events — i.e. whether there is a surface that will actually
|
|
255
|
+
* deliver them.
|
|
256
|
+
*
|
|
257
|
+
* Root cause being guarded: `get_pending_mesh_events` marks rows drained=1
|
|
258
|
+
* atomically and unconditionally. When the live CLI coordinator for the mesh is
|
|
259
|
+
* GENERATING (or modal-parked), the reconcile loop correctly HOLDS its terminal
|
|
260
|
+
* events (drained=0) for the coordinator's next idle tick — but a concurrent MCP
|
|
261
|
+
* poll draining the SAME queue consumes those held rows into a tool result that
|
|
262
|
+
* the busy coordinator never surfaces as a turn, so the completion is lost
|
|
263
|
+
* forever (drained=1, never re-queued). The reconcile loop is the authoritative
|
|
264
|
+
* delivery path for a live CLI coordinator; the MCP poll must defer to it.
|
|
265
|
+
*
|
|
266
|
+
* Returns:
|
|
267
|
+
* - hasLiveCliCoordinator: a CLI session with meshCoordinatorFor === meshId
|
|
268
|
+
* exists on this daemon (the reconcile loop owns its delivery).
|
|
269
|
+
* - deliverableNow: there is an IDLE live CLI coordinator (reconcile would
|
|
270
|
+
* full-drain into it) — draining now is safe and equivalent.
|
|
271
|
+
* - holdForReconcile: a live CLI coordinator exists but is non-idle
|
|
272
|
+
* (generating / modal-parked). The MCP poll MUST NOT drain; the reconcile
|
|
273
|
+
* loop holds the events undrained and injects them on the next idle tick.
|
|
274
|
+
*
|
|
275
|
+
* A mesh with NO live CLI coordinator on this daemon is a pure stdio MCP / LLM
|
|
276
|
+
* coordinator: the MCP tool result IS the only surface, so the poll legitimately
|
|
277
|
+
* drains (holdForReconcile=false). No regression to that path.
|
|
278
|
+
*/
|
|
279
|
+
export function resolveCoordinatorDrainDeliverability(
|
|
280
|
+
components: Pick<DaemonComponents, 'instanceManager'>,
|
|
281
|
+
meshId: string,
|
|
282
|
+
): { hasLiveCliCoordinator: boolean; deliverableNow: boolean; holdForReconcile: boolean } {
|
|
283
|
+
const coordinators = findLiveCoordinators(components as DaemonComponents).filter(c => c.meshId === meshId);
|
|
284
|
+
if (coordinators.length === 0) {
|
|
285
|
+
return { hasLiveCliCoordinator: false, deliverableNow: false, holdForReconcile: false };
|
|
286
|
+
}
|
|
287
|
+
const hasIdle = coordinators.some(c => c.idle);
|
|
288
|
+
return {
|
|
289
|
+
hasLiveCliCoordinator: true,
|
|
290
|
+
deliverableNow: hasIdle,
|
|
291
|
+
// A live CLI coordinator exists but none is idle → the reconcile loop is
|
|
292
|
+
// holding the events; the poll must not steal them.
|
|
293
|
+
holdForReconcile: !hasIdle,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* DRAIN-WITHOUT-INJECT guard for the `get_pending_mesh_events` daemon handler.
|
|
299
|
+
*
|
|
300
|
+
* Decides whether an incoming pending-events DRAIN must be held (return nothing,
|
|
301
|
+
* leave rows drained=0) because the only surface for those events is a LOCAL live
|
|
302
|
+
* CLI coordinator that is currently busy (generating / modal-parked) — in which
|
|
303
|
+
* case the reconcile loop owns delivery on the coordinator's next idle tick, and
|
|
304
|
+
* the poll draining them now would lose them.
|
|
305
|
+
*
|
|
306
|
+
* The hold applies ONLY when BOTH:
|
|
307
|
+
* 1) a live CLI coordinator for this mesh on THIS daemon is non-idle, AND
|
|
308
|
+
* 2) the drain is targeted at THIS daemon (the requested coordinatorDaemonId is
|
|
309
|
+
* empty/broadcast, or matches one of this daemon's id forms).
|
|
310
|
+
*
|
|
311
|
+
* A REMOTE coordinator pulling our worker's events passes its own (remote)
|
|
312
|
+
* coordinatorDaemonId — condition (2) is false — so the drain proceeds and the
|
|
313
|
+
* remote pull is never blocked by our local coordinator's busy state. A pure
|
|
314
|
+
* stdio MCP coordinator (no live CLI session) never satisfies (1), so its tool
|
|
315
|
+
* result remains the surface and the drain proceeds. No regression to either.
|
|
316
|
+
*/
|
|
317
|
+
export function shouldHoldPendingDrainForBusyLocalCoordinator(
|
|
318
|
+
components: Pick<DaemonComponents, 'instanceManager'> & { statusInstanceId?: string },
|
|
319
|
+
meshId: string,
|
|
320
|
+
requestedCoordinatorDaemonId?: string | null,
|
|
321
|
+
): boolean {
|
|
322
|
+
if (!meshId) return false;
|
|
323
|
+
const deliverability = resolveCoordinatorDrainDeliverability(components, meshId);
|
|
324
|
+
if (!deliverability.holdForReconcile) return false;
|
|
325
|
+
// The local CLI coordinator is busy. Hold only when the drain is for THIS daemon.
|
|
326
|
+
const requested = readNonEmptyString(requestedCoordinatorDaemonId);
|
|
327
|
+
if (!requested) return true; // broadcast drain → would consume the held local events
|
|
328
|
+
const localIds = expandDaemonIdForms([
|
|
329
|
+
readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId),
|
|
330
|
+
readNonEmptyString(loadConfig().machineId),
|
|
331
|
+
]);
|
|
332
|
+
return localIds.some(id => daemonIdsEquivalent(id, requested));
|
|
333
|
+
}
|
|
334
|
+
|
|
251
335
|
// Inject a drained pending event into a live coordinator session. Force-inject
|
|
252
336
|
// events carry force:true so they bypass the busy send-guard and land in the PTY
|
|
253
337
|
// even while the coordinator is generating (see shouldForceInjectMeshEvent).
|