@adhdev/daemon-core 0.9.82-rc.12 → 0.9.82-rc.121
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/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +21 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +4 -0
- package/dist/config/mesh-config.d.ts +68 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.js +7029 -1368
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6984 -1351
- 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 +73 -0
- package/dist/mesh/mesh-events.d.ts +26 -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-refine-status.d.ts +27 -0
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +4 -1
- package/dist/repo-mesh-types.d.ts +62 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/chat/subscription-updates.ts +5 -1
- package/src/cli-adapters/provider-cli-adapter.ts +453 -17
- 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 +32 -10
- package/src/commands/chat-commands.ts +730 -30
- package/src/commands/cli-manager.ts +129 -1
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3239 -430
- package/src/config/chat-history.ts +37 -9
- package/src/config/mesh-config.ts +249 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/index.ts +41 -4
- 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 +31 -8
- package/src/mesh/mesh-active-work.ts +292 -0
- package/src/mesh/mesh-events.ts +389 -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-refine-status.ts +145 -0
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +234 -0
- package/src/providers/approval-utils.ts +12 -5
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +201 -28
- 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 +67 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
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 {
|
|
@@ -35,39 +55,125 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
35
55
|
nodeId?: string;
|
|
36
56
|
workspace?: string;
|
|
37
57
|
metadataEvent: Record<string, unknown>;
|
|
58
|
+
coordinatorMessage?: string;
|
|
38
59
|
queuedAt: number;
|
|
39
60
|
}
|
|
40
61
|
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
+
}
|
|
43
114
|
|
|
44
115
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
45
|
-
|
|
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}`);
|
|
46
129
|
return false;
|
|
47
130
|
}
|
|
48
|
-
pendingMeshCoordinatorEvents.push(event);
|
|
49
|
-
return true;
|
|
50
131
|
}
|
|
51
132
|
|
|
52
|
-
/** Drain and return all pending coordinator events,
|
|
53
|
-
export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
|
|
54
|
-
|
|
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 []; }
|
|
55
145
|
}
|
|
56
146
|
|
|
57
147
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
58
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
59
|
-
|
|
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 []; }
|
|
60
158
|
}
|
|
61
159
|
|
|
62
|
-
/** Explicitly clear all pending coordinator events. */
|
|
63
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
64
|
-
|
|
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 */ }
|
|
65
165
|
}
|
|
66
166
|
|
|
67
167
|
function readNonEmptyString(value: unknown): string {
|
|
68
168
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
69
169
|
}
|
|
70
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
|
+
|
|
71
177
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
72
178
|
return readNonEmptyString(event.targetSessionId)
|
|
73
179
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -82,6 +188,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
82
188
|
'agent:stopped',
|
|
83
189
|
'agent:ready',
|
|
84
190
|
'monitor:long_generating',
|
|
191
|
+
'refine:accepted',
|
|
192
|
+
'refine:completed',
|
|
193
|
+
'refine:failed',
|
|
85
194
|
]);
|
|
86
195
|
|
|
87
196
|
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
@@ -96,10 +205,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
96
205
|
}
|
|
97
206
|
|
|
98
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
|
+
: '';
|
|
99
217
|
const parts = [
|
|
100
218
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
101
219
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
102
220
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
221
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
222
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
103
223
|
].filter(Boolean);
|
|
104
224
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
105
225
|
}
|
|
@@ -150,6 +270,74 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
150
270
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
151
271
|
}
|
|
152
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
|
+
|
|
153
341
|
|
|
154
342
|
export function tryAssignQueueTask(
|
|
155
343
|
components: DaemonComponents,
|
|
@@ -180,7 +368,16 @@ export function tryAssignQueueTask(
|
|
|
180
368
|
message: task.message,
|
|
181
369
|
}).catch((e: any) => {
|
|
182
370
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
183
|
-
|
|
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 */ }
|
|
184
381
|
});
|
|
185
382
|
return true;
|
|
186
383
|
}
|
|
@@ -575,6 +772,71 @@ function buildMeshSystemMessage(args: {
|
|
|
575
772
|
if (args.event === 'monitor:long_generating') {
|
|
576
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.`;
|
|
577
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
|
+
}
|
|
578
840
|
return '';
|
|
579
841
|
}
|
|
580
842
|
|
|
@@ -603,6 +865,28 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
603
865
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
604
866
|
}
|
|
605
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
|
+
|
|
606
890
|
// ── Task Queue & Ledger ──
|
|
607
891
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
608
892
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -611,20 +895,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
611
895
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
612
896
|
|
|
613
897
|
if (sessionId) {
|
|
614
|
-
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
|
+
});
|
|
615
901
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
616
902
|
if (nodeId && providerType) {
|
|
617
|
-
//
|
|
618
|
-
|
|
903
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
904
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
905
|
+
setImmediate(() => {
|
|
619
906
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
620
|
-
}
|
|
907
|
+
});
|
|
621
908
|
}
|
|
622
909
|
}
|
|
623
910
|
} else if (args.event === 'agent:ready') {
|
|
624
911
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
625
912
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
626
913
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
627
|
-
const
|
|
914
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
915
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
916
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
917
|
+
const hasCompletionEvidence = !!finalSummary || !!workerResult;
|
|
918
|
+
const completedTask = sessionId && hasCompletionEvidence
|
|
628
919
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
629
920
|
: null;
|
|
630
921
|
if (completedTask) {
|
|
@@ -640,15 +931,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
640
931
|
nodeLabel: args.nodeLabel,
|
|
641
932
|
taskId: completedTask.id,
|
|
642
933
|
completedViaReady: true,
|
|
643
|
-
providerSessionId
|
|
644
|
-
finalSummary
|
|
934
|
+
providerSessionId,
|
|
935
|
+
finalSummary,
|
|
936
|
+
workerResult,
|
|
645
937
|
evidence: buildTaskCompletionEvidence({
|
|
646
938
|
event: 'agent:ready',
|
|
647
939
|
nodeId,
|
|
648
940
|
sessionId,
|
|
649
941
|
providerType: providerType || undefined,
|
|
650
|
-
providerSessionId
|
|
651
|
-
finalSummary
|
|
942
|
+
providerSessionId,
|
|
943
|
+
finalSummary,
|
|
944
|
+
workerResult,
|
|
652
945
|
}),
|
|
653
946
|
},
|
|
654
947
|
});
|
|
@@ -658,13 +951,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
658
951
|
}
|
|
659
952
|
|
|
660
953
|
if (sessionId && nodeId && providerType) {
|
|
661
|
-
|
|
662
|
-
|
|
954
|
+
sweepExpiredRemoteIdleSessions();
|
|
955
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
956
|
+
nodeId, sessionId, providerType,
|
|
957
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
958
|
+
});
|
|
959
|
+
setImmediate(() => {
|
|
663
960
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
664
|
-
if (assigned) {
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
}, 500);
|
|
961
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
962
|
+
});
|
|
668
963
|
}
|
|
669
964
|
} else if (args.event === 'agent:generating_started') {
|
|
670
965
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -679,7 +974,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
974
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
680
975
|
}
|
|
681
976
|
if (sessionId) {
|
|
682
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
977
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
978
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
683
979
|
}
|
|
684
980
|
}
|
|
685
981
|
|
|
@@ -689,14 +985,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
689
985
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
690
986
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
691
987
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
988
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
989
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
990
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
692
991
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
693
992
|
? buildTaskCompletionEvidence({
|
|
694
993
|
event: 'agent:generating_completed',
|
|
695
994
|
nodeId: ledgerNodeId,
|
|
696
995
|
sessionId: ledgerSessionId,
|
|
697
996
|
providerType: ledgerProviderType,
|
|
698
|
-
providerSessionId
|
|
699
|
-
finalSummary
|
|
997
|
+
providerSessionId,
|
|
998
|
+
finalSummary,
|
|
999
|
+
workerResult,
|
|
700
1000
|
})
|
|
701
1001
|
: undefined;
|
|
702
1002
|
appendLedgerEntry(args.meshId, {
|
|
@@ -708,8 +1008,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
708
1008
|
event: args.event,
|
|
709
1009
|
nodeLabel: args.nodeLabel,
|
|
710
1010
|
taskId: completedTaskForLedger?.id || undefined,
|
|
711
|
-
providerSessionId
|
|
712
|
-
finalSummary
|
|
1011
|
+
providerSessionId,
|
|
1012
|
+
finalSummary,
|
|
1013
|
+
workerResult,
|
|
1014
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
1015
|
+
? args.metadataEvent.completionDiagnostic
|
|
1016
|
+
: undefined,
|
|
713
1017
|
evidence: completionEvidence,
|
|
714
1018
|
},
|
|
715
1019
|
});
|
|
@@ -782,6 +1086,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
782
1086
|
}
|
|
783
1087
|
}
|
|
784
1088
|
|
|
1089
|
+
const messageText = buildMeshSystemMessage({
|
|
1090
|
+
event: args.event,
|
|
1091
|
+
nodeLabel: args.nodeLabel,
|
|
1092
|
+
metadataEvent: args.metadataEvent,
|
|
1093
|
+
recoveryContext,
|
|
1094
|
+
});
|
|
1095
|
+
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1096
|
+
|
|
785
1097
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
786
1098
|
const instState = inst.getState();
|
|
787
1099
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
@@ -789,6 +1101,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
789
1101
|
return true;
|
|
790
1102
|
});
|
|
791
1103
|
|
|
1104
|
+
// Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
|
|
1105
|
+
// synchronously; only buffer them for MCP when no CLI coordinator is present.
|
|
1106
|
+
// Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
|
|
1107
|
+
// receive them regardless of whether a live CLI coordinator session is active.
|
|
1108
|
+
const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
|
|
1109
|
+
|
|
792
1110
|
if (coordinatorInstances.length === 0) {
|
|
793
1111
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
794
1112
|
if (queuePendingMeshCoordinatorEvent({
|
|
@@ -801,6 +1119,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
801
1119
|
...args.metadataEvent,
|
|
802
1120
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
803
1121
|
},
|
|
1122
|
+
coordinatorMessage: messageText,
|
|
804
1123
|
queuedAt: Date.now(),
|
|
805
1124
|
})) {
|
|
806
1125
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
@@ -808,13 +1127,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
808
1127
|
return { success: true, forwarded: 0 };
|
|
809
1128
|
}
|
|
810
1129
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1130
|
+
// CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
|
|
1131
|
+
// that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
|
|
1132
|
+
// forwarded directly only — they must not accumulate in the pending queue when a live
|
|
1133
|
+
// coordinator already received them.
|
|
1134
|
+
if (!isRefineTerminalEvent) {
|
|
1135
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
1136
|
+
event: args.event,
|
|
1137
|
+
meshId: args.meshId,
|
|
1138
|
+
nodeLabel: args.nodeLabel,
|
|
1139
|
+
nodeId: args.nodeId || undefined,
|
|
1140
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1141
|
+
metadataEvent: {
|
|
1142
|
+
...args.metadataEvent,
|
|
1143
|
+
...(recoveryContext ? { recoveryContext } : {}),
|
|
1144
|
+
},
|
|
1145
|
+
coordinatorMessage: messageText,
|
|
1146
|
+
queuedAt: Date.now(),
|
|
1147
|
+
})) {
|
|
1148
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
818
1151
|
|
|
819
1152
|
for (const coord of coordinatorInstances) {
|
|
820
1153
|
const coordState = coord.getState();
|
|
@@ -845,6 +1178,15 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
845
1178
|
providerType: readNonEmptyString(payload.providerType),
|
|
846
1179
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
847
1180
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1181
|
+
jobId: readNonEmptyString(payload.jobId),
|
|
1182
|
+
interactionId: readNonEmptyString(payload.interactionId),
|
|
1183
|
+
status: readNonEmptyString(payload.status),
|
|
1184
|
+
targetDaemonId: readNonEmptyString(payload.targetDaemonId),
|
|
1185
|
+
startedAt: readNonEmptyString(payload.startedAt),
|
|
1186
|
+
completedAt: readNonEmptyString(payload.completedAt),
|
|
1187
|
+
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1188
|
+
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1189
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
848
1190
|
intentional: payload.intentional === true,
|
|
849
1191
|
intentionalStop: payload.intentionalStop === true,
|
|
850
1192
|
operatorCleanup: payload.operatorCleanup === true,
|