@adhdev/daemon-core 0.5.62 → 0.5.64

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.5.62",
3
+ "version": "0.5.64",
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",
@@ -11,7 +11,7 @@
11
11
  * binary: string — binary name
12
12
  * spawn: { command, args, shell, env }
13
13
  * patterns: { prompt, generating, approval, ready }
14
- * timeouts?: { idleFinish, generatingIdle, maxResponse, approvalCooldown, ... }
14
+ * timeouts?: { idleFinish, generatingIdle, maxResponse, approvalCooldown, outputSettle, ... }
15
15
  * cleanOutput(raw, lastUserInput): string
16
16
  */
17
17
 
@@ -89,6 +89,8 @@ export interface CliProviderModule {
89
89
  maxResponse?: number;
90
90
  /** shutdown after kill wait (default 1000ms) */
91
91
  shutdownGrace?: number;
92
+ /** Output settle debounce before evaluating idle/approval (default 300ms) */
93
+ outputSettle?: number;
92
94
  };
93
95
  cleanOutput(raw: string, lastUserInput?: string): string;
94
96
  }
@@ -271,6 +273,10 @@ export class ProviderCliAdapter implements CliAdapter {
271
273
  private approvalTransitionBuffer: string = '';
272
274
  private approvalExitTimeout: NodeJS.Timeout | null = null;
273
275
 
276
+ // Output settle debounce — fires after PTY output goes quiet
277
+ private settleTimer: NodeJS.Timeout | null = null;
278
+ private settledBuffer: string = ''; // snapshot of recentOutputBuffer at settle time
279
+
274
280
  // Resize redraw suppression
275
281
  private resizeSuppressUntil: number = 0;
276
282
 
@@ -310,6 +316,7 @@ export class ProviderCliAdapter implements CliAdapter {
310
316
  idleFinish: t.idleFinish ?? 5000,
311
317
  maxResponse: t.maxResponse ?? 300000,
312
318
  shutdownGrace: t.shutdownGrace ?? 1000,
319
+ outputSettle: t.outputSettle ?? 300,
313
320
  };
314
321
 
315
322
  // Load approval key mapping from provider (e.g. approvalKeys: {"0":"1","1":"2","2":"3"})
@@ -474,55 +481,69 @@ export class ProviderCliAdapter implements CliAdapter {
474
481
  return;
475
482
  }
476
483
 
477
- // ─── Phase 2: Approval detect
478
- // DEBUG: log recent output for approval pattern debugging
479
484
  if (cleanData.trim().length > 5) {
480
485
  LOG.debug('CLI', `[${this.cliType}] output chunk (${cleanData.length}): ${cleanData.slice(0, 300).replace(/\n/g, '\\n')}`);
481
486
  }
482
- const hasApproval = patterns.approval.some(p => p.test(this.recentOutputBuffer));
483
- if (hasApproval && this.currentStatus !== 'waiting_approval') {
484
- const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
485
- if (!inCooldown) {
486
- // Capture context before clearing (recentOutputBuffer still has content here)
487
- const ctxLines = this.recentOutputBuffer.split('\n')
488
- .map(l => l.trim())
489
- .filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
487
+
488
+ // ─── Phase 2: generating — immediate detection (fast UX response)
489
+ if (!this.isWaitingForResponse) {
490
+ if (patterns.generating.some(p => p.test(cleanData))) {
491
+ if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
490
492
  this.isWaitingForResponse = true;
491
- this.setStatus('waiting_approval', 'approval_pattern');
492
- this.recentOutputBuffer = '';
493
- this.approvalTransitionBuffer = '';
494
- this.activeModal = {
495
- message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
496
- buttons: this.cliType === 'claude-cli'
497
- ? ['Yes (y)', 'Always allow (a)', 'Deny (Esc)']
498
- : ['Allow once', 'Always allow', 'Deny'],
499
- };
500
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
501
- // Safety timeout if stuck in waiting_approval, auto-exit after 60s
502
- if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
503
- this.approvalExitTimeout = setTimeout(() => {
504
- if (this.currentStatus === 'waiting_approval') {
505
- LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
506
- this.activeModal = null;
507
- this.lastApprovalResolvedAt = Date.now();
508
- this.recentOutputBuffer = '';
509
- this.approvalTransitionBuffer = '';
510
- this.approvalExitTimeout = null;
511
- this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
512
- this.onStatusChange?.();
513
- }
514
- }, 60000);
493
+ this.responseBuffer = '';
494
+ this.setStatus('generating', 'autonomous_gen');
495
+ this.onStatusChange?.();
496
+ }
497
+ }
498
+
499
+ if (this.isWaitingForResponse) {
500
+ this.responseBuffer += cleanData;
501
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
502
+
503
+ // ─── Phase 3: still generating
504
+ if (patterns.generating.some(p => p.test(cleanData))) {
505
+ this.setStatus('generating', 'still_generating');
506
+ this.idleTimeout = setTimeout(() => {
507
+ if (this.isWaitingForResponse) this.finishResponse();
508
+ }, this.timeouts.generatingIdle);
515
509
  this.onStatusChange?.();
510
+ // Cancel any pending settle — we're clearly still generating
511
+ if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
516
512
  return;
517
513
  }
518
- // In cooldown — don't set approval, but DO continue to other phases
519
514
  }
520
515
 
521
- // ─── Phase 3: Approval release
522
- // Accumulate chunks into approvalTransitionBuffer — the approval dialog clears via ANSI
523
- // sequences that strip to nothing, so we can't rely on a single cleanData chunk matching.
516
+ // ─── Phase 4: waiting_approval — accumulate transition buffer
524
517
  if (this.currentStatus === 'waiting_approval') {
525
518
  this.approvalTransitionBuffer = (this.approvalTransitionBuffer + cleanData).slice(-500);
519
+ // Schedule settle check — output may still be arriving (ANSI redraws etc)
520
+ this.scheduleSettle();
521
+ return;
522
+ }
523
+
524
+ // ─── Phase 5: settle debounce — schedule idle/approval evaluation
525
+ this.scheduleSettle();
526
+ }
527
+
528
+ /**
529
+ * Fired after output goes quiet for outputSettle ms.
530
+ * Evaluates the stabilised buffer for approval, prompt (idle), or timeout.
531
+ */
532
+ private scheduleSettle(): void {
533
+ if (this.settleTimer) clearTimeout(this.settleTimer);
534
+ this.settleTimer = setTimeout(() => {
535
+ this.settleTimer = null;
536
+ this.settledBuffer = this.recentOutputBuffer;
537
+ this.evaluateSettled();
538
+ }, this.timeouts.outputSettle);
539
+ }
540
+
541
+ private evaluateSettled(): void {
542
+ const { patterns } = this.provider;
543
+ const buf = this.settledBuffer;
544
+
545
+ // ─── waiting_approval: check for transition out
546
+ if (this.currentStatus === 'waiting_approval') {
526
547
  const genResume = patterns.generating.some(p => p.test(this.approvalTransitionBuffer));
527
548
  const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
528
549
  if (genResume) {
@@ -541,47 +562,56 @@ export class ProviderCliAdapter implements CliAdapter {
541
562
  this.lastApprovalResolvedAt = Date.now();
542
563
  this.finishResponse();
543
564
  }
565
+ // else: still waiting — approvalExitTimeout will handle the 60s safety case
544
566
  return;
545
567
  }
546
568
 
547
- // ─── Phase 4: autonomous generation detection (generating starts without sendMessage)
548
- if (!this.isWaitingForResponse) {
549
- if (patterns.generating.some(p => p.test(cleanData))) {
569
+ // ─── check for approval on stabilised buffer
570
+ const hasApproval = patterns.approval.some(p => p.test(buf));
571
+ if (hasApproval) {
572
+ const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
573
+ if (!inCooldown) {
574
+ const ctxLines = buf.split('\n')
575
+ .map(l => l.trim())
576
+ .filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
550
577
  this.isWaitingForResponse = true;
551
- this.responseBuffer = '';
552
- this.setStatus('generating', 'autonomous_gen');
578
+ this.setStatus('waiting_approval', 'approval_pattern');
579
+ this.recentOutputBuffer = '';
580
+ this.approvalTransitionBuffer = '';
581
+ this.activeModal = {
582
+ message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
583
+ buttons: this.cliType === 'claude-cli'
584
+ ? ['Yes (y)', 'Always allow (a)', 'Deny (Esc)']
585
+ : ['Allow once', 'Always allow', 'Deny'],
586
+ };
587
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
588
+ if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
589
+ this.approvalExitTimeout = setTimeout(() => {
590
+ if (this.currentStatus === 'waiting_approval') {
591
+ LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
592
+ this.activeModal = null;
593
+ this.lastApprovalResolvedAt = Date.now();
594
+ this.recentOutputBuffer = '';
595
+ this.approvalTransitionBuffer = '';
596
+ this.approvalExitTimeout = null;
597
+ this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
598
+ this.onStatusChange?.();
599
+ }
600
+ }, 60000);
553
601
  this.onStatusChange?.();
602
+ return;
554
603
  }
555
604
  }
556
605
 
557
- // ─── Phase 5: response collect
606
+ // ─── check for prompt (idle) on stabilised buffer
558
607
  if (this.isWaitingForResponse) {
559
- this.responseBuffer += cleanData;
560
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
561
-
562
- const stillGenerating = patterns.generating.some(p => p.test(cleanData));
563
- if (stillGenerating) {
564
- this.setStatus('generating', 'still_generating');
565
- this.idleTimeout = setTimeout(() => {
566
- if (this.isWaitingForResponse) this.finishResponse();
567
- }, this.timeouts.generatingIdle);
568
- this.onStatusChange?.();
569
- return;
570
- }
571
-
572
- // Prompt → response complete
573
- // Only check the LAST 2 lines of cleanData — the prompt appears at the very end of the
574
- // output stream. Checking the full chunk causes false positives when response content
575
- // has '>' in code blocks, shell examples, or mid-response lines.
576
- const trailingLines = cleanData.split('\n').slice(-2).join('\n');
577
- if (patterns.prompt.some(p => p.test(trailingLines))) {
578
- // Guard: don't finishResponse if approval text is also present —
579
- // Claude Code's approval UI can contain prompt-like characters
580
- const hasApprovalHere = patterns.approval.some(p => p.test(this.recentOutputBuffer));
581
- if (!hasApprovalHere) {
582
- this.finishResponse();
583
- }
584
- } else {
608
+ // Only look at the trailing portion — prompt appears at the very end
609
+ const trailingLines = buf.split('\n').slice(-3).join('\n');
610
+ if (patterns.prompt.some(p => p.test(trailingLines)) && !hasApproval) {
611
+ this.finishResponse();
612
+ } else if (!patterns.generating.some(p => p.test(buf))) {
613
+ // Output has settled with no generating signal and no prompt — schedule idle finish
614
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
585
615
  this.idleTimeout = setTimeout(() => {
586
616
  if (this.isWaitingForResponse && this.responseBuffer.trim()) {
587
617
  this.finishResponse();
@@ -659,6 +689,7 @@ export class ProviderCliAdapter implements CliAdapter {
659
689
  cancel(): void { this.shutdown(); }
660
690
 
661
691
  shutdown(): void {
692
+ if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
662
693
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
663
694
  if (this.ptyProcess) {
664
695
  this.ptyProcess.write('\x03');
@@ -730,6 +761,7 @@ export class ProviderCliAdapter implements CliAdapter {
730
761
  // Buffers
731
762
  startupBuffer: this.startupBuffer.slice(-500),
732
763
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
764
+ settledBuffer: this.settledBuffer.slice(-500),
733
765
  responseBuffer: this.responseBuffer.slice(-500),
734
766
  approvalTransitionBuffer: this.approvalTransitionBuffer.slice(-500),
735
767
  // State
@@ -117,39 +117,30 @@ export class ExtensionProviderInstance implements ProviderInstance {
117
117
  }
118
118
 
119
119
  // ─── status transition detect ──────────────────────────────
120
+ // NOTE: Extension transitions are TRACKED but NOT emitted as events.
121
+ // The parent IdeProviderInstance already emits identical events
122
+ // (generating_started, generating_completed, waiting_approval)
123
+ // via its own detectAgentTransitions(). Emitting here would cause
124
+ // duplicate toasts with slightly different content.
120
125
 
121
- private detectTransition(newStatus: string, data: any): void {
126
+ private detectTransition(newStatus: string, _data: any): void {
122
127
  const now = Date.now();
123
128
  const agentStatus = (newStatus === 'streaming' || newStatus === 'generating') ? 'generating'
124
129
  : newStatus === 'waiting_approval' ? 'waiting_approval'
125
130
  : 'idle';
126
131
 
127
132
  if (agentStatus !== this.lastAgentStatus) {
128
- const chatTitle = this.provider.name;
129
-
133
+ // Track generating start time (for monitor elapsed calculation)
130
134
  if (this.lastAgentStatus === 'idle' && agentStatus === 'generating') {
131
135
  this.generatingStartedAt = now;
132
- this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
133
- } else if (agentStatus === 'waiting_approval') {
134
- if (!this.generatingStartedAt) this.generatingStartedAt = now;
135
- const msg = data?.activeModal?.message || data?.modalMessage;
136
- this.pushEvent({
137
- event: 'agent:waiting_approval', chatTitle, timestamp: now,
138
- ideType: this.ideType,
139
- agentType: this.type,
140
- modalMessage: msg,
141
- modalButtons: data?.activeModal?.buttons || data?.modalButtons,
142
- });
143
136
  } else if (agentStatus === 'idle' && (this.lastAgentStatus === 'generating' || this.lastAgentStatus === 'waiting_approval')) {
144
- const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
145
- this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
146
137
  this.generatingStartedAt = 0;
147
138
  }
148
-
139
+ // Do NOT pushEvent for transitions — parent IDE instance handles these
149
140
  this.lastAgentStatus = agentStatus;
150
141
  }
151
142
 
152
- // Monitor check (cooldown based notification)
143
+ // Monitor check (cooldown based notification) — keep monitor events (long_generating etc)
153
144
  const agentKey = `${this.type}:ext`;
154
145
  const monitorEvents = this.monitor.check(agentKey, agentStatus, now);
155
146
  for (const me of monitorEvents) {