@adhdev/daemon-core 0.9.82-rc.275 → 0.9.82-rc.277
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/boot/daemon-lifecycle.d.ts +1 -0
- package/dist/index.js +59 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +59 -21
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-pending.d.ts +2 -2
- package/dist/mesh/mesh-runtime-store.d.ts +2 -2
- package/package.json +2 -2
- package/src/boot/daemon-lifecycle.ts +9 -0
- package/src/mesh/mesh-events-coordinator.ts +20 -2
- package/src/mesh/mesh-events-pending.ts +48 -15
- package/src/mesh/mesh-reconcile-loop.ts +23 -1
- package/src/mesh/mesh-runtime-store.ts +21 -8
|
@@ -31,11 +31,11 @@ export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoord
|
|
|
31
31
|
* the coordinator's next idle transition. The atomic SQLite drained=1 marking and the
|
|
32
32
|
* atomic JSONL rename keep force-drain and a concurrent full drain from double-consuming.
|
|
33
33
|
*/
|
|
34
|
-
export declare function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string
|
|
34
|
+
export declare function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>, opts?: {
|
|
35
35
|
onlyEvents?: ReadonlySet<string>;
|
|
36
36
|
}): PendingMeshCoordinatorEvent[];
|
|
37
37
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
38
|
-
export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[];
|
|
38
|
+
export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>): readonly PendingMeshCoordinatorEvent[];
|
|
39
39
|
/**
|
|
40
40
|
* Test helper: purge all pending-event state for a mesh — SQLite rows
|
|
41
41
|
* (including drained fingerprint history) and JSONL files.
|
|
@@ -298,7 +298,7 @@ export declare class MeshRuntimeStore {
|
|
|
298
298
|
* drained=1 marking, so force-drain + a concurrent full drain can never both
|
|
299
299
|
* consume the same row.
|
|
300
300
|
*/
|
|
301
|
-
drainPendingEvents(meshId: string, coordinatorDaemonId?: string | null
|
|
301
|
+
drainPendingEvents(meshId: string, coordinatorDaemonId?: string | null | ReadonlyArray<string>, opts?: {
|
|
302
302
|
onlyEvents?: ReadonlySet<string>;
|
|
303
303
|
}): Array<{
|
|
304
304
|
id: string;
|
|
@@ -306,7 +306,7 @@ export declare class MeshRuntimeStore {
|
|
|
306
306
|
payload: unknown;
|
|
307
307
|
}>;
|
|
308
308
|
/** Non-destructive peek — returns undrained events without marking them drained. */
|
|
309
|
-
peekPendingEvents(meshId: string, coordinatorDaemonId?: string | null): Array<{
|
|
309
|
+
peekPendingEvents(meshId: string, coordinatorDaemonId?: string | null | ReadonlyArray<string>): Array<{
|
|
310
310
|
id: string;
|
|
311
311
|
event: string;
|
|
312
312
|
payload: unknown;
|
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.277",
|
|
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.277",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -123,6 +123,14 @@ export interface DaemonComponents {
|
|
|
123
123
|
// single-model (queue + polling) delivery: drains the pending-events queue
|
|
124
124
|
// on a fixed interval and injects into live CLI coordinators when idle.
|
|
125
125
|
meshReconcileLoop?: { stop(): void };
|
|
126
|
+
// Canonical status/daemon identity (e.g. `standalone_<machineId>` /
|
|
127
|
+
// `daemon_<machineId>`). This is the SAME id the MCP layer stamps as a
|
|
128
|
+
// worker's meshCoordinatorDaemonId (ctx.localDaemonId, sourced from
|
|
129
|
+
// getStatus().status.instanceId), so the reconcile loop MUST drain with it —
|
|
130
|
+
// draining with bare loadConfig().machineId never matches a unicast event
|
|
131
|
+
// stamped with the prefixed status id. Absent → reconcile falls back to
|
|
132
|
+
// machineId only.
|
|
133
|
+
statusInstanceId?: string;
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
export interface DaemonDevSupportOptions {
|
|
@@ -362,6 +370,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
362
370
|
refreshProviderAvailability,
|
|
363
371
|
dispatchMeshCommand: config.dispatchMeshCommand,
|
|
364
372
|
onMeshCoordinatorEventForwarded: config.onMeshCoordinatorEventForwarded,
|
|
373
|
+
statusInstanceId: config.statusInstanceId,
|
|
365
374
|
};
|
|
366
375
|
|
|
367
376
|
// 11. Setup Mesh Event Forwarding (queue persistence) + periodic reconcile loop.
|
|
@@ -29,6 +29,19 @@ import {
|
|
|
29
29
|
readWorkerResultMetadata,
|
|
30
30
|
} from './mesh-events-utils.js';
|
|
31
31
|
|
|
32
|
+
// The set of coordinator-daemon ids this daemon answers to when draining the
|
|
33
|
+
// pending-events queue (canonical status id + bare machineId). Mirrors
|
|
34
|
+
// resolveCoordinatorDaemonIds in mesh-reconcile-loop — a unicast event may be
|
|
35
|
+
// stamped with either id depending on which dispatch path created the worker.
|
|
36
|
+
function resolveCoordinatorDrainDaemonIds(components: DaemonComponents): string[] {
|
|
37
|
+
const ids = new Set<string>();
|
|
38
|
+
const statusInstanceId = readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId);
|
|
39
|
+
if (statusInstanceId) ids.add(statusInstanceId);
|
|
40
|
+
const machineId = readNonEmptyString(loadConfig().machineId);
|
|
41
|
+
if (machineId) ids.add(machineId);
|
|
42
|
+
return [...ids];
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
// ---------------------------------------------------------------------------
|
|
33
46
|
// Remote Node Idle Session Tracking
|
|
34
47
|
// ---------------------------------------------------------------------------
|
|
@@ -1506,8 +1519,13 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
1506
1519
|
const status = readNonEmptyString(flushState.status).toLowerCase();
|
|
1507
1520
|
if (status === 'idle') {
|
|
1508
1521
|
try {
|
|
1509
|
-
|
|
1510
|
-
|
|
1522
|
+
// Drain with the daemon's full coordinator-id set (status id + machineId).
|
|
1523
|
+
// The MCP layer stamps the prefixed status id (`standalone_<machineId>` /
|
|
1524
|
+
// `daemon_<machineId>`) as the worker's meshCoordinatorDaemonId; draining
|
|
1525
|
+
// with bare machineId alone would miss those unicast events. Mirrors
|
|
1526
|
+
// resolveCoordinatorDaemonIds in mesh-reconcile-loop.
|
|
1527
|
+
const drainDaemonIds = resolveCoordinatorDrainDaemonIds(components);
|
|
1528
|
+
const pendingEvents = drainPendingMeshCoordinatorEvents(coordinatorMeshId, drainDaemonIds.length > 0 ? drainDaemonIds : undefined);
|
|
1511
1529
|
if (pendingEvents.length > 0) {
|
|
1512
1530
|
LOG.info('MeshEvents', `Auto-flushing ${pendingEvents.length} pending coordinator event(s) for mesh ${coordinatorMeshId} on coordinator idle`);
|
|
1513
1531
|
for (const pending of pendingEvents) {
|
|
@@ -37,6 +37,27 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
37
37
|
|
|
38
38
|
const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
|
|
39
39
|
|
|
40
|
+
/** Normalise a coordinator-daemon-id argument (single id, list, or undefined) into a
|
|
41
|
+
* de-duplicated list of non-empty strings. The first entry is treated as primary for
|
|
42
|
+
* per-daemon JSONL file naming; all entries are accepted by drain/peek targeting. */
|
|
43
|
+
function normalizeCoordinatorDaemonIds(
|
|
44
|
+
coordinatorDaemonId?: string | null | ReadonlyArray<string>,
|
|
45
|
+
): string[] {
|
|
46
|
+
const raw = Array.isArray(coordinatorDaemonId)
|
|
47
|
+
? coordinatorDaemonId
|
|
48
|
+
: coordinatorDaemonId != null ? [coordinatorDaemonId] : [];
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
for (const id of raw) {
|
|
52
|
+
if (typeof id !== 'string') continue;
|
|
53
|
+
const trimmed = id.trim();
|
|
54
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
55
|
+
seen.add(trimmed);
|
|
56
|
+
out.push(trimmed);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
export function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
|
|
41
62
|
const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
|
|
42
63
|
const result = readRecord(metadata.result);
|
|
@@ -108,11 +129,13 @@ function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): str
|
|
|
108
129
|
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
132
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>): PendingMeshCoordinatorEvent[] {
|
|
112
133
|
if (!meshId) return [];
|
|
134
|
+
const daemonIds = normalizeCoordinatorDaemonIds(coordinatorDaemonId);
|
|
135
|
+
const primaryDaemonId = daemonIds[0];
|
|
113
136
|
// Read coordinator-scoped file first; fall back to legacy shared file.
|
|
114
|
-
const paths =
|
|
115
|
-
? [getPendingEventsPath(meshId,
|
|
137
|
+
const paths = primaryDaemonId
|
|
138
|
+
? [getPendingEventsPath(meshId, primaryDaemonId), getPendingEventsPath(meshId)]
|
|
116
139
|
: [getPendingEventsPath(meshId)];
|
|
117
140
|
const events: PendingMeshCoordinatorEvent[] = [];
|
|
118
141
|
for (const path of paths) {
|
|
@@ -123,8 +146,8 @@ function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDa
|
|
|
123
146
|
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
124
147
|
});
|
|
125
148
|
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
126
|
-
const filtered = (
|
|
127
|
-
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId
|
|
149
|
+
const filtered = (primaryDaemonId && path === getPendingEventsPath(meshId))
|
|
150
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || daemonIds.includes(e.targetCoordinatorDaemonId))
|
|
128
151
|
: parsed;
|
|
129
152
|
events.push(...filtered);
|
|
130
153
|
} catch { /* skip unreadable files */ }
|
|
@@ -342,11 +365,18 @@ function selectiveDrainFile(
|
|
|
342
365
|
*/
|
|
343
366
|
export function drainPendingMeshCoordinatorEvents(
|
|
344
367
|
meshId?: string,
|
|
345
|
-
coordinatorDaemonId?: string
|
|
368
|
+
coordinatorDaemonId?: string | ReadonlyArray<string>,
|
|
346
369
|
opts?: { onlyEvents?: ReadonlySet<string> },
|
|
347
370
|
): PendingMeshCoordinatorEvent[] {
|
|
348
371
|
if (!meshId) return [];
|
|
349
372
|
|
|
373
|
+
// A daemon may answer to more than one coordinator-id form (its canonical
|
|
374
|
+
// status id like `standalone_<machineId>` AND the bare machineId). Normalise
|
|
375
|
+
// to a list so both the SQLite IN-filter and the JSONL targeting predicate
|
|
376
|
+
// accept any of them.
|
|
377
|
+
const daemonIds = normalizeCoordinatorDaemonIds(coordinatorDaemonId);
|
|
378
|
+
const primaryDaemonId = daemonIds[0];
|
|
379
|
+
|
|
350
380
|
const onlyEvents = opts?.onlyEvents;
|
|
351
381
|
const matchesFilter = (eventName: string): boolean => !onlyEvents || onlyEvents.has(eventName);
|
|
352
382
|
|
|
@@ -368,7 +398,7 @@ export function drainPendingMeshCoordinatorEvents(
|
|
|
368
398
|
try {
|
|
369
399
|
const store = MeshRuntimeStore.getInstance();
|
|
370
400
|
if (store.pendingEventCount(meshId) > 0) {
|
|
371
|
-
for (const row of store.drainPendingEvents(meshId,
|
|
401
|
+
for (const row of store.drainPendingEvents(meshId, daemonIds.length > 0 ? daemonIds : undefined, onlyEvents ? { onlyEvents } : undefined)) {
|
|
372
402
|
const event = row.payload as PendingMeshCoordinatorEvent;
|
|
373
403
|
if (event) pushUnique(event);
|
|
374
404
|
}
|
|
@@ -377,16 +407,18 @@ export function drainPendingMeshCoordinatorEvents(
|
|
|
377
407
|
// SQLite drain failed — JSONL below still drains
|
|
378
408
|
}
|
|
379
409
|
|
|
380
|
-
// JSONL (legacy / migration path) — always drained alongside SQLite
|
|
381
|
-
|
|
382
|
-
|
|
410
|
+
// JSONL (legacy / migration path) — always drained alongside SQLite.
|
|
411
|
+
// The scoped per-daemon file is keyed by a single id; use the primary. The
|
|
412
|
+
// shared (unscoped) file's targeting predicate accepts ANY of this daemon's ids.
|
|
413
|
+
const paths = primaryDaemonId
|
|
414
|
+
? [getPendingEventsPath(meshId, primaryDaemonId), getPendingEventsPath(meshId)]
|
|
383
415
|
: [getPendingEventsPath(meshId)];
|
|
384
416
|
for (const path of paths) {
|
|
385
|
-
const isSharedFile =
|
|
417
|
+
const isSharedFile = !!primaryDaemonId && path === getPendingEventsPath(meshId);
|
|
386
418
|
// Targeting predicate for the shared (unscoped) file: only this coordinator's
|
|
387
419
|
// events (or legacy untargeted ones) are eligible.
|
|
388
420
|
const targets = (e: PendingMeshCoordinatorEvent): boolean =>
|
|
389
|
-
!isSharedFile || !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId
|
|
421
|
+
!isSharedFile || !e.targetCoordinatorDaemonId || daemonIds.includes(e.targetCoordinatorDaemonId);
|
|
390
422
|
|
|
391
423
|
if (onlyEvents) {
|
|
392
424
|
// Selective JSONL drain: consume only matching events, rewrite the rest back.
|
|
@@ -413,8 +445,9 @@ export function drainPendingMeshCoordinatorEvents(
|
|
|
413
445
|
}
|
|
414
446
|
|
|
415
447
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
416
|
-
export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
448
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>): readonly PendingMeshCoordinatorEvent[] {
|
|
417
449
|
if (!meshId) return [];
|
|
450
|
+
const daemonIds = normalizeCoordinatorDaemonIds(coordinatorDaemonId);
|
|
418
451
|
|
|
419
452
|
// Merge SQLite (primary) + JSONL (legacy) with fingerprint dedup.
|
|
420
453
|
const merged: PendingMeshCoordinatorEvent[] = [];
|
|
@@ -432,7 +465,7 @@ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaem
|
|
|
432
465
|
try {
|
|
433
466
|
const store = MeshRuntimeStore.getInstance();
|
|
434
467
|
if (store.pendingEventCount(meshId) > 0) {
|
|
435
|
-
for (const row of store.peekPendingEvents(meshId,
|
|
468
|
+
for (const row of store.peekPendingEvents(meshId, daemonIds.length > 0 ? daemonIds : undefined)) {
|
|
436
469
|
const event = row.payload as PendingMeshCoordinatorEvent;
|
|
437
470
|
if (event) pushUnique(event);
|
|
438
471
|
}
|
|
@@ -440,7 +473,7 @@ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaem
|
|
|
440
473
|
} catch { /* SQLite unavailable — JSONL fallback below */ }
|
|
441
474
|
|
|
442
475
|
// JSONL (legacy)
|
|
443
|
-
for (const event of readPendingMeshCoordinatorEventsFromDisk(meshId,
|
|
476
|
+
for (const event of readPendingMeshCoordinatorEventsFromDisk(meshId, daemonIds)) {
|
|
444
477
|
pushUnique(event);
|
|
445
478
|
}
|
|
446
479
|
|
|
@@ -69,6 +69,24 @@ interface LiveCoordinator {
|
|
|
69
69
|
idle: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// The set of coordinator-daemon ids THIS daemon answers to when draining the
|
|
73
|
+
// pending-events queue. A unicast completion event is stamped with the worker's
|
|
74
|
+
// meshCoordinatorDaemonId, which can be either:
|
|
75
|
+
// - the daemon's canonical status id (`standalone_<machineId>` / `daemon_<machineId>`),
|
|
76
|
+
// stamped by the MCP layer via ctx.localDaemonId (= getStatus().status.instanceId), or
|
|
77
|
+
// - the bare machineId, stamped by the local queue-assignment path (loadConfig().machineId).
|
|
78
|
+
// Draining with only one of these silently misses events stamped with the other —
|
|
79
|
+
// the exact reason a generating coordinator never self-received local completions.
|
|
80
|
+
// We accept BOTH so the drain matches regardless of which path stamped the event.
|
|
81
|
+
function resolveCoordinatorDaemonIds(components: DaemonComponents): string[] {
|
|
82
|
+
const ids = new Set<string>();
|
|
83
|
+
const statusInstanceId = readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId);
|
|
84
|
+
if (statusInstanceId) ids.add(statusInstanceId);
|
|
85
|
+
const machineId = readNonEmptyString(loadConfig().machineId);
|
|
86
|
+
if (machineId) ids.add(machineId);
|
|
87
|
+
return [...ids];
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
// Find live CLI coordinator instances on THIS daemon, keyed by mesh.
|
|
73
91
|
function findLiveCoordinators(components: DaemonComponents): LiveCoordinator[] {
|
|
74
92
|
const out: LiveCoordinator[] = [];
|
|
@@ -119,6 +137,10 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
|
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
const localDaemonId = readNonEmptyString(loadConfig().machineId) || undefined;
|
|
140
|
+
// The id-set used to scope the local queue drain (status id + machineId). See
|
|
141
|
+
// resolveCoordinatorDaemonIds — the status id is what the MCP layer stamps and
|
|
142
|
+
// is mandatory here for a generating CLI coordinator to self-receive completions.
|
|
143
|
+
const drainDaemonIds = resolveCoordinatorDaemonIds(components);
|
|
122
144
|
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
123
145
|
const store = (() => {
|
|
124
146
|
try { return MeshRuntimeStore.getInstance(); } catch { return undefined; }
|
|
@@ -163,7 +185,7 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
|
|
|
163
185
|
try {
|
|
164
186
|
pendingEvents = drainPendingMeshCoordinatorEvents(
|
|
165
187
|
meshId,
|
|
166
|
-
localDaemonId,
|
|
188
|
+
drainDaemonIds.length > 0 ? drainDaemonIds : localDaemonId,
|
|
167
189
|
forceOnly ? { onlyEvents: MESH_FORCE_INJECT_EVENTS } : undefined,
|
|
168
190
|
);
|
|
169
191
|
} catch (e: any) {
|
|
@@ -1253,7 +1253,7 @@ export class MeshRuntimeStore {
|
|
|
1253
1253
|
*/
|
|
1254
1254
|
drainPendingEvents(
|
|
1255
1255
|
meshId: string,
|
|
1256
|
-
coordinatorDaemonId?: string | null
|
|
1256
|
+
coordinatorDaemonId?: string | null | ReadonlyArray<string>,
|
|
1257
1257
|
opts?: { onlyEvents?: ReadonlySet<string> },
|
|
1258
1258
|
): Array<{ id: string; event: string; payload: unknown }> {
|
|
1259
1259
|
return this.transaction(() => {
|
|
@@ -1261,14 +1261,23 @@ export class MeshRuntimeStore {
|
|
|
1261
1261
|
// An explicit-but-empty filter means "drain nothing" (no event name can match).
|
|
1262
1262
|
if (onlyEvents && onlyEvents.size === 0) return [];
|
|
1263
1263
|
const eventList = onlyEvents ? [...onlyEvents] : [];
|
|
1264
|
+
// A coordinator daemon can answer to more than one id form: its canonical
|
|
1265
|
+
// status id (e.g. `standalone_<machineId>` / `daemon_<machineId>`, which the
|
|
1266
|
+
// MCP layer stamps via ctx.localDaemonId) AND the bare machineId (stamped by
|
|
1267
|
+
// the local queue-assignment path). Accept ANY of them so a unicast event
|
|
1268
|
+
// stamped with either id is drained here. Unscoped (NULL) rows always match.
|
|
1269
|
+
const daemonIds = (Array.isArray(coordinatorDaemonId)
|
|
1270
|
+
? coordinatorDaemonId
|
|
1271
|
+
: coordinatorDaemonId ? [coordinatorDaemonId] : [])
|
|
1272
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
1264
1273
|
// Filter by event name IN-SQL when onlyEvents is set so the LIMIT applies to
|
|
1265
1274
|
// matching rows — a long run of non-force events ahead in the queue must not
|
|
1266
1275
|
// crowd a force event out of the 100-row window.
|
|
1267
1276
|
const clauses = ['mesh_id = ?', 'drained = 0'];
|
|
1268
1277
|
const params: unknown[] = [meshId];
|
|
1269
|
-
if (
|
|
1270
|
-
clauses.push(
|
|
1271
|
-
params.push(
|
|
1278
|
+
if (daemonIds.length > 0) {
|
|
1279
|
+
clauses.push(`(coordinator_daemon_id IS NULL OR coordinator_daemon_id IN (${daemonIds.map(() => '?').join(',')}))`);
|
|
1280
|
+
params.push(...daemonIds);
|
|
1272
1281
|
}
|
|
1273
1282
|
if (eventList.length > 0) {
|
|
1274
1283
|
clauses.push(`event IN (${eventList.map(() => '?').join(',')})`);
|
|
@@ -1292,11 +1301,15 @@ export class MeshRuntimeStore {
|
|
|
1292
1301
|
}
|
|
1293
1302
|
|
|
1294
1303
|
/** Non-destructive peek — returns undrained events without marking them drained. */
|
|
1295
|
-
peekPendingEvents(meshId: string, coordinatorDaemonId?: string | null): Array<{ id: string; event: string; payload: unknown }> {
|
|
1296
|
-
const
|
|
1297
|
-
?
|
|
1304
|
+
peekPendingEvents(meshId: string, coordinatorDaemonId?: string | null | ReadonlyArray<string>): Array<{ id: string; event: string; payload: unknown }> {
|
|
1305
|
+
const daemonIds = (Array.isArray(coordinatorDaemonId)
|
|
1306
|
+
? coordinatorDaemonId
|
|
1307
|
+
: coordinatorDaemonId ? [coordinatorDaemonId] : [])
|
|
1308
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
1309
|
+
const whereClause = daemonIds.length > 0
|
|
1310
|
+
? `WHERE mesh_id = ? AND drained = 0 AND (coordinator_daemon_id IS NULL OR coordinator_daemon_id IN (${daemonIds.map(() => '?').join(',')}))`
|
|
1298
1311
|
: `WHERE mesh_id = ? AND drained = 0`;
|
|
1299
|
-
const params: unknown[] =
|
|
1312
|
+
const params: unknown[] = daemonIds.length > 0 ? [meshId, ...daemonIds] : [meshId];
|
|
1300
1313
|
const rows = this.db.prepare(
|
|
1301
1314
|
`SELECT id, event, payload FROM mesh_pending_events ${whereClause} ORDER BY queued_at ASC LIMIT 100`
|
|
1302
1315
|
).all(...params) as Array<{ id: string; event: string; payload: string }>;
|