@adhdev/daemon-core 0.8.28 → 0.8.30
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/agent-stream/manager.d.ts +1 -1
- package/dist/agent-stream/provider-adapter.d.ts +5 -0
- package/dist/commands/router.d.ts +5 -0
- package/dist/config/chat-history.d.ts +12 -0
- package/dist/index.js +552 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +552 -52
- package/dist/index.mjs.map +1 -1
- package/dist/providers/acp-provider-instance.d.ts +1 -0
- package/dist/providers/approval-utils.d.ts +7 -0
- package/dist/providers/cli-provider-instance.d.ts +3 -0
- package/dist/providers/contracts.d.ts +2 -0
- package/dist/providers/ide-provider-instance.d.ts +1 -0
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +24 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +24 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +6 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +6 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/agent-stream/manager.ts +2 -2
- package/src/agent-stream/poller.ts +38 -1
- package/src/agent-stream/provider-adapter.ts +97 -3
- package/src/cli-adapters/provider-cli-adapter.ts +3 -0
- package/src/commands/chat-commands.ts +53 -3
- package/src/commands/cli-manager.ts +14 -0
- package/src/commands/router.ts +11 -0
- package/src/config/chat-history.ts +269 -18
- package/src/providers/acp-provider-instance.ts +17 -2
- package/src/providers/approval-utils.ts +66 -0
- package/src/providers/cli-provider-instance.ts +47 -6
- package/src/providers/contracts.d.ts +1 -0
- package/src/providers/contracts.ts +3 -1
- package/src/providers/ide-provider-instance.ts +28 -23
- package/src/providers/provider-loader.ts +26 -2
|
@@ -29,6 +29,93 @@ interface HistoryMessage {
|
|
|
29
29
|
sessionTitle?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const CODEX_STARTER_PROMPT_RE = /^(?:[›❯]\s*)?(?:Find and fix a bug in @filename|Improve documentation in @filename|Write tests for @filename|Explain this codebase|Summarize recent commits|Implement \{feature\}|Use \/skills(?: to list available skills)?|Run \/review on my current changes)$/i;
|
|
33
|
+
|
|
34
|
+
function normalizeHistoryComparable(text: string): string {
|
|
35
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanupHistoryContent(agentType: string, role: HistoryMessage['role'], content: string): string {
|
|
39
|
+
let value = String(content || '').replace(/\r\n/g, '\n').trim();
|
|
40
|
+
if (!value) return '';
|
|
41
|
+
|
|
42
|
+
if (agentType === 'codex-cli' && role === 'assistant') {
|
|
43
|
+
const filtered = value
|
|
44
|
+
.split('\n')
|
|
45
|
+
.filter((line) => !CODEX_STARTER_PROMPT_RE.test(line.trim()))
|
|
46
|
+
.join('\n')
|
|
47
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
48
|
+
.trim();
|
|
49
|
+
value = filtered;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildHistoryMessageHash(
|
|
56
|
+
agentType: string,
|
|
57
|
+
message: Pick<HistoryMessage, 'role' | 'content' | 'receivedAt' | 'kind'> & { historyDedupKey?: string },
|
|
58
|
+
): string {
|
|
59
|
+
if (message.historyDedupKey) return message.historyDedupKey;
|
|
60
|
+
const cleaned = cleanupHistoryContent(agentType, message.role, message.content);
|
|
61
|
+
return `${message.kind || 'standard'}:${message.role}:${message.receivedAt || 0}:${normalizeHistoryComparable(cleaned)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildHistoryMessageSignature(
|
|
65
|
+
agentType: string,
|
|
66
|
+
message: Pick<HistoryMessage, 'role' | 'content' | 'kind'>,
|
|
67
|
+
): string {
|
|
68
|
+
const cleaned = cleanupHistoryContent(agentType, message.role, message.content);
|
|
69
|
+
return `${message.kind || 'standard'}:${message.role}:${normalizeHistoryComparable(cleaned)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isAdjacentHistoryDuplicate(
|
|
73
|
+
agentType: string,
|
|
74
|
+
previous: Pick<HistoryMessage, 'role' | 'content' | 'kind'> | null | undefined,
|
|
75
|
+
next: Pick<HistoryMessage, 'role' | 'content' | 'kind'> | null | undefined,
|
|
76
|
+
): boolean {
|
|
77
|
+
if (!previous || !next) return false;
|
|
78
|
+
return buildHistoryMessageSignature(agentType, previous) === buildHistoryMessageSignature(agentType, next);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collapseReplayAssistantTurns(agentType: string, messages: HistoryMessage[]): HistoryMessage[] {
|
|
82
|
+
if (agentType !== 'codex-cli') return messages;
|
|
83
|
+
|
|
84
|
+
const collapsed: HistoryMessage[] = [];
|
|
85
|
+
let sawAssistantSinceLastUser = false;
|
|
86
|
+
|
|
87
|
+
for (const message of messages) {
|
|
88
|
+
if (message.role === 'user') {
|
|
89
|
+
sawAssistantSinceLastUser = false;
|
|
90
|
+
collapsed.push(message);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (message.role === 'assistant') {
|
|
95
|
+
if (sawAssistantSinceLastUser) continue;
|
|
96
|
+
sawAssistantSinceLastUser = true;
|
|
97
|
+
collapsed.push(message);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
collapsed.push(message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return collapsed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sanitizeHistoryMessage(agentType: string, message: HistoryMessage): HistoryMessage | null {
|
|
108
|
+
if (!message || (message.role !== 'user' && message.role !== 'assistant' && message.role !== 'system')) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const content = cleanupHistoryContent(agentType, message.role, message.content);
|
|
112
|
+
if (!content) return null;
|
|
113
|
+
return {
|
|
114
|
+
...message,
|
|
115
|
+
content,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
32
119
|
export interface SavedHistorySessionSummary {
|
|
33
120
|
historySessionId: string;
|
|
34
121
|
sessionTitle?: string;
|
|
@@ -43,6 +130,10 @@ export class ChatHistoryWriter {
|
|
|
43
130
|
private lastSeenCounts = new Map<string, number>();
|
|
44
131
|
/** Last seen message hash per agent (deduplication) */
|
|
45
132
|
private lastSeenHashes = new Map<string, Set<string>>();
|
|
133
|
+
/** Last appended normalized message signature per agent/session */
|
|
134
|
+
private lastSeenSignatures = new Map<string, string>();
|
|
135
|
+
/** Last appended normalized non-system turn signature per agent/session */
|
|
136
|
+
private lastSeenTurnSignatures = new Map<string, string>();
|
|
46
137
|
private rotated = false;
|
|
47
138
|
|
|
48
139
|
/**
|
|
@@ -75,14 +166,36 @@ export class ChatHistoryWriter {
|
|
|
75
166
|
// Filter new messages
|
|
76
167
|
const newMessages: HistoryMessage[] = [];
|
|
77
168
|
for (const msg of messages) {
|
|
78
|
-
const
|
|
169
|
+
const role = msg.role as 'user' | 'assistant' | 'system';
|
|
170
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;
|
|
171
|
+
const content = cleanupHistoryContent(agentType, role, msg.content || '');
|
|
172
|
+
if (!content) continue;
|
|
173
|
+
const receivedAt = msg.receivedAt || Date.now();
|
|
174
|
+
const hash = buildHistoryMessageHash(agentType, {
|
|
175
|
+
role,
|
|
176
|
+
content,
|
|
177
|
+
receivedAt,
|
|
178
|
+
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
179
|
+
historyDedupKey: msg.historyDedupKey,
|
|
180
|
+
});
|
|
181
|
+
const signature = buildHistoryMessageSignature(agentType, {
|
|
182
|
+
role,
|
|
183
|
+
content,
|
|
184
|
+
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
185
|
+
});
|
|
79
186
|
if (seenHashes.has(hash)) continue;
|
|
187
|
+
if (this.lastSeenSignatures.get(dedupKey) === signature) continue;
|
|
188
|
+
if (role !== 'system' && this.lastSeenTurnSignatures.get(dedupKey) === signature) continue;
|
|
80
189
|
seenHashes.add(hash);
|
|
190
|
+
this.lastSeenSignatures.set(dedupKey, signature);
|
|
191
|
+
if (role !== 'system') {
|
|
192
|
+
this.lastSeenTurnSignatures.set(dedupKey, signature);
|
|
193
|
+
}
|
|
81
194
|
newMessages.push({
|
|
82
|
-
ts: new Date(
|
|
83
|
-
receivedAt
|
|
84
|
-
role
|
|
85
|
-
content
|
|
195
|
+
ts: new Date(receivedAt).toISOString(),
|
|
196
|
+
receivedAt,
|
|
197
|
+
role,
|
|
198
|
+
content,
|
|
86
199
|
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
87
200
|
senderName: typeof msg.senderName === 'string' ? msg.senderName : undefined,
|
|
88
201
|
agent: agentType,
|
|
@@ -108,6 +221,8 @@ export class ChatHistoryWriter {
|
|
|
108
221
|
const prevCount = this.lastSeenCounts.get(dedupKey) || 0;
|
|
109
222
|
if (messages.length < prevCount * 0.5 && prevCount > 3) {
|
|
110
223
|
seenHashes.clear();
|
|
224
|
+
this.lastSeenSignatures.delete(dedupKey);
|
|
225
|
+
this.lastSeenTurnSignatures.delete(dedupKey);
|
|
111
226
|
for (const msg of messages) {
|
|
112
227
|
seenHashes.add(msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`);
|
|
113
228
|
}
|
|
@@ -124,6 +239,62 @@ export class ChatHistoryWriter {
|
|
|
124
239
|
}
|
|
125
240
|
}
|
|
126
241
|
|
|
242
|
+
seedSessionHistory(
|
|
243
|
+
agentType: string,
|
|
244
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; historyDedupKey?: string }> = [],
|
|
245
|
+
historySessionId?: string,
|
|
246
|
+
instanceId?: string,
|
|
247
|
+
): void {
|
|
248
|
+
const effectiveHistoryKey = historySessionId || instanceId;
|
|
249
|
+
const dedupKey = effectiveHistoryKey ? `${agentType}:${effectiveHistoryKey}` : agentType;
|
|
250
|
+
const seenHashes = new Set<string>();
|
|
251
|
+
|
|
252
|
+
for (const raw of messages) {
|
|
253
|
+
const role = raw?.role as 'user' | 'assistant' | 'system';
|
|
254
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;
|
|
255
|
+
const content = cleanupHistoryContent(agentType, role, raw?.content || '');
|
|
256
|
+
if (!content) continue;
|
|
257
|
+
seenHashes.add(buildHistoryMessageHash(agentType, {
|
|
258
|
+
role,
|
|
259
|
+
content,
|
|
260
|
+
receivedAt: raw?.receivedAt || 0,
|
|
261
|
+
kind: typeof raw?.kind === 'string' ? raw.kind : undefined,
|
|
262
|
+
historyDedupKey: raw?.historyDedupKey,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.lastSeenHashes.set(dedupKey, seenHashes);
|
|
267
|
+
this.lastSeenCounts.set(dedupKey, messages.length);
|
|
268
|
+
const lastMessage = [...messages].reverse().find((raw) => {
|
|
269
|
+
const role = raw?.role as 'user' | 'assistant' | 'system';
|
|
270
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') return false;
|
|
271
|
+
return !!cleanupHistoryContent(agentType, role, raw?.content || '');
|
|
272
|
+
});
|
|
273
|
+
const lastTurnMessage = [...messages].reverse().find((raw) => {
|
|
274
|
+
const role = raw?.role as 'user' | 'assistant';
|
|
275
|
+
if (role !== 'user' && role !== 'assistant') return false;
|
|
276
|
+
return !!cleanupHistoryContent(agentType, role, raw?.content || '');
|
|
277
|
+
});
|
|
278
|
+
if (lastMessage) {
|
|
279
|
+
this.lastSeenSignatures.set(dedupKey, buildHistoryMessageSignature(agentType, {
|
|
280
|
+
role: lastMessage.role as HistoryMessage['role'],
|
|
281
|
+
content: lastMessage.content,
|
|
282
|
+
kind: typeof lastMessage.kind === 'string' ? lastMessage.kind : undefined,
|
|
283
|
+
}));
|
|
284
|
+
} else {
|
|
285
|
+
this.lastSeenSignatures.delete(dedupKey);
|
|
286
|
+
}
|
|
287
|
+
if (lastTurnMessage) {
|
|
288
|
+
this.lastSeenTurnSignatures.set(dedupKey, buildHistoryMessageSignature(agentType, {
|
|
289
|
+
role: lastTurnMessage.role as 'user' | 'assistant',
|
|
290
|
+
content: lastTurnMessage.content,
|
|
291
|
+
kind: typeof lastTurnMessage.kind === 'string' ? lastTurnMessage.kind : undefined,
|
|
292
|
+
}));
|
|
293
|
+
} else {
|
|
294
|
+
this.lastSeenTurnSignatures.delete(dedupKey);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
127
298
|
appendSystemMarker(
|
|
128
299
|
agentType: string,
|
|
129
300
|
content: string,
|
|
@@ -171,6 +342,16 @@ export class ChatHistoryWriter {
|
|
|
171
342
|
this.lastSeenHashes.set(toDedupKey, nextHashes);
|
|
172
343
|
this.lastSeenHashes.delete(fromDedupKey);
|
|
173
344
|
}
|
|
345
|
+
const fromSignature = this.lastSeenSignatures.get(fromDedupKey);
|
|
346
|
+
if (fromSignature) {
|
|
347
|
+
this.lastSeenSignatures.set(toDedupKey, fromSignature);
|
|
348
|
+
this.lastSeenSignatures.delete(fromDedupKey);
|
|
349
|
+
}
|
|
350
|
+
const fromTurnSignature = this.lastSeenTurnSignatures.get(fromDedupKey);
|
|
351
|
+
if (fromTurnSignature) {
|
|
352
|
+
this.lastSeenTurnSignatures.set(toDedupKey, fromTurnSignature);
|
|
353
|
+
this.lastSeenTurnSignatures.delete(fromDedupKey);
|
|
354
|
+
}
|
|
174
355
|
const fromCount = this.lastSeenCounts.get(fromDedupKey);
|
|
175
356
|
if (typeof fromCount === 'number') {
|
|
176
357
|
this.lastSeenCounts.set(toDedupKey, Math.max(fromCount, this.lastSeenCounts.get(toDedupKey) || 0));
|
|
@@ -221,10 +402,69 @@ export class ChatHistoryWriter {
|
|
|
221
402
|
}
|
|
222
403
|
}
|
|
223
404
|
|
|
405
|
+
compactHistorySession(agentType: string, historySessionId: string): void {
|
|
406
|
+
const sessionId = String(historySessionId || '').trim();
|
|
407
|
+
if (!sessionId) return;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
411
|
+
if (!fs.existsSync(dir)) return;
|
|
412
|
+
|
|
413
|
+
const prefix = `${this.sanitize(sessionId)}_`;
|
|
414
|
+
const files = fs.readdirSync(dir)
|
|
415
|
+
.filter((file) => file.startsWith(prefix) && file.endsWith('.jsonl'))
|
|
416
|
+
.sort();
|
|
417
|
+
|
|
418
|
+
const seen = new Set<string>();
|
|
419
|
+
for (const file of files) {
|
|
420
|
+
const filePath = path.join(dir, file);
|
|
421
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
|
422
|
+
const next: HistoryMessage[] = [];
|
|
423
|
+
|
|
424
|
+
for (const line of lines) {
|
|
425
|
+
let parsed: HistoryMessage | null = null;
|
|
426
|
+
try {
|
|
427
|
+
parsed = JSON.parse(line) as HistoryMessage;
|
|
428
|
+
} catch {
|
|
429
|
+
parsed = null;
|
|
430
|
+
}
|
|
431
|
+
if (!parsed || parsed.historySessionId !== sessionId) continue;
|
|
432
|
+
const sanitized = sanitizeHistoryMessage(agentType, parsed);
|
|
433
|
+
if (!sanitized) continue;
|
|
434
|
+
const hash = buildHistoryMessageHash(agentType, sanitized);
|
|
435
|
+
if (seen.has(hash)) continue;
|
|
436
|
+
seen.add(hash);
|
|
437
|
+
next.push(sanitized);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
next.sort((a, b) => a.receivedAt - b.receivedAt);
|
|
441
|
+
const dedupedAdjacent: HistoryMessage[] = [];
|
|
442
|
+
let lastTurn: HistoryMessage | null = null;
|
|
443
|
+
for (const entry of next) {
|
|
444
|
+
const previous = dedupedAdjacent[dedupedAdjacent.length - 1];
|
|
445
|
+
if (isAdjacentHistoryDuplicate(agentType, previous, entry)) continue;
|
|
446
|
+
if (entry.role !== 'system' && isAdjacentHistoryDuplicate(agentType, lastTurn, entry)) continue;
|
|
447
|
+
dedupedAdjacent.push(entry);
|
|
448
|
+
if (entry.role !== 'system') lastTurn = entry;
|
|
449
|
+
}
|
|
450
|
+
const collapsed = collapseReplayAssistantTurns(agentType, dedupedAdjacent);
|
|
451
|
+
if (collapsed.length === 0) {
|
|
452
|
+
fs.unlinkSync(filePath);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
fs.writeFileSync(filePath, `${collapsed.map((entry) => JSON.stringify(entry)).join('\n')}\n`, 'utf-8');
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
// Ignore compaction failure.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
224
462
|
/** Called when agent session is explicitly changed */
|
|
225
463
|
onSessionChange(agentType: string): void {
|
|
226
464
|
this.lastSeenHashes.delete(agentType);
|
|
227
465
|
this.lastSeenCounts.delete(agentType);
|
|
466
|
+
this.lastSeenSignatures.delete(agentType);
|
|
467
|
+
this.lastSeenTurnSignatures.delete(agentType);
|
|
228
468
|
}
|
|
229
469
|
|
|
230
470
|
/** Delete history files older than 30 days */
|
|
@@ -293,31 +533,42 @@ export function readChatHistory(
|
|
|
293
533
|
.sort()
|
|
294
534
|
.reverse();
|
|
295
535
|
|
|
296
|
-
// Read lines from all files (reverse order)
|
|
297
536
|
const allMessages: HistoryMessage[] = [];
|
|
298
|
-
const
|
|
537
|
+
const seen = new Set<string>();
|
|
299
538
|
|
|
300
539
|
for (const file of files) {
|
|
301
|
-
if (allMessages.length >= needed) break;
|
|
302
540
|
const filePath = path.join(dir, file);
|
|
303
541
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
304
542
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
308
|
-
if (allMessages.length >= needed) break;
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < lines.length; i++) {
|
|
309
545
|
try {
|
|
310
|
-
|
|
546
|
+
const parsed = JSON.parse(lines[i]) as HistoryMessage;
|
|
547
|
+
const sanitizedMessage = sanitizeHistoryMessage(agentType, parsed);
|
|
548
|
+
if (!sanitizedMessage) continue;
|
|
549
|
+
const hash = buildHistoryMessageHash(agentType, sanitizedMessage);
|
|
550
|
+
if (seen.has(hash)) continue;
|
|
551
|
+
seen.add(hash);
|
|
552
|
+
allMessages.push(sanitizedMessage);
|
|
311
553
|
} catch { /* skip invalid lines */ }
|
|
312
554
|
}
|
|
313
555
|
}
|
|
314
556
|
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
557
|
+
allMessages.sort((a, b) => a.receivedAt - b.receivedAt);
|
|
558
|
+
const chronological: HistoryMessage[] = [];
|
|
559
|
+
let lastTurn: HistoryMessage | null = null;
|
|
560
|
+
for (const message of allMessages) {
|
|
561
|
+
const previous = chronological[chronological.length - 1];
|
|
562
|
+
if (isAdjacentHistoryDuplicate(agentType, previous, message)) continue;
|
|
563
|
+
if (message.role !== 'system' && isAdjacentHistoryDuplicate(agentType, lastTurn, message)) continue;
|
|
564
|
+
chronological.push(message);
|
|
565
|
+
if (message.role !== 'system') lastTurn = message;
|
|
566
|
+
}
|
|
567
|
+
const collapsed = collapseReplayAssistantTurns(agentType, chronological);
|
|
318
568
|
|
|
319
|
-
//
|
|
320
|
-
sliced.
|
|
569
|
+
// offset/limit apply
|
|
570
|
+
const sliced = collapsed.slice(offset, offset + limit);
|
|
571
|
+
const hasMore = collapsed.length > offset + limit;
|
|
321
572
|
|
|
322
573
|
return { messages: sliced, hasMore };
|
|
323
574
|
} catch {
|
|
@@ -600,8 +600,10 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
// ─── Auto-approve: skip user confirmation ───
|
|
603
|
-
if (this.settings.autoApprove) {
|
|
604
|
-
|
|
603
|
+
if (this.settings.autoApprove !== false) {
|
|
604
|
+
const toolTitle = tc.title || tc.toolCallId || 'tool call';
|
|
605
|
+
this.log.info(`[${this.type}] Auto-approving: ${toolTitle}`);
|
|
606
|
+
this.appendSystemMessage(`Auto-approved: ${toolTitle}`);
|
|
605
607
|
const allowOption = params.options.find(o => o.kind === 'allow_once') || params.options.find(o => o.kind === 'allow_always');
|
|
606
608
|
if (allowOption) {
|
|
607
609
|
return { outcome: { outcome: 'selected', optionId: allowOption.optionId } };
|
|
@@ -1143,6 +1145,19 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
1143
1145
|
if (this.events.length > 50) this.events = this.events.slice(-50);
|
|
1144
1146
|
}
|
|
1145
1147
|
|
|
1148
|
+
private appendSystemMessage(content: string, timestamp = Date.now()): void {
|
|
1149
|
+
const normalizedContent = String(content || '').trim();
|
|
1150
|
+
if (!normalizedContent) return;
|
|
1151
|
+
this.messages.push({
|
|
1152
|
+
role: 'system',
|
|
1153
|
+
content: normalizedContent,
|
|
1154
|
+
timestamp,
|
|
1155
|
+
});
|
|
1156
|
+
if (this.messages.length > 200) {
|
|
1157
|
+
this.messages = this.messages.slice(-100);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1146
1161
|
private flushEvents(): ProviderEvent[] {
|
|
1147
1162
|
const events = [...this.events];
|
|
1148
1163
|
this.events = [];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ProviderModule } from './contracts.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_APPROVAL_POSITIVE_HINTS = [
|
|
4
|
+
'run',
|
|
5
|
+
'approve',
|
|
6
|
+
'accept',
|
|
7
|
+
'allow once',
|
|
8
|
+
'always allow',
|
|
9
|
+
'allow',
|
|
10
|
+
'yes',
|
|
11
|
+
'proceed',
|
|
12
|
+
'continue',
|
|
13
|
+
'confirm',
|
|
14
|
+
'save',
|
|
15
|
+
'ok',
|
|
16
|
+
'trust',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function normalizeApprovalLabel(value: string): string {
|
|
20
|
+
return String(value || '')
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[] {
|
|
27
|
+
const customHints = Array.isArray(provider?.approvalPositiveHints)
|
|
28
|
+
? provider.approvalPositiveHints
|
|
29
|
+
.map((hint) => normalizeApprovalLabel(String(hint || '')))
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
: [];
|
|
32
|
+
return customHints.length > 0 ? customHints : DEFAULT_APPROVAL_POSITIVE_HINTS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pickApprovalButton(
|
|
36
|
+
buttons: string[] | null | undefined,
|
|
37
|
+
provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null,
|
|
38
|
+
): { index: number; label: string } {
|
|
39
|
+
const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
|
|
40
|
+
if (labels.length === 0) {
|
|
41
|
+
return { index: 0, label: 'Approve' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const normalizedButtons = labels.map((label) => normalizeApprovalLabel(label));
|
|
45
|
+
const hints = getApprovalPositiveHints(provider);
|
|
46
|
+
|
|
47
|
+
for (const hint of hints) {
|
|
48
|
+
const exactIndex = normalizedButtons.findIndex((label) => label === hint);
|
|
49
|
+
if (exactIndex >= 0) return { index: exactIndex, label: labels[exactIndex] };
|
|
50
|
+
|
|
51
|
+
const prefixIndex = normalizedButtons.findIndex((label) => label.startsWith(hint));
|
|
52
|
+
if (prefixIndex >= 0) return { index: prefixIndex, label: labels[prefixIndex] };
|
|
53
|
+
|
|
54
|
+
const includeIndex = normalizedButtons.findIndex((label) => label.includes(hint));
|
|
55
|
+
if (includeIndex >= 0) return { index: includeIndex, label: labels[includeIndex] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { index: 0, label: labels[0] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {
|
|
62
|
+
const lines = [`Auto-approved${buttonLabel ? `: ${buttonLabel}` : ''}`];
|
|
63
|
+
const cleanMessage = String(modalMessage || '').trim();
|
|
64
|
+
if (cleanMessage) lines.push(cleanMessage);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
@@ -20,6 +20,7 @@ import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
|
|
|
20
20
|
import { LOG } from '../logging/logger.js';
|
|
21
21
|
import type { ChatMessage } from '../types.js';
|
|
22
22
|
import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
|
|
23
|
+
import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
|
|
23
24
|
|
|
24
25
|
let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
|
|
25
26
|
prepare(sql: string): { get(...params: Array<string | number>): unknown };
|
|
@@ -60,6 +61,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
60
61
|
private historyWriter: ChatHistoryWriter;
|
|
61
62
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
62
63
|
readonly instanceId: string;
|
|
64
|
+
private suppressIdleHistoryReplay = false;
|
|
63
65
|
|
|
64
66
|
private presentationMode: 'terminal' | 'chat';
|
|
65
67
|
private providerSessionId?: string;
|
|
@@ -135,7 +137,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
135
137
|
await this.adapter.spawn();
|
|
136
138
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
137
139
|
if (this.providerSessionId) {
|
|
140
|
+
this.historyWriter.compactHistorySession(this.type, this.providerSessionId);
|
|
138
141
|
const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
|
|
142
|
+
this.historyWriter.seedSessionHistory(
|
|
143
|
+
this.type,
|
|
144
|
+
restoredHistory.messages,
|
|
145
|
+
this.providerSessionId,
|
|
146
|
+
this.instanceId,
|
|
147
|
+
);
|
|
148
|
+
this.suppressIdleHistoryReplay = restoredHistory.messages.length > 0;
|
|
139
149
|
if (restoredHistory.messages.length > 0) {
|
|
140
150
|
this.adapter.seedCommittedMessages(
|
|
141
151
|
restoredHistory.messages.map((message) => ({
|
|
@@ -184,7 +194,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
184
194
|
} else if (this.type === 'codex-cli') {
|
|
185
195
|
probedSessionId = this.probeSessionIdFromConfig({
|
|
186
196
|
dbPath: '~/.codex/state_5.sqlite',
|
|
187
|
-
query: 'select id from threads where cwd in ({dirs}) and
|
|
197
|
+
query: 'select id from threads where cwd in ({dirs}) and updated_at >= ? and archived = 0 order by updated_at desc limit 1',
|
|
188
198
|
timestampFormat: 'unix_s',
|
|
189
199
|
});
|
|
190
200
|
} else if (this.type === 'goose-cli') {
|
|
@@ -240,6 +250,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
240
250
|
getState(): ProviderState {
|
|
241
251
|
const adapterStatus = this.adapter.getStatus();
|
|
242
252
|
const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
|
|
253
|
+
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
254
|
+
const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
|
|
243
255
|
const parsedProviderSessionId = typeof parsedStatus?.providerSessionId === 'string'
|
|
244
256
|
? parsedStatus.providerSessionId.trim()
|
|
245
257
|
: '';
|
|
@@ -260,6 +272,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
260
272
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
261
273
|
|
|
262
274
|
if (parsedMessages.length > 0) {
|
|
275
|
+
const shouldSkipReplayPersist =
|
|
276
|
+
this.suppressIdleHistoryReplay
|
|
277
|
+
&& adapterStatus.status === 'idle'
|
|
278
|
+
&& parsedStatus?.status === 'idle';
|
|
263
279
|
let messagesToSave = parsedMessages;
|
|
264
280
|
if ((parsedStatus?.status === 'generating' || parsedStatus?.status === 'long_generating')) {
|
|
265
281
|
const lastIdx = messagesToSave.length - 1;
|
|
@@ -267,7 +283,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
267
283
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
268
284
|
}
|
|
269
285
|
}
|
|
270
|
-
if (messagesToSave.length > 0) {
|
|
286
|
+
if (!shouldSkipReplayPersist && messagesToSave.length > 0) {
|
|
271
287
|
this.historyWriter.appendNewMessages(
|
|
272
288
|
this.type,
|
|
273
289
|
messagesToSave,
|
|
@@ -284,14 +300,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
284
300
|
type: this.type,
|
|
285
301
|
name: this.provider.name,
|
|
286
302
|
category: 'cli',
|
|
287
|
-
status:
|
|
303
|
+
status: visibleStatus,
|
|
288
304
|
mode: this.presentationMode,
|
|
289
305
|
activeChat: {
|
|
290
306
|
id: `${this.type}_${this.workingDir}`,
|
|
291
307
|
title: parsedStatus?.title || dirName,
|
|
292
|
-
status: parsedStatus?.status
|
|
308
|
+
status: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
309
|
+
? 'generating'
|
|
310
|
+
: (parsedStatus?.status || visibleStatus),
|
|
293
311
|
messages: mergedMessages,
|
|
294
|
-
activeModal: parsedStatus?.activeModal ?? adapterStatus.activeModal,
|
|
312
|
+
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
295
313
|
inputContent: '',
|
|
296
314
|
},
|
|
297
315
|
workspace: this.workingDir,
|
|
@@ -362,7 +380,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
362
380
|
const now = Date.now();
|
|
363
381
|
const adapterStatus = this.adapter.getStatus();
|
|
364
382
|
const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
|
|
365
|
-
const
|
|
383
|
+
const rawStatus = adapterStatus.status;
|
|
384
|
+
const autoApproveActive = rawStatus === 'waiting_approval' && this.shouldAutoApprove();
|
|
385
|
+
if (autoApproveActive) {
|
|
386
|
+
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(adapterStatus.activeModal?.buttons, this.provider);
|
|
387
|
+
this.recordAutoApproval(adapterStatus.activeModal?.message, buttonLabel, now);
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
this.adapter.resolveModal(buttonIndex);
|
|
390
|
+
}, 0);
|
|
391
|
+
}
|
|
392
|
+
const newStatus = autoApproveActive ? 'generating' : rawStatus;
|
|
366
393
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
367
394
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
368
395
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -374,6 +401,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
374
401
|
if (newStatus !== this.lastStatus) {
|
|
375
402
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
376
403
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
404
|
+
this.suppressIdleHistoryReplay = false;
|
|
377
405
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
378
406
|
if (this.completedDebouncePending) {
|
|
379
407
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
@@ -394,6 +422,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
394
422
|
this.generatingDebounceTimer = null;
|
|
395
423
|
}, 1000);
|
|
396
424
|
} else if (newStatus === 'waiting_approval') {
|
|
425
|
+
this.suppressIdleHistoryReplay = false;
|
|
397
426
|
// Flush pending generating_started if debounce still pending
|
|
398
427
|
if (this.generatingDebouncePending) {
|
|
399
428
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -588,6 +617,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
588
617
|
get cliType(): string { return this.type; }
|
|
589
618
|
get cliName(): string { return this.provider.name; }
|
|
590
619
|
|
|
620
|
+
private shouldAutoApprove(): boolean {
|
|
621
|
+
return this.settings.autoApprove !== false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private recordAutoApproval(modalMessage?: string, buttonLabel?: string, now = Date.now()): void {
|
|
625
|
+
this.appendRuntimeSystemMessage(
|
|
626
|
+
formatAutoApprovalMessage(modalMessage, buttonLabel),
|
|
627
|
+
`auto_approval:${now}:${buttonLabel || 'approve'}`,
|
|
628
|
+
now,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
591
632
|
recordApprovalSelection(buttonText: string): void {
|
|
592
633
|
const cleanButton = String(buttonText || '').trim();
|
|
593
634
|
if (!cleanButton) return;
|
|
@@ -253,6 +253,7 @@ export interface ProviderModule {
|
|
|
253
253
|
};
|
|
254
254
|
cleanOutput?: (raw: string, lastUserInput?: string) => string;
|
|
255
255
|
resume?: ProviderResumeCapability;
|
|
256
|
+
approvalPositiveHints?: string[];
|
|
256
257
|
scripts?: ProviderScripts;
|
|
257
258
|
vscodeCommands?: {
|
|
258
259
|
focusPanel?: string;
|
|
@@ -388,8 +388,10 @@ export interface ProviderModule {
|
|
|
388
388
|
};
|
|
389
389
|
cleanOutput?: (raw: string, lastUserInput?: string) => string;
|
|
390
390
|
resume?: ProviderResumeCapability;
|
|
391
|
-
|
|
391
|
+
/** Session ID probe config — auto-discovers provider session ID from local SQLite DB */
|
|
392
392
|
sessionProbe?: ProviderSessionProbe;
|
|
393
|
+
/** Approval button priority hints used when auto-approve must pick a positive action */
|
|
394
|
+
approvalPositiveHints?: string[];
|
|
393
395
|
|
|
394
396
|
// ─── CDP scripts (ide/extension category) ───
|
|
395
397
|
scripts?: ProviderScripts;
|