@adhdev/daemon-core 0.9.77-rc.4 → 0.9.77-rc.40
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 +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -4
- package/dist/commands/mesh-coordinator.d.ts +10 -0
- package/dist/commands/router.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +389 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +386 -44
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +2 -7
- package/dist/mesh/mesh-work-queue.d.ts +35 -1
- package/dist/shared-types.d.ts +14 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +5 -0
- package/src/cli-adapters/provider-cli-adapter.ts +25 -4
- package/src/cli-adapters/provider-cli-shared.ts +14 -4
- package/src/commands/cli-manager.ts +0 -4
- package/src/commands/mesh-coordinator.ts +55 -7
- package/src/commands/router.ts +160 -3
- package/src/commands/stream-commands.ts +8 -1
- package/src/index.ts +2 -2
- package/src/mesh/mesh-events.ts +147 -20
- package/src/mesh/mesh-work-queue.ts +103 -6
- package/src/providers/cli-provider-instance.ts +2 -0
- package/src/shared-types.ts +14 -0
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -3,7 +3,20 @@ import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
|
|
|
3
3
|
import { LOG } from '../logging/logger.js';
|
|
4
4
|
import { appendLedgerEntry, getSessionRecoveryContext } from './mesh-ledger.js';
|
|
5
5
|
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
6
|
-
import { claimNextTask, updateSessionTaskStatus, enqueueTask } from './mesh-work-queue.js';
|
|
6
|
+
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus } from './mesh-work-queue.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Remote Node Idle Session Tracking
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
12
|
+
// can assign tasks to them.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
interface RemoteIdleSession {
|
|
15
|
+
nodeId: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
providerType: string;
|
|
18
|
+
}
|
|
19
|
+
const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
|
|
7
20
|
|
|
8
21
|
// ---------------------------------------------------------------------------
|
|
9
22
|
// MCP coordinator pending-event queue
|
|
@@ -33,10 +46,19 @@ function readNonEmptyString(value: unknown): string {
|
|
|
33
46
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
50
|
+
return readNonEmptyString(event.targetSessionId)
|
|
51
|
+
|| readNonEmptyString(event.sessionId)
|
|
52
|
+
|| readNonEmptyString(event.instanceId)
|
|
53
|
+
|| readNonEmptyString(fallback);
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
const MESH_COORDINATOR_EVENTS = new Set([
|
|
57
|
+
'agent:generating_started',
|
|
37
58
|
'agent:generating_completed',
|
|
38
59
|
'agent:waiting_approval',
|
|
39
60
|
'agent:stopped',
|
|
61
|
+
'agent:ready',
|
|
40
62
|
'monitor:long_generating',
|
|
41
63
|
]);
|
|
42
64
|
|
|
@@ -60,25 +82,57 @@ function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
|
60
82
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined {
|
|
86
|
+
const localMesh = getMesh(meshId);
|
|
87
|
+
if (localMesh) return localMesh;
|
|
88
|
+
return components.router?.getCachedInlineMesh(meshId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
63
92
|
export function tryAssignQueueTask(
|
|
64
|
-
components:
|
|
93
|
+
components: DaemonComponents,
|
|
65
94
|
meshId: string,
|
|
66
95
|
nodeId: string,
|
|
67
96
|
sessionId: string,
|
|
68
97
|
providerType: string
|
|
69
98
|
): boolean {
|
|
70
99
|
const task = claimNextTask(meshId, nodeId, sessionId);
|
|
71
|
-
if (!task)
|
|
100
|
+
if (!task) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
72
103
|
|
|
73
104
|
LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
|
|
105
|
+
|
|
106
|
+
// Check if the node is remote
|
|
107
|
+
const mesh = getMeshWithCache(components, meshId);
|
|
108
|
+
const node = mesh?.nodes.find((n: any) => n.id === nodeId);
|
|
74
109
|
|
|
110
|
+
// If the node is explicitly remote and we have a dispatch mechanism, route via P2P
|
|
111
|
+
if (node?.daemonId && components.dispatchMeshCommand) {
|
|
112
|
+
const isLocalNode = components.cliManager.adapters.has(sessionId);
|
|
113
|
+
if (!isLocalNode) {
|
|
114
|
+
components.dispatchMeshCommand(node.daemonId, 'agent_command', {
|
|
115
|
+
targetSessionId: sessionId,
|
|
116
|
+
cliType: providerType,
|
|
117
|
+
action: 'send_chat',
|
|
118
|
+
message: task.message,
|
|
119
|
+
}).catch((e: any) => {
|
|
120
|
+
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
121
|
+
updateTaskStatus(meshId, task.id, 'failed');
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Local routing
|
|
75
128
|
components.cliManager.handleCliCommand('agent_command', {
|
|
76
129
|
targetSessionId: sessionId,
|
|
77
130
|
cliType: providerType,
|
|
78
131
|
action: 'send_chat',
|
|
79
|
-
|
|
132
|
+
message: task.message,
|
|
80
133
|
}).catch((e: any) => {
|
|
81
|
-
LOG.error('MeshQueue', `Failed to dispatch task to node ${nodeId}: ${e?.message}`);
|
|
134
|
+
LOG.error('MeshQueue', `Failed to dispatch task locally to node ${nodeId}: ${e?.message}`);
|
|
135
|
+
updateTaskStatus(meshId, task.id, 'failed');
|
|
82
136
|
});
|
|
83
137
|
|
|
84
138
|
return true;
|
|
@@ -88,8 +142,8 @@ export function tryAssignQueueTask(
|
|
|
88
142
|
* Triggers a queue check for all nodes in the mesh.
|
|
89
143
|
* Called when a new task is enqueued, in case nodes are already idle.
|
|
90
144
|
*/
|
|
91
|
-
export function triggerMeshQueue(components:
|
|
92
|
-
const mesh =
|
|
145
|
+
export function triggerMeshQueue(components: DaemonComponents, meshId: string) {
|
|
146
|
+
const mesh = getMeshWithCache(components, meshId);
|
|
93
147
|
if (!mesh) return;
|
|
94
148
|
|
|
95
149
|
// Find all CLI instances that belong to this mesh and are idle
|
|
@@ -99,13 +153,17 @@ export function triggerMeshQueue(components: { instanceManager: any; cliManager:
|
|
|
99
153
|
const settings = state.settings as Record<string, unknown> || {};
|
|
100
154
|
|
|
101
155
|
const instMeshId = readNonEmptyString(settings.meshNodeFor);
|
|
102
|
-
if (instMeshId !== meshId
|
|
156
|
+
if (instMeshId !== meshId) continue;
|
|
103
157
|
|
|
104
158
|
const nodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
|
|
105
159
|
if (!nodeId) continue;
|
|
106
160
|
|
|
107
|
-
//
|
|
108
|
-
|
|
161
|
+
// Only genuinely idle live sessions can pull work. Restored/stopped
|
|
162
|
+
// records are kept for transcript/recovery visibility, but assigning
|
|
163
|
+
// queue items to them strands tasks in assigned/pending without chat.
|
|
164
|
+
const status = readNonEmptyString(state.status).toLowerCase();
|
|
165
|
+
if (['stopped', 'failed', 'terminated', 'exited', 'closed'].includes(status)) continue;
|
|
166
|
+
if (status !== 'idle' && state.activeChat?.status !== 'waiting_input') continue;
|
|
109
167
|
|
|
110
168
|
const sessionId = state.instanceId;
|
|
111
169
|
const providerType = state.type || readNonEmptyString(settings.providerType);
|
|
@@ -115,6 +173,18 @@ export function triggerMeshQueue(components: { instanceManager: any; cliManager:
|
|
|
115
173
|
tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
|
|
116
174
|
}
|
|
117
175
|
}
|
|
176
|
+
|
|
177
|
+
// Also check known idle remote sessions
|
|
178
|
+
for (const [key, idle] of remoteIdleSessions.entries()) {
|
|
179
|
+
// Find if this node is in the same mesh
|
|
180
|
+
const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
|
|
181
|
+
if (node) {
|
|
182
|
+
const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
|
|
183
|
+
if (assigned) {
|
|
184
|
+
remoteIdleSessions.delete(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
118
188
|
}
|
|
119
189
|
|
|
120
190
|
function buildMeshSystemMessage(args: {
|
|
@@ -164,18 +234,21 @@ function buildMeshSystemMessage(args: {
|
|
|
164
234
|
function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
165
235
|
meshId: string;
|
|
166
236
|
sourceInstanceId?: string;
|
|
237
|
+
nodeId?: string;
|
|
167
238
|
nodeLabel: string;
|
|
168
239
|
event: string;
|
|
169
240
|
metadataEvent: Record<string, unknown>;
|
|
170
241
|
}) {
|
|
171
242
|
// ── Task Queue & Ledger ──
|
|
243
|
+
let completedTaskForLedger: { id?: string } | null = null;
|
|
172
244
|
if (args.event === 'agent:generating_completed') {
|
|
173
|
-
const sessionId =
|
|
174
|
-
const nodeId = readNonEmptyString(args.
|
|
245
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
246
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
175
247
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
176
248
|
|
|
177
249
|
if (sessionId) {
|
|
178
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'completed');
|
|
250
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
|
|
251
|
+
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
179
252
|
if (nodeId && providerType) {
|
|
180
253
|
// Short delay to allow completion event to propagate before pulling next
|
|
181
254
|
setTimeout(() => {
|
|
@@ -183,8 +256,56 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
183
256
|
}, 500);
|
|
184
257
|
}
|
|
185
258
|
}
|
|
259
|
+
} else if (args.event === 'agent:ready') {
|
|
260
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
261
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
262
|
+
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
263
|
+
const completedTask = sessionId
|
|
264
|
+
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
265
|
+
: null;
|
|
266
|
+
if (completedTask) {
|
|
267
|
+
completedTaskForLedger = { id: completedTask.id };
|
|
268
|
+
try {
|
|
269
|
+
appendLedgerEntry(args.meshId, {
|
|
270
|
+
kind: 'task_completed',
|
|
271
|
+
nodeId: nodeId || undefined,
|
|
272
|
+
sessionId,
|
|
273
|
+
providerType: providerType || undefined,
|
|
274
|
+
payload: {
|
|
275
|
+
event: args.event,
|
|
276
|
+
nodeLabel: args.nodeLabel,
|
|
277
|
+
taskId: completedTask.id,
|
|
278
|
+
completedViaReady: true,
|
|
279
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
280
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
} catch (e: any) {
|
|
284
|
+
LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (sessionId && nodeId && providerType) {
|
|
289
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
292
|
+
if (assigned) {
|
|
293
|
+
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
294
|
+
}
|
|
295
|
+
}, 500);
|
|
296
|
+
}
|
|
297
|
+
} else if (args.event === 'agent:generating_started') {
|
|
298
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
299
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
300
|
+
if (sessionId && nodeId) {
|
|
301
|
+
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
302
|
+
}
|
|
186
303
|
} else if (args.event === 'agent:stopped') {
|
|
187
|
-
const sessionId =
|
|
304
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
305
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
306
|
+
if (sessionId && nodeId) {
|
|
307
|
+
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
308
|
+
}
|
|
188
309
|
if (sessionId) {
|
|
189
310
|
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
190
311
|
}
|
|
@@ -195,13 +316,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
195
316
|
try {
|
|
196
317
|
appendLedgerEntry(args.meshId, {
|
|
197
318
|
kind: ledgerKind,
|
|
198
|
-
nodeId: readNonEmptyString(args.
|
|
199
|
-
sessionId:
|
|
319
|
+
nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
|
|
320
|
+
sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
|
|
200
321
|
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
201
322
|
payload: {
|
|
202
323
|
event: args.event,
|
|
203
324
|
nodeLabel: args.nodeLabel,
|
|
325
|
+
taskId: completedTaskForLedger?.id || undefined,
|
|
204
326
|
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
327
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
205
328
|
},
|
|
206
329
|
});
|
|
207
330
|
} catch (e: any) {
|
|
@@ -218,8 +341,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
218
341
|
const maxRetries = mesh?.policy?.maxTaskRetries ?? 1;
|
|
219
342
|
|
|
220
343
|
recoveryContext = getSessionRecoveryContext(args.meshId, {
|
|
221
|
-
sessionId:
|
|
222
|
-
nodeId: readNonEmptyString(args.
|
|
344
|
+
sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
|
|
345
|
+
nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
|
|
223
346
|
maxRetries,
|
|
224
347
|
});
|
|
225
348
|
recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
|
|
@@ -327,12 +450,14 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
327
450
|
const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
|
|
328
451
|
return injectMeshSystemMessage(components, {
|
|
329
452
|
meshId,
|
|
453
|
+
nodeId,
|
|
330
454
|
nodeLabel,
|
|
331
455
|
event: eventName,
|
|
332
456
|
metadataEvent: {
|
|
333
|
-
targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
|
|
457
|
+
targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId) || readNonEmptyString(payload.instanceId),
|
|
334
458
|
providerType: readNonEmptyString(payload.providerType),
|
|
335
459
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
460
|
+
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
336
461
|
},
|
|
337
462
|
});
|
|
338
463
|
}
|
|
@@ -367,13 +492,14 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
367
492
|
const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
|
|
368
493
|
if (!isMeshDelegate) return;
|
|
369
494
|
|
|
370
|
-
const mesh = meshIdFromRuntime ?
|
|
495
|
+
const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getMeshByRepo(workspace);
|
|
371
496
|
const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
|
|
372
497
|
if (!meshId) return;
|
|
373
498
|
|
|
374
499
|
// Determine node label. Inline/cloud meshes may be unavailable here, so preserve runtime node id.
|
|
375
500
|
const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
|
|
376
501
|
const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
|
|
502
|
+
const resolvedNodeId = targetNode?.id || runtimeNodeId;
|
|
377
503
|
const nodeLabel = targetNode
|
|
378
504
|
? `Node '${targetNode.id}'`
|
|
379
505
|
: runtimeNodeId
|
|
@@ -383,6 +509,7 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
383
509
|
injectMeshSystemMessage(components, {
|
|
384
510
|
meshId,
|
|
385
511
|
sourceInstanceId: instanceId,
|
|
512
|
+
nodeId: resolvedNodeId,
|
|
386
513
|
nodeLabel,
|
|
387
514
|
event: event.event,
|
|
388
515
|
metadataEvent: event,
|
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
5
5
|
|
|
6
|
-
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
|
|
6
|
+
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
7
7
|
|
|
8
8
|
export interface MeshWorkQueueEntry {
|
|
9
9
|
id: string;
|
|
@@ -12,10 +12,19 @@ export interface MeshWorkQueueEntry {
|
|
|
12
12
|
status: MeshTaskStatus;
|
|
13
13
|
/** If specified, only this node can claim the task (used by legacy mesh_send_task) */
|
|
14
14
|
targetNodeId?: string;
|
|
15
|
+
/** If specified, only this runtime session can claim the task */
|
|
16
|
+
targetSessionId?: string;
|
|
15
17
|
/** The node that actually claimed and is executing the task */
|
|
16
18
|
assignedNodeId?: string;
|
|
17
19
|
/** The session currently executing the task */
|
|
18
20
|
assignedSessionId?: string;
|
|
21
|
+
/** Human/operator reason for terminal cancellation. */
|
|
22
|
+
cancelReason?: string;
|
|
23
|
+
cancelledAt?: string;
|
|
24
|
+
/** Human/operator reason for manually requeueing a task. */
|
|
25
|
+
requeueReason?: string;
|
|
26
|
+
requeuedAt?: string;
|
|
27
|
+
requeueCount?: number;
|
|
19
28
|
createdAt: string;
|
|
20
29
|
updatedAt: string;
|
|
21
30
|
}
|
|
@@ -47,7 +56,7 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
|
47
56
|
export function enqueueTask(
|
|
48
57
|
meshId: string,
|
|
49
58
|
message: string,
|
|
50
|
-
opts?: { targetNodeId?: string }
|
|
59
|
+
opts?: { targetNodeId?: string; targetSessionId?: string }
|
|
51
60
|
): MeshWorkQueueEntry {
|
|
52
61
|
const queue = readQueue(meshId);
|
|
53
62
|
const entry: MeshWorkQueueEntry = {
|
|
@@ -56,6 +65,7 @@ export function enqueueTask(
|
|
|
56
65
|
message,
|
|
57
66
|
status: 'pending',
|
|
58
67
|
targetNodeId: opts?.targetNodeId,
|
|
68
|
+
targetSessionId: opts?.targetSessionId,
|
|
59
69
|
createdAt: new Date().toISOString(),
|
|
60
70
|
updatedAt: new Date().toISOString(),
|
|
61
71
|
};
|
|
@@ -81,13 +91,25 @@ export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }):
|
|
|
81
91
|
*/
|
|
82
92
|
export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
83
93
|
const queue = readQueue(meshId);
|
|
94
|
+
|
|
95
|
+
// A worker must finish or fail its current queued assignment before it can
|
|
96
|
+
// claim another one. maxParallelTasks limits total mesh concurrency; it is
|
|
97
|
+
// not permission for one node/session to accumulate multiple assigned items.
|
|
98
|
+
const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
|
|
99
|
+
q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
|
|
100
|
+
));
|
|
101
|
+
if (hasActiveAssignment) return null;
|
|
84
102
|
|
|
85
103
|
// Find highest priority task:
|
|
86
|
-
// 1. Pending tasks explicitly targeted at this
|
|
87
|
-
// 2. Pending tasks
|
|
88
|
-
|
|
104
|
+
// 1. Pending tasks explicitly targeted at this runtime session
|
|
105
|
+
// 2. Pending tasks explicitly targeted at this node (but not another session)
|
|
106
|
+
// 3. Pending tasks with no target node/session
|
|
107
|
+
let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
|
|
108
|
+
if (targetIdx === -1) {
|
|
109
|
+
targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
|
|
110
|
+
}
|
|
89
111
|
if (targetIdx === -1) {
|
|
90
|
-
targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId);
|
|
112
|
+
targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
if (targetIdx === -1) return null;
|
|
@@ -121,6 +143,65 @@ export function updateTaskStatus(
|
|
|
121
143
|
return queue[idx];
|
|
122
144
|
}
|
|
123
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Mark a queue task as manually cancelled without deleting audit history.
|
|
148
|
+
*/
|
|
149
|
+
export function cancelTask(
|
|
150
|
+
meshId: string,
|
|
151
|
+
taskId: string,
|
|
152
|
+
opts?: { reason?: string },
|
|
153
|
+
): MeshWorkQueueEntry | null {
|
|
154
|
+
const queue = readQueue(meshId);
|
|
155
|
+
const idx = queue.findIndex(q => q.id === taskId);
|
|
156
|
+
if (idx === -1) return null;
|
|
157
|
+
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
queue[idx].status = 'cancelled';
|
|
160
|
+
queue[idx].updatedAt = now;
|
|
161
|
+
queue[idx].cancelledAt = now;
|
|
162
|
+
if (opts?.reason) queue[idx].cancelReason = opts.reason;
|
|
163
|
+
writeQueue(meshId, queue);
|
|
164
|
+
return queue[idx];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Return a queue task to pending for retry. By default, dead session targeting
|
|
169
|
+
* and assigned ownership are cleared so stale assignments do not strand again.
|
|
170
|
+
*/
|
|
171
|
+
export function requeueTask(
|
|
172
|
+
meshId: string,
|
|
173
|
+
taskId: string,
|
|
174
|
+
opts?: {
|
|
175
|
+
reason?: string;
|
|
176
|
+
targetNodeId?: string;
|
|
177
|
+
targetSessionId?: string;
|
|
178
|
+
clearTargetNode?: boolean;
|
|
179
|
+
clearTargetSession?: boolean;
|
|
180
|
+
},
|
|
181
|
+
): MeshWorkQueueEntry | null {
|
|
182
|
+
const queue = readQueue(meshId);
|
|
183
|
+
const idx = queue.findIndex(q => q.id === taskId);
|
|
184
|
+
if (idx === -1) return null;
|
|
185
|
+
|
|
186
|
+
const entry = queue[idx];
|
|
187
|
+
const now = new Date().toISOString();
|
|
188
|
+
entry.status = 'pending';
|
|
189
|
+
delete entry.assignedNodeId;
|
|
190
|
+
delete entry.assignedSessionId;
|
|
191
|
+
delete entry.cancelledAt;
|
|
192
|
+
delete entry.cancelReason;
|
|
193
|
+
if (opts?.clearTargetNode) delete entry.targetNodeId;
|
|
194
|
+
if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
|
|
195
|
+
if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
|
|
196
|
+
if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
|
|
197
|
+
entry.updatedAt = now;
|
|
198
|
+
entry.requeuedAt = now;
|
|
199
|
+
entry.requeueCount = (entry.requeueCount || 0) + 1;
|
|
200
|
+
if (opts?.reason) entry.requeueReason = opts.reason;
|
|
201
|
+
writeQueue(meshId, queue);
|
|
202
|
+
return entry;
|
|
203
|
+
}
|
|
204
|
+
|
|
124
205
|
/**
|
|
125
206
|
* Update the status of the task currently assigned to a specific session.
|
|
126
207
|
*/
|
|
@@ -148,6 +229,13 @@ export interface MeshWorkQueueStats {
|
|
|
148
229
|
assigned: number;
|
|
149
230
|
completed: number;
|
|
150
231
|
failed: number;
|
|
232
|
+
cancelled: number;
|
|
233
|
+
activeAssignments: Array<{
|
|
234
|
+
id: string;
|
|
235
|
+
nodeId?: string;
|
|
236
|
+
sessionId?: string;
|
|
237
|
+
message: string;
|
|
238
|
+
}>;
|
|
151
239
|
}
|
|
152
240
|
|
|
153
241
|
/**
|
|
@@ -160,5 +248,14 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
|
160
248
|
assigned: queue.filter(q => q.status === 'assigned').length,
|
|
161
249
|
completed: queue.filter(q => q.status === 'completed').length,
|
|
162
250
|
failed: queue.filter(q => q.status === 'failed').length,
|
|
251
|
+
cancelled: queue.filter(q => q.status === 'cancelled').length,
|
|
252
|
+
activeAssignments: queue
|
|
253
|
+
.filter(q => q.status === 'assigned')
|
|
254
|
+
.map(q => ({
|
|
255
|
+
id: q.id,
|
|
256
|
+
nodeId: q.assignedNodeId,
|
|
257
|
+
sessionId: q.assignedSessionId,
|
|
258
|
+
message: q.message,
|
|
259
|
+
})),
|
|
163
260
|
};
|
|
164
261
|
}
|
|
@@ -833,6 +833,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
833
833
|
this.completedDebounceTimer = null;
|
|
834
834
|
}, 3000);
|
|
835
835
|
}
|
|
836
|
+
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
837
|
+
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
836
838
|
} else if (newStatus === 'stopped') {
|
|
837
839
|
// Cancel any pending debounce
|
|
838
840
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
package/src/shared-types.ts
CHANGED
|
@@ -394,6 +394,13 @@ export interface SessionEntry {
|
|
|
394
394
|
assigned: number;
|
|
395
395
|
completed: number;
|
|
396
396
|
failed: number;
|
|
397
|
+
cancelled?: number;
|
|
398
|
+
activeAssignments?: Array<{
|
|
399
|
+
id: string;
|
|
400
|
+
nodeId?: string;
|
|
401
|
+
sessionId?: string;
|
|
402
|
+
message: string;
|
|
403
|
+
}>;
|
|
397
404
|
};
|
|
398
405
|
}
|
|
399
406
|
|
|
@@ -439,6 +446,13 @@ export interface CompactSessionEntry {
|
|
|
439
446
|
assigned: number;
|
|
440
447
|
completed: number;
|
|
441
448
|
failed: number;
|
|
449
|
+
cancelled?: number;
|
|
450
|
+
activeAssignments?: Array<{
|
|
451
|
+
id: string;
|
|
452
|
+
nodeId?: string;
|
|
453
|
+
sessionId?: string;
|
|
454
|
+
message: string;
|
|
455
|
+
}>;
|
|
442
456
|
};
|
|
443
457
|
}
|
|
444
458
|
|