@adhdev/daemon-core 0.9.82-rc.204 → 0.9.82-rc.205

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.
@@ -135,6 +135,8 @@ export declare class SpecDriver {
135
135
  * explicit wake-up there's nothing to trigger the busy → idle
136
136
  * downshift. */
137
137
  private busyExpiryTimer;
138
+ /** Set true while reevaluate() is invoked from busyExpiryTimer callback. */
139
+ private busyExpiryFired;
138
140
  /** Pending idle-commit timer. Armed when the evaluator first returns idle;
139
141
  * fires after idle_hold_ms if no non-idle reading has cancelled it. */
140
142
  private idleHoldTimer;
@@ -162,6 +164,12 @@ export declare class SpecDriver {
162
164
  label: string;
163
165
  at: number;
164
166
  durationMs: number;
167
+ reason: string;
168
+ matchedStateId?: string;
169
+ matchedRules?: string[];
170
+ debounceKind?: string;
171
+ idleHoldMs?: number;
172
+ busyHoldMs?: number;
165
173
  }>;
166
174
  getLastBusyAt(): number;
167
175
  hasIdleHoldPending(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.204",
3
+ "version": "0.9.82-rc.205",
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",
@@ -1568,8 +1568,6 @@ export class CliProviderInstance implements ProviderInstance {
1568
1568
  const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
1569
1569
  LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
1570
1570
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1571
- this.generatingDebouncePending = null;
1572
- this.generatingStartedAt = 0;
1573
1571
  // Emit completion for mesh task association even though the UI generating
1574
1572
  // started/completed pair is suppressed (too short for visible UI update).
1575
1573
  let shortFinalSummary: string | undefined;
@@ -1580,22 +1578,30 @@ export class CliProviderInstance implements ProviderInstance {
1580
1578
  shortEvidenceSource = evidence.source;
1581
1579
  shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
1582
1580
  } catch { /* best-effort */ }
1583
- if (((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary) {
1584
- LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
1585
- } else {
1586
- this.pushEvent({
1587
- event: 'agent:generating_completed',
1588
- chatTitle,
1589
- duration: 0,
1590
- timestamp: now,
1591
- finalSummary: shortFinalSummary,
1592
- completionDiagnostic: {
1593
- reason: 'short_generating_suppressed',
1594
- shortDurationMs,
1595
- finalAssistantEvidenceSource: shortEvidenceSource,
1596
- },
1597
- });
1581
+ // If a real response is confirmed, retroactively emit started so the chat
1582
+ // bubble appears even though the debounce suppressed the original event.
1583
+ if (shortFinalSummary) {
1584
+ this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
1585
+ }
1586
+ this.generatingDebouncePending = null;
1587
+ this.generatingStartedAt = 0;
1588
+ const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary;
1589
+ if (missingEvidence) {
1590
+ LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
1598
1591
  }
1592
+ this.pushEvent({
1593
+ event: 'agent:generating_completed',
1594
+ chatTitle,
1595
+ duration: 0,
1596
+ timestamp: now,
1597
+ finalSummary: shortFinalSummary,
1598
+ completionDiagnostic: {
1599
+ reason: 'short_generating_suppressed',
1600
+ shortDurationMs,
1601
+ finalAssistantEvidenceSource: shortEvidenceSource,
1602
+ ...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
1603
+ },
1604
+ });
1599
1605
  } else {
1600
1606
  // Debounce completed, then require the rich transcript path that read_chat
1601
1607
  // uses to show an idle turn whose last user-facing message is assistant.
@@ -218,6 +218,8 @@ export class SpecDriver {
218
218
  * explicit wake-up there's nothing to trigger the busy → idle
219
219
  * downshift. */
220
220
  private busyExpiryTimer: ReturnType<typeof setTimeout> | null = null;
221
+ /** Set true while reevaluate() is invoked from busyExpiryTimer callback. */
222
+ private busyExpiryFired = false;
221
223
  /** Pending idle-commit timer. Armed when the evaluator first returns idle;
222
224
  * fires after idle_hold_ms if no non-idle reading has cancelled it. */
223
225
  private idleHoldTimer: ReturnType<typeof setTimeout> | null = null;
@@ -225,7 +227,18 @@ export class SpecDriver {
225
227
  private pendingIdleState: SpecEvaluation['state'] | null = null;
226
228
  private specWatcher: fs.FSWatcher | null = null;
227
229
  /** Ring buffer of committed state transitions (max 50). */
228
- private stateHistory: Array<{ stateId: string; label: string; at: number; durationMs: number }> = [];
230
+ private stateHistory: Array<{
231
+ stateId: string;
232
+ label: string;
233
+ at: number;
234
+ durationMs: number;
235
+ reason: string;
236
+ matchedStateId?: string;
237
+ matchedRules?: string[];
238
+ debounceKind?: string;
239
+ idleHoldMs?: number;
240
+ busyHoldMs?: number;
241
+ }> = [];
229
242
  private prevStateAt = 0;
230
243
 
231
244
  constructor(private readonly opts: SpecDriverOpts) {
@@ -296,15 +309,44 @@ export class SpecDriver {
296
309
  this.pendingIdleState = null;
297
310
  }
298
311
 
299
- private pushHistory(stateId: string, label: string): void {
312
+ private pushHistory(stateId: string, label: string, meta?: {
313
+ reason?: string;
314
+ matchedStateId?: string;
315
+ matchedRules?: string[];
316
+ debounceKind?: string;
317
+ idleHoldMs?: number;
318
+ busyHoldMs?: number;
319
+ }): void {
300
320
  const now = Date.now();
301
321
  const durationMs = this.prevStateAt > 0 ? now - this.prevStateAt : 0;
302
322
  this.prevStateAt = now;
303
- this.stateHistory.push({ stateId, label, at: now, durationMs });
323
+ this.stateHistory.push({
324
+ stateId,
325
+ label,
326
+ at: now,
327
+ durationMs,
328
+ reason: meta?.reason ?? 'eval_match',
329
+ ...(meta?.matchedStateId !== undefined ? { matchedStateId: meta.matchedStateId } : {}),
330
+ ...(meta?.matchedRules !== undefined ? { matchedRules: meta.matchedRules } : {}),
331
+ ...(meta?.debounceKind !== undefined ? { debounceKind: meta.debounceKind } : {}),
332
+ ...(meta?.idleHoldMs !== undefined ? { idleHoldMs: meta.idleHoldMs } : {}),
333
+ ...(meta?.busyHoldMs !== undefined ? { busyHoldMs: meta.busyHoldMs } : {}),
334
+ });
304
335
  if (this.stateHistory.length > 50) this.stateHistory.shift();
305
336
  }
306
337
 
307
- getStateHistory(): ReadonlyArray<{ stateId: string; label: string; at: number; durationMs: number }> {
338
+ getStateHistory(): ReadonlyArray<{
339
+ stateId: string;
340
+ label: string;
341
+ at: number;
342
+ durationMs: number;
343
+ reason: string;
344
+ matchedStateId?: string;
345
+ matchedRules?: string[];
346
+ debounceKind?: string;
347
+ idleHoldMs?: number;
348
+ busyHoldMs?: number;
349
+ }> {
308
350
  return this.stateHistory;
309
351
  }
310
352
 
@@ -381,7 +423,9 @@ export class SpecDriver {
381
423
  this.busyExpiryTimer = setTimeout(() => {
382
424
  this.busyExpiryTimer = null;
383
425
  LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] busyExpiry fired holdMs=${holdMs}`);
426
+ this.busyExpiryFired = true;
384
427
  this.reevaluate();
428
+ this.busyExpiryFired = false;
385
429
  }, Math.max(holdMs + 50, 100));
386
430
  }
387
431
 
@@ -477,6 +521,8 @@ export class SpecDriver {
477
521
  if (isIdleState && idleHoldMs > 0 && this.currentStateId !== evState.id) {
478
522
  if (!this.idleHoldTimer) {
479
523
  this.pendingIdleState = evState;
524
+ const capturedEv = ev;
525
+ const capturedMatchedRules = extractMatchedRules(ev);
480
526
  this.idleHoldTimer = setTimeout(() => {
481
527
  this.idleHoldTimer = null;
482
528
  const committed = this.pendingIdleState;
@@ -484,16 +530,22 @@ export class SpecDriver {
484
530
  if (!committed) return;
485
531
  LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] idleHold committed after ${idleHoldMs}ms`);
486
532
  this.currentStateId = committed.id;
487
- this.currentEval = ev;
488
- this.pushHistory(committed.id, committed.label);
533
+ this.currentEval = capturedEv;
534
+ this.pushHistory(committed.id, committed.label, {
535
+ reason: 'idle_hold_committed',
536
+ matchedStateId: capturedEv.state.id,
537
+ matchedRules: capturedMatchedRules,
538
+ debounceKind: 'idle_hold',
539
+ idleHoldMs,
540
+ });
489
541
  this.emit({
490
542
  kind: 'state_changed',
491
543
  state: committed,
492
544
  modal: null,
493
- controls: ev.controls.map(c => ({ id: c.id, label: c.label, action_type: c.actionType })),
545
+ controls: capturedEv.controls.map(c => ({ id: c.id, label: c.label, action_type: c.actionType })),
494
546
  });
495
547
  this.armOrCancelDelegateTimers(committed.id);
496
- if (this.opts.emitTrace) this.emit({ kind: 'spec_trace', entries: ev.trace });
548
+ if (this.opts.emitTrace) this.emit({ kind: 'spec_trace', entries: capturedEv.trace });
497
549
  }, idleHoldMs);
498
550
  }
499
551
  // Don't fall through to the normal changed/emit path for idle.
@@ -538,7 +590,34 @@ export class SpecDriver {
538
590
  }
539
591
  if (changed) {
540
592
  this.currentStateId = evState.id;
541
- this.pushHistory(evState.id, evState.label);
593
+ const matchedRules = extractMatchedRules(ev);
594
+ // Determine reason and debounce kind for this transition
595
+ let transitionReason: string;
596
+ let debounceKind: string;
597
+ let transitionBusyHoldMs: number | undefined;
598
+ if (forceEmit) {
599
+ transitionReason = 'forceEmit';
600
+ debounceKind = 'none';
601
+ } else if (evState.id !== ev.state.id) {
602
+ // evState was overridden — completion_idle_after forced idle
603
+ transitionReason = 'completion_idle_after';
604
+ debounceKind = 'completion_idle_after';
605
+ } else if (this.busyExpiryFired) {
606
+ // busyExpiryTimer woke us up and hold has now expired
607
+ transitionReason = 'busy_hold_expired';
608
+ debounceKind = 'busy_hold';
609
+ transitionBusyHoldMs = busyHoldMs;
610
+ } else {
611
+ transitionReason = 'eval_match';
612
+ debounceKind = 'none';
613
+ }
614
+ this.pushHistory(evState.id, evState.label, {
615
+ reason: transitionReason,
616
+ matchedStateId: ev.state.id,
617
+ matchedRules,
618
+ debounceKind,
619
+ ...(transitionBusyHoldMs !== undefined ? { busyHoldMs: transitionBusyHoldMs } : {}),
620
+ });
542
621
  this.emit({
543
622
  kind: 'state_changed',
544
623
  state: evState,
@@ -731,3 +810,11 @@ function guessExt(mime: string): string {
731
810
  if (/webp/i.test(mime)) return '.webp';
732
811
  return '.bin';
733
812
  }
813
+
814
+ function extractMatchedRules(ev: SpecEvaluation): string[] {
815
+ if (!Array.isArray(ev.trace)) return [];
816
+ return (ev.trace as any[])
817
+ .filter(t => t.matched === true || t.kind === 'state_match')
818
+ .map(t => t.rule ?? t.stateId ?? t.id ?? String(t))
819
+ .filter(Boolean);
820
+ }