@adhdev/daemon-core 0.9.82-rc.7 → 0.9.82-rc.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/daemon-lifecycle.d.ts +2 -0
- 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 +24 -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 +4619 -1143
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4582 -1128
- 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 +48 -0
- package/dist/mesh/mesh-events.d.ts +28 -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 +1 -0
- package/dist/repo-mesh-types.d.ts +160 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +4 -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/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2452 -409
- 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 +39 -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 +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +291 -38
- 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 +66 -1
- 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 +174 -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,149 @@
|
|
|
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>;
|
|
36
58
|
queuedAt: number;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
|
-
const
|
|
40
|
-
|
|
61
|
+
const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
|
|
62
|
+
|
|
63
|
+
function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
|
|
64
|
+
const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
|
|
65
|
+
const result = readRecord(metadata.result);
|
|
66
|
+
const refineJob = readRecord(result?.refineJob);
|
|
67
|
+
return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
|
|
71
|
+
const jobId = readRefineJobId({ metadataEvent });
|
|
72
|
+
return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
76
|
+
if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
|
|
77
|
+
const jobId = readRefineJobId(event);
|
|
78
|
+
if (!jobId) return false;
|
|
79
|
+
return getPendingMeshCoordinatorEvents(event.meshId).some((pending) =>
|
|
80
|
+
pending.event === event.event && readRefineJobId(pending) === jobId,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
41
83
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
84
|
+
function getPendingEventsPath(meshId: string): string {
|
|
85
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
86
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
90
|
+
try {
|
|
91
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
92
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
96
|
+
return true;
|
|
97
|
+
} catch (e: any) {
|
|
98
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
104
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
105
|
+
if (!meshId) return [];
|
|
106
|
+
const path = getPendingEventsPath(meshId);
|
|
107
|
+
if (!existsSync(path)) return [];
|
|
108
|
+
try {
|
|
109
|
+
const raw = readFileSync(path, 'utf-8');
|
|
110
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
111
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
112
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
113
|
+
});
|
|
114
|
+
} catch { return []; }
|
|
45
115
|
}
|
|
46
116
|
|
|
47
117
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
48
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
49
|
-
|
|
118
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
119
|
+
if (!meshId) return [];
|
|
120
|
+
const path = getPendingEventsPath(meshId);
|
|
121
|
+
if (!existsSync(path)) return [];
|
|
122
|
+
try {
|
|
123
|
+
const raw = readFileSync(path, 'utf-8');
|
|
124
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
125
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
126
|
+
});
|
|
127
|
+
} catch { return []; }
|
|
50
128
|
}
|
|
51
129
|
|
|
52
|
-
/** Explicitly clear all pending coordinator events. */
|
|
53
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
54
|
-
|
|
130
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
131
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
132
|
+
if (!meshId) return;
|
|
133
|
+
const path = getPendingEventsPath(meshId);
|
|
134
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
55
135
|
}
|
|
56
136
|
|
|
57
137
|
function readNonEmptyString(value: unknown): string {
|
|
58
138
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
59
139
|
}
|
|
60
140
|
|
|
141
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
142
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
143
|
+
? value as Record<string, unknown>
|
|
144
|
+
: undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
61
147
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
62
148
|
return readNonEmptyString(event.targetSessionId)
|
|
63
149
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -72,6 +158,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
72
158
|
'agent:stopped',
|
|
73
159
|
'agent:ready',
|
|
74
160
|
'monitor:long_generating',
|
|
161
|
+
'refine:accepted',
|
|
162
|
+
'refine:completed',
|
|
163
|
+
'refine:failed',
|
|
75
164
|
]);
|
|
76
165
|
|
|
77
166
|
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
@@ -86,10 +175,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
86
175
|
}
|
|
87
176
|
|
|
88
177
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
178
|
+
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
179
|
+
? event.completionDiagnostic as Record<string, unknown>
|
|
180
|
+
: null;
|
|
181
|
+
const diagnosticReason = completionDiagnostic
|
|
182
|
+
? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
|
|
183
|
+
: '';
|
|
184
|
+
const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
|
|
185
|
+
? String(completionDiagnostic.finalAssistantPresent)
|
|
186
|
+
: '';
|
|
89
187
|
const parts = [
|
|
90
188
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
91
189
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
92
190
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
191
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
192
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
93
193
|
].filter(Boolean);
|
|
94
194
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
95
195
|
}
|
|
@@ -140,6 +240,74 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
140
240
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
141
241
|
}
|
|
142
242
|
|
|
243
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
244
|
+
const recentCompletionFingerprints = new Map<string, number>();
|
|
245
|
+
|
|
246
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
247
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
248
|
+
if (typeof value === 'string' && value.trim()) {
|
|
249
|
+
const numeric = Number(value);
|
|
250
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
251
|
+
const parsed = Date.parse(value);
|
|
252
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildMeshCompletionFingerprint(args: {
|
|
258
|
+
meshId: string;
|
|
259
|
+
event: string;
|
|
260
|
+
sessionId: string;
|
|
261
|
+
providerType?: string;
|
|
262
|
+
providerSessionId?: string;
|
|
263
|
+
timestamp?: number | null;
|
|
264
|
+
finalSummary?: string;
|
|
265
|
+
}): string {
|
|
266
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
267
|
+
? String(args.timestamp)
|
|
268
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
269
|
+
return [
|
|
270
|
+
args.meshId,
|
|
271
|
+
args.event,
|
|
272
|
+
args.sessionId,
|
|
273
|
+
args.providerType || '',
|
|
274
|
+
args.providerSessionId || '',
|
|
275
|
+
timestampPart,
|
|
276
|
+
].join('::');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
280
|
+
meshId: string;
|
|
281
|
+
event: string;
|
|
282
|
+
sessionId: string;
|
|
283
|
+
providerType?: string;
|
|
284
|
+
providerSessionId?: string;
|
|
285
|
+
timestamp?: number | null;
|
|
286
|
+
finalSummary?: string;
|
|
287
|
+
}): boolean {
|
|
288
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
289
|
+
if (!fingerprint) return false;
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
292
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
293
|
+
}
|
|
294
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
295
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
|
|
300
|
+
const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
|
|
301
|
+
if (!fingerprint) return false;
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
304
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
305
|
+
}
|
|
306
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
307
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
143
311
|
|
|
144
312
|
export function tryAssignQueueTask(
|
|
145
313
|
components: DaemonComponents,
|
|
@@ -170,7 +338,16 @@ export function tryAssignQueueTask(
|
|
|
170
338
|
message: task.message,
|
|
171
339
|
}).catch((e: any) => {
|
|
172
340
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
173
|
-
|
|
341
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
342
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
343
|
+
try {
|
|
344
|
+
appendLedgerEntry(meshId, {
|
|
345
|
+
kind: 'dispatch_failed' as any,
|
|
346
|
+
nodeId,
|
|
347
|
+
sessionId,
|
|
348
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
349
|
+
});
|
|
350
|
+
} catch { /* ledger write is best-effort */ }
|
|
174
351
|
});
|
|
175
352
|
return true;
|
|
176
353
|
}
|
|
@@ -565,6 +742,32 @@ function buildMeshSystemMessage(args: {
|
|
|
565
742
|
if (args.event === 'monitor:long_generating') {
|
|
566
743
|
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
744
|
}
|
|
745
|
+
if (args.event === 'refine:accepted') {
|
|
746
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
747
|
+
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.`;
|
|
748
|
+
}
|
|
749
|
+
if (args.event === 'refine:completed') {
|
|
750
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
751
|
+
const result = readRecord(args.metadataEvent.result);
|
|
752
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
753
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
754
|
+
const into = readNonEmptyString(result?.into);
|
|
755
|
+
const branch = readNonEmptyString(result?.branch);
|
|
756
|
+
const details = [
|
|
757
|
+
jobId ? `job_id=${jobId}` : '',
|
|
758
|
+
branch && into ? `${branch}→${into}` : '',
|
|
759
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
760
|
+
].filter(Boolean).join('; ');
|
|
761
|
+
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}. The worktree was merged and cleanup completed; continue from the updated mesh state.`;
|
|
762
|
+
}
|
|
763
|
+
if (args.event === 'refine:failed') {
|
|
764
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
765
|
+
const result = readRecord(args.metadataEvent.result);
|
|
766
|
+
const code = readNonEmptyString(result?.code);
|
|
767
|
+
const error = readNonEmptyString(result?.error);
|
|
768
|
+
const details = [jobId ? `job_id=${jobId}` : '', code ? `code=${code}` : ''].filter(Boolean).join('; ');
|
|
769
|
+
return `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'} Review the terminal refine event/ledger before retrying.`;
|
|
770
|
+
}
|
|
568
771
|
return '';
|
|
569
772
|
}
|
|
570
773
|
|
|
@@ -593,6 +796,28 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
593
796
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
594
797
|
}
|
|
595
798
|
|
|
799
|
+
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
800
|
+
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
801
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
805
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
806
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
807
|
+
meshId: args.meshId,
|
|
808
|
+
event: args.event,
|
|
809
|
+
sessionId: eventSessionId,
|
|
810
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
811
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
812
|
+
timestamp: eventTimestamp,
|
|
813
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
814
|
+
});
|
|
815
|
+
if (duplicateCompletion) {
|
|
816
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
817
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
596
821
|
// ── Task Queue & Ledger ──
|
|
597
822
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
598
823
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -601,19 +826,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
601
826
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
602
827
|
|
|
603
828
|
if (sessionId) {
|
|
604
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
829
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
830
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
831
|
+
});
|
|
605
832
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
606
833
|
if (nodeId && providerType) {
|
|
607
|
-
//
|
|
608
|
-
|
|
834
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
835
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
836
|
+
setImmediate(() => {
|
|
609
837
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
610
|
-
}
|
|
838
|
+
});
|
|
611
839
|
}
|
|
612
840
|
}
|
|
613
841
|
} else if (args.event === 'agent:ready') {
|
|
614
842
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
615
843
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
616
844
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
845
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
846
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
847
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
617
848
|
const completedTask = sessionId
|
|
618
849
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
619
850
|
: null;
|
|
@@ -630,15 +861,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
630
861
|
nodeLabel: args.nodeLabel,
|
|
631
862
|
taskId: completedTask.id,
|
|
632
863
|
completedViaReady: true,
|
|
633
|
-
providerSessionId
|
|
634
|
-
finalSummary
|
|
864
|
+
providerSessionId,
|
|
865
|
+
finalSummary,
|
|
866
|
+
workerResult,
|
|
635
867
|
evidence: buildTaskCompletionEvidence({
|
|
636
868
|
event: 'agent:ready',
|
|
637
869
|
nodeId,
|
|
638
870
|
sessionId,
|
|
639
871
|
providerType: providerType || undefined,
|
|
640
|
-
providerSessionId
|
|
641
|
-
finalSummary
|
|
872
|
+
providerSessionId,
|
|
873
|
+
finalSummary,
|
|
874
|
+
workerResult,
|
|
642
875
|
}),
|
|
643
876
|
},
|
|
644
877
|
});
|
|
@@ -648,13 +881,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
648
881
|
}
|
|
649
882
|
|
|
650
883
|
if (sessionId && nodeId && providerType) {
|
|
651
|
-
|
|
652
|
-
|
|
884
|
+
sweepExpiredRemoteIdleSessions();
|
|
885
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
886
|
+
nodeId, sessionId, providerType,
|
|
887
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
888
|
+
});
|
|
889
|
+
setImmediate(() => {
|
|
653
890
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
654
|
-
if (assigned) {
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
}, 500);
|
|
891
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
892
|
+
});
|
|
658
893
|
}
|
|
659
894
|
} else if (args.event === 'agent:generating_started') {
|
|
660
895
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -679,14 +914,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
914
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
680
915
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
681
916
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
917
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
918
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
919
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
682
920
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
683
921
|
? buildTaskCompletionEvidence({
|
|
684
922
|
event: 'agent:generating_completed',
|
|
685
923
|
nodeId: ledgerNodeId,
|
|
686
924
|
sessionId: ledgerSessionId,
|
|
687
925
|
providerType: ledgerProviderType,
|
|
688
|
-
providerSessionId
|
|
689
|
-
finalSummary
|
|
926
|
+
providerSessionId,
|
|
927
|
+
finalSummary,
|
|
928
|
+
workerResult,
|
|
690
929
|
})
|
|
691
930
|
: undefined;
|
|
692
931
|
appendLedgerEntry(args.meshId, {
|
|
@@ -698,8 +937,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
698
937
|
event: args.event,
|
|
699
938
|
nodeLabel: args.nodeLabel,
|
|
700
939
|
taskId: completedTaskForLedger?.id || undefined,
|
|
701
|
-
providerSessionId
|
|
702
|
-
finalSummary
|
|
940
|
+
providerSessionId,
|
|
941
|
+
finalSummary,
|
|
942
|
+
workerResult,
|
|
943
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
944
|
+
? args.metadataEvent.completionDiagnostic
|
|
945
|
+
: undefined,
|
|
703
946
|
evidence: completionEvidence,
|
|
704
947
|
},
|
|
705
948
|
});
|
|
@@ -781,17 +1024,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
781
1024
|
|
|
782
1025
|
if (coordinatorInstances.length === 0) {
|
|
783
1026
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
784
|
-
if (
|
|
785
|
-
pendingMeshCoordinatorEvents.push({
|
|
1027
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
786
1028
|
event: args.event,
|
|
787
1029
|
meshId: args.meshId,
|
|
788
1030
|
nodeLabel: args.nodeLabel,
|
|
1031
|
+
nodeId: args.nodeId || undefined,
|
|
1032
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
789
1033
|
metadataEvent: {
|
|
790
1034
|
...args.metadataEvent,
|
|
791
1035
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
792
1036
|
},
|
|
793
1037
|
queuedAt: Date.now(),
|
|
794
|
-
})
|
|
1038
|
+
})) {
|
|
795
1039
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
796
1040
|
}
|
|
797
1041
|
return { success: true, forwarded: 0 };
|
|
@@ -834,6 +1078,15 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
834
1078
|
providerType: readNonEmptyString(payload.providerType),
|
|
835
1079
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
836
1080
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1081
|
+
jobId: readNonEmptyString(payload.jobId),
|
|
1082
|
+
interactionId: readNonEmptyString(payload.interactionId),
|
|
1083
|
+
status: readNonEmptyString(payload.status),
|
|
1084
|
+
targetDaemonId: readNonEmptyString(payload.targetDaemonId),
|
|
1085
|
+
startedAt: readNonEmptyString(payload.startedAt),
|
|
1086
|
+
completedAt: readNonEmptyString(payload.completedAt),
|
|
1087
|
+
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1088
|
+
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1089
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
837
1090
|
intentional: payload.intentional === true,
|
|
838
1091
|
intentionalStop: payload.intentionalStop === true,
|
|
839
1092
|
operatorCleanup: payload.operatorCleanup === true,
|