@adhdev/daemon-core 0.9.82-rc.373 → 0.9.82-rc.375

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.
@@ -102,6 +102,24 @@ type ExternalTranscriptProbe = {
102
102
 
103
103
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
104
104
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
105
+ // (FALSEIDLE-BGCHILD-a) Minimum generating→idle settle window for native-history mesh worker
106
+ // sessions. Native-history providers (e.g. claude-cli) normally flush the completion with
107
+ // flushDelay=0 — the transcript is authoritative, so there is no reason to wait. But a worker
108
+ // turn that spawns a BACKGROUND child (e.g. `npm test &`, a backgrounded Bash tool) can paint
109
+ // a burst of child output, fall quiet, and have the screen parser read a PRIOR/intermediate
110
+ // standard assistant as if the turn were done — firing a false idle while the agent is in fact
111
+ // still generating (e.g. mid-commit). With flushDelay=0 there is no window for the resume guard
112
+ // in flushCompletedDebounceIfFinalized (latestVisibleStatus !== 'idle' → cancel) to observe the
113
+ // agent picking the turn back up. A short non-zero settle window restores that resume guard for
114
+ // mesh workers without delaying genuinely-finished turns beyond this bound. Scoped to mesh
115
+ // worker sessions so interactive native-history sessions keep the immediate flush.
116
+ const NATIVE_HISTORY_MESH_IDLE_SETTLE_MS = 1500;
117
+ // TASKBUBBLE-DUP: window during which an identical user-input ack (same trimmed
118
+ // content on the same instance) is treated as a redelivery of one dispatch and
119
+ // suppressed from the chat transcript. Matches the coordinator-side
120
+ // DUPLICATE_DISPATCH_WINDOW_MS (mesh-tools) so the daemon's bubble-level guard
121
+ // covers the same retry horizon as the MCP-level dispatch dedup.
122
+ const USER_INPUT_ACK_DEDUP_WINDOW_MS = 60_000;
105
123
 
106
124
  /** Events that signal a dispatched mesh task has reached a terminal state.
107
125
  * Detach the mesh assignment after emitting one of these so the worker's
@@ -431,6 +449,14 @@ export class CliProviderInstance implements ProviderInstance {
431
449
  private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
432
450
  private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
433
451
  private lastAcknowledgedUserInputAt = 0;
452
+ // TASKBUBBLE-DUP: per-content last-ack timestamps so the same dispatched
453
+ // prompt acked twice in quick succession (the worker buffers the first
454
+ // send during bootstrap/busy, then a redelivery — dispatch-confirm-timeout
455
+ // requeue or a reconcile re-dispatch — fires a SECOND send_chat before the
456
+ // outbound queue drains) collapses to ONE user bubble. Keyed on the trimmed
457
+ // content; an entry older than USER_INPUT_ACK_DEDUP_WINDOW_MS is treated as
458
+ // a fresh, intentional resend and is NOT suppressed.
459
+ private recentUserInputAcks = new Map<string, number>();
434
460
  private lastNativeSourceCanonicalCheckAt = 0;
435
461
  private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
436
462
  private cachedSqliteDb: {
@@ -1056,7 +1082,31 @@ export class CliProviderInstance implements ProviderInstance {
1056
1082
  if (!content) return;
1057
1083
 
1058
1084
  const receivedAt = Date.now();
1085
+
1086
+ // TASKBUBBLE-DUP: collapse a redelivered dispatch to one bubble. A single
1087
+ // mesh_send_task can reach this instance as TWO send_chat calls when the
1088
+ // first injection is buffered during bootstrap/busy and a retry (dispatch-
1089
+ // confirm-timeout requeue, or a reconcile re-dispatch) fires before the
1090
+ // outbound queue drains. The previous dedupKey hashed receivedAt, so the
1091
+ // two acks produced different keys and BOTH bubbled. Suppress an identical
1092
+ // content ack seen within USER_INPUT_ACK_DEDUP_WINDOW_MS; a later resend of
1093
+ // the same text (beyond the window) is a genuine new turn and still shows.
1094
+ const ackContentKey = shortHash(`${this.instanceId}:${content}`, 24);
1095
+ const lastAckAt = this.recentUserInputAcks.get(ackContentKey);
1096
+ if (lastAckAt !== undefined && receivedAt - lastAckAt <= USER_INPUT_ACK_DEDUP_WINDOW_MS) {
1097
+ // Refresh the timestamp so a steady stream of redeliveries keeps
1098
+ // collapsing, and prune stale entries to bound the map size.
1099
+ this.recentUserInputAcks.set(ackContentKey, receivedAt);
1100
+ this.pruneRecentUserInputAcks(receivedAt);
1101
+ return;
1102
+ }
1103
+ this.recentUserInputAcks.set(ackContentKey, receivedAt);
1104
+ this.pruneRecentUserInputAcks(receivedAt);
1105
+
1059
1106
  this.lastAcknowledgedUserInputAt = receivedAt;
1107
+ // The runtimeMessages dedupKey stays per-call unique (includes receivedAt)
1108
+ // so a genuine resend of the same text after the window appends a fresh
1109
+ // bubble; redelivery within the window is already suppressed above.
1060
1110
  const dedupKey = `user_input_ack:${shortHash(`${this.instanceId}:${content}:${receivedAt}`, 24)}`;
1061
1111
  this.appendRuntimeMessage(buildChatMessage({
1062
1112
  role: 'user',
@@ -1074,6 +1124,14 @@ export class CliProviderInstance implements ProviderInstance {
1074
1124
  } as ChatMessage), dedupKey);
1075
1125
  }
1076
1126
 
1127
+ /** Drop user-input ack entries older than the dedup window so the map can't grow unbounded. */
1128
+ private pruneRecentUserInputAcks(now: number): void {
1129
+ if (this.recentUserInputAcks.size <= 1) return;
1130
+ for (const [key, at] of this.recentUserInputAcks) {
1131
+ if (now - at > USER_INPUT_ACK_DEDUP_WINDOW_MS) this.recentUserInputAcks.delete(key);
1132
+ }
1133
+ }
1134
+
1077
1135
  dispose(): void {
1078
1136
  this.adapter.shutdown();
1079
1137
  this.monitor.reset();
@@ -2004,8 +2062,17 @@ export class CliProviderInstance implements ProviderInstance {
2004
2062
  previousStatus: this.lastStatus,
2005
2063
  };
2006
2064
  const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
2007
- const flushDelay = ownsExternalHistory ? 0 : 3000;
2008
- LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
2065
+ // (FALSEIDLE-BGCHILD-a) Native-history providers flush immediately (the
2066
+ // transcript is authoritative). For mesh worker sessions, give the
2067
+ // generating→idle transition a short settle window so a background-child
2068
+ // false idle (quiet after a backgrounded test/command while the parent turn
2069
+ // continues) gets caught by the resume guard in flushCompletedDebounceIfFinalized
2070
+ // instead of firing an early completion the coordinator can never correct.
2071
+ const meshWorkerSession = this.isMeshWorkerSession();
2072
+ const flushDelay = ownsExternalHistory
2073
+ ? (meshWorkerSession ? NATIVE_HISTORY_MESH_IDLE_SETTLE_MS : 0)
2074
+ : 3000;
2075
+ LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} meshWorker=${meshWorkerSession} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
2009
2076
  this.scheduleCompletedDebounceFlush(flushDelay);
2010
2077
  }
2011
2078
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {