@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.81
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/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +5074 -1177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5038 -1163
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +29 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +39 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +91 -3
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +20 -10
- package/src/commands/chat-commands.ts +310 -12
- package/src/commands/cli-manager.ts +101 -0
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2435 -414
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +244 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +31 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +27 -7
- package/src/mesh/mesh-active-work.ts +243 -0
- package/src/mesh/mesh-events.ts +398 -46
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +91 -13
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +43 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,63 +1,179 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
4
|
import { loadConfig } from '../config/config.js';
|
|
3
5
|
import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
|
|
4
6
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
5
7
|
import { LOG } from '../logging/logger.js';
|
|
6
|
-
import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
8
|
+
import { appendLedgerEntry, buildTaskCompletionEvidence, getLedgerDir, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
7
9
|
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
8
10
|
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch } from './mesh-work-queue.js';
|
|
9
11
|
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Remote Node Idle Session Tracking
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
|
-
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
14
|
-
// can assign tasks to them.
|
|
15
|
+
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
16
|
+
// can assign tasks to them. Each entry carries an expiresAt timestamp;
|
|
17
|
+
// entries are swept on insertion to prevent unbounded growth.
|
|
15
18
|
// ---------------------------------------------------------------------------
|
|
16
19
|
interface RemoteIdleSession {
|
|
17
20
|
nodeId: string;
|
|
18
21
|
sessionId: string;
|
|
19
22
|
providerType: string;
|
|
23
|
+
expiresAt: number;
|
|
20
24
|
}
|
|
25
|
+
const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
26
|
const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
|
|
22
27
|
|
|
28
|
+
function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
29
|
+
return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sweepExpiredRemoteIdleSessions(): void {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
for (const [key, session] of remoteIdleSessions) {
|
|
35
|
+
if (session.expiresAt <= now) remoteIdleSessions.delete(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
// ---------------------------------------------------------------------------
|
|
24
|
-
// MCP coordinator pending-event queue
|
|
40
|
+
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
25
41
|
// ---------------------------------------------------------------------------
|
|
26
42
|
// When a mesh event fires but no CLI coordinator session is registered (e.g.
|
|
27
|
-
// the coordinator is Claude Code running via MCP), we
|
|
28
|
-
//
|
|
43
|
+
// the coordinator is Claude Code running via MCP), we persist the event to a
|
|
44
|
+
// per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
|
|
45
|
+
// is removed; the file is drained atomically on each get_pending_mesh_events
|
|
46
|
+
// call and limited to 100 KB to prevent runaway growth.
|
|
47
|
+
//
|
|
48
|
+
// File: <ledgerDir>/<meshId>.pending-events.jsonl
|
|
29
49
|
// ---------------------------------------------------------------------------
|
|
30
50
|
|
|
31
51
|
export interface PendingMeshCoordinatorEvent {
|
|
32
52
|
event: string;
|
|
33
53
|
meshId: string;
|
|
34
54
|
nodeLabel: string;
|
|
55
|
+
nodeId?: string;
|
|
56
|
+
workspace?: string;
|
|
35
57
|
metadataEvent: Record<string, unknown>;
|
|
58
|
+
coordinatorMessage?: string;
|
|
36
59
|
queuedAt: number;
|
|
37
60
|
}
|
|
38
61
|
|
|
39
|
-
const
|
|
40
|
-
|
|
62
|
+
const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
|
|
63
|
+
|
|
64
|
+
function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
|
|
65
|
+
const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
|
|
66
|
+
const result = readRecord(metadata.result);
|
|
67
|
+
const refineJob = readRecord(result?.refineJob);
|
|
68
|
+
return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
|
|
72
|
+
const jobId = readRefineJobId({ metadataEvent });
|
|
73
|
+
return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
77
|
+
if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
|
|
78
|
+
const jobId = readRefineJobId(event);
|
|
79
|
+
if (!jobId) return false;
|
|
80
|
+
return getPendingMeshCoordinatorEvents(event.meshId).some((pending) =>
|
|
81
|
+
pending.event === event.event && readRefineJobId(pending) === jobId,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
|
|
86
|
+
const metadata = readRecord(event.metadataEvent) || {};
|
|
87
|
+
const sessionId = resolveEventSessionId(metadata);
|
|
88
|
+
const providerSessionId = readNonEmptyString(metadata.providerSessionId);
|
|
89
|
+
const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
90
|
+
const jobId = readRefineJobId(event);
|
|
91
|
+
const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
|
|
92
|
+
return [
|
|
93
|
+
event.meshId,
|
|
94
|
+
event.event,
|
|
95
|
+
event.nodeId || '',
|
|
96
|
+
sessionId || '',
|
|
97
|
+
providerSessionId || '',
|
|
98
|
+
taskId || '',
|
|
99
|
+
jobId || '',
|
|
100
|
+
timestamp || '',
|
|
101
|
+
].join('::');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
105
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
106
|
+
if (!fingerprint.trim()) return false;
|
|
107
|
+
return getPendingMeshCoordinatorEvents(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getPendingEventsPath(meshId: string): string {
|
|
111
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
112
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
116
|
+
try {
|
|
117
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
118
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (hasPendingCoordinatorEventDuplicate(event)) {
|
|
122
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
126
|
+
return true;
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
41
132
|
|
|
42
|
-
/** Drain and return all pending coordinator events,
|
|
43
|
-
export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
|
|
44
|
-
|
|
133
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
134
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
135
|
+
if (!meshId) return [];
|
|
136
|
+
const path = getPendingEventsPath(meshId);
|
|
137
|
+
if (!existsSync(path)) return [];
|
|
138
|
+
try {
|
|
139
|
+
const raw = readFileSync(path, 'utf-8');
|
|
140
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
141
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
142
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
143
|
+
});
|
|
144
|
+
} catch { return []; }
|
|
45
145
|
}
|
|
46
146
|
|
|
47
147
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
48
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
49
|
-
|
|
148
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
149
|
+
if (!meshId) return [];
|
|
150
|
+
const path = getPendingEventsPath(meshId);
|
|
151
|
+
if (!existsSync(path)) return [];
|
|
152
|
+
try {
|
|
153
|
+
const raw = readFileSync(path, 'utf-8');
|
|
154
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
155
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
156
|
+
});
|
|
157
|
+
} catch { return []; }
|
|
50
158
|
}
|
|
51
159
|
|
|
52
|
-
/** Explicitly clear all pending coordinator events. */
|
|
53
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
54
|
-
|
|
160
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
161
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
162
|
+
if (!meshId) return;
|
|
163
|
+
const path = getPendingEventsPath(meshId);
|
|
164
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
55
165
|
}
|
|
56
166
|
|
|
57
167
|
function readNonEmptyString(value: unknown): string {
|
|
58
168
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
59
169
|
}
|
|
60
170
|
|
|
171
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
172
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
173
|
+
? value as Record<string, unknown>
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
61
177
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
62
178
|
return readNonEmptyString(event.targetSessionId)
|
|
63
179
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -72,6 +188,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
72
188
|
'agent:stopped',
|
|
73
189
|
'agent:ready',
|
|
74
190
|
'monitor:long_generating',
|
|
191
|
+
'refine:accepted',
|
|
192
|
+
'refine:completed',
|
|
193
|
+
'refine:failed',
|
|
75
194
|
]);
|
|
76
195
|
|
|
77
196
|
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
@@ -86,10 +205,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
86
205
|
}
|
|
87
206
|
|
|
88
207
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
208
|
+
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
209
|
+
? event.completionDiagnostic as Record<string, unknown>
|
|
210
|
+
: null;
|
|
211
|
+
const diagnosticReason = completionDiagnostic
|
|
212
|
+
? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
|
|
213
|
+
: '';
|
|
214
|
+
const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
|
|
215
|
+
? String(completionDiagnostic.finalAssistantPresent)
|
|
216
|
+
: '';
|
|
89
217
|
const parts = [
|
|
90
218
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
91
219
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
92
220
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
221
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
222
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
93
223
|
].filter(Boolean);
|
|
94
224
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
95
225
|
}
|
|
@@ -140,6 +270,74 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
140
270
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
141
271
|
}
|
|
142
272
|
|
|
273
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
274
|
+
const recentCompletionFingerprints = new Map<string, number>();
|
|
275
|
+
|
|
276
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
277
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
278
|
+
if (typeof value === 'string' && value.trim()) {
|
|
279
|
+
const numeric = Number(value);
|
|
280
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
281
|
+
const parsed = Date.parse(value);
|
|
282
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildMeshCompletionFingerprint(args: {
|
|
288
|
+
meshId: string;
|
|
289
|
+
event: string;
|
|
290
|
+
sessionId: string;
|
|
291
|
+
providerType?: string;
|
|
292
|
+
providerSessionId?: string;
|
|
293
|
+
timestamp?: number | null;
|
|
294
|
+
finalSummary?: string;
|
|
295
|
+
}): string {
|
|
296
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
297
|
+
? String(args.timestamp)
|
|
298
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
299
|
+
return [
|
|
300
|
+
args.meshId,
|
|
301
|
+
args.event,
|
|
302
|
+
args.sessionId,
|
|
303
|
+
args.providerType || '',
|
|
304
|
+
args.providerSessionId || '',
|
|
305
|
+
timestampPart,
|
|
306
|
+
].join('::');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
310
|
+
meshId: string;
|
|
311
|
+
event: string;
|
|
312
|
+
sessionId: string;
|
|
313
|
+
providerType?: string;
|
|
314
|
+
providerSessionId?: string;
|
|
315
|
+
timestamp?: number | null;
|
|
316
|
+
finalSummary?: string;
|
|
317
|
+
}): boolean {
|
|
318
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
319
|
+
if (!fingerprint) return false;
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
322
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
323
|
+
}
|
|
324
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
325
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
|
|
330
|
+
const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
|
|
331
|
+
if (!fingerprint) return false;
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
334
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
335
|
+
}
|
|
336
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
337
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
143
341
|
|
|
144
342
|
export function tryAssignQueueTask(
|
|
145
343
|
components: DaemonComponents,
|
|
@@ -170,7 +368,16 @@ export function tryAssignQueueTask(
|
|
|
170
368
|
message: task.message,
|
|
171
369
|
}).catch((e: any) => {
|
|
172
370
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
173
|
-
|
|
371
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
372
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
373
|
+
try {
|
|
374
|
+
appendLedgerEntry(meshId, {
|
|
375
|
+
kind: 'dispatch_failed' as any,
|
|
376
|
+
nodeId,
|
|
377
|
+
sessionId,
|
|
378
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
379
|
+
});
|
|
380
|
+
} catch { /* ledger write is best-effort */ }
|
|
174
381
|
});
|
|
175
382
|
return true;
|
|
176
383
|
}
|
|
@@ -565,6 +772,71 @@ function buildMeshSystemMessage(args: {
|
|
|
565
772
|
if (args.event === 'monitor:long_generating') {
|
|
566
773
|
return `[System] ${args.nodeLabel} has been generating for a long time${metadata}. Use mesh_read_chat once for a status check, but do not poll repeatedly.`;
|
|
567
774
|
}
|
|
775
|
+
if (args.event === 'refine:accepted') {
|
|
776
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
777
|
+
return `[System] Refinery accepted async job${jobId ? ` ${jobId}` : ''} for ${args.nodeLabel}. Completion/failure will be delivered as a terminal refine event; do not poll repeatedly.`;
|
|
778
|
+
}
|
|
779
|
+
if (args.event === 'refine:completed') {
|
|
780
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
781
|
+
const result = readRecord(args.metadataEvent.result);
|
|
782
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
783
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
784
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
785
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
786
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
787
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
788
|
+
const into = readNonEmptyString(result?.into);
|
|
789
|
+
const branch = readNonEmptyString(result?.branch);
|
|
790
|
+
const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
|
|
791
|
+
const convergenceStatus = readNonEmptyString(finalConvergence?.status);
|
|
792
|
+
const nextStep = readNonEmptyString(result?.nextStep)
|
|
793
|
+
|| readNonEmptyString(finalConvergence?.nextStep)
|
|
794
|
+
|| 'Continue from the updated mesh state.';
|
|
795
|
+
const details = [
|
|
796
|
+
jobId ? `job_id=${jobId}` : '',
|
|
797
|
+
branch && into ? `${branch}→${into}` : '',
|
|
798
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
799
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
800
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
801
|
+
convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
|
|
802
|
+
].filter(Boolean).join('; ');
|
|
803
|
+
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
|
|
804
|
+
}
|
|
805
|
+
if (args.event === 'refine:failed') {
|
|
806
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
807
|
+
const result = readRecord(args.metadataEvent.result);
|
|
808
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
809
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
810
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
811
|
+
const code = readNonEmptyString(result?.code);
|
|
812
|
+
const error = readNonEmptyString(result?.error);
|
|
813
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
814
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
815
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
816
|
+
const mergeStatus = result?.merged === true
|
|
817
|
+
? 'merged'
|
|
818
|
+
: finalConvergence?.merged === false
|
|
819
|
+
? 'not_merged'
|
|
820
|
+
: '';
|
|
821
|
+
const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
|
|
822
|
+
|| readNonEmptyString(finalConvergence?.status);
|
|
823
|
+
const blockedReason = readNonEmptyString(result?.blockedReason);
|
|
824
|
+
const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
|
|
825
|
+
const details = [
|
|
826
|
+
jobId ? `job_id=${jobId}` : '',
|
|
827
|
+
code ? `code=${code}` : '',
|
|
828
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
829
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
830
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
831
|
+
convergenceStatus ? `convergence=${convergenceStatus}` : '',
|
|
832
|
+
blockedReason ? `reason=${blockedReason}` : '',
|
|
833
|
+
].filter(Boolean).join('; ');
|
|
834
|
+
const parts = [
|
|
835
|
+
`[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
|
|
836
|
+
nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
|
|
837
|
+
];
|
|
838
|
+
return parts.join('\n');
|
|
839
|
+
}
|
|
568
840
|
return '';
|
|
569
841
|
}
|
|
570
842
|
|
|
@@ -593,6 +865,28 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
593
865
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
594
866
|
}
|
|
595
867
|
|
|
868
|
+
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
869
|
+
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
870
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
874
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
875
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
876
|
+
meshId: args.meshId,
|
|
877
|
+
event: args.event,
|
|
878
|
+
sessionId: eventSessionId,
|
|
879
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
880
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
881
|
+
timestamp: eventTimestamp,
|
|
882
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
883
|
+
});
|
|
884
|
+
if (duplicateCompletion) {
|
|
885
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
886
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
596
890
|
// ── Task Queue & Ledger ──
|
|
597
891
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
598
892
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -601,19 +895,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
601
895
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
602
896
|
|
|
603
897
|
if (sessionId) {
|
|
604
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
898
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
899
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
900
|
+
});
|
|
605
901
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
606
902
|
if (nodeId && providerType) {
|
|
607
|
-
//
|
|
608
|
-
|
|
903
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
904
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
905
|
+
setImmediate(() => {
|
|
609
906
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
610
|
-
}
|
|
907
|
+
});
|
|
611
908
|
}
|
|
612
909
|
}
|
|
613
910
|
} else if (args.event === 'agent:ready') {
|
|
614
911
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
615
912
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
616
913
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
914
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
915
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
916
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
617
917
|
const completedTask = sessionId
|
|
618
918
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
619
919
|
: null;
|
|
@@ -630,15 +930,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
630
930
|
nodeLabel: args.nodeLabel,
|
|
631
931
|
taskId: completedTask.id,
|
|
632
932
|
completedViaReady: true,
|
|
633
|
-
providerSessionId
|
|
634
|
-
finalSummary
|
|
933
|
+
providerSessionId,
|
|
934
|
+
finalSummary,
|
|
935
|
+
workerResult,
|
|
635
936
|
evidence: buildTaskCompletionEvidence({
|
|
636
937
|
event: 'agent:ready',
|
|
637
938
|
nodeId,
|
|
638
939
|
sessionId,
|
|
639
940
|
providerType: providerType || undefined,
|
|
640
|
-
providerSessionId
|
|
641
|
-
finalSummary
|
|
941
|
+
providerSessionId,
|
|
942
|
+
finalSummary,
|
|
943
|
+
workerResult,
|
|
642
944
|
}),
|
|
643
945
|
},
|
|
644
946
|
});
|
|
@@ -648,13 +950,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
648
950
|
}
|
|
649
951
|
|
|
650
952
|
if (sessionId && nodeId && providerType) {
|
|
651
|
-
|
|
652
|
-
|
|
953
|
+
sweepExpiredRemoteIdleSessions();
|
|
954
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
955
|
+
nodeId, sessionId, providerType,
|
|
956
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
957
|
+
});
|
|
958
|
+
setImmediate(() => {
|
|
653
959
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
654
|
-
if (assigned) {
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
}, 500);
|
|
960
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
961
|
+
});
|
|
658
962
|
}
|
|
659
963
|
} else if (args.event === 'agent:generating_started') {
|
|
660
964
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -669,7 +973,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
669
973
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
670
974
|
}
|
|
671
975
|
if (sessionId) {
|
|
672
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
976
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
977
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
673
978
|
}
|
|
674
979
|
}
|
|
675
980
|
|
|
@@ -679,14 +984,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
984
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
680
985
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
681
986
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
987
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
988
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
989
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
682
990
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
683
991
|
? buildTaskCompletionEvidence({
|
|
684
992
|
event: 'agent:generating_completed',
|
|
685
993
|
nodeId: ledgerNodeId,
|
|
686
994
|
sessionId: ledgerSessionId,
|
|
687
995
|
providerType: ledgerProviderType,
|
|
688
|
-
providerSessionId
|
|
689
|
-
finalSummary
|
|
996
|
+
providerSessionId,
|
|
997
|
+
finalSummary,
|
|
998
|
+
workerResult,
|
|
690
999
|
})
|
|
691
1000
|
: undefined;
|
|
692
1001
|
appendLedgerEntry(args.meshId, {
|
|
@@ -698,8 +1007,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
698
1007
|
event: args.event,
|
|
699
1008
|
nodeLabel: args.nodeLabel,
|
|
700
1009
|
taskId: completedTaskForLedger?.id || undefined,
|
|
701
|
-
providerSessionId
|
|
702
|
-
finalSummary
|
|
1010
|
+
providerSessionId,
|
|
1011
|
+
finalSummary,
|
|
1012
|
+
workerResult,
|
|
1013
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
1014
|
+
? args.metadataEvent.completionDiagnostic
|
|
1015
|
+
: undefined,
|
|
703
1016
|
evidence: completionEvidence,
|
|
704
1017
|
},
|
|
705
1018
|
});
|
|
@@ -772,6 +1085,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
772
1085
|
}
|
|
773
1086
|
}
|
|
774
1087
|
|
|
1088
|
+
const messageText = buildMeshSystemMessage({
|
|
1089
|
+
event: args.event,
|
|
1090
|
+
nodeLabel: args.nodeLabel,
|
|
1091
|
+
metadataEvent: args.metadataEvent,
|
|
1092
|
+
recoveryContext,
|
|
1093
|
+
});
|
|
1094
|
+
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1095
|
+
|
|
775
1096
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
776
1097
|
const instState = inst.getState();
|
|
777
1098
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
@@ -779,31 +1100,53 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
779
1100
|
return true;
|
|
780
1101
|
});
|
|
781
1102
|
|
|
1103
|
+
// Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
|
|
1104
|
+
// synchronously; only buffer them for MCP when no CLI coordinator is present.
|
|
1105
|
+
// Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
|
|
1106
|
+
// receive them regardless of whether a live CLI coordinator session is active.
|
|
1107
|
+
const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
|
|
1108
|
+
|
|
782
1109
|
if (coordinatorInstances.length === 0) {
|
|
783
1110
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
784
|
-
if (
|
|
785
|
-
pendingMeshCoordinatorEvents.push({
|
|
1111
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
786
1112
|
event: args.event,
|
|
787
1113
|
meshId: args.meshId,
|
|
788
1114
|
nodeLabel: args.nodeLabel,
|
|
1115
|
+
nodeId: args.nodeId || undefined,
|
|
1116
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
789
1117
|
metadataEvent: {
|
|
790
1118
|
...args.metadataEvent,
|
|
791
1119
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
792
1120
|
},
|
|
1121
|
+
coordinatorMessage: messageText,
|
|
793
1122
|
queuedAt: Date.now(),
|
|
794
|
-
})
|
|
1123
|
+
})) {
|
|
795
1124
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
796
1125
|
}
|
|
797
1126
|
return { success: true, forwarded: 0 };
|
|
798
1127
|
}
|
|
799
1128
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1129
|
+
// CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
|
|
1130
|
+
// that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
|
|
1131
|
+
// forwarded directly only — they must not accumulate in the pending queue when a live
|
|
1132
|
+
// coordinator already received them.
|
|
1133
|
+
if (!isRefineTerminalEvent) {
|
|
1134
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
1135
|
+
event: args.event,
|
|
1136
|
+
meshId: args.meshId,
|
|
1137
|
+
nodeLabel: args.nodeLabel,
|
|
1138
|
+
nodeId: args.nodeId || undefined,
|
|
1139
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1140
|
+
metadataEvent: {
|
|
1141
|
+
...args.metadataEvent,
|
|
1142
|
+
...(recoveryContext ? { recoveryContext } : {}),
|
|
1143
|
+
},
|
|
1144
|
+
coordinatorMessage: messageText,
|
|
1145
|
+
queuedAt: Date.now(),
|
|
1146
|
+
})) {
|
|
1147
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
807
1150
|
|
|
808
1151
|
for (const coord of coordinatorInstances) {
|
|
809
1152
|
const coordState = coord.getState();
|
|
@@ -834,6 +1177,15 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
834
1177
|
providerType: readNonEmptyString(payload.providerType),
|
|
835
1178
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
836
1179
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1180
|
+
jobId: readNonEmptyString(payload.jobId),
|
|
1181
|
+
interactionId: readNonEmptyString(payload.interactionId),
|
|
1182
|
+
status: readNonEmptyString(payload.status),
|
|
1183
|
+
targetDaemonId: readNonEmptyString(payload.targetDaemonId),
|
|
1184
|
+
startedAt: readNonEmptyString(payload.startedAt),
|
|
1185
|
+
completedAt: readNonEmptyString(payload.completedAt),
|
|
1186
|
+
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1187
|
+
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1188
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
837
1189
|
intentional: payload.intentional === true,
|
|
838
1190
|
intentionalStop: payload.intentionalStop === true,
|
|
839
1191
|
operatorCleanup: payload.operatorCleanup === true,
|