@adhdev/daemon-core 0.9.82-rc.13 → 0.9.82-rc.130
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-adapter-types.d.ts +4 -1
- package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
- 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 +7461 -1380
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7417 -1364
- 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 +54 -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 +6 -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-adapter-types.ts +2 -1
- package/src/cli-adapters/provider-cli-adapter.ts +473 -18
- 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 +960 -40
- package/src/commands/cli-manager.ts +138 -2
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3238 -423
- 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 +295 -0
- package/src/mesh/mesh-events.ts +595 -48
- 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 +289 -36
- 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,198 @@ 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 readPendingMeshCoordinatorEventsFromDisk(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 readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getPendingEventsPath(meshId: string): string {
|
|
111
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
112
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
116
|
+
if (!meshId) return [];
|
|
117
|
+
const path = getPendingEventsPath(meshId);
|
|
118
|
+
if (!existsSync(path)) return [];
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(path, 'utf-8');
|
|
121
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
122
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
123
|
+
});
|
|
124
|
+
} catch { return []; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
128
|
+
const acceptedJobIds = new Set(
|
|
129
|
+
pending
|
|
130
|
+
.filter(event => event.event === 'refine:accepted')
|
|
131
|
+
.map(event => readRefineJobId(event))
|
|
132
|
+
.filter(Boolean),
|
|
133
|
+
);
|
|
134
|
+
if (acceptedJobIds.size === 0) return [];
|
|
135
|
+
const existingTerminalJobIds = new Set(
|
|
136
|
+
pending
|
|
137
|
+
.filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
|
|
138
|
+
.map(event => `${event.event}:${readRefineJobId(event)}`)
|
|
139
|
+
.filter(value => !value.endsWith(':')),
|
|
140
|
+
);
|
|
141
|
+
const backfilled: PendingMeshCoordinatorEvent[] = [];
|
|
142
|
+
const entries = readLedgerEntries(meshId);
|
|
143
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
144
|
+
const entry = entries[i];
|
|
145
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
|
|
146
|
+
const payload = readRecord(entry.payload);
|
|
147
|
+
if (payload?.source !== 'refine_mesh_node_async_job') continue;
|
|
148
|
+
const refineJob = readRecord(payload.refineJob);
|
|
149
|
+
const jobId = readNonEmptyString(refineJob?.jobId);
|
|
150
|
+
if (!jobId || !acceptedJobIds.has(jobId)) continue;
|
|
151
|
+
const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
|
|
152
|
+
if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
|
|
153
|
+
existingTerminalJobIds.add(`${eventName}:${jobId}`);
|
|
154
|
+
const result = readRecord(payload.result);
|
|
155
|
+
const metadataEvent = {
|
|
156
|
+
source: 'refine_mesh_node_async_job',
|
|
157
|
+
jobId,
|
|
158
|
+
interactionId: readNonEmptyString(refineJob?.interactionId),
|
|
159
|
+
meshId,
|
|
160
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
161
|
+
targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
|
|
162
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
163
|
+
status: eventName === 'refine:completed' ? 'completed' : 'failed',
|
|
164
|
+
startedAt: readNonEmptyString(refineJob?.startedAt),
|
|
165
|
+
completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
|
|
166
|
+
retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
|
|
167
|
+
...(result ? { result } : {}),
|
|
168
|
+
};
|
|
169
|
+
backfilled.push({
|
|
170
|
+
event: eventName,
|
|
171
|
+
meshId,
|
|
172
|
+
nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
|
|
173
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
174
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
175
|
+
metadataEvent,
|
|
176
|
+
coordinatorMessage: buildMeshSystemMessage({
|
|
177
|
+
event: eventName,
|
|
178
|
+
nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
|
|
179
|
+
metadataEvent,
|
|
180
|
+
}),
|
|
181
|
+
queuedAt: Date.now(),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return backfilled.reverse();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
188
|
+
const backfilled = refineTerminalEventFromLedger(meshId, events);
|
|
189
|
+
if (backfilled.length === 0) return events;
|
|
190
|
+
const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
|
|
191
|
+
return [
|
|
192
|
+
...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
|
|
193
|
+
...backfilled,
|
|
194
|
+
];
|
|
195
|
+
}
|
|
43
196
|
|
|
44
197
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
45
|
-
|
|
198
|
+
try {
|
|
199
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
200
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (hasPendingCoordinatorEventDuplicate(event)) {
|
|
204
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
208
|
+
return true;
|
|
209
|
+
} catch (e: any) {
|
|
210
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
46
211
|
return false;
|
|
47
212
|
}
|
|
48
|
-
pendingMeshCoordinatorEvents.push(event);
|
|
49
|
-
return true;
|
|
50
213
|
}
|
|
51
214
|
|
|
52
|
-
/** Drain and return all pending coordinator events,
|
|
53
|
-
export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
|
|
54
|
-
|
|
215
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
216
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
217
|
+
if (!meshId) return [];
|
|
218
|
+
const path = getPendingEventsPath(meshId);
|
|
219
|
+
if (!existsSync(path)) return [];
|
|
220
|
+
try {
|
|
221
|
+
const parsed = readPendingMeshCoordinatorEventsFromDisk(meshId);
|
|
222
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
223
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, parsed);
|
|
224
|
+
} catch { return []; }
|
|
55
225
|
}
|
|
56
226
|
|
|
57
227
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
58
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
59
|
-
|
|
228
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
229
|
+
if (!meshId) return [];
|
|
230
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId));
|
|
60
231
|
}
|
|
61
232
|
|
|
62
|
-
/** Explicitly clear all pending coordinator events. */
|
|
63
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
64
|
-
|
|
233
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
234
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
235
|
+
if (!meshId) return;
|
|
236
|
+
const path = getPendingEventsPath(meshId);
|
|
237
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
65
238
|
}
|
|
66
239
|
|
|
67
240
|
function readNonEmptyString(value: unknown): string {
|
|
68
241
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
69
242
|
}
|
|
70
243
|
|
|
244
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
245
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
246
|
+
? value as Record<string, unknown>
|
|
247
|
+
: undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
71
250
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
72
251
|
return readNonEmptyString(event.targetSessionId)
|
|
73
252
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -82,6 +261,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
82
261
|
'agent:stopped',
|
|
83
262
|
'agent:ready',
|
|
84
263
|
'monitor:long_generating',
|
|
264
|
+
'refine:accepted',
|
|
265
|
+
'refine:completed',
|
|
266
|
+
'refine:failed',
|
|
85
267
|
]);
|
|
86
268
|
|
|
87
269
|
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
@@ -96,10 +278,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
96
278
|
}
|
|
97
279
|
|
|
98
280
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
281
|
+
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
282
|
+
? event.completionDiagnostic as Record<string, unknown>
|
|
283
|
+
: null;
|
|
284
|
+
const diagnosticReason = completionDiagnostic
|
|
285
|
+
? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
|
|
286
|
+
: '';
|
|
287
|
+
const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
|
|
288
|
+
? String(completionDiagnostic.finalAssistantPresent)
|
|
289
|
+
: '';
|
|
99
290
|
const parts = [
|
|
100
291
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
101
292
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
102
293
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
294
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
295
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
103
296
|
].filter(Boolean);
|
|
104
297
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
105
298
|
}
|
|
@@ -150,6 +343,148 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
150
343
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
151
344
|
}
|
|
152
345
|
|
|
346
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
347
|
+
const recentCompletionFingerprints = new Map<string, number>();
|
|
348
|
+
|
|
349
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
350
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
351
|
+
if (typeof value === 'string' && value.trim()) {
|
|
352
|
+
const numeric = Number(value);
|
|
353
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
354
|
+
const parsed = Date.parse(value);
|
|
355
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildMeshCompletionFingerprint(args: {
|
|
361
|
+
meshId: string;
|
|
362
|
+
event: string;
|
|
363
|
+
sessionId: string;
|
|
364
|
+
providerType?: string;
|
|
365
|
+
providerSessionId?: string;
|
|
366
|
+
timestamp?: number | null;
|
|
367
|
+
finalSummary?: string;
|
|
368
|
+
}): string {
|
|
369
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
370
|
+
? String(args.timestamp)
|
|
371
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
372
|
+
return [
|
|
373
|
+
args.meshId,
|
|
374
|
+
args.event,
|
|
375
|
+
args.sessionId,
|
|
376
|
+
args.providerType || '',
|
|
377
|
+
args.providerSessionId || '',
|
|
378
|
+
timestampPart,
|
|
379
|
+
].join('::');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
383
|
+
meshId: string;
|
|
384
|
+
event: string;
|
|
385
|
+
sessionId: string;
|
|
386
|
+
providerType?: string;
|
|
387
|
+
providerSessionId?: string;
|
|
388
|
+
timestamp?: number | null;
|
|
389
|
+
finalSummary?: string;
|
|
390
|
+
}): boolean {
|
|
391
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
392
|
+
if (!fingerprint) return false;
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
395
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
396
|
+
}
|
|
397
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
398
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
|
|
403
|
+
const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
|
|
404
|
+
if (!fingerprint) return false;
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
407
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
408
|
+
}
|
|
409
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
410
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function findRecentTerminalLedgerEvidence(args: {
|
|
415
|
+
meshId: string;
|
|
416
|
+
sessionId?: string;
|
|
417
|
+
nodeId?: string;
|
|
418
|
+
}): { kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
419
|
+
if (!args.sessionId && !args.nodeId) return null;
|
|
420
|
+
const entries = readLedgerEntries(args.meshId);
|
|
421
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
422
|
+
const entry = entries[i];
|
|
423
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
424
|
+
if (args.sessionId && entry.sessionId === args.sessionId) {
|
|
425
|
+
return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
426
|
+
}
|
|
427
|
+
if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
|
|
428
|
+
return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function buildLongGeneratingCompletionReconciliation(args: {
|
|
435
|
+
meshId: string;
|
|
436
|
+
nodeId?: string;
|
|
437
|
+
nodeLabel: string;
|
|
438
|
+
metadataEvent: Record<string, unknown>;
|
|
439
|
+
sourceInstanceId?: string;
|
|
440
|
+
}): Record<string, unknown> | null {
|
|
441
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
442
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
443
|
+
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
444
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
445
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
446
|
+
const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
|
|
447
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
448
|
+
const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
|
|
449
|
+
const explicitCompletionEvidence = Boolean(
|
|
450
|
+
finalSummary
|
|
451
|
+
|| workerResult
|
|
452
|
+
|| completionDiagnostic?.finalAssistantPresent === true
|
|
453
|
+
|| status === 'idle'
|
|
454
|
+
|| status === 'ready'
|
|
455
|
+
|| status === 'completed',
|
|
456
|
+
);
|
|
457
|
+
if (explicitCompletionEvidence) {
|
|
458
|
+
return {
|
|
459
|
+
...args.metadataEvent,
|
|
460
|
+
targetSessionId: sessionId,
|
|
461
|
+
providerType,
|
|
462
|
+
providerSessionId,
|
|
463
|
+
finalSummary,
|
|
464
|
+
source: 'long_generating_reconciliation',
|
|
465
|
+
reconciledFromEvent: 'monitor:long_generating',
|
|
466
|
+
timestamp: args.metadataEvent.timestamp ?? Date.now(),
|
|
467
|
+
completionDiagnostic: {
|
|
468
|
+
...(completionDiagnostic || {}),
|
|
469
|
+
reconciliationReason: 'provider_completion_evidence',
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
475
|
+
meshId: args.meshId,
|
|
476
|
+
sessionId: sessionId || undefined,
|
|
477
|
+
nodeId: nodeId || undefined,
|
|
478
|
+
});
|
|
479
|
+
if (!terminal) return null;
|
|
480
|
+
return {
|
|
481
|
+
...args.metadataEvent,
|
|
482
|
+
source: 'long_generating_terminal_ledger_suppression',
|
|
483
|
+
terminalLedgerKind: terminal.kind,
|
|
484
|
+
terminalLedgerAt: terminal.timestamp,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
153
488
|
|
|
154
489
|
export function tryAssignQueueTask(
|
|
155
490
|
components: DaemonComponents,
|
|
@@ -180,7 +515,16 @@ export function tryAssignQueueTask(
|
|
|
180
515
|
message: task.message,
|
|
181
516
|
}).catch((e: any) => {
|
|
182
517
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
183
|
-
|
|
518
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
519
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
520
|
+
try {
|
|
521
|
+
appendLedgerEntry(meshId, {
|
|
522
|
+
kind: 'dispatch_failed' as any,
|
|
523
|
+
nodeId,
|
|
524
|
+
sessionId,
|
|
525
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
526
|
+
});
|
|
527
|
+
} catch { /* ledger write is best-effort */ }
|
|
184
528
|
});
|
|
185
529
|
return true;
|
|
186
530
|
}
|
|
@@ -277,6 +621,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
|
|
|
277
621
|
return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
|
|
278
622
|
}
|
|
279
623
|
|
|
624
|
+
function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
|
|
625
|
+
return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
|
|
626
|
+
}
|
|
627
|
+
|
|
280
628
|
function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
|
|
281
629
|
return components.instanceManager.getByCategory('cli').filter((inst: any) => {
|
|
282
630
|
const state = inst.getState();
|
|
@@ -542,6 +890,9 @@ function buildMeshSystemMessage(args: {
|
|
|
542
890
|
}): string {
|
|
543
891
|
const metadata = formatCompletionMetadata(args.metadataEvent);
|
|
544
892
|
if (args.event === 'agent:generating_completed') {
|
|
893
|
+
if (args.metadataEvent.source === 'long_generating_reconciliation') {
|
|
894
|
+
return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The long-generating monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
|
|
895
|
+
}
|
|
545
896
|
return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
|
|
546
897
|
}
|
|
547
898
|
if (args.event === 'agent:waiting_approval') {
|
|
@@ -573,7 +924,72 @@ function buildMeshSystemMessage(args: {
|
|
|
573
924
|
return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
|
|
574
925
|
}
|
|
575
926
|
if (args.event === 'monitor:long_generating') {
|
|
576
|
-
return `[System] ${args.nodeLabel}
|
|
927
|
+
return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
|
|
928
|
+
}
|
|
929
|
+
if (args.event === 'refine:accepted') {
|
|
930
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
931
|
+
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.`;
|
|
932
|
+
}
|
|
933
|
+
if (args.event === 'refine:completed') {
|
|
934
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
935
|
+
const result = readRecord(args.metadataEvent.result);
|
|
936
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
937
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
938
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
939
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
940
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
941
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
942
|
+
const into = readNonEmptyString(result?.into);
|
|
943
|
+
const branch = readNonEmptyString(result?.branch);
|
|
944
|
+
const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
|
|
945
|
+
const convergenceStatus = readNonEmptyString(finalConvergence?.status);
|
|
946
|
+
const nextStep = readNonEmptyString(result?.nextStep)
|
|
947
|
+
|| readNonEmptyString(finalConvergence?.nextStep)
|
|
948
|
+
|| 'Continue from the updated mesh state.';
|
|
949
|
+
const details = [
|
|
950
|
+
jobId ? `job_id=${jobId}` : '',
|
|
951
|
+
branch && into ? `${branch}→${into}` : '',
|
|
952
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
953
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
954
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
955
|
+
convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
|
|
956
|
+
].filter(Boolean).join('; ');
|
|
957
|
+
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
|
|
958
|
+
}
|
|
959
|
+
if (args.event === 'refine:failed') {
|
|
960
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
961
|
+
const result = readRecord(args.metadataEvent.result);
|
|
962
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
963
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
964
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
965
|
+
const code = readNonEmptyString(result?.code);
|
|
966
|
+
const error = readNonEmptyString(result?.error);
|
|
967
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
968
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
969
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
970
|
+
const mergeStatus = result?.merged === true
|
|
971
|
+
? 'merged'
|
|
972
|
+
: finalConvergence?.merged === false
|
|
973
|
+
? 'not_merged'
|
|
974
|
+
: '';
|
|
975
|
+
const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
|
|
976
|
+
|| readNonEmptyString(finalConvergence?.status);
|
|
977
|
+
const blockedReason = readNonEmptyString(result?.blockedReason);
|
|
978
|
+
const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
|
|
979
|
+
const details = [
|
|
980
|
+
jobId ? `job_id=${jobId}` : '',
|
|
981
|
+
code ? `code=${code}` : '',
|
|
982
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
983
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
984
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
985
|
+
convergenceStatus ? `convergence=${convergenceStatus}` : '',
|
|
986
|
+
blockedReason ? `reason=${blockedReason}` : '',
|
|
987
|
+
].filter(Boolean).join('; ');
|
|
988
|
+
const parts = [
|
|
989
|
+
`[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
|
|
990
|
+
nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
|
|
991
|
+
];
|
|
992
|
+
return parts.join('\n');
|
|
577
993
|
}
|
|
578
994
|
return '';
|
|
579
995
|
}
|
|
@@ -603,6 +1019,75 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
603
1019
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
604
1020
|
}
|
|
605
1021
|
|
|
1022
|
+
if (args.event === 'monitor:long_generating') {
|
|
1023
|
+
const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
|
|
1024
|
+
meshId: args.meshId,
|
|
1025
|
+
nodeId: args.nodeId,
|
|
1026
|
+
nodeLabel: args.nodeLabel,
|
|
1027
|
+
metadataEvent: args.metadataEvent,
|
|
1028
|
+
sourceInstanceId: args.sourceInstanceId,
|
|
1029
|
+
});
|
|
1030
|
+
if (reconciledCompletion?.source === 'long_generating_reconciliation') {
|
|
1031
|
+
LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
|
|
1032
|
+
return injectMeshSystemMessage(components, {
|
|
1033
|
+
...args,
|
|
1034
|
+
event: 'agent:generating_completed',
|
|
1035
|
+
metadataEvent: reconciledCompletion,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
|
|
1039
|
+
LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
|
|
1040
|
+
return {
|
|
1041
|
+
success: true,
|
|
1042
|
+
forwarded: 0,
|
|
1043
|
+
suppressed: true,
|
|
1044
|
+
terminalLedgerEvidence: true,
|
|
1045
|
+
terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
1051
|
+
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
1052
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
1056
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
1057
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
1058
|
+
meshId: args.meshId,
|
|
1059
|
+
sessionId: eventSessionId,
|
|
1060
|
+
nodeId: eventNodeId || undefined,
|
|
1061
|
+
});
|
|
1062
|
+
if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
|
|
1063
|
+
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
1064
|
+
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
1065
|
+
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
1066
|
+
const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
1067
|
+
if (
|
|
1068
|
+
(terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
|
|
1069
|
+
|| (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
|
|
1070
|
+
|| args.metadataEvent.source === 'long_generating_reconciliation'
|
|
1071
|
+
) {
|
|
1072
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1073
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
1077
|
+
meshId: args.meshId,
|
|
1078
|
+
event: args.event,
|
|
1079
|
+
sessionId: eventSessionId,
|
|
1080
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
1081
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
1082
|
+
timestamp: eventTimestamp,
|
|
1083
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
1084
|
+
});
|
|
1085
|
+
if (duplicateCompletion) {
|
|
1086
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1087
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
606
1091
|
// ── Task Queue & Ledger ──
|
|
607
1092
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
608
1093
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -611,20 +1096,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
611
1096
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
612
1097
|
|
|
613
1098
|
if (sessionId) {
|
|
614
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
1099
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
1100
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
1101
|
+
});
|
|
615
1102
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
616
1103
|
if (nodeId && providerType) {
|
|
617
|
-
//
|
|
618
|
-
|
|
1104
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
1105
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
1106
|
+
setImmediate(() => {
|
|
619
1107
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
620
|
-
}
|
|
1108
|
+
});
|
|
621
1109
|
}
|
|
622
1110
|
}
|
|
623
1111
|
} else if (args.event === 'agent:ready') {
|
|
624
1112
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
625
1113
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
626
1114
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
627
|
-
const
|
|
1115
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
1116
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
1117
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
1118
|
+
const hasCompletionEvidence = !!finalSummary || !!workerResult;
|
|
1119
|
+
const completedTask = sessionId && hasCompletionEvidence
|
|
628
1120
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
629
1121
|
: null;
|
|
630
1122
|
if (completedTask) {
|
|
@@ -640,15 +1132,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
640
1132
|
nodeLabel: args.nodeLabel,
|
|
641
1133
|
taskId: completedTask.id,
|
|
642
1134
|
completedViaReady: true,
|
|
643
|
-
providerSessionId
|
|
644
|
-
finalSummary
|
|
1135
|
+
providerSessionId,
|
|
1136
|
+
finalSummary,
|
|
1137
|
+
workerResult,
|
|
645
1138
|
evidence: buildTaskCompletionEvidence({
|
|
646
1139
|
event: 'agent:ready',
|
|
647
1140
|
nodeId,
|
|
648
1141
|
sessionId,
|
|
649
1142
|
providerType: providerType || undefined,
|
|
650
|
-
providerSessionId
|
|
651
|
-
finalSummary
|
|
1143
|
+
providerSessionId,
|
|
1144
|
+
finalSummary,
|
|
1145
|
+
workerResult,
|
|
652
1146
|
}),
|
|
653
1147
|
},
|
|
654
1148
|
});
|
|
@@ -658,13 +1152,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
658
1152
|
}
|
|
659
1153
|
|
|
660
1154
|
if (sessionId && nodeId && providerType) {
|
|
661
|
-
|
|
662
|
-
|
|
1155
|
+
sweepExpiredRemoteIdleSessions();
|
|
1156
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
1157
|
+
nodeId, sessionId, providerType,
|
|
1158
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
1159
|
+
});
|
|
1160
|
+
setImmediate(() => {
|
|
663
1161
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
664
|
-
if (assigned) {
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
}, 500);
|
|
1162
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
1163
|
+
});
|
|
668
1164
|
}
|
|
669
1165
|
} else if (args.event === 'agent:generating_started') {
|
|
670
1166
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -679,7 +1175,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
1175
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
680
1176
|
}
|
|
681
1177
|
if (sessionId) {
|
|
682
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
1178
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
1179
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
683
1180
|
}
|
|
684
1181
|
}
|
|
685
1182
|
|
|
@@ -689,14 +1186,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
689
1186
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
690
1187
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
691
1188
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
1189
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
1190
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
1191
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
692
1192
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
693
1193
|
? buildTaskCompletionEvidence({
|
|
694
1194
|
event: 'agent:generating_completed',
|
|
695
1195
|
nodeId: ledgerNodeId,
|
|
696
1196
|
sessionId: ledgerSessionId,
|
|
697
1197
|
providerType: ledgerProviderType,
|
|
698
|
-
providerSessionId
|
|
699
|
-
finalSummary
|
|
1198
|
+
providerSessionId,
|
|
1199
|
+
finalSummary,
|
|
1200
|
+
workerResult,
|
|
700
1201
|
})
|
|
701
1202
|
: undefined;
|
|
702
1203
|
appendLedgerEntry(args.meshId, {
|
|
@@ -708,8 +1209,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
708
1209
|
event: args.event,
|
|
709
1210
|
nodeLabel: args.nodeLabel,
|
|
710
1211
|
taskId: completedTaskForLedger?.id || undefined,
|
|
711
|
-
providerSessionId
|
|
712
|
-
finalSummary
|
|
1212
|
+
providerSessionId,
|
|
1213
|
+
finalSummary,
|
|
1214
|
+
workerResult,
|
|
1215
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
1216
|
+
? args.metadataEvent.completionDiagnostic
|
|
1217
|
+
: undefined,
|
|
713
1218
|
evidence: completionEvidence,
|
|
714
1219
|
},
|
|
715
1220
|
});
|
|
@@ -782,6 +1287,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
782
1287
|
}
|
|
783
1288
|
}
|
|
784
1289
|
|
|
1290
|
+
const messageText = buildMeshSystemMessage({
|
|
1291
|
+
event: args.event,
|
|
1292
|
+
nodeLabel: args.nodeLabel,
|
|
1293
|
+
metadataEvent: args.metadataEvent,
|
|
1294
|
+
recoveryContext,
|
|
1295
|
+
});
|
|
1296
|
+
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1297
|
+
|
|
785
1298
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
786
1299
|
const instState = inst.getState();
|
|
787
1300
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
@@ -789,6 +1302,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
789
1302
|
return true;
|
|
790
1303
|
});
|
|
791
1304
|
|
|
1305
|
+
// Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
|
|
1306
|
+
// synchronously; only buffer them for MCP when no CLI coordinator is present.
|
|
1307
|
+
// Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
|
|
1308
|
+
// receive them regardless of whether a live CLI coordinator session is active.
|
|
1309
|
+
const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
|
|
1310
|
+
|
|
792
1311
|
if (coordinatorInstances.length === 0) {
|
|
793
1312
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
794
1313
|
if (queuePendingMeshCoordinatorEvent({
|
|
@@ -801,6 +1320,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
801
1320
|
...args.metadataEvent,
|
|
802
1321
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
803
1322
|
},
|
|
1323
|
+
coordinatorMessage: messageText,
|
|
804
1324
|
queuedAt: Date.now(),
|
|
805
1325
|
})) {
|
|
806
1326
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
@@ -808,13 +1328,27 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
808
1328
|
return { success: true, forwarded: 0 };
|
|
809
1329
|
}
|
|
810
1330
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1331
|
+
// CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
|
|
1332
|
+
// that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
|
|
1333
|
+
// forwarded directly only — they must not accumulate in the pending queue when a live
|
|
1334
|
+
// coordinator already received them.
|
|
1335
|
+
if (!isRefineTerminalEvent) {
|
|
1336
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
1337
|
+
event: args.event,
|
|
1338
|
+
meshId: args.meshId,
|
|
1339
|
+
nodeLabel: args.nodeLabel,
|
|
1340
|
+
nodeId: args.nodeId || undefined,
|
|
1341
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1342
|
+
metadataEvent: {
|
|
1343
|
+
...args.metadataEvent,
|
|
1344
|
+
...(recoveryContext ? { recoveryContext } : {}),
|
|
1345
|
+
},
|
|
1346
|
+
coordinatorMessage: messageText,
|
|
1347
|
+
queuedAt: Date.now(),
|
|
1348
|
+
})) {
|
|
1349
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
818
1352
|
|
|
819
1353
|
for (const coord of coordinatorInstances) {
|
|
820
1354
|
const coordState = coord.getState();
|
|
@@ -845,6 +1379,19 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
845
1379
|
providerType: readNonEmptyString(payload.providerType),
|
|
846
1380
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
847
1381
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1382
|
+
jobId: readNonEmptyString(payload.jobId),
|
|
1383
|
+
interactionId: readNonEmptyString(payload.interactionId),
|
|
1384
|
+
status: readNonEmptyString(payload.status),
|
|
1385
|
+
targetDaemonId: readNonEmptyString(payload.targetDaemonId),
|
|
1386
|
+
startedAt: readNonEmptyString(payload.startedAt),
|
|
1387
|
+
completedAt: readNonEmptyString(payload.completedAt),
|
|
1388
|
+
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1389
|
+
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1390
|
+
...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
|
|
1391
|
+
...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
|
|
1392
|
+
...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
|
|
1393
|
+
...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
|
|
1394
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
848
1395
|
intentional: payload.intentional === true,
|
|
849
1396
|
intentionalStop: payload.intentionalStop === true,
|
|
850
1397
|
operatorCleanup: payload.operatorCleanup === true,
|