@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.61
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/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 +3522 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3496 -587
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +48 -0
- package/dist/mesh/mesh-events.d.ts +17 -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 +23 -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/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/commands/router.ts +2178 -419
- package/src/config/mesh-config.ts +244 -1
- 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/mesh/coordinator-prompt.ts +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +210 -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 +137 -0
- package/src/mesh/mesh-work-queue.ts +202 -122
- 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/repo-mesh-types.ts +174 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
2
|
+
import type { MeshWorkQueueEntry } from './mesh-work-queue.js';
|
|
3
|
+
|
|
4
|
+
export type MeshActiveWorkSource = 'queue' | 'direct';
|
|
5
|
+
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
|
|
6
|
+
|
|
7
|
+
export interface MeshActiveWorkRecord {
|
|
8
|
+
taskId: string;
|
|
9
|
+
source: MeshActiveWorkSource;
|
|
10
|
+
status: MeshActiveWorkStatus;
|
|
11
|
+
nodeId?: string;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
providerType?: string;
|
|
14
|
+
taskTitle: string;
|
|
15
|
+
taskSummary: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
taskMode?: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
dispatchedAt?: string;
|
|
21
|
+
elapsedMs: number;
|
|
22
|
+
terminal?: boolean;
|
|
23
|
+
terminalKind?: string;
|
|
24
|
+
terminalAt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MeshActiveWorkSummary {
|
|
28
|
+
totalActiveCount: number;
|
|
29
|
+
queueActiveCount: number;
|
|
30
|
+
directActiveCount: number;
|
|
31
|
+
awaitingApprovalCount: number;
|
|
32
|
+
generatingCount: number;
|
|
33
|
+
failedCount: number;
|
|
34
|
+
idleCount: number;
|
|
35
|
+
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
36
|
+
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BuildMeshActiveWorkOptions {
|
|
40
|
+
meshId: string;
|
|
41
|
+
queue?: MeshWorkQueueEntry[];
|
|
42
|
+
ledgerEntries?: MeshLedgerEntry[];
|
|
43
|
+
nodes?: any[];
|
|
44
|
+
now?: number;
|
|
45
|
+
/** Include terminal direct rows (idle/failed) for handoff/recent-work surfaces. Defaults false. */
|
|
46
|
+
includeTerminalDirect?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DIRECT_DISPATCH_VIA = new Set(['p2p_direct', 'local_direct', 'mesh_send_task']);
|
|
50
|
+
const TERMINAL_LEDGER_KINDS = new Set(['task_completed', 'task_failed', 'task_stalled']);
|
|
51
|
+
|
|
52
|
+
function readString(value: unknown): string | undefined {
|
|
53
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function summarizeMessage(message: string): { title: string; summary: string } {
|
|
57
|
+
const oneLine = message.replace(/\s+/g, ' ').trim();
|
|
58
|
+
const title = oneLine.length > 96 ? `${oneLine.slice(0, 93)}...` : oneLine;
|
|
59
|
+
return { title: title || '(untitled task)', summary: oneLine };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function elapsedSince(value: string | undefined, now: number): number {
|
|
63
|
+
const started = value ? new Date(value).getTime() : Number.NaN;
|
|
64
|
+
return Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): MeshActiveWorkStatus | undefined {
|
|
68
|
+
if (!nodeId || !sessionId || !Array.isArray(nodes)) return undefined;
|
|
69
|
+
const node = nodes.find(item => readString(item?.id) === nodeId || readString(item?.nodeId) === nodeId || readString(item?.node_id) === nodeId);
|
|
70
|
+
if (!node) return undefined;
|
|
71
|
+
const candidates: any[] = [];
|
|
72
|
+
for (const value of [node.sessions, node.activeSessions, node.active_sessions, node.lastProbe?.sessions, node.last_probe?.sessions, node.lastProbe?.status?.sessions, node.last_probe?.status?.sessions]) {
|
|
73
|
+
if (Array.isArray(value)) candidates.push(...value);
|
|
74
|
+
}
|
|
75
|
+
for (const value of [node.activeSession, node.active_session, node.currentSession, node.current_session, node.runtimeSession, node.runtime_session, node.session]) {
|
|
76
|
+
if (value && typeof value === 'object') candidates.push(value);
|
|
77
|
+
}
|
|
78
|
+
const session = candidates.find(item => {
|
|
79
|
+
const id = readString(item?.id) || readString(item?.sessionId) || readString(item?.session_id) || readString(item?.runtimeSessionId) || readString(item?.instanceId);
|
|
80
|
+
return id === sessionId;
|
|
81
|
+
});
|
|
82
|
+
if (!session) return undefined;
|
|
83
|
+
const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
|
|
84
|
+
if (raw.includes('approval')) return 'awaiting_approval';
|
|
85
|
+
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return 'generating';
|
|
86
|
+
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return 'failed';
|
|
87
|
+
if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return 'idle';
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isDirectDispatch(entry: MeshLedgerEntry): boolean {
|
|
92
|
+
if (entry.kind !== 'task_dispatched') return false;
|
|
93
|
+
const payload = entry.payload || {};
|
|
94
|
+
if (payload.source === 'direct') return true;
|
|
95
|
+
const via = readString(payload.via);
|
|
96
|
+
return Boolean(via && DIRECT_DISPATCH_VIA.has(via) && payload.source !== 'queue');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function directDispatchTaskId(entry: MeshLedgerEntry): string {
|
|
100
|
+
return readString(entry.payload?.taskId) || entry.id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedgerEntry, taskId: string): boolean {
|
|
104
|
+
const terminalTaskId = readString(terminal.payload?.taskId);
|
|
105
|
+
if (terminalTaskId && terminalTaskId === taskId) return true;
|
|
106
|
+
if (terminalTaskId && terminalTaskId !== taskId) return false;
|
|
107
|
+
if (dispatch.sessionId && terminal.sessionId === dispatch.sessionId) return true;
|
|
108
|
+
return Boolean(dispatch.nodeId && terminal.nodeId === dispatch.nodeId && !dispatch.sessionId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
112
|
+
if (entry.kind === 'task_approval_needed') return 'awaiting_approval';
|
|
113
|
+
if (entry.kind === 'task_completed') return 'idle';
|
|
114
|
+
return 'failed';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
|
|
118
|
+
const statusCounts: Record<MeshActiveWorkStatus, number> = {
|
|
119
|
+
pending: 0,
|
|
120
|
+
assigned: 0,
|
|
121
|
+
generating: 0,
|
|
122
|
+
idle: 0,
|
|
123
|
+
failed: 0,
|
|
124
|
+
awaiting_approval: 0,
|
|
125
|
+
};
|
|
126
|
+
const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
|
|
127
|
+
for (const item of activeWork) {
|
|
128
|
+
sourceCounts[item.source] += 1;
|
|
129
|
+
statusCounts[item.status] += 1;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
totalActiveCount: activeWork.length,
|
|
133
|
+
queueActiveCount: sourceCounts.queue,
|
|
134
|
+
directActiveCount: sourceCounts.direct,
|
|
135
|
+
awaitingApprovalCount: statusCounts.awaiting_approval,
|
|
136
|
+
generatingCount: statusCounts.generating,
|
|
137
|
+
failedCount: statusCounts.failed,
|
|
138
|
+
idleCount: statusCounts.idle,
|
|
139
|
+
sourceCounts,
|
|
140
|
+
statusCounts,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
|
|
145
|
+
const now = opts.now ?? Date.now();
|
|
146
|
+
const records: MeshActiveWorkRecord[] = [];
|
|
147
|
+
|
|
148
|
+
for (const task of opts.queue || []) {
|
|
149
|
+
if (task.status !== 'pending' && task.status !== 'assigned') continue;
|
|
150
|
+
const { title, summary } = summarizeMessage(task.message || '');
|
|
151
|
+
records.push({
|
|
152
|
+
taskId: task.id,
|
|
153
|
+
source: 'queue',
|
|
154
|
+
status: task.status,
|
|
155
|
+
nodeId: task.assignedNodeId || task.targetNodeId,
|
|
156
|
+
sessionId: task.assignedSessionId || task.targetSessionId,
|
|
157
|
+
taskTitle: title,
|
|
158
|
+
taskSummary: summary,
|
|
159
|
+
message: task.message,
|
|
160
|
+
taskMode: task.taskMode,
|
|
161
|
+
createdAt: task.createdAt,
|
|
162
|
+
updatedAt: task.updatedAt,
|
|
163
|
+
dispatchedAt: task.dispatchTimestamp,
|
|
164
|
+
elapsedMs: elapsedSince(task.dispatchTimestamp || task.createdAt, now),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
169
|
+
const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
|
|
170
|
+
for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
|
|
171
|
+
const taskId = directDispatchTaskId(dispatch);
|
|
172
|
+
const terminal = terminals
|
|
173
|
+
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
174
|
+
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
175
|
+
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
176
|
+
const liveStatus = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
177
|
+
const status = terminalStatus || liveStatus || 'assigned';
|
|
178
|
+
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
179
|
+
if (terminalRow && opts.includeTerminalDirect !== true) continue;
|
|
180
|
+
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
181
|
+
const { title, summary } = summarizeMessage(message);
|
|
182
|
+
records.push({
|
|
183
|
+
taskId,
|
|
184
|
+
source: 'direct',
|
|
185
|
+
status,
|
|
186
|
+
nodeId: dispatch.nodeId,
|
|
187
|
+
sessionId: dispatch.sessionId,
|
|
188
|
+
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
189
|
+
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
190
|
+
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
191
|
+
message,
|
|
192
|
+
taskMode: readString(dispatch.payload?.taskMode),
|
|
193
|
+
createdAt: dispatch.timestamp,
|
|
194
|
+
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
195
|
+
dispatchedAt: dispatch.timestamp,
|
|
196
|
+
elapsedMs: elapsedSince(dispatch.timestamp, now),
|
|
197
|
+
terminal: terminalRow,
|
|
198
|
+
terminalKind: terminal?.kind,
|
|
199
|
+
terminalAt: terminal?.timestamp,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
204
|
+
return { activeWork: records, summary: buildMeshActiveWorkSummary(records) };
|
|
205
|
+
}
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,63 +1,122 @@
|
|
|
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
|
-
|
|
40
|
-
const
|
|
61
|
+
function getPendingEventsPath(meshId: string): string {
|
|
62
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
63
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
67
|
+
try {
|
|
68
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
69
|
+
return true;
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
41
75
|
|
|
42
|
-
/** Drain and return all pending coordinator events,
|
|
43
|
-
export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
|
|
44
|
-
|
|
76
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
77
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
78
|
+
if (!meshId) return [];
|
|
79
|
+
const path = getPendingEventsPath(meshId);
|
|
80
|
+
if (!existsSync(path)) return [];
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync(path, 'utf-8');
|
|
83
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
84
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
85
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
86
|
+
});
|
|
87
|
+
} catch { return []; }
|
|
45
88
|
}
|
|
46
89
|
|
|
47
90
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
48
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
49
|
-
|
|
91
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
92
|
+
if (!meshId) return [];
|
|
93
|
+
const path = getPendingEventsPath(meshId);
|
|
94
|
+
if (!existsSync(path)) return [];
|
|
95
|
+
try {
|
|
96
|
+
const raw = readFileSync(path, 'utf-8');
|
|
97
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
98
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
99
|
+
});
|
|
100
|
+
} catch { return []; }
|
|
50
101
|
}
|
|
51
102
|
|
|
52
|
-
/** Explicitly clear all pending coordinator events. */
|
|
53
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
54
|
-
|
|
103
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
104
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
105
|
+
if (!meshId) return;
|
|
106
|
+
const path = getPendingEventsPath(meshId);
|
|
107
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
55
108
|
}
|
|
56
109
|
|
|
57
110
|
function readNonEmptyString(value: unknown): string {
|
|
58
111
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
59
112
|
}
|
|
60
113
|
|
|
114
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
115
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
116
|
+
? value as Record<string, unknown>
|
|
117
|
+
: undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
61
120
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
62
121
|
return readNonEmptyString(event.targetSessionId)
|
|
63
122
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -86,10 +145,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
86
145
|
}
|
|
87
146
|
|
|
88
147
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
148
|
+
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
149
|
+
? event.completionDiagnostic as Record<string, unknown>
|
|
150
|
+
: null;
|
|
151
|
+
const diagnosticReason = completionDiagnostic
|
|
152
|
+
? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
|
|
153
|
+
: '';
|
|
154
|
+
const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
|
|
155
|
+
? String(completionDiagnostic.finalAssistantPresent)
|
|
156
|
+
: '';
|
|
89
157
|
const parts = [
|
|
90
158
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
91
159
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
92
160
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
161
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
162
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
93
163
|
].filter(Boolean);
|
|
94
164
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
95
165
|
}
|
|
@@ -140,6 +210,62 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
140
210
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
141
211
|
}
|
|
142
212
|
|
|
213
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
214
|
+
const recentCompletionFingerprints = new Map<string, number>();
|
|
215
|
+
|
|
216
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
217
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
218
|
+
if (typeof value === 'string' && value.trim()) {
|
|
219
|
+
const numeric = Number(value);
|
|
220
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
221
|
+
const parsed = Date.parse(value);
|
|
222
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildMeshCompletionFingerprint(args: {
|
|
228
|
+
meshId: string;
|
|
229
|
+
event: string;
|
|
230
|
+
sessionId: string;
|
|
231
|
+
providerType?: string;
|
|
232
|
+
providerSessionId?: string;
|
|
233
|
+
timestamp?: number | null;
|
|
234
|
+
finalSummary?: string;
|
|
235
|
+
}): string {
|
|
236
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
237
|
+
? String(args.timestamp)
|
|
238
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
239
|
+
return [
|
|
240
|
+
args.meshId,
|
|
241
|
+
args.event,
|
|
242
|
+
args.sessionId,
|
|
243
|
+
args.providerType || '',
|
|
244
|
+
args.providerSessionId || '',
|
|
245
|
+
timestampPart,
|
|
246
|
+
].join('::');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
250
|
+
meshId: string;
|
|
251
|
+
event: string;
|
|
252
|
+
sessionId: string;
|
|
253
|
+
providerType?: string;
|
|
254
|
+
providerSessionId?: string;
|
|
255
|
+
timestamp?: number | null;
|
|
256
|
+
finalSummary?: string;
|
|
257
|
+
}): boolean {
|
|
258
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
259
|
+
if (!fingerprint) return false;
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
262
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
263
|
+
}
|
|
264
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
265
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
143
269
|
|
|
144
270
|
export function tryAssignQueueTask(
|
|
145
271
|
components: DaemonComponents,
|
|
@@ -170,7 +296,16 @@ export function tryAssignQueueTask(
|
|
|
170
296
|
message: task.message,
|
|
171
297
|
}).catch((e: any) => {
|
|
172
298
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
173
|
-
|
|
299
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
300
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
301
|
+
try {
|
|
302
|
+
appendLedgerEntry(meshId, {
|
|
303
|
+
kind: 'dispatch_failed' as any,
|
|
304
|
+
nodeId,
|
|
305
|
+
sessionId,
|
|
306
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
307
|
+
});
|
|
308
|
+
} catch { /* ledger write is best-effort */ }
|
|
174
309
|
});
|
|
175
310
|
return true;
|
|
176
311
|
}
|
|
@@ -593,6 +728,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
593
728
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
594
729
|
}
|
|
595
730
|
|
|
731
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
732
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
733
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
734
|
+
meshId: args.meshId,
|
|
735
|
+
event: args.event,
|
|
736
|
+
sessionId: eventSessionId,
|
|
737
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
738
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
739
|
+
timestamp: eventTimestamp,
|
|
740
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
741
|
+
});
|
|
742
|
+
if (duplicateCompletion) {
|
|
743
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
744
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
596
748
|
// ── Task Queue & Ledger ──
|
|
597
749
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
598
750
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -601,19 +753,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
601
753
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
602
754
|
|
|
603
755
|
if (sessionId) {
|
|
604
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
756
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
757
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
758
|
+
});
|
|
605
759
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
606
760
|
if (nodeId && providerType) {
|
|
607
|
-
//
|
|
608
|
-
|
|
761
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
762
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
763
|
+
setImmediate(() => {
|
|
609
764
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
610
|
-
}
|
|
765
|
+
});
|
|
611
766
|
}
|
|
612
767
|
}
|
|
613
768
|
} else if (args.event === 'agent:ready') {
|
|
614
769
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
615
770
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
616
771
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
772
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
773
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
774
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
617
775
|
const completedTask = sessionId
|
|
618
776
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
619
777
|
: null;
|
|
@@ -630,15 +788,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
630
788
|
nodeLabel: args.nodeLabel,
|
|
631
789
|
taskId: completedTask.id,
|
|
632
790
|
completedViaReady: true,
|
|
633
|
-
providerSessionId
|
|
634
|
-
finalSummary
|
|
791
|
+
providerSessionId,
|
|
792
|
+
finalSummary,
|
|
793
|
+
workerResult,
|
|
635
794
|
evidence: buildTaskCompletionEvidence({
|
|
636
795
|
event: 'agent:ready',
|
|
637
796
|
nodeId,
|
|
638
797
|
sessionId,
|
|
639
798
|
providerType: providerType || undefined,
|
|
640
|
-
providerSessionId
|
|
641
|
-
finalSummary
|
|
799
|
+
providerSessionId,
|
|
800
|
+
finalSummary,
|
|
801
|
+
workerResult,
|
|
642
802
|
}),
|
|
643
803
|
},
|
|
644
804
|
});
|
|
@@ -648,13 +808,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
648
808
|
}
|
|
649
809
|
|
|
650
810
|
if (sessionId && nodeId && providerType) {
|
|
651
|
-
|
|
652
|
-
|
|
811
|
+
sweepExpiredRemoteIdleSessions();
|
|
812
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
813
|
+
nodeId, sessionId, providerType,
|
|
814
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
815
|
+
});
|
|
816
|
+
setImmediate(() => {
|
|
653
817
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
654
|
-
if (assigned) {
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
}, 500);
|
|
818
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
819
|
+
});
|
|
658
820
|
}
|
|
659
821
|
} else if (args.event === 'agent:generating_started') {
|
|
660
822
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -679,14 +841,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
841
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
680
842
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
681
843
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
844
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
845
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
846
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
682
847
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
683
848
|
? buildTaskCompletionEvidence({
|
|
684
849
|
event: 'agent:generating_completed',
|
|
685
850
|
nodeId: ledgerNodeId,
|
|
686
851
|
sessionId: ledgerSessionId,
|
|
687
852
|
providerType: ledgerProviderType,
|
|
688
|
-
providerSessionId
|
|
689
|
-
finalSummary
|
|
853
|
+
providerSessionId,
|
|
854
|
+
finalSummary,
|
|
855
|
+
workerResult,
|
|
690
856
|
})
|
|
691
857
|
: undefined;
|
|
692
858
|
appendLedgerEntry(args.meshId, {
|
|
@@ -698,8 +864,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
698
864
|
event: args.event,
|
|
699
865
|
nodeLabel: args.nodeLabel,
|
|
700
866
|
taskId: completedTaskForLedger?.id || undefined,
|
|
701
|
-
providerSessionId
|
|
702
|
-
finalSummary
|
|
867
|
+
providerSessionId,
|
|
868
|
+
finalSummary,
|
|
869
|
+
workerResult,
|
|
870
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
871
|
+
? args.metadataEvent.completionDiagnostic
|
|
872
|
+
: undefined,
|
|
703
873
|
evidence: completionEvidence,
|
|
704
874
|
},
|
|
705
875
|
});
|
|
@@ -781,17 +951,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
781
951
|
|
|
782
952
|
if (coordinatorInstances.length === 0) {
|
|
783
953
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
784
|
-
if (
|
|
785
|
-
pendingMeshCoordinatorEvents.push({
|
|
954
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
786
955
|
event: args.event,
|
|
787
956
|
meshId: args.meshId,
|
|
788
957
|
nodeLabel: args.nodeLabel,
|
|
958
|
+
nodeId: args.nodeId || undefined,
|
|
959
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
789
960
|
metadataEvent: {
|
|
790
961
|
...args.metadataEvent,
|
|
791
962
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
792
963
|
},
|
|
793
964
|
queuedAt: Date.now(),
|
|
794
|
-
})
|
|
965
|
+
})) {
|
|
795
966
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
796
967
|
}
|
|
797
968
|
return { success: true, forwarded: 0 };
|
|
@@ -834,6 +1005,7 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
834
1005
|
providerType: readNonEmptyString(payload.providerType),
|
|
835
1006
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
836
1007
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1008
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
837
1009
|
intentional: payload.intentional === true,
|
|
838
1010
|
intentionalStop: payload.intentionalStop === true,
|
|
839
1011
|
operatorCleanup: payload.operatorCleanup === true,
|