@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.
- package/dist/cli-adapter-types.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/index.js +55 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +55 -2
- package/dist/index.mjs.map +1 -1
- package/dist/providers/approval-utils.d.ts +9 -0
- package/package.json +1 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +22 -0
- package/src/cli-adapters/provider-cli-parse.ts +6 -0
- package/src/cli-adapters/provider-cli-shared.ts +2 -0
- package/src/commands/chat-commands.ts +6 -0
- package/src/commands/cli-manager.ts +3 -1
- package/src/providers/approval-utils.ts +27 -0
- package/src/providers/cli-provider-instance.ts +26 -2
|
@@ -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
|
@@ -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;
|
package/src/cli-adapter-types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|