@adhdev/daemon-core 0.9.82-rc.23 → 0.9.82-rc.25
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/commands/router.d.ts +2 -0
- package/dist/index.js +345 -166
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +345 -166
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +14 -5
- package/dist/mesh/mesh-work-queue.d.ts +3 -1
- package/package.json +1 -1
- package/src/commands/router.ts +48 -22
- package/src/mesh/mesh-events.ts +157 -30
- package/src/mesh/mesh-work-queue.ts +135 -119
|
@@ -9,12 +9,12 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
9
9
|
queuedAt: number;
|
|
10
10
|
}
|
|
11
11
|
export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean;
|
|
12
|
-
/** Drain and return all pending coordinator events,
|
|
13
|
-
export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
|
|
12
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
13
|
+
export declare function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[];
|
|
14
14
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
15
|
-
export declare function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[];
|
|
16
|
-
/** Explicitly clear all pending coordinator events. */
|
|
17
|
-
export declare function clearPendingMeshCoordinatorEvents(): void;
|
|
15
|
+
export declare function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[];
|
|
16
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
17
|
+
export declare function clearPendingMeshCoordinatorEvents(meshId?: string): void;
|
|
18
18
|
export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
|
|
19
19
|
/**
|
|
20
20
|
* Triggers a queue check for all nodes in the mesh.
|
|
@@ -26,12 +26,21 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
26
26
|
forwarded: number;
|
|
27
27
|
suppressed: boolean;
|
|
28
28
|
intentionalCleanupStop: boolean;
|
|
29
|
+
duplicateCompletion?: undefined;
|
|
30
|
+
error?: undefined;
|
|
31
|
+
} | {
|
|
32
|
+
success: boolean;
|
|
33
|
+
forwarded: number;
|
|
34
|
+
suppressed: boolean;
|
|
35
|
+
duplicateCompletion: boolean;
|
|
36
|
+
intentionalCleanupStop?: undefined;
|
|
29
37
|
error?: undefined;
|
|
30
38
|
} | {
|
|
31
39
|
success: boolean;
|
|
32
40
|
forwarded: number;
|
|
33
41
|
suppressed?: undefined;
|
|
34
42
|
intentionalCleanupStop?: undefined;
|
|
43
|
+
duplicateCompletion?: undefined;
|
|
35
44
|
error?: undefined;
|
|
36
45
|
} | {
|
|
37
46
|
success: boolean;
|
|
@@ -80,7 +80,9 @@ export declare function requeueTask(meshId: string, taskId: string, opts?: {
|
|
|
80
80
|
/**
|
|
81
81
|
* Update the status of the task currently assigned to a specific session.
|
|
82
82
|
*/
|
|
83
|
-
export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus
|
|
83
|
+
export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus, opts?: {
|
|
84
|
+
occurredAt?: string;
|
|
85
|
+
}): MeshWorkQueueEntry | null;
|
|
84
86
|
export interface MeshWorkQueueStats {
|
|
85
87
|
total: number;
|
|
86
88
|
active: number;
|
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -964,6 +964,8 @@ export interface CommandRouterDeps {
|
|
|
964
964
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
965
965
|
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
966
966
|
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
967
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
968
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
967
969
|
}
|
|
968
970
|
|
|
969
971
|
export interface CommandRouterResult {
|
|
@@ -1688,7 +1690,8 @@ export class DaemonCommandRouter {
|
|
|
1688
1690
|
}
|
|
1689
1691
|
|
|
1690
1692
|
case 'get_pending_mesh_events': {
|
|
1691
|
-
const
|
|
1693
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1694
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1692
1695
|
return { success: true, events };
|
|
1693
1696
|
}
|
|
1694
1697
|
|
|
@@ -3344,29 +3347,52 @@ export class DaemonCommandRouter {
|
|
|
3344
3347
|
}
|
|
3345
3348
|
if (workspace) {
|
|
3346
3349
|
if (!fs.existsSync(workspace)) {
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3350
|
+
// Workspace not local — attempt a P2P git probe for remote nodes.
|
|
3351
|
+
let remoteProbeApplied = false;
|
|
3352
|
+
if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand) {
|
|
3353
|
+
try {
|
|
3354
|
+
const remoteResult = await Promise.race([
|
|
3355
|
+
this.deps.dispatchMeshCommand(daemonId, 'git_status', { workspace }),
|
|
3356
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000)),
|
|
3357
|
+
]) as any;
|
|
3358
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
3359
|
+
if (remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean') {
|
|
3360
|
+
status.git = remoteGit;
|
|
3361
|
+
status.health = remoteGit.isGitRepo
|
|
3362
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
3363
|
+
: 'degraded';
|
|
3364
|
+
remoteProbeApplied = true;
|
|
3365
|
+
}
|
|
3366
|
+
} catch {
|
|
3367
|
+
// Probe timed out or P2P unavailable — fall back to cached status
|
|
3368
|
+
}
|
|
3356
3369
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3370
|
+
if (!remoteProbeApplied) {
|
|
3371
|
+
if (applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3372
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3373
|
+
nodeStatuses.push(status);
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
3377
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3378
|
+
nodeStatuses.push(status);
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3366
3381
|
}
|
|
3367
|
-
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3382
|
+
} else {
|
|
3383
|
+
try {
|
|
3384
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
3385
|
+
status.git = gitStatus;
|
|
3386
|
+
if (gitStatus.isGitRepo) {
|
|
3387
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
3388
|
+
} else {
|
|
3389
|
+
status.health = 'degraded';
|
|
3390
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
3391
|
+
}
|
|
3392
|
+
} catch {
|
|
3393
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3394
|
+
status.health = 'degraded';
|
|
3395
|
+
}
|
|
3370
3396
|
}
|
|
3371
3397
|
}
|
|
3372
3398
|
} else {
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,31 +1,47 @@
|
|
|
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 sweepExpiredRemoteIdleSessions(): void {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [key, session] of remoteIdleSessions) {
|
|
31
|
+
if (session.expiresAt <= now) remoteIdleSessions.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
// ---------------------------------------------------------------------------
|
|
24
|
-
// MCP coordinator pending-event queue
|
|
36
|
+
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
25
37
|
// ---------------------------------------------------------------------------
|
|
26
38
|
// 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
|
-
//
|
|
39
|
+
// the coordinator is Claude Code running via MCP), we persist the event to a
|
|
40
|
+
// per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
|
|
41
|
+
// is removed; the file is drained atomically on each get_pending_mesh_events
|
|
42
|
+
// call and limited to 100 KB to prevent runaway growth.
|
|
43
|
+
//
|
|
44
|
+
// File: <ledgerDir>/<meshId>.pending-events.jsonl
|
|
29
45
|
// ---------------------------------------------------------------------------
|
|
30
46
|
|
|
31
47
|
export interface PendingMeshCoordinatorEvent {
|
|
@@ -38,30 +54,53 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
38
54
|
queuedAt: number;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
const
|
|
57
|
+
function getPendingEventsPath(meshId: string): string {
|
|
58
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
59
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
45
|
-
|
|
63
|
+
try {
|
|
64
|
+
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
46
68
|
return false;
|
|
47
69
|
}
|
|
48
|
-
pendingMeshCoordinatorEvents.push(event);
|
|
49
|
-
return true;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
/** Drain and return all pending coordinator events,
|
|
53
|
-
export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
|
|
54
|
-
|
|
72
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
73
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
74
|
+
if (!meshId) return [];
|
|
75
|
+
const path = getPendingEventsPath(meshId);
|
|
76
|
+
if (!existsSync(path)) return [];
|
|
77
|
+
try {
|
|
78
|
+
const raw = readFileSync(path, 'utf-8');
|
|
79
|
+
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
80
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
81
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
82
|
+
});
|
|
83
|
+
} catch { return []; }
|
|
55
84
|
}
|
|
56
85
|
|
|
57
86
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
58
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
59
|
-
|
|
87
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
88
|
+
if (!meshId) return [];
|
|
89
|
+
const path = getPendingEventsPath(meshId);
|
|
90
|
+
if (!existsSync(path)) return [];
|
|
91
|
+
try {
|
|
92
|
+
const raw = readFileSync(path, 'utf-8');
|
|
93
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
94
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
95
|
+
});
|
|
96
|
+
} catch { return []; }
|
|
60
97
|
}
|
|
61
98
|
|
|
62
|
-
/** Explicitly clear all pending coordinator events. */
|
|
63
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
64
|
-
|
|
99
|
+
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
100
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string): void {
|
|
101
|
+
if (!meshId) return;
|
|
102
|
+
const path = getPendingEventsPath(meshId);
|
|
103
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
function readNonEmptyString(value: unknown): string {
|
|
@@ -150,6 +189,62 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
150
189
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
151
190
|
}
|
|
152
191
|
|
|
192
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
193
|
+
const recentCompletionFingerprints = new Map<string, number>();
|
|
194
|
+
|
|
195
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
196
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
197
|
+
if (typeof value === 'string' && value.trim()) {
|
|
198
|
+
const numeric = Number(value);
|
|
199
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
200
|
+
const parsed = Date.parse(value);
|
|
201
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildMeshCompletionFingerprint(args: {
|
|
207
|
+
meshId: string;
|
|
208
|
+
event: string;
|
|
209
|
+
sessionId: string;
|
|
210
|
+
providerType?: string;
|
|
211
|
+
providerSessionId?: string;
|
|
212
|
+
timestamp?: number | null;
|
|
213
|
+
finalSummary?: string;
|
|
214
|
+
}): string {
|
|
215
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
216
|
+
? String(args.timestamp)
|
|
217
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
218
|
+
return [
|
|
219
|
+
args.meshId,
|
|
220
|
+
args.event,
|
|
221
|
+
args.sessionId,
|
|
222
|
+
args.providerType || '',
|
|
223
|
+
args.providerSessionId || '',
|
|
224
|
+
timestampPart,
|
|
225
|
+
].join('::');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
229
|
+
meshId: string;
|
|
230
|
+
event: string;
|
|
231
|
+
sessionId: string;
|
|
232
|
+
providerType?: string;
|
|
233
|
+
providerSessionId?: string;
|
|
234
|
+
timestamp?: number | null;
|
|
235
|
+
finalSummary?: string;
|
|
236
|
+
}): boolean {
|
|
237
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
238
|
+
if (!fingerprint) return false;
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
for (const [key, seenAt] of recentCompletionFingerprints.entries()) {
|
|
241
|
+
if (now - seenAt > RECENT_COMPLETION_FINGERPRINT_TTL_MS) recentCompletionFingerprints.delete(key);
|
|
242
|
+
}
|
|
243
|
+
if (recentCompletionFingerprints.has(fingerprint)) return true;
|
|
244
|
+
recentCompletionFingerprints.set(fingerprint, now);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
153
248
|
|
|
154
249
|
export function tryAssignQueueTask(
|
|
155
250
|
components: DaemonComponents,
|
|
@@ -180,7 +275,16 @@ export function tryAssignQueueTask(
|
|
|
180
275
|
message: task.message,
|
|
181
276
|
}).catch((e: any) => {
|
|
182
277
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
183
|
-
|
|
278
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
279
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
280
|
+
try {
|
|
281
|
+
appendLedgerEntry(meshId, {
|
|
282
|
+
kind: 'dispatch_failed' as any,
|
|
283
|
+
nodeId,
|
|
284
|
+
sessionId,
|
|
285
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
286
|
+
});
|
|
287
|
+
} catch { /* ledger write is best-effort */ }
|
|
184
288
|
});
|
|
185
289
|
return true;
|
|
186
290
|
}
|
|
@@ -603,6 +707,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
603
707
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
604
708
|
}
|
|
605
709
|
|
|
710
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
711
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
712
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
713
|
+
meshId: args.meshId,
|
|
714
|
+
event: args.event,
|
|
715
|
+
sessionId: eventSessionId,
|
|
716
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
717
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
718
|
+
timestamp: eventTimestamp,
|
|
719
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
720
|
+
});
|
|
721
|
+
if (duplicateCompletion) {
|
|
722
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
723
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
606
727
|
// ── Task Queue & Ledger ──
|
|
607
728
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
608
729
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -611,13 +732,16 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
611
732
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
612
733
|
|
|
613
734
|
if (sessionId) {
|
|
614
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
735
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
736
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
737
|
+
});
|
|
615
738
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
616
739
|
if (nodeId && providerType) {
|
|
617
|
-
//
|
|
618
|
-
|
|
740
|
+
// Queue state is already updated above; setImmediate avoids the
|
|
741
|
+
// 500 ms artificial delay while still deferring past this call frame.
|
|
742
|
+
setImmediate(() => {
|
|
619
743
|
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
620
|
-
}
|
|
744
|
+
});
|
|
621
745
|
}
|
|
622
746
|
}
|
|
623
747
|
} else if (args.event === 'agent:ready') {
|
|
@@ -658,13 +782,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
658
782
|
}
|
|
659
783
|
|
|
660
784
|
if (sessionId && nodeId && providerType) {
|
|
661
|
-
|
|
662
|
-
|
|
785
|
+
sweepExpiredRemoteIdleSessions();
|
|
786
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
787
|
+
nodeId, sessionId, providerType,
|
|
788
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
789
|
+
});
|
|
790
|
+
setImmediate(() => {
|
|
663
791
|
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
664
|
-
if (assigned) {
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
}, 500);
|
|
792
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
793
|
+
});
|
|
668
794
|
}
|
|
669
795
|
} else if (args.event === 'agent:generating_started') {
|
|
670
796
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -845,6 +971,7 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
845
971
|
providerType: readNonEmptyString(payload.providerType),
|
|
846
972
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
847
973
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
974
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
848
975
|
intentional: payload.intentional === true,
|
|
849
976
|
intentionalStop: payload.intentionalStop === true,
|
|
850
977
|
operatorCleanup: payload.operatorCleanup === true,
|