@adhdev/daemon-core 0.9.82-rc.131 → 0.9.82-rc.133

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.
@@ -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
- * When staleDirectCount > 0, this note clarifies that stale direct records are
38
- * historical/recovery evidenceorphaned ledger entries whose original node or session
39
- * is no longer present in the live mesh. They are NOT active or unresolved work items.
40
- * The active queue (queue source) is the authoritative source for pending/assigned work.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.131",
3
+ "version": "0.9.82-rc.133",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
- * When staleDirectCount > 0, this note clarifies that stale direct records are
41
- * historical/recovery evidenceorphaned ledger entries whose original node or session
42
- * is no longer present in the live mesh. They are NOT active or unresolved work items.
43
- * The active queue (queue source) is the authoritative source for pending/assigned work.
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
- const ledgerOnlyStaleReason = !terminalRow && (status === 'idle' || (!terminalStatus && !live.status))
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
- ? '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.'
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;
@@ -1332,7 +1332,24 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1332
1332
  // that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
1333
1333
  // forwarded directly only — they must not accumulate in the pending queue when a live
1334
1334
  // coordinator already received them.
1335
- if (!isRefineTerminalEvent) {
1335
+ //
1336
+ // Exception: if ALL live CLI coordinator instances are in a generating/active state
1337
+ // when a refine terminal event fires (e.g. a Codex CLI coordinator that triggered an
1338
+ // async refine job and is still in the generating turn that sent it), the coordinator
1339
+ // cannot immediately receive send_message input. Buffer the event to the pending queue
1340
+ // so it is available via get_pending_mesh_events when the coordinator returns to idle.
1341
+ // Critically: do NOT attempt send_message injection into a generating PTY coordinator for
1342
+ // terminal refine events — injecting text into an active PTY can corrupt the input stream
1343
+ // and leave the coordinator stuck in generating state, unable to process the refine result.
1344
+ const allCoordinatorsGenerating = isRefineTerminalEvent && coordinatorInstances.every((inst) => {
1345
+ const s = inst.getState();
1346
+ const status = readNonEmptyString(s.status).toLowerCase();
1347
+ const activeChatStatus = readNonEmptyString(s.activeChat?.status).toLowerCase();
1348
+ return status === 'generating' || status === 'streaming' || status === 'long_generating'
1349
+ || activeChatStatus === 'generating' || activeChatStatus === 'streaming';
1350
+ });
1351
+
1352
+ if (!isRefineTerminalEvent || allCoordinatorsGenerating) {
1336
1353
  if (queuePendingMeshCoordinatorEvent({
1337
1354
  event: args.event,
1338
1355
  meshId: args.meshId,
@@ -1346,10 +1363,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1346
1363
  coordinatorMessage: messageText,
1347
1364
  queuedAt: Date.now(),
1348
1365
  })) {
1349
- LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1366
+ if (allCoordinatorsGenerating) {
1367
+ 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`);
1368
+ } else {
1369
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1370
+ }
1350
1371
  }
1351
1372
  }
1352
1373
 
1374
+ // When all CLI coordinators are actively generating and a terminal refine event fires,
1375
+ // skip send_message injection entirely. The event is already buffered to the pending queue.
1376
+ // Injecting into a generating PTY coordinator can corrupt its input stream and cause it to
1377
+ // remain stuck in generating state, never processing the refine result.
1378
+ // The coordinator will drain pending events via get_pending_mesh_events on its next idle cycle.
1379
+ if (allCoordinatorsGenerating) {
1380
+ return { success: true, forwarded: 0, bufferedForGeneratingCoordinator: true };
1381
+ }
1382
+
1353
1383
  for (const coord of coordinatorInstances) {
1354
1384
  const coordState = coord.getState();
1355
1385
  LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);