@adhdev/daemon-core 0.6.56 → 0.6.57

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.6.56",
3
+ "version": "0.6.57",
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",
@@ -193,6 +193,56 @@ function shSingleQuote(arg: string): string {
193
193
  return `'${arg.replace(/'/g, `'\\''`)}'`;
194
194
  }
195
195
 
196
+ function estimatePromptDisplayLines(text: string, cols = 100): number {
197
+ const normalized = String(text || '').replace(/\r/g, '');
198
+ if (!normalized) return 1;
199
+ return normalized
200
+ .split('\n')
201
+ .reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / cols)), 0);
202
+ }
203
+
204
+ function extractPromptRetrySnippet(text: string): string {
205
+ const lines = String(text || '')
206
+ .replace(/\r/g, '')
207
+ .split('\n')
208
+ .map(line => line.trim())
209
+ .filter(Boolean);
210
+ const candidate = lines[lines.length - 1] || lines[0] || '';
211
+ return candidate.slice(-120);
212
+ }
213
+
214
+ function normalizePromptText(text: string): string {
215
+ return String(text || '').replace(/\s+/g, ' ').trim();
216
+ }
217
+
218
+ function compactPromptText(text: string): string {
219
+ return String(text || '').replace(/\s+/g, '').trim();
220
+ }
221
+
222
+ function promptLikelyVisible(screenText: string, promptSnippet: string): boolean {
223
+ const snippet = normalizePromptText(promptSnippet);
224
+ if (!snippet) return false;
225
+
226
+ const normalizedScreen = normalizePromptText(screenText);
227
+ if (normalizedScreen.includes(snippet)) return true;
228
+
229
+ const compactScreen = compactPromptText(screenText);
230
+ const compactSnippet = compactPromptText(promptSnippet);
231
+ if (compactSnippet && compactScreen.includes(compactSnippet)) return true;
232
+
233
+ const tokens = snippet
234
+ .split(/[^A-Za-z0-9_.:/-]+/)
235
+ .map(token => token.trim())
236
+ .filter(token => token.length >= 4);
237
+ if (tokens.length === 0) return false;
238
+
239
+ const required = Math.min(tokens.length, 3);
240
+ const matched = tokens.filter(token =>
241
+ normalizedScreen.includes(token) || compactScreen.includes(compactPromptText(token)),
242
+ ).length;
243
+ return matched >= required;
244
+ }
245
+
196
246
  /**
197
247
  * Normalize provider.json for auto-implement approval detection.
198
248
  * Kept for backward compat with dev-server auto-impl pipeline only.
@@ -273,6 +323,11 @@ export class ProviderCliAdapter implements CliAdapter {
273
323
  private settleTimer: NodeJS.Timeout | null = null;
274
324
  private settledBuffer: string = '';
275
325
  private submitPendingUntil = 0;
326
+ private responseSettleIgnoreUntil = 0;
327
+ private responseEpoch = 0;
328
+ private submitRetryTimer: NodeJS.Timeout | null = null;
329
+ private submitRetryUsed = false;
330
+ private submitRetryPromptSnippet = '';
276
331
 
277
332
  // Resize redraw suppression
278
333
  private resizeSuppressUntil: number = 0;
@@ -459,7 +514,7 @@ export class ProviderCliAdapter implements CliAdapter {
459
514
  this.startupParseGate = true;
460
515
  this.startupBuffer = '';
461
516
  this.terminalScreen.reset(40, 120);
462
- this.ready = true;
517
+ this.ready = false;
463
518
  this.setStatus('idle', 'pty_ready');
464
519
  this.onStatusChange?.();
465
520
  }
@@ -521,7 +576,9 @@ export class ProviderCliAdapter implements CliAdapter {
521
576
 
522
577
  if (isReady) {
523
578
  this.startupParseGate = false;
579
+ this.ready = true;
524
580
  LOG.info('CLI', `[${this.cliType}] Startup gate end (${elapsed}ms, scriptStatus=${scriptStatus})`);
581
+ this.onStatusChange?.();
525
582
  } else {
526
583
  return;
527
584
  }
@@ -533,6 +590,7 @@ export class ProviderCliAdapter implements CliAdapter {
533
590
 
534
591
  private scheduleSettle(): void {
535
592
  if (this.settleTimer) clearTimeout(this.settleTimer);
593
+ const settleEpoch = this.responseEpoch;
536
594
  const delay = Math.max(
537
595
  this.timeouts.outputSettle,
538
596
  this.submitPendingUntil > Date.now()
@@ -541,14 +599,40 @@ export class ProviderCliAdapter implements CliAdapter {
541
599
  );
542
600
  this.settleTimer = setTimeout(() => {
543
601
  this.settleTimer = null;
602
+ if (settleEpoch !== this.responseEpoch) return;
544
603
  this.settledBuffer = this.recentOutputBuffer;
545
604
  this.evaluateSettled();
546
605
  }, delay);
547
606
  }
548
607
 
608
+ private armApprovalExitTimeout(): void {
609
+ if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
610
+ this.approvalExitTimeout = setTimeout(() => {
611
+ if (this.currentStatus !== 'waiting_approval') return;
612
+ const tail = this.recentOutputBuffer;
613
+ const modal = this.runParseApproval(tail);
614
+ const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
615
+ if (stillWaiting) {
616
+ this.activeModal = modal || this.activeModal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
617
+ this.onStatusChange?.();
618
+ this.armApprovalExitTimeout();
619
+ return;
620
+ }
621
+ LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
622
+ this.activeModal = null;
623
+ this.lastApprovalResolvedAt = Date.now();
624
+ this.setStatus('idle', 'approval_timeout');
625
+ this.onStatusChange?.();
626
+ }, 60000);
627
+ }
628
+
549
629
  private evaluateSettled(): void {
630
+ if (this.submitPendingUntil > Date.now()) return;
631
+ if (this.responseSettleIgnoreUntil > Date.now()) return;
550
632
  const tail = this.settledBuffer;
551
- const scriptStatus = this.runDetectStatus(tail);
633
+ const modal = this.runParseApproval(tail);
634
+ const rawScriptStatus = this.runDetectStatus(tail);
635
+ const scriptStatus = rawScriptStatus === 'waiting_approval' || modal ? 'waiting_approval' : rawScriptStatus;
552
636
  if (!scriptStatus) return;
553
637
 
554
638
  const prevStatus = this.currentStatus;
@@ -560,20 +644,10 @@ export class ProviderCliAdapter implements CliAdapter {
560
644
  this.setStatus('waiting_approval', 'script_detect');
561
645
 
562
646
  // Use parseApproval script for modal info
563
- const modal = this.runParseApproval(tail);
564
647
  this.activeModal = modal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
565
648
 
566
649
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
567
- if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
568
- this.approvalExitTimeout = setTimeout(() => {
569
- if (this.currentStatus === 'waiting_approval') {
570
- LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
571
- this.activeModal = null;
572
- this.lastApprovalResolvedAt = Date.now();
573
- this.setStatus('idle', 'approval_timeout');
574
- this.onStatusChange?.();
575
- }
576
- }, 60000);
650
+ this.armApprovalExitTimeout();
577
651
  this.onStatusChange?.();
578
652
  return;
579
653
  }
@@ -607,7 +681,12 @@ export class ProviderCliAdapter implements CliAdapter {
607
681
  this.lastApprovalResolvedAt = Date.now();
608
682
  }
609
683
  if (this.isWaitingForResponse) {
610
- this.finishResponse();
684
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
685
+ this.idleTimeout = setTimeout(() => {
686
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
687
+ this.finishResponse();
688
+ }
689
+ }, this.timeouts.idleFinish);
611
690
  } else if (prevStatus !== 'idle') {
612
691
  this.setStatus('idle', 'script_detect');
613
692
  this.onStatusChange?.();
@@ -616,12 +695,18 @@ export class ProviderCliAdapter implements CliAdapter {
616
695
  }
617
696
 
618
697
  private finishResponse(): void {
698
+ if (this.submitPendingUntil > Date.now()) return;
699
+ if (this.responseSettleIgnoreUntil > Date.now()) return;
619
700
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
620
701
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
621
702
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
703
+ if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
622
704
 
623
705
  this.responseBuffer = '';
624
706
  this.isWaitingForResponse = false;
707
+ this.responseSettleIgnoreUntil = 0;
708
+ this.submitRetryUsed = false;
709
+ this.submitRetryPromptSnippet = '';
625
710
  this.activeModal = null;
626
711
  this.setStatus('idle', 'response_finished');
627
712
  this.onStatusChange?.();
@@ -744,6 +829,12 @@ export class ProviderCliAdapter implements CliAdapter {
744
829
 
745
830
  async sendMessage(text: string): Promise<void> {
746
831
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
832
+ if (this.startupParseGate) {
833
+ const deadline = Date.now() + 10000;
834
+ while (this.startupParseGate && Date.now() < deadline) {
835
+ await new Promise(resolve => setTimeout(resolve, 50));
836
+ }
837
+ }
747
838
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
748
839
  if (this.isWaitingForResponse) return;
749
840
 
@@ -751,24 +842,87 @@ export class ProviderCliAdapter implements CliAdapter {
751
842
  this.structuredMessages.push({ role: 'user', content: text, timestamp: Date.now() });
752
843
  this.isWaitingForResponse = true;
753
844
  this.responseBuffer = '';
845
+ this.submitRetryUsed = false;
846
+ this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
847
+ const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
848
+ if (this.submitRetryTimer) {
849
+ clearTimeout(this.submitRetryTimer);
850
+ this.submitRetryTimer = null;
851
+ }
852
+ const estimatedLines = estimatePromptDisplayLines(text);
853
+ const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
854
+ const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
855
+ const retryDelayMs = Math.max(350, Math.min(1500, Math.max(this.sendDelayMs, submitDelayMs)));
856
+ if (this.settleTimer) {
857
+ clearTimeout(this.settleTimer);
858
+ this.settleTimer = null;
859
+ }
860
+ this.responseEpoch += 1;
861
+ this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
754
862
  this.setStatus('generating', 'sendMessage');
755
863
  this.onStatusChange?.();
864
+ if (submitDelayMs > 0) {
865
+ this.submitPendingUntil = Date.now() + submitDelayMs;
866
+ }
756
867
  this.ptyProcess.write(text);
757
868
 
758
869
  const submit = () => {
759
870
  if (!this.ptyProcess) return;
760
871
  this.submitPendingUntil = 0;
761
872
  this.ptyProcess.write(this.sendKey);
873
+ const retrySubmitIfStuck = (attempt: number) => {
874
+ this.submitRetryTimer = null;
875
+ if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
876
+ if (this.currentStatus !== 'generating') return;
877
+ if ((this.responseBuffer || '').trim()) return;
878
+ const screenText = this.terminalScreen.getText();
879
+ if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
880
+ if (/Esc to interrupt|Do you want to proceed|This command requires approval|Allow Codex to|Approve and run now|Always approve this session|Running…|Running\.\.\./i.test(screenText)) return;
881
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
882
+ LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
883
+ this.ptyProcess.write(this.sendKey);
884
+ if (attempt >= 3) {
885
+ this.submitRetryUsed = true;
886
+ return;
887
+ }
888
+ this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(attempt + 1), retryDelayMs);
889
+ };
890
+ this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(1), retryDelayMs);
762
891
  this.responseTimeout = setTimeout(() => {
763
892
  if (this.isWaitingForResponse) this.finishResponse();
764
893
  }, this.timeouts.maxResponse);
765
894
  };
766
- if (this.sendDelayMs > 0) {
767
- this.submitPendingUntil = Date.now() + this.sendDelayMs;
768
- setTimeout(submit, this.sendDelayMs);
769
- } else {
770
- submit();
771
- }
895
+ const submitStartedAt = Date.now();
896
+ let lastNormalizedScreen = '';
897
+ let lastScreenChangeAt = submitStartedAt;
898
+ const waitForEchoAndSubmit = () => {
899
+ if (!this.ptyProcess) return;
900
+ const now = Date.now();
901
+ const elapsed = now - submitStartedAt;
902
+ const screenText = this.terminalScreen.getText();
903
+ const normalizedScreen = normalizePromptText(screenText);
904
+ if (normalizedScreen !== lastNormalizedScreen) {
905
+ lastNormalizedScreen = normalizedScreen;
906
+ lastScreenChangeAt = now;
907
+ }
908
+ const echoVisible = !normalizedPromptSnippet || promptLikelyVisible(screenText, normalizedPromptSnippet);
909
+
910
+ if (echoVisible) {
911
+ const screenSettled = (now - lastScreenChangeAt) >= 500;
912
+ if (elapsed >= submitDelayMs && screenSettled) {
913
+ submit();
914
+ return;
915
+ }
916
+ }
917
+
918
+ if (elapsed >= maxEchoWaitMs) {
919
+ submit();
920
+ return;
921
+ }
922
+
923
+ setTimeout(waitForEchoAndSubmit, 50);
924
+ };
925
+ waitForEchoAndSubmit();
772
926
  }
773
927
 
774
928
  getPartialResponse(): string {
@@ -781,6 +935,7 @@ export class ProviderCliAdapter implements CliAdapter {
781
935
  shutdown(): void {
782
936
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
783
937
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
938
+ if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
784
939
  if (this.ptyProcess) {
785
940
  this.ptyProcess.write('\x03');
786
941
  setTimeout(() => {
@@ -800,6 +955,8 @@ export class ProviderCliAdapter implements CliAdapter {
800
955
  this.structuredMessages = [];
801
956
  this.accumulatedBuffer = '';
802
957
  this.accumulatedRawBuffer = '';
958
+ this.submitRetryUsed = false;
959
+ this.submitRetryPromptSnippet = '';
803
960
  this.terminalScreen.reset();
804
961
  this.onStatusChange?.();
805
962
  }
@@ -812,7 +969,16 @@ export class ProviderCliAdapter implements CliAdapter {
812
969
  }
813
970
 
814
971
  resolveModal(buttonIndex: number): void {
815
- if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
972
+ if (!this.ptyProcess || (this.currentStatus !== 'waiting_approval' && !this.activeModal)) return;
973
+ this.activeModal = null;
974
+ this.lastApprovalResolvedAt = Date.now();
975
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
976
+ if (this.approvalExitTimeout) {
977
+ clearTimeout(this.approvalExitTimeout);
978
+ this.approvalExitTimeout = null;
979
+ }
980
+ this.setStatus('generating', 'approval_resolved');
981
+ this.onStatusChange?.();
816
982
  if (buttonIndex in this.approvalKeys) {
817
983
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
818
984
  } else {
@@ -855,6 +1021,10 @@ export class ProviderCliAdapter implements CliAdapter {
855
1021
  isWaitingForResponse: this.isWaitingForResponse,
856
1022
  activeModal: this.activeModal,
857
1023
  lastApprovalResolvedAt: this.lastApprovalResolvedAt,
1024
+ sendDelayMs: this.sendDelayMs,
1025
+ sendKey: this.sendKey,
1026
+ submitPendingUntil: this.submitPendingUntil,
1027
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
858
1028
  resizeSuppressUntil: this.resizeSuppressUntil,
859
1029
  hasCliScripts: this.hasCliScripts(),
860
1030
  scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),