@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.137
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/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +154 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +73 -74
- package/dist/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2591 -1966
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2594 -1974
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/mesh-active-work.d.ts +7 -1
- package/dist/mesh/mesh-events.d.ts +10 -4
- package/dist/mesh/mesh-ledger.d.ts +21 -1
- package/dist/mesh/mesh-refine-status.d.ts +2 -3
- package/dist/mesh/mesh-work-queue.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
- package/dist/repo-mesh-types.d.ts +5 -0
- package/package.json +1 -1
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +957 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/src/cli-adapters/provider-cli-adapter.ts +365 -1397
- package/src/cli-adapters/provider-cli-shared.ts +4 -0
- package/src/commands/chat-commands.ts +17 -1
- package/src/commands/router.ts +8 -0
- package/src/config/chat-history.ts +7 -3
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +3 -2
- package/src/mesh/beads-db.ts +305 -2
- package/src/mesh/coordinator-prompt.ts +12 -17
- package/src/mesh/mesh-active-work.ts +162 -59
- package/src/mesh/mesh-events.ts +198 -53
- package/src/mesh/mesh-ledger.ts +321 -105
- package/src/mesh/mesh-refine-status.ts +2 -3
- package/src/mesh/mesh-work-queue.ts +116 -120
- package/src/mesh/worktree-bootstrap-config.ts +17 -4
- package/src/providers/provider-schema.ts +2 -0
- package/src/repo-mesh-types.ts +10 -0
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, readFileSync, unlinkSync } from 'fs';
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
4
4
|
import { loadConfig } from '../config/config.js';
|
|
@@ -7,7 +7,8 @@ import { detectCLI } from '../detection/cli-detector.js';
|
|
|
7
7
|
import { LOG } from '../logging/logger.js';
|
|
8
8
|
import { appendLedgerEntry, buildTaskCompletionEvidence, getLedgerDir, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
9
9
|
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
10
|
-
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch } from './mesh-work-queue.js';
|
|
10
|
+
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches } from './mesh-work-queue.js';
|
|
11
|
+
import { BeadsDB } from './beads-db.js';
|
|
11
12
|
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Remote Node Idle Session Tracking
|
|
@@ -25,6 +26,23 @@ interface RemoteIdleSession {
|
|
|
25
26
|
const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
26
27
|
const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
|
|
27
28
|
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Workspace-to-mesh lookup cache
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// getMeshByRepo is called on every coordinator event for sessions without
|
|
33
|
+
// meshNodeFor. Cache results for 5 seconds to avoid repeated config reads.
|
|
34
|
+
const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
|
|
35
|
+
const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
|
|
36
|
+
|
|
37
|
+
function getCachedMeshByWorkspace(workspace: string): any {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const cached = meshByWorkspaceCache.get(workspace);
|
|
40
|
+
if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
|
|
41
|
+
const mesh = getMeshByRepo(workspace);
|
|
42
|
+
meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
|
|
43
|
+
return mesh;
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
29
47
|
return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
|
|
30
48
|
}
|
|
@@ -57,6 +75,12 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
57
75
|
metadataEvent: Record<string, unknown>;
|
|
58
76
|
coordinatorMessage?: string;
|
|
59
77
|
queuedAt: number;
|
|
78
|
+
/**
|
|
79
|
+
* When set, this event is intended for a specific coordinator daemon.
|
|
80
|
+
* Coordinators on other daemons should ignore it during drain.
|
|
81
|
+
* Absent on legacy events — treated as broadcast to any coordinator.
|
|
82
|
+
*/
|
|
83
|
+
targetCoordinatorDaemonId?: string;
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
|
|
@@ -107,21 +131,37 @@ function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent)
|
|
|
107
131
|
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
108
132
|
}
|
|
109
133
|
|
|
110
|
-
function getPendingEventsPath(meshId: string): string {
|
|
134
|
+
function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
|
|
111
135
|
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
136
|
+
if (coordinatorDaemonId) {
|
|
137
|
+
const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
138
|
+
return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
|
|
139
|
+
}
|
|
112
140
|
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
113
141
|
}
|
|
114
142
|
|
|
115
|
-
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
143
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
116
144
|
if (!meshId) return [];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
// Read coordinator-scoped file first; fall back to legacy shared file.
|
|
146
|
+
const paths = coordinatorDaemonId
|
|
147
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
148
|
+
: [getPendingEventsPath(meshId)];
|
|
149
|
+
const events: PendingMeshCoordinatorEvent[] = [];
|
|
150
|
+
for (const path of paths) {
|
|
151
|
+
if (!existsSync(path)) continue;
|
|
152
|
+
try {
|
|
153
|
+
const raw = readFileSync(path, 'utf-8');
|
|
154
|
+
const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
|
|
155
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
156
|
+
});
|
|
157
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
158
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
159
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
160
|
+
: parsed;
|
|
161
|
+
events.push(...filtered);
|
|
162
|
+
} catch { /* skip unreadable files */ }
|
|
163
|
+
}
|
|
164
|
+
return events;
|
|
125
165
|
}
|
|
126
166
|
|
|
127
167
|
function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
@@ -139,7 +179,7 @@ function refineTerminalEventFromLedger(meshId: string, pending: readonly Pending
|
|
|
139
179
|
.filter(value => !value.endsWith(':')),
|
|
140
180
|
);
|
|
141
181
|
const backfilled: PendingMeshCoordinatorEvent[] = [];
|
|
142
|
-
const entries = readLedgerEntries(meshId);
|
|
182
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
143
183
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
144
184
|
const entry = entries[i];
|
|
145
185
|
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
|
|
@@ -194,6 +234,19 @@ function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMe
|
|
|
194
234
|
];
|
|
195
235
|
}
|
|
196
236
|
|
|
237
|
+
const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
|
|
238
|
+
const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
|
|
239
|
+
|
|
240
|
+
function trimPendingEventsIfNeeded(path: string): void {
|
|
241
|
+
try {
|
|
242
|
+
if (!existsSync(path)) return;
|
|
243
|
+
if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
|
|
244
|
+
const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
|
|
245
|
+
if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
|
|
246
|
+
writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
|
|
247
|
+
} catch { /* best-effort; if trim fails, append still proceeds */ }
|
|
248
|
+
}
|
|
249
|
+
|
|
197
250
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
198
251
|
try {
|
|
199
252
|
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
@@ -204,7 +257,11 @@ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEv
|
|
|
204
257
|
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
205
258
|
return true;
|
|
206
259
|
}
|
|
207
|
-
|
|
260
|
+
// Write to the coordinator-scoped file when the target coordinator is known;
|
|
261
|
+
// fall back to the shared file for legacy/unscoped events.
|
|
262
|
+
const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
|
|
263
|
+
trimPendingEventsIfNeeded(path);
|
|
264
|
+
appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
|
|
208
265
|
return true;
|
|
209
266
|
} catch (e: any) {
|
|
210
267
|
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
@@ -212,29 +269,64 @@ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEv
|
|
|
212
269
|
}
|
|
213
270
|
}
|
|
214
271
|
|
|
272
|
+
// Atomically rename the file before reading so concurrent drains can't both consume
|
|
273
|
+
// the same events. renameSync is atomic on POSIX (same filesystem); only one caller
|
|
274
|
+
// wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
|
|
275
|
+
function atomicDrainFile(path: string): string | null {
|
|
276
|
+
const tmpPath = `${path}.draining`;
|
|
277
|
+
try {
|
|
278
|
+
renameSync(path, tmpPath);
|
|
279
|
+
} catch {
|
|
280
|
+
return null; // another drain already renamed it, or file doesn't exist
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const content = readFileSync(tmpPath, 'utf-8');
|
|
284
|
+
try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
|
|
285
|
+
return content;
|
|
286
|
+
} catch {
|
|
287
|
+
try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
215
292
|
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
216
|
-
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
293
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
217
294
|
if (!meshId) return [];
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
295
|
+
const paths = coordinatorDaemonId
|
|
296
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
297
|
+
: [getPendingEventsPath(meshId)];
|
|
298
|
+
const all: PendingMeshCoordinatorEvent[] = [];
|
|
299
|
+
for (const path of paths) {
|
|
300
|
+
const content = atomicDrainFile(path);
|
|
301
|
+
if (!content) continue;
|
|
302
|
+
const parsed = content.split('\n').filter(Boolean).flatMap(line => {
|
|
303
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
304
|
+
});
|
|
305
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
306
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
307
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
308
|
+
: parsed;
|
|
309
|
+
all.push(...filtered);
|
|
310
|
+
}
|
|
311
|
+
if (all.length === 0) return [];
|
|
312
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, all);
|
|
225
313
|
}
|
|
226
314
|
|
|
227
315
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
228
|
-
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
316
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
229
317
|
if (!meshId) return [];
|
|
230
|
-
return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId));
|
|
318
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId));
|
|
231
319
|
}
|
|
232
320
|
|
|
233
|
-
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
234
|
-
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
321
|
+
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
322
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
|
|
235
323
|
if (!meshId) return;
|
|
236
|
-
const
|
|
237
|
-
|
|
324
|
+
const paths = coordinatorDaemonId
|
|
325
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
326
|
+
: [getPendingEventsPath(meshId)];
|
|
327
|
+
for (const path of paths) {
|
|
328
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
329
|
+
}
|
|
238
330
|
}
|
|
239
331
|
|
|
240
332
|
function readNonEmptyString(value: unknown): string {
|
|
@@ -319,7 +411,7 @@ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boole
|
|
|
319
411
|
function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
|
|
320
412
|
if (!sessionId && !nodeId) return false;
|
|
321
413
|
const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
|
|
322
|
-
const entries = readLedgerEntries(meshId);
|
|
414
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
323
415
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
324
416
|
const entry = entries[i];
|
|
325
417
|
const timestamp = new Date(entry.timestamp).getTime();
|
|
@@ -344,7 +436,22 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
344
436
|
}
|
|
345
437
|
|
|
346
438
|
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
347
|
-
|
|
439
|
+
|
|
440
|
+
function hasFingerprintSeen(fingerprint: string): boolean {
|
|
441
|
+
try {
|
|
442
|
+
return BeadsDB.getInstance().hasCompletionFingerprint(fingerprint);
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function recordFingerprintSeen(fingerprint: string): void {
|
|
449
|
+
try {
|
|
450
|
+
const db = BeadsDB.getInstance();
|
|
451
|
+
db.recordCompletionFingerprint(fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
|
|
452
|
+
db.sweepExpiredFingerprints();
|
|
453
|
+
} catch { /* best-effort; duplicate events are preferable to a crash */ }
|
|
454
|
+
}
|
|
348
455
|
|
|
349
456
|
function readEventTimestamp(value: unknown): number | null {
|
|
350
457
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
@@ -365,6 +472,10 @@ function buildMeshCompletionFingerprint(args: {
|
|
|
365
472
|
providerSessionId?: string;
|
|
366
473
|
timestamp?: number | null;
|
|
367
474
|
finalSummary?: string;
|
|
475
|
+
/** When set, scopes the fingerprint to a specific coordinator daemon so
|
|
476
|
+
* two coordinators processing events from their respective workers don't
|
|
477
|
+
* suppress each other's completion events. */
|
|
478
|
+
coordinatorDaemonId?: string;
|
|
368
479
|
}): string {
|
|
369
480
|
const timestampPart = Number.isFinite(args.timestamp)
|
|
370
481
|
? String(args.timestamp)
|
|
@@ -376,6 +487,7 @@ function buildMeshCompletionFingerprint(args: {
|
|
|
376
487
|
args.providerType || '',
|
|
377
488
|
args.providerSessionId || '',
|
|
378
489
|
timestampPart,
|
|
490
|
+
args.coordinatorDaemonId || '',
|
|
379
491
|
].join('::');
|
|
380
492
|
}
|
|
381
493
|
|
|
@@ -387,27 +499,20 @@ function isDuplicateMeshCompletionEvent(args: {
|
|
|
387
499
|
providerSessionId?: string;
|
|
388
500
|
timestamp?: number | null;
|
|
389
501
|
finalSummary?: string;
|
|
502
|
+
coordinatorDaemonId?: string;
|
|
390
503
|
}): boolean {
|
|
391
504
|
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
392
505
|
if (!fingerprint) return false;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
396
|
-
}
|
|
397
|
-
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
398
|
-
recentCompletionFingerprints.set(fingerprint, now);
|
|
506
|
+
if (hasFingerprintSeen(fingerprint)) return true;
|
|
507
|
+
recordFingerprintSeen(fingerprint);
|
|
399
508
|
return false;
|
|
400
509
|
}
|
|
401
510
|
|
|
402
511
|
function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
|
|
403
512
|
const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
|
|
404
513
|
if (!fingerprint) return false;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
408
|
-
}
|
|
409
|
-
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
410
|
-
recentCompletionFingerprints.set(fingerprint, now);
|
|
514
|
+
if (hasFingerprintSeen(fingerprint)) return true;
|
|
515
|
+
recordFingerprintSeen(fingerprint);
|
|
411
516
|
return false;
|
|
412
517
|
}
|
|
413
518
|
|
|
@@ -417,7 +522,10 @@ function findRecentTerminalLedgerEvidence(args: {
|
|
|
417
522
|
nodeId?: string;
|
|
418
523
|
}): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
419
524
|
if (!args.sessionId && !args.nodeId) return null;
|
|
420
|
-
|
|
525
|
+
// Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
|
|
526
|
+
// sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
|
|
527
|
+
// the BeadsDB fingerprint dedup will still block duplicate processing downstream.
|
|
528
|
+
const entries = readLedgerEntries(args.meshId, { tail: 200 });
|
|
421
529
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
422
530
|
const entry = entries[i];
|
|
423
531
|
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
@@ -435,7 +543,8 @@ function findRecentTerminalLedgerEvidence(args: {
|
|
|
435
543
|
// entry (identified by terminalId) in ledger order. Positional (append) order is used rather
|
|
436
544
|
// than timestamp comparison because both entries may share the same millisecond.
|
|
437
545
|
function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
|
|
438
|
-
|
|
546
|
+
// 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
|
|
547
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
439
548
|
let pastTerminal = false;
|
|
440
549
|
for (const entry of entries) {
|
|
441
550
|
if (!pastTerminal) {
|
|
@@ -564,6 +673,13 @@ const autoLaunchInProgress = new Set<string>();
|
|
|
564
673
|
const autoLaunchCooldownUntil = new Map<string, number>();
|
|
565
674
|
const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
|
|
566
675
|
|
|
676
|
+
function sweepExpiredCooldowns(): void {
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
for (const [key, until] of autoLaunchCooldownUntil) {
|
|
679
|
+
if (now >= until) autoLaunchCooldownUntil.delete(key);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
567
683
|
function normalizeProviderPriority(policy: unknown): string[] {
|
|
568
684
|
const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
569
685
|
? (policy as Record<string, unknown>).providerPriority
|
|
@@ -770,12 +886,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
770
886
|
const nodeId = readNonEmptyString(node?.id);
|
|
771
887
|
if (!nodeId) continue;
|
|
772
888
|
const launchKey = `${meshId}:${nodeId}`;
|
|
889
|
+
const now = Date.now();
|
|
773
890
|
const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
|
|
891
|
+
if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
|
|
774
892
|
if (autoLaunchInProgress.has(launchKey)) {
|
|
775
893
|
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_in_progress', nodeId });
|
|
776
894
|
continue;
|
|
777
895
|
}
|
|
778
|
-
if (
|
|
896
|
+
if (now < cooldownUntil) {
|
|
779
897
|
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_cooldown', nodeId });
|
|
780
898
|
continue;
|
|
781
899
|
}
|
|
@@ -825,13 +943,13 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
825
943
|
if (!launchResult?.success) {
|
|
826
944
|
const reason = launchResult?.error || 'launch_cli_failed';
|
|
827
945
|
markAutoLaunch(meshId, task.id, { status: 'failed', reason, nodeId, providerType: resolved.providerType });
|
|
828
|
-
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
|
|
946
|
+
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
829
947
|
return false;
|
|
830
948
|
}
|
|
831
949
|
const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
|
|
832
950
|
if (!sessionId) {
|
|
833
951
|
markAutoLaunch(meshId, task.id, { status: 'failed', reason: 'launch_missing_session_id', nodeId, providerType: resolved.providerType });
|
|
834
|
-
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
|
|
952
|
+
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
835
953
|
return false;
|
|
836
954
|
}
|
|
837
955
|
markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
|
|
@@ -1020,6 +1138,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1020
1138
|
}) {
|
|
1021
1139
|
const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
1022
1140
|
const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
1141
|
+
|
|
1142
|
+
// Resolve coordinator ownership early — used in fingerprinting and coordinator routing.
|
|
1143
|
+
const sourceSession = args.sourceInstanceId
|
|
1144
|
+
? components.instanceManager.getInstance(args.sourceInstanceId)
|
|
1145
|
+
: undefined;
|
|
1146
|
+
const workerCoordinatorDaemonId = readNonEmptyString(
|
|
1147
|
+
(sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
|
|
1148
|
+
);
|
|
1149
|
+
const localDaemonId = readNonEmptyString(loadConfig().machineId);
|
|
1023
1150
|
const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
|
|
1024
1151
|
event: args.event,
|
|
1025
1152
|
meshId: args.meshId,
|
|
@@ -1102,6 +1229,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1102
1229
|
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
1103
1230
|
timestamp: eventTimestamp,
|
|
1104
1231
|
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
1232
|
+
// Scope dedup to the coordinator daemon so two coordinators for the same mesh
|
|
1233
|
+
// don't suppress each other's completion events via shared fingerprint table.
|
|
1234
|
+
coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
|
|
1105
1235
|
});
|
|
1106
1236
|
if (duplicateCompletion) {
|
|
1107
1237
|
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
@@ -1121,9 +1251,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1121
1251
|
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
1122
1252
|
});
|
|
1123
1253
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
1254
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
|
|
1255
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
1124
1256
|
if (nodeId && providerType) {
|
|
1125
|
-
// Queue state is already updated above; setImmediate avoids the
|
|
1126
|
-
// 500 ms artificial delay while still deferring past this call frame.
|
|
1127
1257
|
setImmediate(() => {
|
|
1128
1258
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
1129
1259
|
});
|
|
@@ -1140,8 +1270,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1140
1270
|
const completedTask = sessionId && hasCompletionEvidence
|
|
1141
1271
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
1142
1272
|
: null;
|
|
1143
|
-
if (completedTask) {
|
|
1273
|
+
if (completedTask && sessionId) {
|
|
1144
1274
|
completedTaskForLedger = { id: completedTask.id };
|
|
1275
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
|
|
1276
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
1145
1277
|
try {
|
|
1146
1278
|
appendLedgerEntry(args.meshId, {
|
|
1147
1279
|
kind: 'task_completed',
|
|
@@ -1189,6 +1321,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1189
1321
|
if (sessionId && nodeId) {
|
|
1190
1322
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
1191
1323
|
}
|
|
1324
|
+
if (sessionId) {
|
|
1325
|
+
// Mark direct dispatch as acknowledged — the session started generating.
|
|
1326
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
|
|
1327
|
+
}
|
|
1192
1328
|
} else if (args.event === 'agent:stopped') {
|
|
1193
1329
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
1194
1330
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
@@ -1198,6 +1334,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1198
1334
|
if (sessionId) {
|
|
1199
1335
|
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
1200
1336
|
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
1337
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'failed');
|
|
1201
1338
|
}
|
|
1202
1339
|
}
|
|
1203
1340
|
|
|
@@ -1320,6 +1457,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1320
1457
|
const instState = inst.getState();
|
|
1321
1458
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
1322
1459
|
if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
|
|
1460
|
+
// If the worker knows which coordinator daemon launched it, only route to coordinators
|
|
1461
|
+
// on that specific daemon. This prevents cross-contamination when multiple coordinator
|
|
1462
|
+
// sessions run simultaneously for the same mesh on different daemons.
|
|
1463
|
+
if (workerCoordinatorDaemonId && localDaemonId && workerCoordinatorDaemonId !== localDaemonId) return false;
|
|
1323
1464
|
return true;
|
|
1324
1465
|
});
|
|
1325
1466
|
|
|
@@ -1330,7 +1471,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1330
1471
|
const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
|
|
1331
1472
|
|
|
1332
1473
|
if (coordinatorInstances.length === 0) {
|
|
1333
|
-
// No CLI coordinator
|
|
1474
|
+
// No local CLI coordinator — buffer for MCP-based coordinator on the target daemon.
|
|
1334
1475
|
if (queuePendingMeshCoordinatorEvent({
|
|
1335
1476
|
event: args.event,
|
|
1336
1477
|
meshId: args.meshId,
|
|
@@ -1343,8 +1484,11 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1343
1484
|
},
|
|
1344
1485
|
coordinatorMessage: messageText,
|
|
1345
1486
|
queuedAt: Date.now(),
|
|
1487
|
+
// Scope to the coordinator daemon that launched this worker so drain
|
|
1488
|
+
// by other coordinators on the same daemon doesn't consume this event.
|
|
1489
|
+
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
1346
1490
|
})) {
|
|
1347
|
-
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1491
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
|
|
1348
1492
|
}
|
|
1349
1493
|
return { success: true, forwarded: 0 };
|
|
1350
1494
|
}
|
|
@@ -1383,6 +1527,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1383
1527
|
},
|
|
1384
1528
|
coordinatorMessage: messageText,
|
|
1385
1529
|
queuedAt: Date.now(),
|
|
1530
|
+
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
1386
1531
|
})) {
|
|
1387
1532
|
if (allCoordinatorsGenerating) {
|
|
1388
1533
|
LOG.info('MeshEvents', `Queued ${args.event} for generating CLI coordinator (mesh ${args.meshId}) — will be delivered via get_pending_mesh_events when coordinator returns to idle`);
|
|
@@ -1484,7 +1629,7 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
1484
1629
|
const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
|
|
1485
1630
|
if (!isMeshDelegate) return;
|
|
1486
1631
|
|
|
1487
|
-
const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) :
|
|
1632
|
+
const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getCachedMeshByWorkspace(workspace);
|
|
1488
1633
|
const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
|
|
1489
1634
|
if (!meshId) return;
|
|
1490
1635
|
|