@adhdev/daemon-core 0.9.82-rc.214 → 0.9.82-rc.215

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.
@@ -146,6 +146,12 @@ export declare class SpecDriver {
146
146
  private completionIdleKey;
147
147
  /** Previous screen lines — passed to evaluate() for `changed` condition detection. */
148
148
  private prevScreenLines;
149
+ /** Timestamp when idle was last committed (either direct or via idle_hold_ms).
150
+ * Used to suppress immediate idle → busy re-entry from transient `changed`
151
+ * condition blips (e.g. completion-marker counter "Completed for Xs" updating
152
+ * every second, which triggers cursor_above:changed and bounces back to busy
153
+ * right after an idle commit). */
154
+ private lastIdleCommittedAt;
149
155
  /** Timestamp of the last PTY frame that changed the screen content.
150
156
  * Used by screen_active_hold_ms to suppress idle downshifts while
151
157
  * the terminal is still actively updating. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.214",
3
+ "version": "0.9.82-rc.215",
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",
@@ -230,6 +230,12 @@ export class SpecDriver {
230
230
  private completionIdleKey = '';
231
231
  /** Previous screen lines — passed to evaluate() for `changed` condition detection. */
232
232
  private prevScreenLines: string[] = [];
233
+ /** Timestamp when idle was last committed (either direct or via idle_hold_ms).
234
+ * Used to suppress immediate idle → busy re-entry from transient `changed`
235
+ * condition blips (e.g. completion-marker counter "Completed for Xs" updating
236
+ * every second, which triggers cursor_above:changed and bounces back to busy
237
+ * right after an idle commit). */
238
+ private lastIdleCommittedAt = 0;
233
239
  /** Timestamp of the last PTY frame that changed the screen content.
234
240
  * Used by screen_active_hold_ms to suppress idle downshifts while
235
241
  * the terminal is still actively updating. */
@@ -303,7 +309,7 @@ export class SpecDriver {
303
309
  case 'click_modal_button': this.handleClickModalButton(cmd.index); return;
304
310
  case 'attach_image': this.handleAttachImage(cmd.blob, cmd.mime); return;
305
311
  case 'resize': this.adapter.resize(cmd.cols, cmd.rows); return;
306
- case 'cancel': this.adapter.send_keys('\x03'); return;
312
+ case 'cancel': this.lastIdleCommittedAt = 0; this.adapter.send_keys('\x03'); return;
307
313
  case 'shutdown': this.shutdown(); return;
308
314
  }
309
315
  }
@@ -595,6 +601,29 @@ export class SpecDriver {
595
601
  evState = this.lastBusyState ?? evState;
596
602
  }
597
603
 
604
+ // Idle → busy re-entry hold: suppress false-busy transitions that fire
605
+ // immediately after an idle commit. The most common source is the
606
+ // `cursor_above: 3, changed: true` condition matching tiny updates
607
+ // (e.g. "✻ Completed for Ns" counter ticking once per second) right
608
+ // after the completion hold expired and idle was committed. Without
609
+ // this guard the dashboard bounces back to generating within 1s of
610
+ // seeing idle.
611
+ //
612
+ // Guard window: prefer screen_active_hold_ms (already the "micro-change
613
+ // suppressor" window), floor to 1500ms so specs that omit it still
614
+ // get protection. This is intentionally shorter than busy_hold_ms —
615
+ // a real new generation (user types a message) will produce many
616
+ // more decisive signals (spinner, token counter, esc-to-interrupt)
617
+ // that override the hold long before it expires.
618
+ const idleReentryHoldMs = Math.max(screenActiveMs, 1500);
619
+ if (evState.id === 'busy' && this.currentStateId === (this.spec.default_state ?? 'idle')
620
+ && this.lastIdleCommittedAt > 0
621
+ && (now - this.lastIdleCommittedAt) < idleReentryHoldMs) {
622
+ LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] idle→busy suppressed (idle_reentry_hold ageMs=${now - this.lastIdleCommittedAt} holdMs=${idleReentryHoldMs})`);
623
+ this.scheduleBusyExpiry(idleReentryHoldMs - (now - this.lastIdleCommittedAt) + 50);
624
+ return;
625
+ }
626
+
598
627
  if (evState.id === 'busy') {
599
628
  this.lastBusyAt = Date.now();
600
629
  this.lastBusyState = evState;
@@ -661,6 +690,7 @@ export class SpecDriver {
661
690
  this.pendingIdleState = null;
662
691
  if (!committed) return;
663
692
  LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] idleHold committed after ${idleHoldMs}ms`);
693
+ this.lastIdleCommittedAt = Date.now();
664
694
  this.currentStateId = committed.id;
665
695
  this.currentEval = capturedEv;
666
696
  this.pushHistory(committed.id, committed.label, {
@@ -721,6 +751,9 @@ export class SpecDriver {
721
751
  }
722
752
  }
723
753
  if (changed) {
754
+ if (evState.id === (this.spec.default_state ?? 'idle')) {
755
+ this.lastIdleCommittedAt = Date.now();
756
+ }
724
757
  this.currentStateId = evState.id;
725
758
  const matchedRules = extractMatchedRules(ev);
726
759
  // Determine reason and debounce kind for this transition
@@ -799,6 +832,9 @@ export class SpecDriver {
799
832
  this.pendingSends.push(text);
800
833
  return;
801
834
  }
835
+ // Clear the idle re-entry hold so a new generation starting
836
+ // immediately after idle doesn't get its busy state suppressed.
837
+ this.lastIdleCommittedAt = 0;
802
838
  this.actuallySendMessage(text);
803
839
  }
804
840