@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.80
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 +4936 -1171
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4900 -1157
- 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 +51 -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 +242 -7
- package/src/commands/cli-manager.ts +63 -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 +222 -0
- package/src/mesh/mesh-events.ts +342 -47
- 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/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,150 @@
|
|
|
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
|
+
}
|
|
41
84
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
85
|
+
function getPendingEventsPath(meshId: string): string {
|
|
86
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
87
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
91
|
+
try {
|
|
92
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
93
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
97
|
+
return true;
|
|
98
|
+
} catch (e: any) {
|
|
99
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
105
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
106
|
+
if (!meshId) return [];
|
|
107
|
+
const path = getPendingEventsPath(meshId);
|
|
108
|
+
if (!existsSync(path)) return [];
|
|
109
|
+
try {
|
|
110
|
+
const raw = readFileSync(path, 'utf-8');
|
|
111
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
112
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
113
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
114
|
+
});
|
|
115
|
+
} catch { return []; }
|
|
45
116
|
}
|
|
46
117
|
|
|
47
118
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
48
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
49
|
-
|
|
119
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
120
|
+
if (!meshId) return [];
|
|
121
|
+
const path = getPendingEventsPath(meshId);
|
|
122
|
+
if (!existsSync(path)) return [];
|
|
123
|
+
try {
|
|
124
|
+
const raw = readFileSync(path, 'utf-8');
|
|
125
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
126
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
127
|
+
});
|
|
128
|
+
} catch { return []; }
|
|
50
129
|
}
|
|
51
130
|
|
|
52
|
-
/** Explicitly clear all pending coordinator events. */
|
|
53
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
54
|
-
|
|
131
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
132
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
133
|
+
if (!meshId) return;
|
|
134
|
+
const path = getPendingEventsPath(meshId);
|
|
135
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
55
136
|
}
|
|
56
137
|
|
|
57
138
|
function readNonEmptyString(value: unknown): string {
|
|
58
139
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
59
140
|
}
|
|
60
141
|
|
|
142
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
143
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
144
|
+
? value as Record<string, unknown>
|
|
145
|
+
: undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
61
148
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
62
149
|
return readNonEmptyString(event.targetSessionId)
|
|
63
150
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -72,6 +159,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
72
159
|
'agent:stopped',
|
|
73
160
|
'agent:ready',
|
|
74
161
|
'monitor:long_generating',
|
|
162
|
+
'refine:accepted',
|
|
163
|
+
'refine:completed',
|
|
164
|
+
'refine:failed',
|
|
75
165
|
]);
|
|
76
166
|
|
|
77
167
|
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
@@ -86,10 +176,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
86
176
|
}
|
|
87
177
|
|
|
88
178
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
179
|
+
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
180
|
+
? event.completionDiagnostic as Record<string, unknown>
|
|
181
|
+
: null;
|
|
182
|
+
const diagnosticReason = completionDiagnostic
|
|
183
|
+
? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
|
|
184
|
+
: '';
|
|
185
|
+
const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
|
|
186
|
+
? String(completionDiagnostic.finalAssistantPresent)
|
|
187
|
+
: '';
|
|
89
188
|
const parts = [
|
|
90
189
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
91
190
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
92
191
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
192
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
193
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
93
194
|
].filter(Boolean);
|
|
94
195
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
95
196
|
}
|
|
@@ -140,6 +241,74 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
140
241
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
141
242
|
}
|
|
142
243
|
|
|
244
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
245
|
+
const recentCompletionFingerprints = new Map<string, number>();
|
|
246
|
+
|
|
247
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
248
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
249
|
+
if (typeof value === 'string' && value.trim()) {
|
|
250
|
+
const numeric = Number(value);
|
|
251
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
252
|
+
const parsed = Date.parse(value);
|
|
253
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildMeshCompletionFingerprint(args: {
|
|
259
|
+
meshId: string;
|
|
260
|
+
event: string;
|
|
261
|
+
sessionId: string;
|
|
262
|
+
providerType?: string;
|
|
263
|
+
providerSessionId?: string;
|
|
264
|
+
timestamp?: number | null;
|
|
265
|
+
finalSummary?: string;
|
|
266
|
+
}): string {
|
|
267
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
268
|
+
? String(args.timestamp)
|
|
269
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
270
|
+
return [
|
|
271
|
+
args.meshId,
|
|
272
|
+
args.event,
|
|
273
|
+
args.sessionId,
|
|
274
|
+
args.providerType || '',
|
|
275
|
+
args.providerSessionId || '',
|
|
276
|
+
timestampPart,
|
|
277
|
+
].join('::');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
281
|
+
meshId: string;
|
|
282
|
+
event: string;
|
|
283
|
+
sessionId: string;
|
|
284
|
+
providerType?: string;
|
|
285
|
+
providerSessionId?: string;
|
|
286
|
+
timestamp?: number | null;
|
|
287
|
+
finalSummary?: string;
|
|
288
|
+
}): boolean {
|
|
289
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
290
|
+
if (!fingerprint) return false;
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
293
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
294
|
+
}
|
|
295
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
296
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
|
|
301
|
+
const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
|
|
302
|
+
if (!fingerprint) return false;
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
305
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
306
|
+
}
|
|
307
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
308
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
143
312
|
|
|
144
313
|
export function tryAssignQueueTask(
|
|
145
314
|
components: DaemonComponents,
|
|
@@ -170,7 +339,16 @@ export function tryAssignQueueTask(
|
|
|
170
339
|
message: task.message,
|
|
171
340
|
}).catch((e: any) => {
|
|
172
341
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
173
|
-
|
|
342
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
343
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
344
|
+
try {
|
|
345
|
+
appendLedgerEntry(meshId, {
|
|
346
|
+
kind: 'dispatch_failed' as any,
|
|
347
|
+
nodeId,
|
|
348
|
+
sessionId,
|
|
349
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
350
|
+
});
|
|
351
|
+
} catch { /* ledger write is best-effort */ }
|
|
174
352
|
});
|
|
175
353
|
return true;
|
|
176
354
|
}
|
|
@@ -565,6 +743,71 @@ function buildMeshSystemMessage(args: {
|
|
|
565
743
|
if (args.event === 'monitor:long_generating') {
|
|
566
744
|
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
745
|
}
|
|
746
|
+
if (args.event === 'refine:accepted') {
|
|
747
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
748
|
+
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.`;
|
|
749
|
+
}
|
|
750
|
+
if (args.event === 'refine:completed') {
|
|
751
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
752
|
+
const result = readRecord(args.metadataEvent.result);
|
|
753
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
754
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
755
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
756
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
757
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
758
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
759
|
+
const into = readNonEmptyString(result?.into);
|
|
760
|
+
const branch = readNonEmptyString(result?.branch);
|
|
761
|
+
const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
|
|
762
|
+
const convergenceStatus = readNonEmptyString(finalConvergence?.status);
|
|
763
|
+
const nextStep = readNonEmptyString(result?.nextStep)
|
|
764
|
+
|| readNonEmptyString(finalConvergence?.nextStep)
|
|
765
|
+
|| 'Continue from the updated mesh state.';
|
|
766
|
+
const details = [
|
|
767
|
+
jobId ? `job_id=${jobId}` : '',
|
|
768
|
+
branch && into ? `${branch}→${into}` : '',
|
|
769
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
770
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
771
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
772
|
+
convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
|
|
773
|
+
].filter(Boolean).join('; ');
|
|
774
|
+
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
|
|
775
|
+
}
|
|
776
|
+
if (args.event === 'refine:failed') {
|
|
777
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
778
|
+
const result = readRecord(args.metadataEvent.result);
|
|
779
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
780
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
781
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
782
|
+
const code = readNonEmptyString(result?.code);
|
|
783
|
+
const error = readNonEmptyString(result?.error);
|
|
784
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
785
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
786
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
787
|
+
const mergeStatus = result?.merged === true
|
|
788
|
+
? 'merged'
|
|
789
|
+
: finalConvergence?.merged === false
|
|
790
|
+
? 'not_merged'
|
|
791
|
+
: '';
|
|
792
|
+
const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
|
|
793
|
+
|| readNonEmptyString(finalConvergence?.status);
|
|
794
|
+
const blockedReason = readNonEmptyString(result?.blockedReason);
|
|
795
|
+
const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
|
|
796
|
+
const details = [
|
|
797
|
+
jobId ? `job_id=${jobId}` : '',
|
|
798
|
+
code ? `code=${code}` : '',
|
|
799
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
800
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
801
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
802
|
+
convergenceStatus ? `convergence=${convergenceStatus}` : '',
|
|
803
|
+
blockedReason ? `reason=${blockedReason}` : '',
|
|
804
|
+
].filter(Boolean).join('; ');
|
|
805
|
+
const parts = [
|
|
806
|
+
`[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
|
|
807
|
+
nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
|
|
808
|
+
];
|
|
809
|
+
return parts.join('\n');
|
|
810
|
+
}
|
|
568
811
|
return '';
|
|
569
812
|
}
|
|
570
813
|
|
|
@@ -593,6 +836,28 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
593
836
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
594
837
|
}
|
|
595
838
|
|
|
839
|
+
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
840
|
+
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
841
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
845
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
846
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
847
|
+
meshId: args.meshId,
|
|
848
|
+
event: args.event,
|
|
849
|
+
sessionId: eventSessionId,
|
|
850
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
851
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
852
|
+
timestamp: eventTimestamp,
|
|
853
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
854
|
+
});
|
|
855
|
+
if (duplicateCompletion) {
|
|
856
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
857
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
596
861
|
// ── Task Queue & Ledger ──
|
|
597
862
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
598
863
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -601,19 +866,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
601
866
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
602
867
|
|
|
603
868
|
if (sessionId) {
|
|
604
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
869
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
870
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
871
|
+
});
|
|
605
872
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
606
873
|
if (nodeId && providerType) {
|
|
607
|
-
//
|
|
608
|
-
|
|
874
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
875
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
876
|
+
setImmediate(() => {
|
|
609
877
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
610
|
-
}
|
|
878
|
+
});
|
|
611
879
|
}
|
|
612
880
|
}
|
|
613
881
|
} else if (args.event === 'agent:ready') {
|
|
614
882
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
615
883
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
616
884
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
885
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
886
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
887
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
617
888
|
const completedTask = sessionId
|
|
618
889
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
619
890
|
: null;
|
|
@@ -630,15 +901,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
630
901
|
nodeLabel: args.nodeLabel,
|
|
631
902
|
taskId: completedTask.id,
|
|
632
903
|
completedViaReady: true,
|
|
633
|
-
providerSessionId
|
|
634
|
-
finalSummary
|
|
904
|
+
providerSessionId,
|
|
905
|
+
finalSummary,
|
|
906
|
+
workerResult,
|
|
635
907
|
evidence: buildTaskCompletionEvidence({
|
|
636
908
|
event: 'agent:ready',
|
|
637
909
|
nodeId,
|
|
638
910
|
sessionId,
|
|
639
911
|
providerType: providerType || undefined,
|
|
640
|
-
providerSessionId
|
|
641
|
-
finalSummary
|
|
912
|
+
providerSessionId,
|
|
913
|
+
finalSummary,
|
|
914
|
+
workerResult,
|
|
642
915
|
}),
|
|
643
916
|
},
|
|
644
917
|
});
|
|
@@ -648,13 +921,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
648
921
|
}
|
|
649
922
|
|
|
650
923
|
if (sessionId && nodeId && providerType) {
|
|
651
|
-
|
|
652
|
-
|
|
924
|
+
sweepExpiredRemoteIdleSessions();
|
|
925
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
926
|
+
nodeId, sessionId, providerType,
|
|
927
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
928
|
+
});
|
|
929
|
+
setImmediate(() => {
|
|
653
930
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
654
|
-
if (assigned) {
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
}, 500);
|
|
931
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
932
|
+
});
|
|
658
933
|
}
|
|
659
934
|
} else if (args.event === 'agent:generating_started') {
|
|
660
935
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -669,7 +944,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
669
944
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
670
945
|
}
|
|
671
946
|
if (sessionId) {
|
|
672
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
947
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
948
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
673
949
|
}
|
|
674
950
|
}
|
|
675
951
|
|
|
@@ -679,14 +955,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
955
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
680
956
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
681
957
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
958
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
959
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
960
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
682
961
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
683
962
|
? buildTaskCompletionEvidence({
|
|
684
963
|
event: 'agent:generating_completed',
|
|
685
964
|
nodeId: ledgerNodeId,
|
|
686
965
|
sessionId: ledgerSessionId,
|
|
687
966
|
providerType: ledgerProviderType,
|
|
688
|
-
providerSessionId
|
|
689
|
-
finalSummary
|
|
967
|
+
providerSessionId,
|
|
968
|
+
finalSummary,
|
|
969
|
+
workerResult,
|
|
690
970
|
})
|
|
691
971
|
: undefined;
|
|
692
972
|
appendLedgerEntry(args.meshId, {
|
|
@@ -698,8 +978,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
698
978
|
event: args.event,
|
|
699
979
|
nodeLabel: args.nodeLabel,
|
|
700
980
|
taskId: completedTaskForLedger?.id || undefined,
|
|
701
|
-
providerSessionId
|
|
702
|
-
finalSummary
|
|
981
|
+
providerSessionId,
|
|
982
|
+
finalSummary,
|
|
983
|
+
workerResult,
|
|
984
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
985
|
+
? args.metadataEvent.completionDiagnostic
|
|
986
|
+
: undefined,
|
|
703
987
|
evidence: completionEvidence,
|
|
704
988
|
},
|
|
705
989
|
});
|
|
@@ -772,6 +1056,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
772
1056
|
}
|
|
773
1057
|
}
|
|
774
1058
|
|
|
1059
|
+
const messageText = buildMeshSystemMessage({
|
|
1060
|
+
event: args.event,
|
|
1061
|
+
nodeLabel: args.nodeLabel,
|
|
1062
|
+
metadataEvent: args.metadataEvent,
|
|
1063
|
+
recoveryContext,
|
|
1064
|
+
});
|
|
1065
|
+
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1066
|
+
|
|
775
1067
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
776
1068
|
const instState = inst.getState();
|
|
777
1069
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
@@ -781,30 +1073,24 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
781
1073
|
|
|
782
1074
|
if (coordinatorInstances.length === 0) {
|
|
783
1075
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
784
|
-
if (
|
|
785
|
-
pendingMeshCoordinatorEvents.push({
|
|
1076
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
786
1077
|
event: args.event,
|
|
787
1078
|
meshId: args.meshId,
|
|
788
1079
|
nodeLabel: args.nodeLabel,
|
|
1080
|
+
nodeId: args.nodeId || undefined,
|
|
1081
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
789
1082
|
metadataEvent: {
|
|
790
1083
|
...args.metadataEvent,
|
|
791
1084
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
792
1085
|
},
|
|
1086
|
+
coordinatorMessage: messageText,
|
|
793
1087
|
queuedAt: Date.now(),
|
|
794
|
-
})
|
|
1088
|
+
})) {
|
|
795
1089
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
796
1090
|
}
|
|
797
1091
|
return { success: true, forwarded: 0 };
|
|
798
1092
|
}
|
|
799
1093
|
|
|
800
|
-
const messageText = buildMeshSystemMessage({
|
|
801
|
-
event: args.event,
|
|
802
|
-
nodeLabel: args.nodeLabel,
|
|
803
|
-
metadataEvent: args.metadataEvent,
|
|
804
|
-
recoveryContext,
|
|
805
|
-
});
|
|
806
|
-
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
807
|
-
|
|
808
1094
|
for (const coord of coordinatorInstances) {
|
|
809
1095
|
const coordState = coord.getState();
|
|
810
1096
|
LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);
|
|
@@ -834,6 +1120,15 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
834
1120
|
providerType: readNonEmptyString(payload.providerType),
|
|
835
1121
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
836
1122
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1123
|
+
jobId: readNonEmptyString(payload.jobId),
|
|
1124
|
+
interactionId: readNonEmptyString(payload.interactionId),
|
|
1125
|
+
status: readNonEmptyString(payload.status),
|
|
1126
|
+
targetDaemonId: readNonEmptyString(payload.targetDaemonId),
|
|
1127
|
+
startedAt: readNonEmptyString(payload.startedAt),
|
|
1128
|
+
completedAt: readNonEmptyString(payload.completedAt),
|
|
1129
|
+
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1130
|
+
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1131
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
837
1132
|
intentional: payload.intentional === true,
|
|
838
1133
|
intentionalStop: payload.intentionalStop === true,
|
|
839
1134
|
operatorCleanup: payload.operatorCleanup === true,
|