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

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.
@@ -156,6 +156,9 @@ export declare class SpecDriver {
156
156
  * Used by screen_active_hold_ms to suppress idle downshifts while
157
157
  * the terminal is still actively updating. */
158
158
  private lastScreenChangedAt;
159
+ /** Per-cursor_above region last-changed timestamps for stable_ms tracking.
160
+ * Key: cursor_above value. Value: last time that region changed. */
161
+ private regionLastChangedAt;
159
162
  /** Timer that re-runs evaluate() once the hold window expires. Needed
160
163
  * because the PTY stops emitting once the agent finishes; without an
161
164
  * explicit wake-up there's nothing to trigger the busy → idle
@@ -908,7 +908,10 @@ export declare const SCHEMA_V3: {
908
908
  };
909
909
  readonly changed: {
910
910
  readonly type: "boolean";
911
- readonly const: true;
911
+ };
912
+ readonly stable_ms: {
913
+ readonly type: "integer";
914
+ readonly minimum: 0;
912
915
  };
913
916
  };
914
917
  };
@@ -1608,7 +1611,10 @@ export declare const SCHEMA: {
1608
1611
  };
1609
1612
  readonly changed: {
1610
1613
  readonly type: "boolean";
1611
- readonly const: true;
1614
+ };
1615
+ readonly stable_ms: {
1616
+ readonly type: "integer";
1617
+ readonly minimum: 0;
1612
1618
  };
1613
1619
  };
1614
1620
  };
@@ -155,10 +155,13 @@ export interface RegexCondition {
155
155
  cursor_col_min?: number;
156
156
  cursor_col_max?: number;
157
157
  }
158
- /** v3 delta condition: N lines above cursor changed vs prevLines. */
158
+ /** v3 delta condition: N lines above cursor changed vs prevLines.
159
+ * changed:false + stable_ms: region must be stable for at least N ms. */
159
160
  export interface ChangedCondition {
160
161
  cursor_above: number;
161
- changed: true;
162
+ changed: boolean;
163
+ /** Only for changed:false — region must have been stable for this many ms. */
164
+ stable_ms?: number;
162
165
  }
163
166
  /** v3 AND composite. */
164
167
  export interface AllCondition {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.215",
3
+ "version": "0.9.82-rc.217",
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",
@@ -41,7 +41,7 @@ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
41
41
  import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
42
42
  import { evaluate, type SpecEvaluation, type TraceEntry } from './evaluator.js';
43
43
  import { loadSpec } from './loader.js';
44
- import type { CliSpec, Control, DelegateTrigger, SectionDef } from './types.js';
44
+ import type { CliSpec, Condition, ChangedCondition, Control, DelegateTrigger, SectionDef } from './types.js';
45
45
  import { LOG } from '../../logging/logger.js';
46
46
 
47
47
  export type DashboardEvent =
@@ -112,6 +112,14 @@ const BUSY_HOLD_MS = 6000;
112
112
  * codex's explicit setting and is barely perceptible to a human caller. */
113
113
  const SUBMIT_DELAY_FLOOR_MS = 200;
114
114
 
115
+ function collectChangedConditions(when: Condition | undefined): ChangedCondition[] {
116
+ if (!when) return [];
117
+ if ('cursor_above' in when && 'changed' in when) return [when as ChangedCondition];
118
+ if ('all' in when) return when.all.flatMap(c => collectChangedConditions(c));
119
+ if ('any' in when) return when.any.flatMap(c => collectChangedConditions(c));
120
+ return [];
121
+ }
122
+
115
123
  function countNewlines(s: string): number {
116
124
  let n = 0;
117
125
  for (let i = 0; i < s.length; i += 1) if (s.charCodeAt(i) === 10) n += 1;
@@ -240,6 +248,9 @@ export class SpecDriver {
240
248
  * Used by screen_active_hold_ms to suppress idle downshifts while
241
249
  * the terminal is still actively updating. */
242
250
  private lastScreenChangedAt = 0;
251
+ /** Per-cursor_above region last-changed timestamps for stable_ms tracking.
252
+ * Key: cursor_above value. Value: last time that region changed. */
253
+ private regionLastChangedAt = new Map<number, number>();
243
254
  /** Timer that re-runs evaluate() once the hold window expires. Needed
244
255
  * because the PTY stops emitting once the agent finishes; without an
245
256
  * explicit wake-up there's nothing to trigger the busy → idle
@@ -482,9 +493,24 @@ export class SpecDriver {
482
493
  const cursor = this.adapter.getCursorPosition();
483
494
  const currentLines = screen.split('\n').map(l => l.endsWith('\r') ? l.slice(0, -1) : l);
484
495
  const ev = evaluate(this.spec, screen, cursor, this.prevScreenLines.length > 0 ? this.prevScreenLines : undefined);
496
+ const now = Date.now();
485
497
  // Track when the screen last changed for screen_active_hold_ms.
486
498
  if (this.prevScreenLines.length > 0 && currentLines.join('\n') !== this.prevScreenLines.join('\n')) {
487
- this.lastScreenChangedAt = Date.now();
499
+ this.lastScreenChangedAt = now;
500
+ }
501
+ // Track per-region last-changed timestamps for stable_ms conditions.
502
+ if (cursor && this.prevScreenLines.length > 0) {
503
+ for (const state of this.spec.states) {
504
+ const conditions = collectChangedConditions(state.when);
505
+ for (const cond of conditions) {
506
+ if (cond.stable_ms == null) continue;
507
+ const startRow = Math.max(0, cursor.row - cond.cursor_above);
508
+ const endRow = cursor.row;
509
+ const cur = currentLines.slice(startRow, endRow).join('\n');
510
+ const prev = this.prevScreenLines.slice(startRow, endRow).join('\n');
511
+ if (cur !== prev) this.regionLastChangedAt.set(cond.cursor_above, now);
512
+ }
513
+ }
488
514
  }
489
515
  // Update prevScreenLines for next evaluation's `changed` condition detection.
490
516
  this.prevScreenLines = currentLines;
@@ -500,7 +526,7 @@ export class SpecDriver {
500
526
  let evState = ev.state;
501
527
  const busyHoldMs = this.spec.debounce?.busy_hold_ms ?? BUSY_HOLD_MS;
502
528
  if (this.currentStateId === 'busy' && evState.id === 'idle') {
503
- const ageMs = Date.now() - this.lastBusyAt;
529
+ const ageMs = now - this.lastBusyAt;
504
530
  if (ageMs < busyHoldMs) {
505
531
  // Pin to the last seen busy state directly — currentEval can
506
532
  // already be idle at this point (it tracks the previous tick,
@@ -509,6 +535,24 @@ export class SpecDriver {
509
535
  evState = this.lastBusyState ?? evState;
510
536
  }
511
537
  }
538
+ // stable_ms gate: if the matched idle state has a changed:false/stable_ms
539
+ // condition, verify the region has been stable long enough. If not, pin
540
+ // to busy and schedule a re-evaluation when the stable window expires.
541
+ if (evState.id === (this.spec.default_state ?? 'idle') && cursor) {
542
+ const stableConditions = collectChangedConditions(
543
+ this.spec.states.find(s => s.id === evState.id)?.when
544
+ ).filter(c => c.changed === false && c.stable_ms != null);
545
+ for (const cond of stableConditions) {
546
+ const lastChanged = this.regionLastChangedAt.get(cond.cursor_above) ?? 0;
547
+ const stableMs = cond.stable_ms!;
548
+ const stableAge = lastChanged > 0 ? now - lastChanged : Infinity;
549
+ if (stableAge < stableMs) {
550
+ evState = this.lastBusyState ?? { id: 'busy', label: 'Generating', title: null };
551
+ this.scheduleBusyExpiry(stableMs - stableAge + 50);
552
+ break;
553
+ }
554
+ }
555
+ }
512
556
  // Modal hold: when in a modal state (approval, picker, etc.) a brief
513
557
  // busy reading should not interrupt the modal. Claude Code streams
514
558
  // body content while the approval modal is visible, causing a spinner
@@ -538,7 +582,6 @@ export class SpecDriver {
538
582
  // approval appeared and its hold expires immediately after dismissal,
539
583
  // causing a false-idle even though the agent is still generating.
540
584
  const postModalGraceMs = busyHoldMs;
541
- const now = Date.now();
542
585
  const recentlyInModal = isModalState(this.currentStateId) || (this.lastModalAt > 0 && now - this.lastModalAt < postModalGraceMs);
543
586
  const recentlyLeftModal = !recentlyInModal && this.lastModalExitAt > 0 && now - this.lastModalExitAt < postModalGraceMs;
544
587
  // screen_active_hold_ms: suppress idle downshift while the screen is
@@ -555,7 +598,6 @@ export class SpecDriver {
555
598
  if (evState.id === 'busy' && completionIdleRule && !recentlyInModal && !recentlyLeftModal && !screenIsActive) {
556
599
  const completionKey = matchesCompletionIdleRule(this.spec, ev, screen);
557
600
  if (completionKey) {
558
- const now = Date.now();
559
601
  if (completionKey !== this.completionIdleKey) {
560
602
  this.completionIdleKey = completionKey;
561
603
  this.completionIdleFirstSeenAt = now;
@@ -227,8 +227,11 @@ function evaluateCondition(
227
227
  const endRow = cursor.row; // exclusive
228
228
  const currentSlice = curLines.slice(startRow, endRow).join('\n');
229
229
  const prevSlice = prevLines.slice(startRow, endRow).join('\n');
230
- const result = currentSlice !== prevSlice;
231
- trace.push({ kind: 'section', text: `state[${stateId}] changed cond cursor_above=${cond.cursor_above} rows[${startRow},${endRow}) changed=${result}` });
230
+ const didChange = currentSlice !== prevSlice;
231
+ // changed:false means "region is currently stable" stable_ms duration
232
+ // is enforced by the driver, not here.
233
+ const result = cond.changed ? didChange : !didChange;
234
+ trace.push({ kind: 'section', text: `state[${stateId}] changed cond cursor_above=${cond.cursor_above} rows[${startRow},${endRow}) changed=${didChange} expected=${cond.changed} result=${result}` });
232
235
  return result;
233
236
  }
234
237
 
@@ -460,7 +460,8 @@ export const SCHEMA_V3 = {
460
460
  "required": ["cursor_above", "changed"],
461
461
  "properties": {
462
462
  "cursor_above": { "type": "integer", "minimum": 1 },
463
- "changed": { "type": "boolean", "const": true }
463
+ "changed": { "type": "boolean" },
464
+ "stable_ms": { "type": "integer", "minimum": 0 }
464
465
  }
465
466
  },
466
467
  "allCondition": {
@@ -192,10 +192,13 @@ export interface RegexCondition {
192
192
  cursor_col_max?: number;
193
193
  }
194
194
 
195
- /** v3 delta condition: N lines above cursor changed vs prevLines. */
195
+ /** v3 delta condition: N lines above cursor changed vs prevLines.
196
+ * changed:false + stable_ms: region must be stable for at least N ms. */
196
197
  export interface ChangedCondition {
197
198
  cursor_above: number;
198
- changed: true;
199
+ changed: boolean;
200
+ /** Only for changed:false — region must have been stable for this many ms. */
201
+ stable_ms?: number;
199
202
  }
200
203
 
201
204
  /** v3 AND composite. */