@adhdev/daemon-core 0.9.82-rc.210 → 0.9.82-rc.212

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,10 @@ 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 of the last PTY frame that changed the screen content.
150
+ * Used by screen_active_hold_ms to suppress idle downshifts while
151
+ * the terminal is still actively updating. */
152
+ private lastScreenChangedAt;
149
153
  /** Timer that re-runs evaluate() once the hold window expires. Needed
150
154
  * because the PTY stops emitting once the agent finishes; without an
151
155
  * explicit wake-up there's nothing to trigger the busy → idle
@@ -755,6 +755,10 @@ export declare const SCHEMA_V3: {
755
755
  readonly type: "integer";
756
756
  readonly minimum: 0;
757
757
  };
758
+ readonly screen_active_hold_ms: {
759
+ readonly type: "integer";
760
+ readonly minimum: 0;
761
+ };
758
762
  readonly completion_marker: {
759
763
  readonly type: "object";
760
764
  readonly additionalProperties: false;
@@ -1451,6 +1455,10 @@ export declare const SCHEMA: {
1451
1455
  readonly type: "integer";
1452
1456
  readonly minimum: 0;
1453
1457
  };
1458
+ readonly screen_active_hold_ms: {
1459
+ readonly type: "integer";
1460
+ readonly minimum: 0;
1461
+ };
1454
1462
  readonly completion_marker: {
1455
1463
  readonly type: "object";
1456
1464
  readonly additionalProperties: false;
@@ -225,6 +225,11 @@ export interface CliSpec {
225
225
  busy_hold_ms?: number;
226
226
  idle_hold_ms?: number;
227
227
  startup_grace_ms?: number;
228
+ /** Suppress idle downshift while the screen is actively changing.
229
+ * When set, any PTY frame that changes the screen resets a timer;
230
+ * idle transitions (busy_hold expiry and completion_idle_after) are
231
+ * blocked until the screen has been stable for this many ms. */
232
+ screen_active_hold_ms?: number;
228
233
  completion_marker?: {
229
234
  section?: string;
230
235
  matches: string;
@@ -243,6 +248,7 @@ export interface CliSpec {
243
248
  busy_hold_ms?: number;
244
249
  idle_hold_ms?: number;
245
250
  startup_grace_ms?: number;
251
+ screen_active_hold_ms?: number;
246
252
  /**
247
253
  * Legacy field name for completion_marker. Populated by the loader
248
254
  * from `timing.completion_marker` for backward compat.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.210",
3
+ "version": "0.9.82-rc.212",
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",
@@ -38,12 +38,14 @@ function formatCompletionMetadata(event: Record<string, unknown>): string {
38
38
  const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
39
39
  ? String(completionDiagnostic.finalAssistantPresent)
40
40
  : '';
41
+ const evidenceLevel = readNonEmptyString(event.evidenceLevel);
41
42
  const parts = [
42
43
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
43
44
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
44
45
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
45
46
  diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
46
47
  finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
48
+ evidenceLevel && evidenceLevel !== 'sufficient' ? `evidence_level=${evidenceLevel}` : '',
47
49
  ].filter(Boolean);
48
50
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
49
51
  }
@@ -59,7 +61,10 @@ export function buildMeshSystemMessage(args: {
59
61
  if (args.metadataEvent.source === 'long_generating_reconciliation') {
60
62
  return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The long-generating monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
61
63
  }
62
- return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
64
+ const reviewNote = args.metadataEvent.reviewRecommended === true
65
+ ? ' Completion evidence is insufficient — verify via git status or provider_session_id before assuming the task is done. Use mesh_read_chat once if needed, but do not poll repeatedly.'
66
+ : ' Use mesh_read_chat once to review its final progress, but do not poll repeatedly.';
67
+ return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path;${reviewNote}`;
63
68
  }
64
69
  if (args.event === 'agent:waiting_approval') {
65
70
  return `[System] ${args.nodeLabel} is waiting for approval to proceed${metadata}. You may use mesh_read_chat and mesh_approve to handle it.`;
@@ -1544,9 +1544,10 @@ export class CliProviderInstance implements ProviderInstance {
1544
1544
  message: typeof modal?.message === 'string' ? modal.message.trim() : '',
1545
1545
  buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
1546
1546
  });
1547
- // PTY redraws can briefly leave waiting_approval and then re-enter it.
1548
- // Keep one coordinator event per logical modal until the turn completes.
1549
- if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
1547
+ // PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
1548
+ // Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
1549
+ // (e.g. antigravity-cli resolves one prompt and immediately shows the next) and would be silently dropped.
1550
+ if (approvalFingerprint !== this.lastApprovalEventFingerprint) {
1550
1551
  this.lastApprovalEventFingerprint = approvalFingerprint;
1551
1552
  this.appendRuntimeSystemMessage(
1552
1553
  this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
@@ -230,6 +230,10 @@ 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 of the last PTY frame that changed the screen content.
234
+ * Used by screen_active_hold_ms to suppress idle downshifts while
235
+ * the terminal is still actively updating. */
236
+ private lastScreenChangedAt = 0;
233
237
  /** Timer that re-runs evaluate() once the hold window expires. Needed
234
238
  * because the PTY stops emitting once the agent finishes; without an
235
239
  * explicit wake-up there's nothing to trigger the busy → idle
@@ -322,7 +326,17 @@ export class SpecDriver {
322
326
  }
323
327
 
324
328
  private cancelIdleHold(): void {
325
- if (this.idleHoldTimer) { clearTimeout(this.idleHoldTimer); this.idleHoldTimer = null; }
329
+ if (this.idleHoldTimer) {
330
+ clearTimeout(this.idleHoldTimer);
331
+ this.idleHoldTimer = null;
332
+ // If the idle hold was armed by a completion_idle_after forced-idle,
333
+ // cancelling it means new activity arrived while we were waiting.
334
+ // Reset the completion tracker so the hold window restarts fresh
335
+ // when the marker reappears — otherwise the stale firstSeenAt makes
336
+ // the hold appear already-expired on the next PTY frame that matches.
337
+ this.completionIdleKey = '';
338
+ this.completionIdleFirstSeenAt = 0;
339
+ }
326
340
  this.pendingIdleState = null;
327
341
  }
328
342
 
@@ -460,9 +474,14 @@ export class SpecDriver {
460
474
  private reevaluate(forceEmit = false): void {
461
475
  const screen = this.adapter.snapshot();
462
476
  const cursor = this.adapter.getCursorPosition();
477
+ const currentLines = screen.split('\n').map(l => l.endsWith('\r') ? l.slice(0, -1) : l);
463
478
  const ev = evaluate(this.spec, screen, cursor, this.prevScreenLines.length > 0 ? this.prevScreenLines : undefined);
479
+ // Track when the screen last changed for screen_active_hold_ms.
480
+ if (this.prevScreenLines.length > 0 && currentLines.join('\n') !== this.prevScreenLines.join('\n')) {
481
+ this.lastScreenChangedAt = Date.now();
482
+ }
464
483
  // Update prevScreenLines for next evaluation's `changed` condition detection.
465
- this.prevScreenLines = screen.split('\n').map(l => l.endsWith('\r') ? l.slice(0, -1) : l);
484
+ this.prevScreenLines = currentLines;
466
485
 
467
486
  // Busy hold: many TUIs flicker between busy and idle every frame
468
487
  // (claude in particular — its token counter appears and disappears
@@ -503,6 +522,7 @@ export class SpecDriver {
503
522
  }
504
523
  }
505
524
  const completionIdleRule = this.spec.debounce?.completion_idle_after;
525
+ const screenActiveHoldMs = this.spec.debounce?.screen_active_hold_ms;
506
526
  let busyWakeMs = busyHoldMs;
507
527
  // Don't fire completion_idle_after while in a modal state (approval,
508
528
  // picker, etc.) or within a grace period after leaving one. Two cases:
@@ -515,7 +535,18 @@ export class SpecDriver {
515
535
  const now = Date.now();
516
536
  const recentlyInModal = isModalState(this.currentStateId) || (this.lastModalAt > 0 && now - this.lastModalAt < postModalGraceMs);
517
537
  const recentlyLeftModal = !recentlyInModal && this.lastModalExitAt > 0 && now - this.lastModalExitAt < postModalGraceMs;
518
- if (evState.id === 'busy' && completionIdleRule && !recentlyInModal && !recentlyLeftModal) {
538
+ // screen_active_hold_ms: suppress idle downshift while the screen is
539
+ // actively changing. Any PTY frame that mutates screen content resets
540
+ // lastScreenChangedAt; we hold off until it has been stable for the
541
+ // configured duration. Applies to both completion_idle_after and the
542
+ // busy_hold expiry path below.
543
+ const screenActiveMs = screenActiveHoldMs ?? 0;
544
+ const screenStableMs = this.lastScreenChangedAt > 0 ? now - this.lastScreenChangedAt : Infinity;
545
+ const screenIsActive = screenActiveMs > 0 && screenStableMs < screenActiveMs;
546
+ if (screenIsActive) {
547
+ busyWakeMs = Math.min(busyWakeMs, screenActiveMs - screenStableMs + 50);
548
+ }
549
+ if (evState.id === 'busy' && completionIdleRule && !recentlyInModal && !recentlyLeftModal && !screenIsActive) {
519
550
  const completionKey = matchesCompletionIdleRule(this.spec, ev, screen);
520
551
  if (completionKey) {
521
552
  const now = Date.now();
@@ -550,8 +581,18 @@ export class SpecDriver {
550
581
  this.completionIdleFirstSeenAt = 0;
551
582
  }
552
583
  } else if (evState.id !== 'busy') {
553
- this.completionIdleKey = '';
554
- this.completionIdleFirstSeenAt = 0;
584
+ if (!screenIsActive) {
585
+ this.completionIdleKey = '';
586
+ this.completionIdleFirstSeenAt = 0;
587
+ }
588
+ }
589
+
590
+ // screen_active_hold_ms: if the screen is still changing and the
591
+ // evaluator wants to downshift from busy to idle, pin to busy instead.
592
+ // This catches paths not covered by the completion_idle_after gate above
593
+ // (e.g. direct idle via busy_hold expiry or default-state fallback).
594
+ if (screenIsActive && this.currentStateId === 'busy' && evState.id === (this.spec.default_state ?? 'idle')) {
595
+ evState = this.lastBusyState ?? evState;
555
596
  }
556
597
 
557
598
  if (evState.id === 'busy') {
@@ -79,6 +79,7 @@ function attachDebounceAlias(spec: CliSpec): void {
79
79
  ...(t.busy_hold_ms !== undefined ? { busy_hold_ms: t.busy_hold_ms } : {}),
80
80
  ...(t.idle_hold_ms !== undefined ? { idle_hold_ms: t.idle_hold_ms } : {}),
81
81
  ...(t.startup_grace_ms !== undefined ? { startup_grace_ms: t.startup_grace_ms } : {}),
82
+ ...(t.screen_active_hold_ms !== undefined ? { screen_active_hold_ms: t.screen_active_hold_ms } : {}),
82
83
  ...(cm ? {
83
84
  completion_idle_after: {
84
85
  ...(cm.section ? { section: cm.section } : {}),
@@ -384,6 +384,7 @@ export const SCHEMA_V3 = {
384
384
  "busy_hold_ms": { "type": "integer", "minimum": 0 },
385
385
  "idle_hold_ms": { "type": "integer", "minimum": 0 },
386
386
  "startup_grace_ms": { "type": "integer", "minimum": 0 },
387
+ "screen_active_hold_ms": { "type": "integer", "minimum": 0 },
387
388
  "completion_marker": {
388
389
  "type": "object",
389
390
  "additionalProperties": false,
@@ -46,6 +46,7 @@
46
46
  "busy_hold_ms": { "type": "integer", "minimum": 0 },
47
47
  "idle_hold_ms": { "type": "integer", "minimum": 0 },
48
48
  "startup_grace_ms": { "type": "integer", "minimum": 0 },
49
+ "screen_active_hold_ms": { "type": "integer", "minimum": 0 },
49
50
  "completion_marker": {
50
51
  "type": "object",
51
52
  "additionalProperties": false,
@@ -273,6 +273,11 @@ export interface CliSpec {
273
273
  busy_hold_ms?: number;
274
274
  idle_hold_ms?: number;
275
275
  startup_grace_ms?: number;
276
+ /** Suppress idle downshift while the screen is actively changing.
277
+ * When set, any PTY frame that changes the screen resets a timer;
278
+ * idle transitions (busy_hold expiry and completion_idle_after) are
279
+ * blocked until the screen has been stable for this many ms. */
280
+ screen_active_hold_ms?: number;
276
281
  completion_marker?: {
277
282
  section?: string;
278
283
  matches: string;
@@ -291,6 +296,7 @@ export interface CliSpec {
291
296
  busy_hold_ms?: number;
292
297
  idle_hold_ms?: number;
293
298
  startup_grace_ms?: number;
299
+ screen_active_hold_ms?: number;
294
300
  /**
295
301
  * Legacy field name for completion_marker. Populated by the loader
296
302
  * from `timing.completion_marker` for backward compat.