@adhdev/daemon-core 0.8.72 → 0.8.74
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/chat/subscription-updates.d.ts +5 -2
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/commands/stream-commands.d.ts +1 -1
- package/dist/config/chat-history.d.ts +5 -4
- package/dist/index.js +239 -111
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +239 -111
- package/dist/index.mjs.map +1 -1
- package/dist/providers/contracts.d.ts +2 -0
- package/node_modules/@adhdev/session-host-core/package.json +2 -2
- package/package.json +2 -2
- package/src/chat/subscription-updates.ts +5 -1
- package/src/cli-adapters/provider-cli-adapter.ts +8 -2
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +2 -0
- package/src/commands/chat-commands.ts +79 -3
- package/src/commands/stream-commands.ts +47 -26
- package/src/config/chat-history.d.ts +5 -4
- package/src/config/chat-history.ts +125 -74
- package/src/daemon/dev-server.ts +15 -1
- package/src/providers/approval-utils.ts +7 -6
- package/src/providers/contracts.d.ts +2 -0
- package/src/providers/contracts.ts +2 -0
|
@@ -356,6 +356,8 @@ export interface ProviderModule {
|
|
|
356
356
|
resume?: ProviderResumeCapability;
|
|
357
357
|
/** Session ID probe config — auto-discovers provider session ID from local SQLite DB */
|
|
358
358
|
sessionProbe?: ProviderSessionProbe;
|
|
359
|
+
/** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
|
|
360
|
+
allowInputDuringGeneration?: boolean;
|
|
359
361
|
/** Approval button priority hints used when auto-approve must pick a positive action */
|
|
360
362
|
approvalPositiveHints?: string[];
|
|
361
363
|
scripts?: ProviderScripts;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/session-host-core",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "ADHDev local session host core
|
|
3
|
+
"version": "0.8.74",
|
|
4
|
+
"description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "ADHDev daemon core
|
|
3
|
+
"version": "0.8.74",
|
|
4
|
+
"description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
buildSessionModalDeliverySignature,
|
|
11
11
|
} from './chat-signatures.js'
|
|
12
12
|
|
|
13
|
-
export interface ChatTailSubscriptionCursor
|
|
13
|
+
export interface ChatTailSubscriptionCursor {
|
|
14
|
+
knownMessageCount: number
|
|
15
|
+
lastMessageSignature: string
|
|
16
|
+
tailLimit: number
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
export type SessionChatTailCommandResult = Partial<Omit<ReadChatSyncResult, 'activeModal'>> & {
|
|
16
20
|
success?: boolean
|
|
@@ -1439,6 +1439,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1439
1439
|
|
|
1440
1440
|
async sendMessage(text: string): Promise<void> {
|
|
1441
1441
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1442
|
+
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1443
|
+
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1444
|
+
&& this.isWaitingForResponse
|
|
1445
|
+
&& this.currentStatus !== 'waiting_approval';
|
|
1442
1446
|
if (this.startupParseGate) {
|
|
1443
1447
|
const deadline = Date.now() + 10000;
|
|
1444
1448
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1446,7 +1450,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1446
1450
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1447
1451
|
}
|
|
1448
1452
|
}
|
|
1449
|
-
|
|
1453
|
+
if (!allowInterventionPrompt) {
|
|
1454
|
+
await this.waitForInteractivePrompt();
|
|
1455
|
+
}
|
|
1450
1456
|
if (!this.ready) {
|
|
1451
1457
|
this.resolveStartupState('send_precheck');
|
|
1452
1458
|
const screenText = this.terminalScreen.getText() || '';
|
|
@@ -1458,7 +1464,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1458
1464
|
}
|
|
1459
1465
|
}
|
|
1460
1466
|
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
1461
|
-
if (this.isWaitingForResponse) {
|
|
1467
|
+
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1462
1468
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1463
1469
|
}
|
|
1464
1470
|
const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || '');
|
|
@@ -105,6 +105,8 @@ export interface CliProviderModule {
|
|
|
105
105
|
sendDelayMs?: number;
|
|
106
106
|
sendKey?: string;
|
|
107
107
|
submitStrategy?: 'wait_for_echo' | 'immediate';
|
|
108
|
+
/** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
|
|
109
|
+
allowInputDuringGeneration?: boolean;
|
|
108
110
|
scripts?: CliScripts;
|
|
109
111
|
spawn: {
|
|
110
112
|
command: string;
|
|
@@ -110,6 +110,8 @@ export interface CliProviderModule {
|
|
|
110
110
|
sendDelayMs?: number;
|
|
111
111
|
sendKey?: string;
|
|
112
112
|
submitStrategy?: 'wait_for_echo' | 'immediate';
|
|
113
|
+
/** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
|
|
114
|
+
allowInputDuringGeneration?: boolean;
|
|
113
115
|
scripts?: CliScripts;
|
|
114
116
|
spawn: {
|
|
115
117
|
command: string;
|
|
@@ -175,6 +175,48 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
|
|
|
175
175
|
return normalizeChatMessages(messages);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function buildReadChatReplayCollapseSignature(message: ChatMessage | null | undefined): string {
|
|
179
|
+
if (!message) return '';
|
|
180
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
181
|
+
const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
|
|
182
|
+
const senderName = typeof message.senderName === 'string' ? message.senderName.trim().toLowerCase() : '';
|
|
183
|
+
const content = flattenContent(message.content || '').replace(/\s+/g, ' ').trim();
|
|
184
|
+
return `${role}:${kind}:${senderName}:${content}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function shouldCollapseReadChatReplayDuplicate(message: ChatMessage | null | undefined): boolean {
|
|
188
|
+
if (!message) return false;
|
|
189
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
190
|
+
if (role !== 'assistant' && role !== 'system') return false;
|
|
191
|
+
const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
|
|
192
|
+
return kind === 'tool' || kind === 'terminal' || kind === 'thought' || kind === 'system';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function collapseReplayDuplicatesFromReadChat(messages: ChatMessage[]): ChatMessage[] {
|
|
196
|
+
const collapsed: ChatMessage[] = [];
|
|
197
|
+
let lastReplayTurnSignature = '';
|
|
198
|
+
|
|
199
|
+
for (const message of messages) {
|
|
200
|
+
const signature = buildReadChatReplayCollapseSignature(message);
|
|
201
|
+
const previous = collapsed[collapsed.length - 1];
|
|
202
|
+
const previousSignature = buildReadChatReplayCollapseSignature(previous);
|
|
203
|
+
|
|
204
|
+
if (shouldCollapseReadChatReplayDuplicate(message) && signature) {
|
|
205
|
+
if (previousSignature === signature) continue;
|
|
206
|
+
if (lastReplayTurnSignature === signature) continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
collapsed.push(message);
|
|
210
|
+
if (shouldCollapseReadChatReplayDuplicate(message) && signature) {
|
|
211
|
+
lastReplayTurnSignature = signature;
|
|
212
|
+
} else if ((message.role || '').toLowerCase() === 'user') {
|
|
213
|
+
lastReplayTurnSignature = '';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return collapsed;
|
|
218
|
+
}
|
|
219
|
+
|
|
178
220
|
function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
|
|
179
221
|
const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
|
|
180
222
|
if (unitKey) return `read_chat:${unitKey}`;
|
|
@@ -273,14 +315,40 @@ function computeReadChatSync(messages: ChatMessage[], cursor: Required<ReadChatC
|
|
|
273
315
|
};
|
|
274
316
|
}
|
|
275
317
|
|
|
318
|
+
function hasNonEmptyModalButtons(activeModal: unknown): boolean {
|
|
319
|
+
if (!activeModal || typeof activeModal !== 'object') return false;
|
|
320
|
+
const buttons = (activeModal as { buttons?: unknown }).buttons;
|
|
321
|
+
return Array.isArray(buttons) && buttons.some((button) => typeof button === 'string' && button.trim().length > 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown): string {
|
|
325
|
+
const raw = typeof status === 'string' ? status.trim() : '';
|
|
326
|
+
if (!raw) {
|
|
327
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'idle';
|
|
328
|
+
}
|
|
329
|
+
switch (raw) {
|
|
330
|
+
case 'starting':
|
|
331
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
|
|
332
|
+
case 'stopped':
|
|
333
|
+
case 'disconnected':
|
|
334
|
+
case 'not_monitored':
|
|
335
|
+
return 'error';
|
|
336
|
+
default:
|
|
337
|
+
return raw;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
276
341
|
function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
|
|
277
342
|
let validatedPayload: Record<string, any>;
|
|
278
343
|
try {
|
|
279
|
-
validatedPayload = validateReadChatResultPayload(
|
|
344
|
+
validatedPayload = validateReadChatResultPayload({
|
|
345
|
+
...payload,
|
|
346
|
+
status: normalizeReadChatCommandStatus(payload?.status, payload?.activeModal),
|
|
347
|
+
}, 'read_chat command result') as Record<string, any>;
|
|
280
348
|
} catch (error: any) {
|
|
281
349
|
return { success: false, error: error?.message || String(error) };
|
|
282
350
|
}
|
|
283
|
-
const messages = normalizeReadChatMessages(validatedPayload);
|
|
351
|
+
const messages = collapseReplayDuplicatesFromReadChat(normalizeReadChatMessages(validatedPayload));
|
|
284
352
|
const cursor = normalizeReadChatCursor(args);
|
|
285
353
|
if (!cursor.knownMessageCount && !cursor.lastMessageSignature && cursor.tailLimit > 0 && messages.length > cursor.tailLimit) {
|
|
286
354
|
const tailMessages = messages.slice(-cursor.tailLimit);
|
|
@@ -374,7 +442,15 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
374
442
|
try {
|
|
375
443
|
const provider = h.getProvider(agentType);
|
|
376
444
|
const agentStr = provider?.type || agentType || getCurrentProviderType(h);
|
|
377
|
-
const
|
|
445
|
+
const transport = getTargetTransport(h, provider);
|
|
446
|
+
let excludeRecentCount = Math.max(0, Number(args?.excludeRecentCount || 0));
|
|
447
|
+
if (isCliLikeTransport(transport)) {
|
|
448
|
+
const adapter = getTargetedCliAdapter(h, args, provider?.type);
|
|
449
|
+
const status = adapter?.getStatus?.();
|
|
450
|
+
const visibleCount = Array.isArray(status?.messages) ? status.messages.length : 0;
|
|
451
|
+
if (visibleCount > excludeRecentCount) excludeRecentCount = visibleCount;
|
|
452
|
+
}
|
|
453
|
+
const result = readChatHistory(agentStr, offset || 0, limit || 30, historySessionId, excludeRecentCount);
|
|
378
454
|
return { success: true, ...result, agent: agentStr };
|
|
379
455
|
} catch (e: any) {
|
|
380
456
|
return { success: false, error: e.message };
|
|
@@ -113,9 +113,6 @@ export async function handleOpenPanel(h: CommandHelpers, args: any): Promise<Com
|
|
|
113
113
|
export function handlePtyInput(h: CommandHelpers, args: any): CommandResult {
|
|
114
114
|
const { cliType, data, targetSessionId } = args || {};
|
|
115
115
|
if (!data) return { success: false, error: 'data required' };
|
|
116
|
-
if (getCliPresentationMode(h, targetSessionId) === 'chat') {
|
|
117
|
-
return { success: false, error: 'CLI session is in chat mode', code: 'CLI_VIEW_MODE_NOT_TERMINAL' };
|
|
118
|
-
}
|
|
119
116
|
const adapter = h.getCliAdapter(targetSessionId || cliType);
|
|
120
117
|
if (!adapter || typeof adapter.writeRaw !== 'function') {
|
|
121
118
|
return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
|
|
@@ -124,24 +121,10 @@ export function handlePtyInput(h: CommandHelpers, args: any): CommandResult {
|
|
|
124
121
|
return { success: true };
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
export function handlePtyResize(
|
|
128
|
-
const {
|
|
124
|
+
export function handlePtyResize(_h: CommandHelpers, args: any): CommandResult {
|
|
125
|
+
const { cols, rows } = args || {};
|
|
129
126
|
if (!cols || !rows) return { success: false, error: 'cols and rows required' };
|
|
130
|
-
|
|
131
|
-
return { success: false, error: 'CLI session is in chat mode', code: 'CLI_VIEW_MODE_NOT_TERMINAL' };
|
|
132
|
-
}
|
|
133
|
-
const adapter = h.getCliAdapter(targetSessionId || cliType);
|
|
134
|
-
if (!adapter || typeof adapter.resize !== 'function') {
|
|
135
|
-
return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
|
|
136
|
-
}
|
|
137
|
-
const resize = adapter.resize;
|
|
138
|
-
if (force) {
|
|
139
|
-
resize(cols - 1, rows);
|
|
140
|
-
setTimeout(() => resize(cols, rows), 50);
|
|
141
|
-
} else {
|
|
142
|
-
resize(cols, rows);
|
|
143
|
-
}
|
|
144
|
-
return { success: true };
|
|
127
|
+
return { success: false, error: 'PTY resize temporarily disabled', code: 'PTY_RESIZE_DISABLED' };
|
|
145
128
|
}
|
|
146
129
|
|
|
147
130
|
// ─── Provider Settings ────────────────────────
|
|
@@ -251,18 +234,56 @@ export function normalizeProviderScriptArgs(args: any, scriptName?: string): Rec
|
|
|
251
234
|
|
|
252
235
|
function buildControlScriptResult(scriptName: string, payload: any): Record<string, unknown> {
|
|
253
236
|
if (!payload || typeof payload !== 'object') return {};
|
|
254
|
-
|
|
255
|
-
|
|
237
|
+
|
|
238
|
+
const legacyListPayload = (() => {
|
|
239
|
+
if (Array.isArray(payload.options)) return payload;
|
|
240
|
+
if (/^listmodels$/i.test(scriptName) && Array.isArray(payload.models)) {
|
|
241
|
+
return {
|
|
242
|
+
options: payload.models,
|
|
243
|
+
currentValue: payload.currentValue ?? payload.current ?? payload.currentModel,
|
|
244
|
+
...(typeof payload.error === 'string' ? { error: payload.error } : {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (/^listmodes$/i.test(scriptName) && Array.isArray(payload.modes)) {
|
|
248
|
+
return {
|
|
249
|
+
options: payload.modes,
|
|
250
|
+
currentValue: payload.currentValue ?? payload.current ?? payload.currentMode ?? payload.mode,
|
|
251
|
+
...(typeof payload.error === 'string' ? { error: payload.error } : {}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
})();
|
|
256
|
+
if (legacyListPayload) {
|
|
257
|
+
return { controlResult: normalizeControlListResult(legacyListPayload) };
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
const legacyMutationPayload = (() => {
|
|
261
|
+
if (typeof payload.ok === 'boolean') return payload;
|
|
262
|
+
if (typeof payload.success === 'boolean') {
|
|
263
|
+
return {
|
|
264
|
+
ok: payload.success,
|
|
265
|
+
currentValue: payload.currentValue
|
|
266
|
+
?? payload.value
|
|
267
|
+
?? payload.model
|
|
268
|
+
?? payload.mode
|
|
269
|
+
?? payload.selectedModel
|
|
270
|
+
?? payload.selectedMode,
|
|
271
|
+
...(Array.isArray(payload.effects) ? { effects: payload.effects } : {}),
|
|
272
|
+
...(typeof payload.error === 'string' ? { error: payload.error } : {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
})();
|
|
277
|
+
|
|
258
278
|
const looksLikeValueMutation = /^set|^change/i.test(scriptName)
|
|
259
279
|
|| payload.currentValue !== undefined
|
|
260
|
-
|| payload.value !== undefined
|
|
280
|
+
|| payload.value !== undefined
|
|
281
|
+
|| payload.success !== undefined;
|
|
261
282
|
if (looksLikeValueMutation) {
|
|
262
|
-
return { controlResult: normalizeControlSetResult(payload) };
|
|
283
|
+
return { controlResult: normalizeControlSetResult(legacyMutationPayload || payload) };
|
|
263
284
|
}
|
|
264
285
|
if (payload.ok !== undefined || Array.isArray(payload.effects) || typeof payload.error === 'string') {
|
|
265
|
-
return { controlResult: normalizeControlInvokeResult(payload) };
|
|
286
|
+
return { controlResult: normalizeControlInvokeResult(legacyMutationPayload || payload) };
|
|
266
287
|
}
|
|
267
288
|
return {};
|
|
268
289
|
}
|
|
@@ -359,7 +380,7 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
|
|
|
359
380
|
|
|
360
381
|
// IDE-level scripts (model/mode) — try session frame first, fallback to main page
|
|
361
382
|
const IDE_LEVEL_SCRIPTS = provider.type === 'claude-code-vscode'
|
|
362
|
-
? ['listModes', 'setMode']
|
|
383
|
+
? ['listModes', 'setMode', 'listModels', 'setModel', 'setModelGui']
|
|
363
384
|
: ['listModes', 'setMode', 'listModels', 'setModel'];
|
|
364
385
|
if (IDE_LEVEL_SCRIPTS.includes(scriptName)) {
|
|
365
386
|
// Try session frame first (some extensions embed mode selector in their webview)
|
|
@@ -81,11 +81,12 @@ export declare class ChatHistoryWriter {
|
|
|
81
81
|
/**
|
|
82
82
|
* Read history (static — called from P2P commands)
|
|
83
83
|
*
|
|
84
|
-
* Read JSONL files
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* Read JSONL files for a session and return a chronological page while paging
|
|
85
|
+
* backwards from the newest saved messages. When excludeRecentCount is set,
|
|
86
|
+
* the newest N messages are skipped so older-history pagination can avoid
|
|
87
|
+
* duplicating the live transcript tail already shown in the UI.
|
|
87
88
|
*/
|
|
88
|
-
export declare function readChatHistory(agentType: string, offset?: number, limit?: number, historySessionId?: string): {
|
|
89
|
+
export declare function readChatHistory(agentType: string, offset?: number, limit?: number, historySessionId?: string, excludeRecentCount?: number): {
|
|
89
90
|
messages: HistoryMessage[];
|
|
90
91
|
hasMore: boolean;
|
|
91
92
|
};
|
|
@@ -17,6 +17,13 @@ import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normali
|
|
|
17
17
|
const HISTORY_DIR = path.join(os.homedir(), '.adhdev', 'history');
|
|
18
18
|
const RETAIN_DAYS = 30;
|
|
19
19
|
|
|
20
|
+
interface SavedHistorySessionCacheEntry {
|
|
21
|
+
signature: string;
|
|
22
|
+
summaries: SavedHistorySessionSummary[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const savedHistorySessionCache = new Map<string, SavedHistorySessionCacheEntry>();
|
|
26
|
+
|
|
20
27
|
interface HistoryMessage {
|
|
21
28
|
ts: string; // ISO timestamp
|
|
22
29
|
receivedAt: number; // epoch ms
|
|
@@ -128,6 +135,96 @@ export interface SavedHistorySessionSummary {
|
|
|
128
135
|
workspace?: string;
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
function sanitizeHistoryFileSegment(value?: string): string {
|
|
139
|
+
return String(value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function listHistoryFiles(dir: string, historySessionId?: string): string[] {
|
|
143
|
+
const sanitizedSessionId = historySessionId ? sanitizeHistoryFileSegment(historySessionId) : '';
|
|
144
|
+
return fs.readdirSync(dir)
|
|
145
|
+
.filter((file) => {
|
|
146
|
+
if (!file.endsWith('.jsonl')) return false;
|
|
147
|
+
if (sanitizedSessionId) {
|
|
148
|
+
return file.startsWith(`${sanitizedSessionId}_`);
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
})
|
|
152
|
+
.sort()
|
|
153
|
+
.reverse();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildSavedHistoryCacheSignature(dir: string, files: string[]): string {
|
|
157
|
+
return files.map((file) => {
|
|
158
|
+
try {
|
|
159
|
+
const stat = fs.statSync(path.join(dir, file));
|
|
160
|
+
return `${file}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
161
|
+
} catch {
|
|
162
|
+
return `${file}:missing`;
|
|
163
|
+
}
|
|
164
|
+
}).join('|');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function computeSavedHistorySessionSummaries(agentType: string, dir: string, files: string[]): SavedHistorySessionSummary[] {
|
|
168
|
+
const groupedFiles = new Map<string, string[]>();
|
|
169
|
+
const filePattern = /^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const match = file.match(filePattern);
|
|
172
|
+
if (!match?.[1]) continue;
|
|
173
|
+
const historySessionId = match[1];
|
|
174
|
+
const grouped = groupedFiles.get(historySessionId) || [];
|
|
175
|
+
grouped.push(file);
|
|
176
|
+
groupedFiles.set(historySessionId, grouped);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const summaries: SavedHistorySessionSummary[] = [];
|
|
180
|
+
for (const [historySessionId, grouped] of groupedFiles.entries()) {
|
|
181
|
+
let messageCount = 0;
|
|
182
|
+
let firstMessageAt = 0;
|
|
183
|
+
let lastMessageAt = 0;
|
|
184
|
+
let sessionTitle = '';
|
|
185
|
+
let preview = '';
|
|
186
|
+
let workspace = '';
|
|
187
|
+
|
|
188
|
+
for (const file of grouped.sort()) {
|
|
189
|
+
const filePath = path.join(dir, file);
|
|
190
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
191
|
+
const lines = content.split('\n').filter(Boolean);
|
|
192
|
+
for (const line of lines) {
|
|
193
|
+
let parsed: HistoryMessage | null = null;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(line) as HistoryMessage;
|
|
196
|
+
} catch {
|
|
197
|
+
parsed = null;
|
|
198
|
+
}
|
|
199
|
+
if (!parsed || parsed.historySessionId !== historySessionId) continue;
|
|
200
|
+
if (parsed.kind === 'session_start') {
|
|
201
|
+
if (!workspace && parsed.workspace) workspace = parsed.workspace;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
messageCount += 1;
|
|
205
|
+
if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
|
|
206
|
+
if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
|
|
207
|
+
if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
|
|
208
|
+
if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (messageCount === 0 || !lastMessageAt) continue;
|
|
213
|
+
summaries.push({
|
|
214
|
+
historySessionId,
|
|
215
|
+
sessionTitle: sessionTitle || undefined,
|
|
216
|
+
messageCount,
|
|
217
|
+
firstMessageAt,
|
|
218
|
+
lastMessageAt,
|
|
219
|
+
preview: preview || undefined,
|
|
220
|
+
workspace: workspace || undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
225
|
+
return summaries;
|
|
226
|
+
}
|
|
227
|
+
|
|
131
228
|
export class ChatHistoryWriter {
|
|
132
229
|
/** Last seen message count per agent (deduplication) */
|
|
133
230
|
private lastSeenCounts = new Map<string, number>();
|
|
@@ -539,15 +636,17 @@ export class ChatHistoryWriter {
|
|
|
539
636
|
/**
|
|
540
637
|
* Read history (static — called from P2P commands)
|
|
541
638
|
*
|
|
542
|
-
* Read JSONL files
|
|
543
|
-
*
|
|
544
|
-
*
|
|
639
|
+
* Read JSONL files for a session and return a chronological page while paging
|
|
640
|
+
* backwards from the newest saved messages. When excludeRecentCount is set,
|
|
641
|
+
* the newest N messages are skipped so older-history pagination can avoid
|
|
642
|
+
* duplicating the live transcript tail already shown in the UI.
|
|
545
643
|
*/
|
|
546
644
|
export function readChatHistory(
|
|
547
645
|
agentType: string,
|
|
548
646
|
offset: number = 0,
|
|
549
647
|
limit: number = 30,
|
|
550
648
|
historySessionId?: string,
|
|
649
|
+
excludeRecentCount: number = 0,
|
|
551
650
|
): { messages: HistoryMessage[]; hasMore: boolean } {
|
|
552
651
|
try {
|
|
553
652
|
const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
@@ -555,19 +654,7 @@ export function readChatHistory(
|
|
|
555
654
|
if (!fs.existsSync(dir)) return { messages: [], hasMore: false };
|
|
556
655
|
|
|
557
656
|
// JSONL file list — filter by persistent history key when specified
|
|
558
|
-
const
|
|
559
|
-
const files = fs.readdirSync(dir)
|
|
560
|
-
.filter(f => {
|
|
561
|
-
if (!f.endsWith('.jsonl')) return false;
|
|
562
|
-
if (sanitizedInstance) {
|
|
563
|
-
// With instanceId: only that instance's files
|
|
564
|
-
return f.startsWith(`${sanitizedInstance}_`);
|
|
565
|
-
}
|
|
566
|
-
// Without instanceId: include ALL files (legacy + instanced)
|
|
567
|
-
return true;
|
|
568
|
-
})
|
|
569
|
-
.sort()
|
|
570
|
-
.reverse();
|
|
657
|
+
const files = listHistoryFiles(dir, historySessionId);
|
|
571
658
|
|
|
572
659
|
const allMessages: HistoryMessage[] = [];
|
|
573
660
|
const seen = new Set<string>();
|
|
@@ -602,9 +689,15 @@ export function readChatHistory(
|
|
|
602
689
|
}
|
|
603
690
|
const collapsed = collapseReplayAssistantTurns(agentType, chronological);
|
|
604
691
|
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
const
|
|
692
|
+
// Page backwards from the newest saved messages while keeping the returned
|
|
693
|
+
// slice in chronological order for prepend-based UI rendering.
|
|
694
|
+
const boundedLimit = Math.max(1, limit);
|
|
695
|
+
const boundedOffset = Math.max(0, offset);
|
|
696
|
+
const boundedExclude = Math.max(0, Math.min(excludeRecentCount, collapsed.length));
|
|
697
|
+
const endExclusive = Math.max(0, collapsed.length - boundedExclude - boundedOffset);
|
|
698
|
+
const startInclusive = Math.max(0, endExclusive - boundedLimit);
|
|
699
|
+
const sliced = collapsed.slice(startInclusive, endExclusive);
|
|
700
|
+
const hasMore = startInclusive > 0;
|
|
608
701
|
|
|
609
702
|
return { messages: sliced, hasMore };
|
|
610
703
|
} catch {
|
|
@@ -619,66 +712,24 @@ export function listSavedHistorySessions(
|
|
|
619
712
|
try {
|
|
620
713
|
const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
621
714
|
const dir = path.join(HISTORY_DIR, sanitized);
|
|
622
|
-
if (!fs.existsSync(dir))
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const filePattern = /^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
626
|
-
for (const file of fs.readdirSync(dir)) {
|
|
627
|
-
if (!file.endsWith('.jsonl')) continue;
|
|
628
|
-
const match = file.match(filePattern);
|
|
629
|
-
if (!match?.[1]) continue;
|
|
630
|
-
const historySessionId = match[1];
|
|
631
|
-
const files = groupedFiles.get(historySessionId) || [];
|
|
632
|
-
files.push(file);
|
|
633
|
-
groupedFiles.set(historySessionId, files);
|
|
715
|
+
if (!fs.existsSync(dir)) {
|
|
716
|
+
savedHistorySessionCache.delete(sanitized);
|
|
717
|
+
return { sessions: [], hasMore: false };
|
|
634
718
|
}
|
|
635
719
|
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const filePath = path.join(dir, file);
|
|
647
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
648
|
-
const lines = content.split('\n').filter(Boolean);
|
|
649
|
-
for (const line of lines) {
|
|
650
|
-
let parsed: HistoryMessage | null = null;
|
|
651
|
-
try {
|
|
652
|
-
parsed = JSON.parse(line) as HistoryMessage;
|
|
653
|
-
} catch {
|
|
654
|
-
parsed = null;
|
|
655
|
-
}
|
|
656
|
-
if (!parsed || parsed.historySessionId !== historySessionId) continue;
|
|
657
|
-
if (parsed.kind === 'session_start') {
|
|
658
|
-
if (!workspace && parsed.workspace) workspace = parsed.workspace;
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
messageCount += 1;
|
|
662
|
-
if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
|
|
663
|
-
if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
|
|
664
|
-
if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
|
|
665
|
-
if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (messageCount === 0 || !lastMessageAt) continue;
|
|
670
|
-
summaries.push({
|
|
671
|
-
historySessionId,
|
|
672
|
-
sessionTitle: sessionTitle || undefined,
|
|
673
|
-
messageCount,
|
|
674
|
-
firstMessageAt,
|
|
675
|
-
lastMessageAt,
|
|
676
|
-
preview: preview || undefined,
|
|
677
|
-
workspace: workspace || undefined,
|
|
720
|
+
const files = listHistoryFiles(dir);
|
|
721
|
+
const signature = buildSavedHistoryCacheSignature(dir, files);
|
|
722
|
+
const cached = savedHistorySessionCache.get(sanitized);
|
|
723
|
+
const summaries = cached?.signature === signature
|
|
724
|
+
? cached.summaries
|
|
725
|
+
: computeSavedHistorySessionSummaries(agentType, dir, files);
|
|
726
|
+
if (!cached || cached.signature !== signature) {
|
|
727
|
+
savedHistorySessionCache.set(sanitized, {
|
|
728
|
+
signature,
|
|
729
|
+
summaries,
|
|
678
730
|
});
|
|
679
731
|
}
|
|
680
732
|
|
|
681
|
-
summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
682
733
|
const offset = Math.max(0, options.offset || 0);
|
|
683
734
|
const limit = Math.max(1, options.limit || 30);
|
|
684
735
|
const sliced = summaries.slice(offset, offset + limit);
|
package/src/daemon/dev-server.ts
CHANGED
|
@@ -507,6 +507,20 @@ export class DevServer implements DevServerContext {
|
|
|
507
507
|
private async handleReload(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
508
508
|
try {
|
|
509
509
|
this.providerLoader.reload();
|
|
510
|
+
let refreshedInstances = 0;
|
|
511
|
+
if (this.instanceManager) {
|
|
512
|
+
for (const id of this.instanceManager.listInstanceIds()) {
|
|
513
|
+
const instance = this.instanceManager.getInstance(id) as any;
|
|
514
|
+
const providerType = typeof instance?.type === 'string' ? instance.type : '';
|
|
515
|
+
if (!providerType) continue;
|
|
516
|
+
const resolved = this.providerLoader.resolve(providerType);
|
|
517
|
+
if (!resolved) continue;
|
|
518
|
+
if (instance && typeof instance === 'object' && 'provider' in instance) {
|
|
519
|
+
instance.provider = resolved;
|
|
520
|
+
refreshedInstances += 1;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
510
524
|
const providers = this.providerLoader.getAll().map(p => ({
|
|
511
525
|
type: p.type, name: p.name, category: p.category,
|
|
512
526
|
}));
|
|
@@ -515,7 +529,7 @@ export class DevServer implements DevServerContext {
|
|
|
515
529
|
cdp.clearTargetId();
|
|
516
530
|
}
|
|
517
531
|
}
|
|
518
|
-
this.json(res, 200, { reloaded: true, providers });
|
|
532
|
+
this.json(res, 200, { reloaded: true, refreshedInstances, providers });
|
|
519
533
|
} catch (e: any) {
|
|
520
534
|
this.json(res, 500, { error: e.message });
|
|
521
535
|
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import type { ProviderModule } from './contracts.js';
|
|
2
2
|
|
|
3
3
|
const DEFAULT_APPROVAL_POSITIVE_HINTS = [
|
|
4
|
-
'
|
|
4
|
+
'yes',
|
|
5
|
+
'allow once',
|
|
5
6
|
'approve',
|
|
6
7
|
'accept',
|
|
7
|
-
'allow once',
|
|
8
|
-
'always allow',
|
|
9
|
-
'allow',
|
|
10
|
-
'yes',
|
|
11
|
-
'proceed',
|
|
12
8
|
'continue',
|
|
9
|
+
'run',
|
|
10
|
+
'proceed',
|
|
13
11
|
'confirm',
|
|
14
12
|
'save',
|
|
15
13
|
'ok',
|
|
16
14
|
'trust',
|
|
15
|
+
'allow',
|
|
16
|
+
'always allow',
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
function normalizeApprovalLabel(value: string): string {
|
|
20
20
|
return String(value || '')
|
|
21
21
|
.toLowerCase()
|
|
22
|
+
.replace(/^[\s\[(<{]*\d+(?:\s*[.)\]}>:-]|\s)+/, '')
|
|
22
23
|
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
23
24
|
.trim();
|
|
24
25
|
}
|
|
@@ -333,6 +333,8 @@ export interface ProviderModule {
|
|
|
333
333
|
resume?: ProviderResumeCapability;
|
|
334
334
|
/** Session ID probe config — auto-discovers provider session ID from local SQLite DB */
|
|
335
335
|
sessionProbe?: ProviderSessionProbe;
|
|
336
|
+
/** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
|
|
337
|
+
allowInputDuringGeneration?: boolean;
|
|
336
338
|
/** Approval button priority hints used when auto-approve must pick a positive action */
|
|
337
339
|
approvalPositiveHints?: string[];
|
|
338
340
|
scripts?: ProviderScripts;
|