@adhdev/daemon-core 0.8.14 → 0.8.16
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-adapters/provider-cli-adapter.d.ts +9 -0
- package/dist/daemon/dev-auto-implement.d.ts +1 -0
- package/dist/index.js +278 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +281 -62
- package/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +189 -18
- package/src/cli-adapters/pty-transport.ts +17 -6
- package/src/commands/upgrade-helper.ts +9 -3
- package/src/daemon/dev-auto-implement.ts +9 -0
- package/src/daemon/dev-cli-debug.ts +58 -21
package/package.json
CHANGED
|
@@ -27,15 +27,7 @@ import {
|
|
|
27
27
|
type PtyRuntimeTransport,
|
|
28
28
|
type PtyTransportFactory,
|
|
29
29
|
} from './pty-transport.js';
|
|
30
|
-
import { sanitizeSpawnEnv
|
|
31
|
-
|
|
32
|
-
let pty: any;
|
|
33
|
-
try {
|
|
34
|
-
pty = require('node-pty');
|
|
35
|
-
ensureNodePtySpawnHelperPermissions((msg: string) => LOG.info('CLI', msg));
|
|
36
|
-
} catch {
|
|
37
|
-
LOG.error('CLI', '[ProviderCliAdapter] node-pty not found. Terminal features disabled.');
|
|
38
|
-
}
|
|
30
|
+
import { sanitizeSpawnEnv } from './spawn-env.js';
|
|
39
31
|
|
|
40
32
|
// ─── Types ──────────────────────────────────────────
|
|
41
33
|
|
|
@@ -77,6 +69,7 @@ export interface CliScriptInput {
|
|
|
77
69
|
screenText: string; // Current visible screen snapshot
|
|
78
70
|
messages: CliChatMessage[]; // Previously parsed messages
|
|
79
71
|
partialResponse: string; // Current partial response being generated
|
|
72
|
+
promptText?: string; // Current turn prompt when available
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
interface TurnParseScope {
|
|
@@ -320,6 +313,45 @@ function normalizeComparableMessageContent(text: string): string {
|
|
|
320
313
|
.trim();
|
|
321
314
|
}
|
|
322
315
|
|
|
316
|
+
function trimPromptEchoPrefix(text: string, promptText?: string | null): string {
|
|
317
|
+
const prompt = normalizeComparableMessageContent(String(promptText || ''));
|
|
318
|
+
if (!prompt) return String(text || '');
|
|
319
|
+
|
|
320
|
+
const lines = String(text || '').split(/\r\n|\n|\r/g);
|
|
321
|
+
let dropCount = 0;
|
|
322
|
+
for (let index = 0; index < Math.min(lines.length, 6); index += 1) {
|
|
323
|
+
const fragment = normalizeComparableMessageContent(lines[index].replace(/^[.…]+\s*/, ''));
|
|
324
|
+
if (!fragment) {
|
|
325
|
+
if (dropCount === index) dropCount = index + 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const fragmentWordCount = fragment ? fragment.split(/\s+/).filter(Boolean).length : 0;
|
|
329
|
+
const canBePromptEcho = fragment.length >= 16 || fragmentWordCount >= 4;
|
|
330
|
+
if (canBePromptEcho && prompt.includes(fragment)) {
|
|
331
|
+
dropCount = index + 1;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return lines.slice(dropCount).join('\n').trim();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getLastUserPromptText(messages: Array<{ role?: string; content?: string }> | null | undefined): string {
|
|
341
|
+
const items = Array.isArray(messages) ? messages : [];
|
|
342
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
343
|
+
const message = items[index];
|
|
344
|
+
if (message?.role === 'user' && typeof message.content === 'string' && message.content.trim()) {
|
|
345
|
+
return message.content;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return '';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function looksLikeConfirmOnlyLabel(label: string): boolean {
|
|
352
|
+
return /^(?:continue|confirm|ok|yes|trust|proceed|enter)$/i.test(String(label || '').trim());
|
|
353
|
+
}
|
|
354
|
+
|
|
323
355
|
/**
|
|
324
356
|
* Normalize provider.json for auto-implement approval detection.
|
|
325
357
|
* Kept for backward compat with dev-server auto-impl pipeline only.
|
|
@@ -418,6 +450,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
418
450
|
private submitRetryUsed = false;
|
|
419
451
|
private submitRetryPromptSnippet = '';
|
|
420
452
|
private idleFinishCandidate: IdleFinishCandidate | null = null;
|
|
453
|
+
private finishRetryTimer: NodeJS.Timeout | null = null;
|
|
454
|
+
private finishRetryCount = 0;
|
|
421
455
|
|
|
422
456
|
// Resize redraw suppression
|
|
423
457
|
private resizeSuppressUntil: number = 0;
|
|
@@ -442,6 +476,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
442
476
|
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
443
477
|
private readonly providerResolutionMeta: Record<string, any>;
|
|
444
478
|
private static readonly IDLE_FINISH_CONFIRM_MS = 900;
|
|
479
|
+
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
480
|
+
private static readonly MAX_FINISH_RETRIES = 2;
|
|
445
481
|
|
|
446
482
|
private syncMessageViews(): void {
|
|
447
483
|
this.messages = [...this.committedMessages];
|
|
@@ -527,6 +563,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
527
563
|
screenText: this.terminalScreen.getText(),
|
|
528
564
|
messages: [...baseMessages],
|
|
529
565
|
partialResponse,
|
|
566
|
+
promptText: scope?.prompt || '',
|
|
530
567
|
};
|
|
531
568
|
}
|
|
532
569
|
|
|
@@ -882,6 +919,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
882
919
|
this.terminalScreen.reset(24, 80);
|
|
883
920
|
this.pendingTerminalQueryTail = '';
|
|
884
921
|
this.currentTurnScope = null;
|
|
922
|
+
this.finishRetryCount = 0;
|
|
923
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
885
924
|
this.ready = false;
|
|
886
925
|
await this.ptyProcess.ready;
|
|
887
926
|
this.recordTrace('ready', {
|
|
@@ -937,11 +976,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
937
976
|
if (this.startupParseGate) {
|
|
938
977
|
this.startupBuffer += cleanData;
|
|
939
978
|
const elapsed = Date.now() - this.spawnAt;
|
|
940
|
-
const scriptStatus = this.runDetectStatus(this.startupBuffer);
|
|
941
979
|
const screenText = this.terminalScreen.getText() || '';
|
|
980
|
+
const startupModal = this.getStartupConfirmationModal(screenText);
|
|
981
|
+
const scriptStatus = startupModal ? 'waiting_approval' : this.runDetectStatus(this.startupBuffer);
|
|
942
982
|
const hasInteractivePrompt = this.looksLikeVisibleIdlePrompt(screenText);
|
|
943
983
|
const startupStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
944
984
|
const isReady = ((scriptStatus === 'idle' || scriptStatus === 'waiting_approval') && hasInteractivePrompt && startupStableMs >= 700)
|
|
985
|
+
|| (!!startupModal && startupStableMs >= 700)
|
|
945
986
|
|| elapsed > 8000
|
|
946
987
|
|| this.startupBuffer.length > 12000;
|
|
947
988
|
|
|
@@ -983,7 +1024,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
983
1024
|
this.approvalExitTimeout = setTimeout(() => {
|
|
984
1025
|
if (this.currentStatus !== 'waiting_approval') return;
|
|
985
1026
|
const tail = this.recentOutputBuffer;
|
|
986
|
-
const
|
|
1027
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1028
|
+
const startupModal = this.getStartupConfirmationModal(screenText);
|
|
1029
|
+
const modal = this.runParseApproval(tail) || startupModal;
|
|
987
1030
|
const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
|
|
988
1031
|
if (stillWaiting) {
|
|
989
1032
|
this.activeModal = modal || this.activeModal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
|
|
@@ -1010,6 +1053,76 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1010
1053
|
|| /for\s*shortcuts/i.test(text);
|
|
1011
1054
|
}
|
|
1012
1055
|
|
|
1056
|
+
private looksLikeVisibleAssistantCandidate(screenText: string): boolean {
|
|
1057
|
+
const lines = sanitizeTerminalText(String(screenText || '')).split(/\r\n|\n|\r/g);
|
|
1058
|
+
for (const line of lines) {
|
|
1059
|
+
const trimmed = String(line || '').trim();
|
|
1060
|
+
if (!trimmed) continue;
|
|
1061
|
+
if (/^➜\s+\S+/.test(trimmed)) continue;
|
|
1062
|
+
if (/^Update available!/i.test(trimmed)) continue;
|
|
1063
|
+
if (/Claude Code v\d/i.test(trimmed)) continue;
|
|
1064
|
+
if (/^⏵⏵\s+accept edits on/i.test(trimmed)) continue;
|
|
1065
|
+
if (/^[◐◑◒◓◴◵◶◷◸◹◺◿].*\/effort/i.test(trimmed)) continue;
|
|
1066
|
+
if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+$/.test(trimmed)) continue;
|
|
1067
|
+
if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) continue;
|
|
1068
|
+
const assistantMatch = trimmed.match(/^⏺\s+(.+)$/);
|
|
1069
|
+
if (!assistantMatch) continue;
|
|
1070
|
+
const content = assistantMatch[1].trim();
|
|
1071
|
+
if (!content) continue;
|
|
1072
|
+
if (/^(?:Bash|Read|Write|Edit|MultiEdit|Task|Glob|Grep|LS|NotebookEdit)\(/.test(content)) continue;
|
|
1073
|
+
if (/This command requires approval|Do you want to proceed|Allow once|Always allow/i.test(content)) continue;
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
|
|
1080
|
+
if (!this.currentTurnScope) return false;
|
|
1081
|
+
if (this.currentStatus === 'waiting_approval' || this.activeModal) return false;
|
|
1082
|
+
if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
|
|
1083
|
+
if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
|
|
1084
|
+
|
|
1085
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1086
|
+
if (!this.looksLikeVisibleAssistantCandidate(screenText)) return false;
|
|
1087
|
+
|
|
1088
|
+
const now = Date.now();
|
|
1089
|
+
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1090
|
+
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1091
|
+
return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private getStartupConfirmationModal(screenText: string): { message: string; buttons: string[] } | null {
|
|
1095
|
+
const text = sanitizeTerminalText(String(screenText || ''));
|
|
1096
|
+
if (!text.trim()) return null;
|
|
1097
|
+
|
|
1098
|
+
if (this.cliType === 'claude-cli') {
|
|
1099
|
+
const hasTrustPrompt = /Quick safety check/i.test(text)
|
|
1100
|
+
|| /Is this a project you trust/i.test(text)
|
|
1101
|
+
|| /Do you trust (?:this project|the contents of this directory|the files in this folder)/i.test(text);
|
|
1102
|
+
const hasConfirmFooter = /Press Enter to (?:continue|confirm)/i.test(text)
|
|
1103
|
+
|| /Enter to confirm/i.test(text)
|
|
1104
|
+
|| /Esc to (?:cancel|exit)/i.test(text);
|
|
1105
|
+
if (hasTrustPrompt || (hasConfirmFooter && /trust/i.test(text))) {
|
|
1106
|
+
return {
|
|
1107
|
+
message: 'Confirm Claude Code project trust',
|
|
1108
|
+
buttons: ['Continue'],
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private shouldResolveModalWithEnter(modal: { message: string; buttons: string[] } | null, buttonIndex: number): boolean {
|
|
1117
|
+
if (!modal || buttonIndex !== 0) return false;
|
|
1118
|
+
const buttons = Array.isArray(modal.buttons) ? modal.buttons : [];
|
|
1119
|
+
if (buttons.length !== 1) return false;
|
|
1120
|
+
const buttonLabel = String(buttons[0] || '').trim();
|
|
1121
|
+
const modalText = `${modal.message || ''} ${buttonLabel}`.trim();
|
|
1122
|
+
return looksLikeConfirmOnlyLabel(buttonLabel)
|
|
1123
|
+
|| /Quick safety check|project trust|trust (?:this project|the contents of this directory|the files in this folder)|Enter to confirm/i.test(modalText);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1013
1126
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
1014
1127
|
const startedAt = Date.now();
|
|
1015
1128
|
let loggedWait = false;
|
|
@@ -1068,10 +1181,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1068
1181
|
}
|
|
1069
1182
|
const tail = this.settledBuffer;
|
|
1070
1183
|
const screenText = this.terminalScreen.getText() || '';
|
|
1071
|
-
const
|
|
1184
|
+
const startupModal = this.getStartupConfirmationModal(screenText);
|
|
1185
|
+
const modal = this.runParseApproval(tail) || startupModal;
|
|
1072
1186
|
const rawScriptStatus = this.runDetectStatus(tail);
|
|
1073
1187
|
// detectStatus is the sole authority for status. parseApproval only enriches modal info.
|
|
1074
|
-
const scriptStatus = rawScriptStatus;
|
|
1188
|
+
const scriptStatus = startupModal ? 'waiting_approval' : rawScriptStatus;
|
|
1075
1189
|
const parsedTranscript = this.parseCurrentTranscript(
|
|
1076
1190
|
this.committedMessages,
|
|
1077
1191
|
this.responseBuffer,
|
|
@@ -1292,24 +1406,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1292
1406
|
this.recordTrace('finish_response', {
|
|
1293
1407
|
...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
|
|
1294
1408
|
});
|
|
1295
|
-
this.commitCurrentTranscript();
|
|
1409
|
+
const commitResult = this.commitCurrentTranscript();
|
|
1410
|
+
if (this.shouldRetryFinishResponse(commitResult)) {
|
|
1411
|
+
this.finishRetryCount += 1;
|
|
1412
|
+
this.recordTrace('finish_response_retry', {
|
|
1413
|
+
retryCount: this.finishRetryCount,
|
|
1414
|
+
retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
|
|
1415
|
+
assistantContent: this.summarizeTraceText(commitResult.assistantContent, 220),
|
|
1416
|
+
...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
|
|
1417
|
+
});
|
|
1418
|
+
if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
|
|
1419
|
+
this.finishRetryTimer = setTimeout(() => {
|
|
1420
|
+
this.finishRetryTimer = null;
|
|
1421
|
+
if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
|
|
1422
|
+
this.finishResponse();
|
|
1423
|
+
}
|
|
1424
|
+
}, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1296
1427
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1297
1428
|
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
1298
1429
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1299
1430
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
1431
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1300
1432
|
|
|
1301
1433
|
this.responseBuffer = '';
|
|
1302
1434
|
this.isWaitingForResponse = false;
|
|
1303
1435
|
this.responseSettleIgnoreUntil = 0;
|
|
1304
1436
|
this.submitRetryUsed = false;
|
|
1305
1437
|
this.submitRetryPromptSnippet = '';
|
|
1438
|
+
this.finishRetryCount = 0;
|
|
1306
1439
|
this.currentTurnScope = null;
|
|
1307
1440
|
this.activeModal = null;
|
|
1308
1441
|
this.setStatus('idle', 'response_finished');
|
|
1309
1442
|
this.onStatusChange?.();
|
|
1310
1443
|
}
|
|
1311
1444
|
|
|
1312
|
-
private commitCurrentTranscript():
|
|
1445
|
+
private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
|
|
1313
1446
|
const parsed = this.parseCurrentTranscript(
|
|
1314
1447
|
this.committedMessages,
|
|
1315
1448
|
this.responseBuffer,
|
|
@@ -1317,6 +1450,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1317
1450
|
);
|
|
1318
1451
|
if (parsed && Array.isArray(parsed.messages)) {
|
|
1319
1452
|
this.committedMessages = this.normalizeParsedMessages(parsed.messages);
|
|
1453
|
+
const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
|
|
1454
|
+
if (promptForTrim) {
|
|
1455
|
+
const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1456
|
+
if (lastAssistantForTrim) {
|
|
1457
|
+
lastAssistantForTrim.content = trimPromptEchoPrefix(lastAssistantForTrim.content, promptForTrim);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1320
1460
|
this.syncMessageViews();
|
|
1321
1461
|
const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1322
1462
|
this.recordTrace('commit_transcript', {
|
|
@@ -1332,7 +1472,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1332
1472
|
`[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(this.summarizeTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
1333
1473
|
);
|
|
1334
1474
|
}
|
|
1475
|
+
return {
|
|
1476
|
+
hasAssistant: !!lastAssistant,
|
|
1477
|
+
assistantContent: lastAssistant?.content || '',
|
|
1478
|
+
};
|
|
1335
1479
|
}
|
|
1480
|
+
return {
|
|
1481
|
+
hasAssistant: false,
|
|
1482
|
+
assistantContent: '',
|
|
1483
|
+
};
|
|
1336
1484
|
}
|
|
1337
1485
|
|
|
1338
1486
|
// ─── Script Execution ──────────────────────────
|
|
@@ -1419,7 +1567,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1419
1567
|
if (!this.cliScripts?.parseOutput) return null;
|
|
1420
1568
|
try {
|
|
1421
1569
|
const input = this.buildParseInput(baseMessages, partialResponse, scope);
|
|
1422
|
-
|
|
1570
|
+
const parsed = this.cliScripts.parseOutput(input);
|
|
1571
|
+
const promptForTrim = scope?.prompt || getLastUserPromptText(baseMessages);
|
|
1572
|
+
if (parsed && Array.isArray(parsed.messages) && promptForTrim) {
|
|
1573
|
+
const lastAssistant = [...parsed.messages].reverse().find((message: any) => message?.role === 'assistant' && typeof message.content === 'string');
|
|
1574
|
+
if (lastAssistant) {
|
|
1575
|
+
lastAssistant.content = trimPromptEchoPrefix(lastAssistant.content, promptForTrim);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return parsed;
|
|
1423
1579
|
} catch (e: any) {
|
|
1424
1580
|
LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${e.message}`);
|
|
1425
1581
|
return null;
|
|
@@ -1464,11 +1620,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1464
1620
|
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
1465
1621
|
if (this.isWaitingForResponse) return;
|
|
1466
1622
|
await this.waitForInteractivePrompt();
|
|
1623
|
+
const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || '');
|
|
1624
|
+
if (blockingModal || this.currentStatus === 'waiting_approval') {
|
|
1625
|
+
throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
|
|
1626
|
+
}
|
|
1467
1627
|
|
|
1468
1628
|
this.committedMessages.push({ role: 'user', content: text, timestamp: Date.now() });
|
|
1469
1629
|
this.syncMessageViews();
|
|
1470
1630
|
this.isWaitingForResponse = true;
|
|
1471
1631
|
this.responseBuffer = '';
|
|
1632
|
+
this.finishRetryCount = 0;
|
|
1633
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1472
1634
|
this.clearIdleFinishCandidate('send_message');
|
|
1473
1635
|
this.currentTurnScope = {
|
|
1474
1636
|
prompt: text,
|
|
@@ -1705,6 +1867,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1705
1867
|
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
1706
1868
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1707
1869
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
1870
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1708
1871
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1709
1872
|
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
1710
1873
|
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
@@ -1713,6 +1876,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1713
1876
|
this.pendingTerminalQueryTail = '';
|
|
1714
1877
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
1715
1878
|
this.ptyOutputBuffer = '';
|
|
1879
|
+
this.finishRetryCount = 0;
|
|
1716
1880
|
if (this.ptyProcess) {
|
|
1717
1881
|
this.ptyProcess.write('\x03');
|
|
1718
1882
|
setTimeout(() => {
|
|
@@ -1732,6 +1896,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1732
1896
|
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
1733
1897
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1734
1898
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
1899
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1735
1900
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1736
1901
|
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
1737
1902
|
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
@@ -1740,6 +1905,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1740
1905
|
this.pendingTerminalQueryTail = '';
|
|
1741
1906
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
1742
1907
|
this.ptyOutputBuffer = '';
|
|
1908
|
+
this.finishRetryCount = 0;
|
|
1743
1909
|
if (this.ptyProcess) {
|
|
1744
1910
|
try {
|
|
1745
1911
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -1770,6 +1936,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1770
1936
|
this.pendingTerminalQueryTail = '';
|
|
1771
1937
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
1772
1938
|
this.ptyOutputBuffer = '';
|
|
1939
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1940
|
+
this.finishRetryCount = 0;
|
|
1773
1941
|
this.terminalScreen.reset();
|
|
1774
1942
|
this.ptyProcess?.clearBuffer?.();
|
|
1775
1943
|
this.onStatusChange?.();
|
|
@@ -1788,10 +1956,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1788
1956
|
|
|
1789
1957
|
resolveModal(buttonIndex: number): void {
|
|
1790
1958
|
if (!this.ptyProcess || (this.currentStatus !== 'waiting_approval' && !this.activeModal)) return;
|
|
1959
|
+
const modal = this.activeModal;
|
|
1791
1960
|
this.clearIdleFinishCandidate('resolve_modal');
|
|
1792
1961
|
this.recordTrace('resolve_modal', {
|
|
1793
1962
|
buttonIndex,
|
|
1794
|
-
activeModal:
|
|
1963
|
+
activeModal: modal,
|
|
1795
1964
|
});
|
|
1796
1965
|
this.activeModal = null;
|
|
1797
1966
|
this.lastApprovalResolvedAt = Date.now();
|
|
@@ -1802,7 +1971,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1802
1971
|
}
|
|
1803
1972
|
this.setStatus('generating', 'approval_resolved');
|
|
1804
1973
|
this.onStatusChange?.();
|
|
1805
|
-
if (
|
|
1974
|
+
if (this.shouldResolveModalWithEnter(modal, buttonIndex)) {
|
|
1975
|
+
this.ptyProcess.write('\r');
|
|
1976
|
+
} else if (buttonIndex in this.approvalKeys) {
|
|
1806
1977
|
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
1807
1978
|
} else {
|
|
1808
1979
|
const DOWN = '\x1B[B';
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import * as os from 'os';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { ensureNodePtySpawnHelperPermissions } from './spawn-env.js';
|
|
3
|
+
|
|
4
|
+
let cachedPty: any | null | undefined;
|
|
5
|
+
|
|
6
|
+
function loadNodePty(): any {
|
|
7
|
+
if (cachedPty !== undefined) return cachedPty;
|
|
8
|
+
try {
|
|
9
|
+
// Keep node-pty out of processes that delegate PTY ownership elsewhere
|
|
10
|
+
// (for example via session-host on Windows), so native PTY crashes do not
|
|
11
|
+
// take down the daemon just by importing this module.
|
|
12
|
+
cachedPty = require('node-pty');
|
|
13
|
+
ensureNodePtySpawnHelperPermissions();
|
|
14
|
+
} catch {
|
|
15
|
+
cachedPty = null;
|
|
16
|
+
}
|
|
17
|
+
return cachedPty;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
export interface PtySpawnOptions {
|
|
@@ -90,6 +100,7 @@ class NodePtyRuntimeTransport implements PtyRuntimeTransport {
|
|
|
90
100
|
|
|
91
101
|
export class NodePtyTransportFactory implements PtyTransportFactory {
|
|
92
102
|
spawn(command: string, args: string[], options: PtySpawnOptions): PtyRuntimeTransport {
|
|
103
|
+
const pty = loadNodePty();
|
|
93
104
|
if (!pty) throw new Error('node-pty is not installed');
|
|
94
105
|
// Validate cwd — an invalid directory causes a native crash on Windows
|
|
95
106
|
// (node-pty error code 267: ERROR_DIRECTORY) that bypasses JS try/catch
|
|
@@ -32,7 +32,11 @@ function appendUpgradeLog(message: string): void {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function getNpmExecutable(): string {
|
|
35
|
-
return
|
|
35
|
+
return 'npm';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getNpmExecOptions(): { shell: boolean } {
|
|
39
|
+
return { shell: process.platform === 'win32' };
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
function killPid(pid: number): boolean {
|
|
@@ -104,9 +108,10 @@ function removeDaemonPidFile(): void {
|
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
function cleanupStaleGlobalInstallDirs(pkgName: string): void {
|
|
107
|
-
const
|
|
111
|
+
const npmExecOpts = getNpmExecOptions();
|
|
112
|
+
const npmRoot = execFileSync(getNpmExecutable(), ['root', '-g'], { encoding: 'utf8', ...npmExecOpts }).trim();
|
|
108
113
|
if (!npmRoot) return;
|
|
109
|
-
const npmPrefix = execFileSync(getNpmExecutable(), ['prefix', '-g'], { encoding: 'utf8' }).trim();
|
|
114
|
+
const npmPrefix = execFileSync(getNpmExecutable(), ['prefix', '-g'], { encoding: 'utf8', ...npmExecOpts }).trim();
|
|
110
115
|
const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
|
111
116
|
const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
|
|
112
117
|
const binNames = new Set<string>([packageBaseName]);
|
|
@@ -175,6 +180,7 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
175
180
|
encoding: 'utf8',
|
|
176
181
|
stdio: 'pipe',
|
|
177
182
|
maxBuffer: 20 * 1024 * 1024,
|
|
183
|
+
...getNpmExecOptions(),
|
|
178
184
|
},
|
|
179
185
|
);
|
|
180
186
|
if (installOutput.trim()) {
|
|
@@ -27,6 +27,7 @@ type CliExerciseVerification = {
|
|
|
27
27
|
lastAssistantMustNotMatchAny?: string[];
|
|
28
28
|
inspectFields?: string[];
|
|
29
29
|
description?: string;
|
|
30
|
+
focusAreas?: string[];
|
|
30
31
|
fixtureName?: string;
|
|
31
32
|
fixtureNames?: string[];
|
|
32
33
|
};
|
|
@@ -1226,6 +1227,14 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
|
|
|
1226
1227
|
lines.push('20. When a bug comes from noisy PTY text, first normalize and classify the line family; do NOT just append another special-case substring to the parser.');
|
|
1227
1228
|
lines.push('');
|
|
1228
1229
|
|
|
1230
|
+
if (verification?.focusAreas?.length) {
|
|
1231
|
+
lines.push('## Provider-Specific Focus Areas');
|
|
1232
|
+
for (const area of verification.focusAreas) {
|
|
1233
|
+
lines.push(`- ${area}`);
|
|
1234
|
+
}
|
|
1235
|
+
lines.push('');
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1229
1238
|
lines.push('## Task');
|
|
1230
1239
|
lines.push(`Edit files in \`${providerDir}\` to implement: **${functions.join(', ')}**`);
|
|
1231
1240
|
lines.push('');
|
|
@@ -383,6 +383,61 @@ export async function runCliExerciseInternal(ctx: DevServerContext, body: CliExe
|
|
|
383
383
|
let idleSince = 0;
|
|
384
384
|
let sawBusy = false;
|
|
385
385
|
|
|
386
|
+
const noteStatus = (status: string) => {
|
|
387
|
+
if (status !== lastStatus) {
|
|
388
|
+
statusesSeen.push(status);
|
|
389
|
+
lastStatus = status;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const resolveActiveModalIfNeeded = (status: string, modal: any): boolean => {
|
|
394
|
+
if (!autoResolveApprovals || status !== 'waiting_approval' || !modal || !Array.isArray(modal.buttons) || modal.buttons.length === 0) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const clampedIndex = Math.max(0, Math.min(Number(approvalButtonIndex) || 0, modal.buttons.length - 1));
|
|
398
|
+
const modalKey = JSON.stringify({
|
|
399
|
+
message: modal.message || '',
|
|
400
|
+
buttons: modal.buttons,
|
|
401
|
+
index: clampedIndex,
|
|
402
|
+
});
|
|
403
|
+
if (modalKey === lastModalKey || typeof bundle?.adapter?.resolveModal !== 'function') {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
lastModalKey = modalKey;
|
|
407
|
+
approvalsResolved.push({
|
|
408
|
+
at: Date.now(),
|
|
409
|
+
buttonIndex: clampedIndex,
|
|
410
|
+
label: modal.buttons[clampedIndex] || null,
|
|
411
|
+
});
|
|
412
|
+
bundle.adapter.resolveModal(clampedIndex);
|
|
413
|
+
return true;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Exercise runs are easiest to reason about when startup/trust confirmations are
|
|
417
|
+
// cleared before we attempt to type the repro prompt into the CLI.
|
|
418
|
+
const preflightStartedAt = Date.now();
|
|
419
|
+
while (Date.now() - preflightStartedAt < Math.max(1_000, readyTimeoutMs)) {
|
|
420
|
+
bundle = getCliTargetBundle(ctx, type, bundle.target.instanceId);
|
|
421
|
+
if (!bundle) {
|
|
422
|
+
throw new Error('CLI instance disappeared before exercise send');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const debug = typeof bundle.adapter.getDebugState === 'function' ? bundle.adapter.getDebugState() : null;
|
|
426
|
+
const trace = typeof bundle.adapter.getTraceState === 'function' ? bundle.adapter.getTraceState(traceLimit) : null;
|
|
427
|
+
const status = String(debug?.status || bundle.target.status || 'unknown');
|
|
428
|
+
const modal = debug?.activeModal || trace?.activeModal || null;
|
|
429
|
+
noteStatus(status);
|
|
430
|
+
|
|
431
|
+
if (resolveActiveModalIfNeeded(status, modal)) {
|
|
432
|
+
await sleep(150);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const startupParseGate = !!debug?.startupParseGate;
|
|
437
|
+
if (status === 'idle' && !startupParseGate) break;
|
|
438
|
+
await sleep(150);
|
|
439
|
+
}
|
|
440
|
+
|
|
386
441
|
ctx.instanceManager.sendEvent(bundle.target.instanceId, 'send_message', { text });
|
|
387
442
|
|
|
388
443
|
while (Date.now() - startAt < Math.max(1_000, timeoutMs)) {
|
|
@@ -400,10 +455,7 @@ export async function runCliExerciseInternal(ctx: DevServerContext, body: CliExe
|
|
|
400
455
|
const sawSubmitWrite = traceEntries.some((entry: any) => entry?.type === 'submit_write');
|
|
401
456
|
const hasTurnStarted = sawSendMessage || sawSubmitWrite || !!debug?.currentTurnScope;
|
|
402
457
|
|
|
403
|
-
|
|
404
|
-
statusesSeen.push(status);
|
|
405
|
-
lastStatus = status;
|
|
406
|
-
}
|
|
458
|
+
noteStatus(status);
|
|
407
459
|
|
|
408
460
|
if (status === 'generating' || status === 'waiting_approval') {
|
|
409
461
|
sawBusy = true;
|
|
@@ -411,23 +463,8 @@ export async function runCliExerciseInternal(ctx: DevServerContext, body: CliExe
|
|
|
411
463
|
}
|
|
412
464
|
|
|
413
465
|
const modal = debug?.activeModal || trace?.activeModal || null;
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
const modalKey = JSON.stringify({
|
|
417
|
-
message: modal.message || '',
|
|
418
|
-
buttons: modal.buttons,
|
|
419
|
-
index: clampedIndex,
|
|
420
|
-
});
|
|
421
|
-
if (modalKey !== lastModalKey && typeof bundle.adapter.resolveModal === 'function') {
|
|
422
|
-
lastModalKey = modalKey;
|
|
423
|
-
approvalsResolved.push({
|
|
424
|
-
at: Date.now(),
|
|
425
|
-
buttonIndex: clampedIndex,
|
|
426
|
-
label: modal.buttons[clampedIndex] || null,
|
|
427
|
-
});
|
|
428
|
-
bundle.adapter.resolveModal(clampedIndex);
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
466
|
+
if (resolveActiveModalIfNeeded(status, modal)) {
|
|
467
|
+
continue;
|
|
431
468
|
}
|
|
432
469
|
|
|
433
470
|
const traceCount = Number(trace?.entryCount || 0);
|