@adhdev/daemon-core 0.9.82-rc.132 → 0.9.82-rc.134
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/index.js +76 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +76 -17
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +15 -4
- package/dist/mesh/mesh-events.d.ts +17 -0
- package/package.json +1 -1
- package/src/mesh/coordinator-prompt.ts +1 -1
- package/src/mesh/mesh-active-work.ts +45 -6
- package/src/mesh/mesh-events.ts +67 -16
- package/src/providers/cli-provider-instance.ts +30 -3
|
@@ -21,6 +21,12 @@ export interface MeshActiveWorkRecord {
|
|
|
21
21
|
terminalKind?: string;
|
|
22
22
|
terminalAt?: string;
|
|
23
23
|
staleReason?: string;
|
|
24
|
+
/**
|
|
25
|
+
* When true, the session targeted by this direct dispatch still exists in the live mesh
|
|
26
|
+
* but did not transition to generating — the dispatch was not acknowledged by the provider.
|
|
27
|
+
* Distinct from historical/orphaned stale entries where the node/session is gone.
|
|
28
|
+
*/
|
|
29
|
+
staleDispatchUnacknowledged?: boolean;
|
|
24
30
|
}
|
|
25
31
|
export interface MeshActiveWorkSummary {
|
|
26
32
|
totalActiveCount: number;
|
|
@@ -34,10 +40,15 @@ export interface MeshActiveWorkSummary {
|
|
|
34
40
|
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
35
41
|
staleDirectCount: number;
|
|
36
42
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
43
|
+
* Count of stale direct entries where the target session still exists in the live mesh
|
|
44
|
+
* but never transitioned to generating — these are fresh dispatch failures, not historical orphans.
|
|
45
|
+
* Requires immediate recovery: launch a fresh session and retry the task.
|
|
46
|
+
*/
|
|
47
|
+
staleDirectUnacknowledgedCount?: number;
|
|
48
|
+
/**
|
|
49
|
+
* When staleDirectCount > 0, this note clarifies what kind of stale records exist.
|
|
50
|
+
* Fresh unacknowledged dispatches carry an actionable recovery note; orphaned historical
|
|
51
|
+
* entries carry the "historical evidence only" note. Both may coexist.
|
|
41
52
|
*/
|
|
42
53
|
staleDirectNote?: string;
|
|
43
54
|
}
|
|
@@ -32,6 +32,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
32
32
|
duplicateRefineTerminalEvent?: undefined;
|
|
33
33
|
duplicateCompletion?: undefined;
|
|
34
34
|
error?: undefined;
|
|
35
|
+
bufferedForGeneratingCoordinator?: undefined;
|
|
35
36
|
} | {
|
|
36
37
|
success: boolean;
|
|
37
38
|
forwarded: number;
|
|
@@ -42,6 +43,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
42
43
|
duplicateRefineTerminalEvent?: undefined;
|
|
43
44
|
duplicateCompletion?: undefined;
|
|
44
45
|
error?: undefined;
|
|
46
|
+
bufferedForGeneratingCoordinator?: undefined;
|
|
45
47
|
} | {
|
|
46
48
|
success: boolean;
|
|
47
49
|
forwarded: number;
|
|
@@ -52,6 +54,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
52
54
|
terminalLedgerKind?: undefined;
|
|
53
55
|
duplicateCompletion?: undefined;
|
|
54
56
|
error?: undefined;
|
|
57
|
+
bufferedForGeneratingCoordinator?: undefined;
|
|
55
58
|
} | {
|
|
56
59
|
success: boolean;
|
|
57
60
|
forwarded: number;
|
|
@@ -62,6 +65,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
62
65
|
terminalLedgerKind?: undefined;
|
|
63
66
|
duplicateRefineTerminalEvent?: undefined;
|
|
64
67
|
error?: undefined;
|
|
68
|
+
bufferedForGeneratingCoordinator?: undefined;
|
|
65
69
|
} | {
|
|
66
70
|
success: boolean;
|
|
67
71
|
forwarded: number;
|
|
@@ -72,6 +76,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
72
76
|
terminalLedgerKind?: undefined;
|
|
73
77
|
duplicateRefineTerminalEvent?: undefined;
|
|
74
78
|
error?: undefined;
|
|
79
|
+
bufferedForGeneratingCoordinator?: undefined;
|
|
75
80
|
} | {
|
|
76
81
|
success: boolean;
|
|
77
82
|
forwarded: number;
|
|
@@ -82,6 +87,18 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
82
87
|
duplicateRefineTerminalEvent?: undefined;
|
|
83
88
|
duplicateCompletion?: undefined;
|
|
84
89
|
error?: undefined;
|
|
90
|
+
bufferedForGeneratingCoordinator?: undefined;
|
|
91
|
+
} | {
|
|
92
|
+
success: boolean;
|
|
93
|
+
forwarded: number;
|
|
94
|
+
bufferedForGeneratingCoordinator: boolean;
|
|
95
|
+
suppressed?: undefined;
|
|
96
|
+
intentionalCleanupStop?: undefined;
|
|
97
|
+
terminalLedgerEvidence?: undefined;
|
|
98
|
+
terminalLedgerKind?: undefined;
|
|
99
|
+
duplicateRefineTerminalEvent?: undefined;
|
|
100
|
+
duplicateCompletion?: undefined;
|
|
101
|
+
error?: undefined;
|
|
85
102
|
} | {
|
|
86
103
|
success: boolean;
|
|
87
104
|
error: string;
|
package/package.json
CHANGED
|
@@ -211,7 +211,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
211
211
|
|
|
212
212
|
- **Minimize coordinator context.** The coordinator's job is routing, not implementing. Do not read source files, run commands, or analyze code directly — delegate all of that to node agents. Your context should stay lean.
|
|
213
213
|
- **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to the queue or a node. Do not do it yourself.
|
|
214
|
-
- **Respect explicit provider requests.** If the user names an agent/provider, pass the matching provider type to \`mesh_launch_session\`: Hermes → \`hermes-cli\`, Claude Code/Claude → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`. Never substitute \`claude-cli\` just because the coordinator itself is Claude Code.
|
|
214
|
+
- **Respect explicit provider requests.** If the user names an agent/provider, pass the matching provider type to \`mesh_launch_session\`: Hermes → \`hermes-cli\`, Claude Code/Claude → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`, Antigravity → \`antigravity-cli\`. Never substitute \`claude-cli\` just because the coordinator itself is Claude Code.
|
|
215
215
|
- **Front-load new task messages.** When calling \`mesh_enqueue_task\` or \`mesh_send_task\` for a new task, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
|
|
216
216
|
- **Avoid context-wasting restarts.** For follow-up, retry, commit/push, preview, or cleanup work on the same issue, prefer the existing idle session and send only the delta from its last verified state. Start a fresh chat/session only for genuinely independent work, explicit provider/user request, unsafe transcript contamination, or required branch/worktree isolation.
|
|
217
217
|
- **Don't inspect code.** Treat delegated agent summaries as self-reports, not verification. Verify side effects via \`mesh_git_status\` (including related repo freshness when configured), not by reading source files.
|
|
@@ -23,6 +23,12 @@ export interface MeshActiveWorkRecord {
|
|
|
23
23
|
terminalKind?: string;
|
|
24
24
|
terminalAt?: string;
|
|
25
25
|
staleReason?: string;
|
|
26
|
+
/**
|
|
27
|
+
* When true, the session targeted by this direct dispatch still exists in the live mesh
|
|
28
|
+
* but did not transition to generating — the dispatch was not acknowledged by the provider.
|
|
29
|
+
* Distinct from historical/orphaned stale entries where the node/session is gone.
|
|
30
|
+
*/
|
|
31
|
+
staleDispatchUnacknowledged?: boolean;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export interface MeshActiveWorkSummary {
|
|
@@ -37,10 +43,15 @@ export interface MeshActiveWorkSummary {
|
|
|
37
43
|
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
38
44
|
staleDirectCount: number;
|
|
39
45
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
46
|
+
* Count of stale direct entries where the target session still exists in the live mesh
|
|
47
|
+
* but never transitioned to generating — these are fresh dispatch failures, not historical orphans.
|
|
48
|
+
* Requires immediate recovery: launch a fresh session and retry the task.
|
|
49
|
+
*/
|
|
50
|
+
staleDirectUnacknowledgedCount?: number;
|
|
51
|
+
/**
|
|
52
|
+
* When staleDirectCount > 0, this note clarifies what kind of stale records exist.
|
|
53
|
+
* Fresh unacknowledged dispatches carry an actionable recovery note; orphaned historical
|
|
54
|
+
* entries carry the "historical evidence only" note. Both may coexist.
|
|
44
55
|
*/
|
|
45
56
|
staleDirectNote?: string;
|
|
46
57
|
}
|
|
@@ -163,6 +174,7 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
163
174
|
statusCounts[item.status] += 1;
|
|
164
175
|
}
|
|
165
176
|
const staleDirectCount = activeWork.filter(item => item.source === 'direct' && item.staleReason).length;
|
|
177
|
+
const staleDirectUnacknowledgedCount = activeWork.filter(item => item.source === 'direct' && item.staleDispatchUnacknowledged).length;
|
|
166
178
|
return {
|
|
167
179
|
totalActiveCount: activeWork.length,
|
|
168
180
|
queueActiveCount: sourceCounts.queue,
|
|
@@ -174,6 +186,7 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
174
186
|
sourceCounts,
|
|
175
187
|
statusCounts,
|
|
176
188
|
staleDirectCount,
|
|
189
|
+
...(staleDirectUnacknowledgedCount > 0 ? { staleDirectUnacknowledgedCount } : {}),
|
|
177
190
|
...(staleDirectCount > 0 ? { staleDirectNote: 'Stale direct records are orphaned ledger entries whose node/session no longer exists. They are historical recovery evidence only — not active or unresolved work. The queue (source: queue) is authoritative for pending/assigned tasks.' } : {}),
|
|
178
191
|
};
|
|
179
192
|
}
|
|
@@ -216,11 +229,25 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
216
229
|
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
217
230
|
const status = terminalStatus || live.status || 'assigned';
|
|
218
231
|
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
219
|
-
|
|
232
|
+
// dispatchedToIdleSession is recorded by mesh_send_task when the target session was idle
|
|
233
|
+
// at dispatch time. Combined with status === 'idle', it confirms no runtime transition.
|
|
234
|
+
const dispatchedToIdleSession = dispatch.payload?.dispatchedToIdleSession === true;
|
|
235
|
+
// Flag stale when no terminal event AND either:
|
|
236
|
+
// - session is now idle (dispatch not acknowledged), OR
|
|
237
|
+
// - no live status reported (node/session absent from live mesh), OR
|
|
238
|
+
// - payload flagged idle-at-dispatch AND session is still not generating (redundant guard for clarity).
|
|
239
|
+
const isNoTransition = !terminalStatus && !live.status;
|
|
240
|
+
const isIdleUnacknowledged = status === 'idle';
|
|
241
|
+
const ledgerOnlyStaleReason = !terminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
|
|
220
242
|
? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
|
|
221
243
|
: undefined;
|
|
222
244
|
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
223
245
|
const { title, summary } = summarizeMessage(message);
|
|
246
|
+
// A dispatch is a fresh unacknowledged failure when the node and session both still exist
|
|
247
|
+
// in the live mesh but the session never transitioned to generating. This is distinct from
|
|
248
|
+
// historical/orphaned stale entries where the node or session is gone from the live mesh.
|
|
249
|
+
// fresh-unacknowledged: ledgerOnlyStaleReason is set (session didn't ack) AND no live.staleReason (node/session is present)
|
|
250
|
+
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
|
|
224
251
|
const record: MeshActiveWorkRecord = {
|
|
225
252
|
taskId,
|
|
226
253
|
source: 'direct',
|
|
@@ -240,6 +267,7 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
240
267
|
terminalKind: terminal?.kind,
|
|
241
268
|
terminalAt: terminal?.timestamp,
|
|
242
269
|
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
270
|
+
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
243
271
|
};
|
|
244
272
|
if (terminalRow) {
|
|
245
273
|
terminalDirectWork.push(record);
|
|
@@ -257,8 +285,19 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
257
285
|
terminalDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
258
286
|
const summary = buildMeshActiveWorkSummary(records);
|
|
259
287
|
summary.staleDirectCount = staleDirectWork.length;
|
|
288
|
+
const unacknowledgedCount = staleDirectWork.filter(r => r.staleDispatchUnacknowledged).length;
|
|
289
|
+
if (unacknowledgedCount > 0) {
|
|
290
|
+
summary.staleDirectUnacknowledgedCount = unacknowledgedCount;
|
|
291
|
+
}
|
|
292
|
+
// Build a note that accurately describes what's in the stale set.
|
|
293
|
+
// When there are fresh unacknowledged entries (node/session still live, session never started),
|
|
294
|
+
// use the actionable recovery note instead of the misleading "historical evidence only" note.
|
|
260
295
|
const staleDirectWorkNote = staleDirectWork.length > 0
|
|
261
|
-
?
|
|
296
|
+
? unacknowledgedCount > 0 && unacknowledgedCount === staleDirectWork.length
|
|
297
|
+
? `${unacknowledgedCount} direct dispatch(es) were not acknowledged by the target session — the session received the agent_command but never transitioned to generating. This is a fresh dispatch failure, not historical noise. Recovery: launch a fresh session on the same node and retry the task, or use mesh_enqueue_task for queue-based assignment.`
|
|
298
|
+
: unacknowledgedCount > 0
|
|
299
|
+
? `${unacknowledgedCount} of ${staleDirectWork.length} stale direct record(s) are fresh unacknowledged dispatch failures (session still live but never transitioned to generating); the rest are orphaned historical entries whose node/session no longer exists. Fresh unacknowledged dispatches need recovery: launch a fresh session and retry. Orphaned entries are historical evidence only — not active or unresolved work.`
|
|
300
|
+
: 'These are orphaned ledger entries whose original node or session no longer exists in the live mesh. They are historical/recovery evidence only — not active or unresolved work. Do not treat staleDirectCount as a status mismatch; use the queue (source: queue) as authoritative for pending/assigned tasks.'
|
|
262
301
|
: undefined;
|
|
263
302
|
if (staleDirectWorkNote) {
|
|
264
303
|
summary.staleDirectNote = staleDirectWorkNote;
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -415,22 +415,38 @@ function findRecentTerminalLedgerEvidence(args: {
|
|
|
415
415
|
meshId: string;
|
|
416
416
|
sessionId?: string;
|
|
417
417
|
nodeId?: string;
|
|
418
|
-
}): { kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
418
|
+
}): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
419
419
|
if (!args.sessionId && !args.nodeId) return null;
|
|
420
420
|
const entries = readLedgerEntries(args.meshId);
|
|
421
421
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
422
422
|
const entry = entries[i];
|
|
423
423
|
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
424
424
|
if (args.sessionId && entry.sessionId === args.sessionId) {
|
|
425
|
-
return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
425
|
+
return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
426
426
|
}
|
|
427
427
|
if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
|
|
428
|
-
return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
428
|
+
return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
return null;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
// Returns true when a task_dispatched entry for the given session appears AFTER the terminal
|
|
435
|
+
// entry (identified by terminalId) in ledger order. Positional (append) order is used rather
|
|
436
|
+
// than timestamp comparison because both entries may share the same millisecond.
|
|
437
|
+
function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
|
|
438
|
+
const entries = readLedgerEntries(meshId);
|
|
439
|
+
let pastTerminal = false;
|
|
440
|
+
for (const entry of entries) {
|
|
441
|
+
if (!pastTerminal) {
|
|
442
|
+
if (entry.id === terminalId) pastTerminal = true;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
434
450
|
function buildLongGeneratingCompletionReconciliation(args: {
|
|
435
451
|
meshId: string;
|
|
436
452
|
nodeId?: string;
|
|
@@ -1060,17 +1076,22 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1060
1076
|
nodeId: eventNodeId || undefined,
|
|
1061
1077
|
});
|
|
1062
1078
|
if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1079
|
+
// If a new task_dispatched was recorded for this session after the prior terminal,
|
|
1080
|
+
// this completion belongs to the new task — never suppress it as a duplicate.
|
|
1081
|
+
const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
|
|
1082
|
+
if (!newDispatchAfterTerminal) {
|
|
1083
|
+
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
1084
|
+
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
1085
|
+
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
1086
|
+
const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
1087
|
+
if (
|
|
1088
|
+
(terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
|
|
1089
|
+
|| (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
|
|
1090
|
+
|| args.metadataEvent.source === 'long_generating_reconciliation'
|
|
1091
|
+
) {
|
|
1092
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1093
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
|
|
1094
|
+
}
|
|
1074
1095
|
}
|
|
1075
1096
|
}
|
|
1076
1097
|
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
@@ -1332,7 +1353,24 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1332
1353
|
// that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
|
|
1333
1354
|
// forwarded directly only — they must not accumulate in the pending queue when a live
|
|
1334
1355
|
// coordinator already received them.
|
|
1335
|
-
|
|
1356
|
+
//
|
|
1357
|
+
// Exception: if ALL live CLI coordinator instances are in a generating/active state
|
|
1358
|
+
// when a refine terminal event fires (e.g. a Codex CLI coordinator that triggered an
|
|
1359
|
+
// async refine job and is still in the generating turn that sent it), the coordinator
|
|
1360
|
+
// cannot immediately receive send_message input. Buffer the event to the pending queue
|
|
1361
|
+
// so it is available via get_pending_mesh_events when the coordinator returns to idle.
|
|
1362
|
+
// Critically: do NOT attempt send_message injection into a generating PTY coordinator for
|
|
1363
|
+
// terminal refine events — injecting text into an active PTY can corrupt the input stream
|
|
1364
|
+
// and leave the coordinator stuck in generating state, unable to process the refine result.
|
|
1365
|
+
const allCoordinatorsGenerating = isRefineTerminalEvent && coordinatorInstances.every((inst) => {
|
|
1366
|
+
const s = inst.getState();
|
|
1367
|
+
const status = readNonEmptyString(s.status).toLowerCase();
|
|
1368
|
+
const activeChatStatus = readNonEmptyString(s.activeChat?.status).toLowerCase();
|
|
1369
|
+
return status === 'generating' || status === 'streaming' || status === 'long_generating'
|
|
1370
|
+
|| activeChatStatus === 'generating' || activeChatStatus === 'streaming';
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
if (!isRefineTerminalEvent || allCoordinatorsGenerating) {
|
|
1336
1374
|
if (queuePendingMeshCoordinatorEvent({
|
|
1337
1375
|
event: args.event,
|
|
1338
1376
|
meshId: args.meshId,
|
|
@@ -1346,10 +1384,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1346
1384
|
coordinatorMessage: messageText,
|
|
1347
1385
|
queuedAt: Date.now(),
|
|
1348
1386
|
})) {
|
|
1349
|
-
|
|
1387
|
+
if (allCoordinatorsGenerating) {
|
|
1388
|
+
LOG.info('MeshEvents', `Queued ${args.event} for generating CLI coordinator (mesh ${args.meshId}) — will be delivered via get_pending_mesh_events when coordinator returns to idle`);
|
|
1389
|
+
} else {
|
|
1390
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1391
|
+
}
|
|
1350
1392
|
}
|
|
1351
1393
|
}
|
|
1352
1394
|
|
|
1395
|
+
// When all CLI coordinators are actively generating and a terminal refine event fires,
|
|
1396
|
+
// skip send_message injection entirely. The event is already buffered to the pending queue.
|
|
1397
|
+
// Injecting into a generating PTY coordinator can corrupt its input stream and cause it to
|
|
1398
|
+
// remain stuck in generating state, never processing the refine result.
|
|
1399
|
+
// The coordinator will drain pending events via get_pending_mesh_events on its next idle cycle.
|
|
1400
|
+
if (allCoordinatorsGenerating) {
|
|
1401
|
+
return { success: true, forwarded: 0, bufferedForGeneratingCoordinator: true };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1353
1404
|
for (const coord of coordinatorInstances) {
|
|
1354
1405
|
const coordState = coord.getState();
|
|
1355
1406
|
LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);
|
|
@@ -899,7 +899,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
899
899
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
900
900
|
if (adapterRawStatus !== 'idle') return false;
|
|
901
901
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
902
|
-
|
|
902
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
903
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
904
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
905
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
906
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
907
|
+
// finalization block here would emit a false completion event while the provider
|
|
908
|
+
// session is still actively processing its response stream.
|
|
909
|
+
const adapterAny = this.adapter as any;
|
|
910
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
911
|
+
return true;
|
|
903
912
|
}
|
|
904
913
|
|
|
905
914
|
private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
|
|
@@ -1122,12 +1131,30 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1122
1131
|
}
|
|
1123
1132
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
1124
1133
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
1125
|
-
// If debounce still pending (generating lasted < 1s), cancel both events
|
|
1134
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1135
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1136
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
1126
1137
|
if (this.generatingDebouncePending) {
|
|
1127
|
-
|
|
1138
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1139
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
1128
1140
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1129
1141
|
this.generatingDebouncePending = null;
|
|
1130
1142
|
this.generatingStartedAt = 0;
|
|
1143
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1144
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1145
|
+
let shortFinalSummary: string | undefined;
|
|
1146
|
+
try { shortFinalSummary = extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages); } catch { /* best-effort */ }
|
|
1147
|
+
this.pushEvent({
|
|
1148
|
+
event: 'agent:generating_completed',
|
|
1149
|
+
chatTitle,
|
|
1150
|
+
duration: 0,
|
|
1151
|
+
timestamp: now,
|
|
1152
|
+
finalSummary: shortFinalSummary,
|
|
1153
|
+
completionDiagnostic: {
|
|
1154
|
+
reason: 'short_generating_suppressed',
|
|
1155
|
+
shortDurationMs,
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1131
1158
|
} else {
|
|
1132
1159
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
1133
1160
|
// uses to show an idle turn whose last user-facing message is assistant.
|