@adhdev/daemon-core 0.9.82-rc.134 → 0.9.82-rc.136

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.
@@ -5,3 +5,12 @@ export declare function pickApprovalButton(buttons: string[] | null | undefined,
5
5
  label: string;
6
6
  };
7
7
  export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string;
8
+ /**
9
+ * Returns true when the given text (e.g. last assistant message content, or
10
+ * the tail of the PTY screen) looks like an active approval/input prompt
11
+ * rather than a completed assistant response. Used to prevent false
12
+ * idle/completion events when Claude Code surfaces a "Do you want to proceed?"
13
+ * style prompt that the PTY parser captures as an assistant turn while the
14
+ * session is still awaiting user input.
15
+ */
16
+ export declare function looksLikeActiveApprovalPromptText(content: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.134",
3
+ "version": "0.9.82-rc.136",
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",
@@ -53,6 +53,7 @@ export interface CliAdapter {
53
53
  clearHistory?(): void;
54
54
  resolveAction?(data: unknown): Promise<void>;
55
55
  resolveModal?(buttonIndex: number): void;
56
+ isApprovalRecentlyResolved?(): boolean;
56
57
  setOnPtyData?(callback: (data: string) => void): void;
57
58
  writeRaw?(data: string): void;
58
59
  resize?(cols: number, rows: number): void;
@@ -58,6 +58,7 @@ export interface CliAdapter {
58
58
  clearHistory?(): void;
59
59
  resolveAction?(data: unknown): Promise<void>;
60
60
  resolveModal?(buttonIndex: number): void;
61
+ isApprovalRecentlyResolved?(): boolean;
61
62
  // Raw PTY I/O (for terminal view)
62
63
  setOnPtyData?(callback: (data: string) => void): void;
63
64
  writeRaw?(data: string): void;
@@ -164,6 +164,7 @@ export declare class ProviderCliAdapter implements CliAdapter {
164
164
  isReady(): boolean;
165
165
  writeRaw(data: string): Promise<void>;
166
166
  resolveModal(buttonIndex: number): void;
167
+ isApprovalRecentlyResolved(): boolean;
167
168
  resize(cols: number, rows: number): void;
168
169
  getDebugState(): Record<string, any>;
169
170
  getTraceState(limit?: number): Record<string, any>;
@@ -1669,6 +1669,8 @@ export class ProviderCliAdapter implements CliAdapter {
1669
1669
  recentOutputBuffer: this.recentOutputBuffer,
1670
1670
  terminalScreenText: parseScreenText,
1671
1671
  workingDir: this.workingDir,
1672
+ providerSessionId: this.providerSessionId || undefined,
1673
+ historySessionId: this.providerSessionId || undefined,
1672
1674
  baseMessages: [],
1673
1675
  partialResponse: this.responseBuffer,
1674
1676
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1951,6 +1953,8 @@ export class ProviderCliAdapter implements CliAdapter {
1951
1953
  recentOutputBuffer: this.recentOutputBuffer,
1952
1954
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1953
1955
  workingDir: this.workingDir,
1956
+ providerSessionId: this.providerSessionId || undefined,
1957
+ historySessionId: this.providerSessionId || undefined,
1954
1958
  baseMessages: [],
1955
1959
  partialResponse: this.responseBuffer,
1956
1960
  isWaitingForResponse: this.isWaitingForResponse,
@@ -2572,6 +2576,12 @@ export class ProviderCliAdapter implements CliAdapter {
2572
2576
  }
2573
2577
 
2574
2578
  updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
2579
+ const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
2580
+ ? meta.providerSessionId.trim()
2581
+ : '';
2582
+ if (nextProviderSessionId) {
2583
+ this.providerSessionId = nextProviderSessionId;
2584
+ }
2575
2585
  if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
2576
2586
  this.ptyProcess.updateMeta(meta, replace);
2577
2587
  }
@@ -2727,6 +2737,13 @@ export class ProviderCliAdapter implements CliAdapter {
2727
2737
  }
2728
2738
 
2729
2739
  resolveModal(buttonIndex: number): void {
2740
+ // Idempotency guard: if we already resolved an approval within the cooldown
2741
+ // window, do not write another key to the PTY. This prevents double-writes when
2742
+ // auto-approve fires and then the status poller re-enters before the PTY absorbs
2743
+ // the first keystroke, or when an external mesh_approve command races with auto-approve.
2744
+ if (this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown) {
2745
+ return;
2746
+ }
2730
2747
  let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
2731
2748
  if (!modal && typeof this.cliScripts?.parseSession === 'function') {
2732
2749
  try {
@@ -2775,6 +2792,11 @@ export class ProviderCliAdapter implements CliAdapter {
2775
2792
  }
2776
2793
  }
2777
2794
 
2795
+ /** Returns true if an approval was resolved within the adapter's cooldown window. */
2796
+ isApprovalRecentlyResolved(): boolean {
2797
+ return !!(this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown);
2798
+ }
2799
+
2778
2800
  resize(cols: number, rows: number): void {
2779
2801
  if (this.ptyProcess) {
2780
2802
  try {
@@ -36,6 +36,8 @@ export function buildCliParseInput(options: {
36
36
  recentOutputBuffer: string;
37
37
  terminalScreenText: string;
38
38
  workingDir?: string;
39
+ providerSessionId?: string;
40
+ historySessionId?: string;
39
41
  baseMessages: CliChatMessage[];
40
42
  partialResponse: string;
41
43
  isWaitingForResponse?: boolean;
@@ -48,6 +50,8 @@ export function buildCliParseInput(options: {
48
50
  recentOutputBuffer,
49
51
  terminalScreenText,
50
52
  workingDir,
53
+ providerSessionId,
54
+ historySessionId,
51
55
  baseMessages,
52
56
  partialResponse,
53
57
  isWaitingForResponse,
@@ -70,6 +74,8 @@ export function buildCliParseInput(options: {
70
74
  screenText,
71
75
  workspace: workingDir,
72
76
  workingDir,
77
+ providerSessionId,
78
+ historySessionId,
73
79
  screen: buildCliScreenSnapshot(screenText),
74
80
  bufferScreen: buildCliScreenSnapshot(buffer),
75
81
  recentScreen: buildCliScreenSnapshot(recentBuffer),
@@ -108,6 +108,8 @@ export interface CliScriptInput {
108
108
  screenText: string;
109
109
  workspace?: string;
110
110
  workingDir?: string;
111
+ providerSessionId?: string;
112
+ historySessionId?: string;
111
113
  screen: CliScreenSnapshot;
112
114
  bufferScreen: CliScreenSnapshot;
113
115
  recentScreen: CliScreenSnapshot;
@@ -2770,6 +2770,12 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
2770
2770
  if (buttonIndex < 0) {
2771
2771
  return { success: false, error: 'Approval action did not match any visible button' };
2772
2772
  }
2773
+ // Idempotency: if the adapter already resolved this approval within cooldown, report
2774
+ // stale_prompt rather than writing a second key to the PTY.
2775
+ if (typeof adapter.isApprovalRecentlyResolved === 'function' && adapter.isApprovalRecentlyResolved()) {
2776
+ LOG.info('Command', `[resolveAction] CLI PTY → stale_prompt (already resolved within cooldown)`);
2777
+ return { success: true, stalePrompt: true, buttonIndex, button: buttons[buttonIndex] ?? button };
2778
+ }
2773
2779
  if (typeof adapter.resolveModal === 'function') {
2774
2780
  adapter.resolveModal(buttonIndex);
2775
2781
  } else {
@@ -553,7 +553,9 @@ export class DaemonCliManager {
553
553
  providerSessionId,
554
554
  attachExisting,
555
555
  );
556
- return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
556
+ const adapter = new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
557
+ if (providerSessionId) adapter.updateRuntimeMeta({ providerSessionId });
558
+ return adapter;
557
559
  }
558
560
 
559
561
  throw new Error(`No CLI provider found for '${cliType}'. Create a provider.js in providers/cli/${cliType}/`);
@@ -72,3 +72,30 @@ export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: s
72
72
  if (cleanMessage) lines.push(cleanMessage);
73
73
  return lines.join('\n');
74
74
  }
75
+
76
+ /**
77
+ * Returns true when the given text (e.g. last assistant message content, or
78
+ * the tail of the PTY screen) looks like an active approval/input prompt
79
+ * rather than a completed assistant response. Used to prevent false
80
+ * idle/completion events when Claude Code surfaces a "Do you want to proceed?"
81
+ * style prompt that the PTY parser captures as an assistant turn while the
82
+ * session is still awaiting user input.
83
+ */
84
+ export function looksLikeActiveApprovalPromptText(content: string): boolean {
85
+ const text = content.trim();
86
+ if (!text || text.length > 2000) return false;
87
+ const hasApprovalQuestion = /do you want to (?:proceed|allow|run|make this edit|create)/i.test(text)
88
+ || /this command requires approval/i.test(text)
89
+ || /quick safety check/i.test(text)
90
+ || /is this a project you trust/i.test(text);
91
+ const hasNumberedChoices = /^\s*[❯›>]?\s*1[.)]\s+(?:yes|allow|proceed|run)/im.test(text)
92
+ || /^\s*1[.)]\s+yes\b/im.test(text);
93
+ if (hasApprovalQuestion && hasNumberedChoices) return true;
94
+ const lastLines = text.split(/\r?\n/).slice(-12).join('\n');
95
+ const hasDontAskAgain = /yes.*don'?t ask again/i.test(lastLines)
96
+ || /yes.*always allow/i.test(lastLines);
97
+ const hasNoOption = /^\s*[❯›>]?\s*\d+[.)]\s+no\b/im.test(lastLines);
98
+ if (hasDontAskAgain && hasNoOption) return true;
99
+ if (/what do you want to do\?/i.test(text) && /^\s*\d+[.)]\s+\S/m.test(text)) return true;
100
+ return false;
101
+ }
@@ -21,7 +21,7 @@ import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderN
21
21
  import { LOG } from '../logging/logger.js';
22
22
  import type { ChatMessage } from '../types.js';
23
23
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
24
- import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
24
+ import { formatAutoApprovalMessage, pickApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
25
25
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
26
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
27
  import { normalizeProviderSessionId } from './provider-session-id.js';
@@ -48,6 +48,7 @@ function isIdleStatus(value: unknown): boolean {
48
48
  return !status || status === 'idle' || status === 'ready';
49
49
  }
50
50
 
51
+
51
52
  function getMessageTime(message: unknown): number {
52
53
  if (!message || typeof message !== 'object') return 0;
53
54
  const record = message as { receivedAt?: unknown; timestamp?: unknown };
@@ -389,6 +390,9 @@ export class CliProviderInstance implements ProviderInstance {
389
390
  this.launchMode = options?.launchMode || 'new';
390
391
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
391
392
  this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
393
+ if (this.providerSessionId) {
394
+ this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
395
+ }
392
396
  this.monitor = new StatusMonitor();
393
397
  this.historyWriter = new ChatHistoryWriter();
394
398
  }
@@ -825,7 +829,11 @@ export class CliProviderInstance implements ProviderInstance {
825
829
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
826
830
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
827
831
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
828
- return role === 'assistant' && !!content;
832
+ if (role !== 'assistant' || !content) return false;
833
+ // Guard: if the last assistant message looks like an active approval/input prompt,
834
+ // it is not a real completion — the session is still awaiting user input.
835
+ if (looksLikeActiveApprovalPromptText(content)) return false;
836
+ return true;
829
837
  }
830
838
 
831
839
  private buildCompletedFinalizationDiagnostic(args: {
@@ -940,6 +948,22 @@ export class CliProviderInstance implements ProviderInstance {
940
948
  if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
941
949
  if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
942
950
 
951
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
952
+ // the turn is not complete even if the parsed status says idle and there is an assistant
953
+ // message. This catches the case where waiting_approval→idle transitions occur before
954
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
955
+ try {
956
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
957
+ ? String((this.adapter as any).getScreenText() || '')
958
+ : '';
959
+ if (screenText) {
960
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
961
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
962
+ return { reason: 'screen_shows_approval_prompt', terminal: false };
963
+ }
964
+ }
965
+ } catch { /* defensive: screen text read is best-effort */ }
966
+
943
967
  return null;
944
968
  }
945
969