@adhdev/daemon-core 0.8.75 → 0.8.77
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/cdp/manager.d.ts +9 -2
- package/dist/chat/async-batch.d.ts +4 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -0
- package/dist/config/chat-history.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +713 -147
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +712 -147
- package/dist/index.mjs.map +1 -1
- package/dist/providers/provider-instance.d.ts +4 -0
- package/dist/shared-types.d.ts +4 -0
- package/dist/status/chat-tail-hot-sessions.d.ts +4 -0
- package/node_modules/@adhdev/session-host-core/package.json +2 -2
- package/package.json +2 -2
- package/src/agent-stream/manager.ts +47 -8
- package/src/agent-stream/poller.ts +1 -0
- package/src/cdp/manager.ts +65 -13
- package/src/chat/async-batch.ts +26 -0
- package/src/cli-adapters/pty-transport.ts +2 -0
- package/src/cli-adapters/session-host-transport.ts +2 -0
- package/src/commands/stream-commands.ts +8 -5
- package/src/config/chat-history.ts +596 -63
- package/src/index.ts +2 -0
- package/src/providers/cli-provider-instance.ts +4 -0
- package/src/providers/provider-instance.ts +4 -0
- package/src/shared-types.ts +4 -0
- package/src/status/builders.ts +33 -1
- package/src/status/chat-tail-hot-sessions.ts +35 -1
|
@@ -13,9 +13,17 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normalization.js';
|
|
16
|
+
import { normalizeProviderSessionId } from '../providers/provider-session-id.js';
|
|
16
17
|
|
|
17
18
|
const HISTORY_DIR = path.join(os.homedir(), '.adhdev', 'history');
|
|
18
19
|
const RETAIN_DAYS = 30;
|
|
20
|
+
const SAVED_HISTORY_INDEX_VERSION = 1;
|
|
21
|
+
const SAVED_HISTORY_INDEX_FILE = '.saved-history-index.json';
|
|
22
|
+
const SAVED_HISTORY_INDEX_LOCK_SUFFIX = '.lock';
|
|
23
|
+
const SAVED_HISTORY_INDEX_LOCK_WAIT_MS = 1500;
|
|
24
|
+
const SAVED_HISTORY_INDEX_LOCK_STALE_MS = 15_000;
|
|
25
|
+
const SAVED_HISTORY_INDEX_LOCK_POLL_MS = 25;
|
|
26
|
+
export const SAVED_HISTORY_ROLLUP_THRESHOLD_BYTES = 16 * 1024 * 1024;
|
|
19
27
|
|
|
20
28
|
interface SavedHistorySessionCacheEntry {
|
|
21
29
|
signature: string;
|
|
@@ -24,6 +32,32 @@ interface SavedHistorySessionCacheEntry {
|
|
|
24
32
|
|
|
25
33
|
const savedHistorySessionCache = new Map<string, SavedHistorySessionCacheEntry>();
|
|
26
34
|
|
|
35
|
+
interface SavedHistoryFileSummaryCacheEntry {
|
|
36
|
+
signature: string;
|
|
37
|
+
summary: SavedHistoryFileSummary | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SavedHistoryFileSummary {
|
|
41
|
+
file: string;
|
|
42
|
+
historySessionId: string;
|
|
43
|
+
messageCount: number;
|
|
44
|
+
firstMessageAt: number;
|
|
45
|
+
lastMessageAt: number;
|
|
46
|
+
sessionTitle?: string;
|
|
47
|
+
preview?: string;
|
|
48
|
+
workspace?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface PersistedSavedHistoryIndexFile {
|
|
52
|
+
version: number;
|
|
53
|
+
files: Record<string, SavedHistoryFileSummaryCacheEntry>;
|
|
54
|
+
sessions?: Record<string, SavedHistorySessionSummary>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const savedHistoryFileSummaryCache = new Map<string, SavedHistoryFileSummaryCacheEntry>();
|
|
58
|
+
const savedHistoryBackgroundRefresh = new Set<string>();
|
|
59
|
+
const savedHistoryRollupInFlight = new Set<string>();
|
|
60
|
+
|
|
27
61
|
interface HistoryMessage {
|
|
28
62
|
ts: string; // ISO timestamp
|
|
29
63
|
receivedAt: number; // epoch ms
|
|
@@ -135,6 +169,76 @@ export interface SavedHistorySessionSummary {
|
|
|
135
169
|
workspace?: string;
|
|
136
170
|
}
|
|
137
171
|
|
|
172
|
+
function sortSavedHistorySessionSummaries(summaries: SavedHistorySessionSummary[]): SavedHistorySessionSummary[] {
|
|
173
|
+
return summaries.slice().sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildSavedHistorySessionSummaryMapFromEntries(entries: Map<string, SavedHistoryFileSummaryCacheEntry>): Record<string, SavedHistorySessionSummary> {
|
|
177
|
+
const summaries = new Map<string, SavedHistorySessionSummary>();
|
|
178
|
+
|
|
179
|
+
for (const entry of Array.from(entries.values())) {
|
|
180
|
+
const fileSummary = entry.summary;
|
|
181
|
+
if (!fileSummary || fileSummary.messageCount <= 0 || !fileSummary.lastMessageAt) continue;
|
|
182
|
+
const existing = summaries.get(fileSummary.historySessionId);
|
|
183
|
+
if (!existing) {
|
|
184
|
+
summaries.set(fileSummary.historySessionId, {
|
|
185
|
+
historySessionId: fileSummary.historySessionId,
|
|
186
|
+
sessionTitle: fileSummary.sessionTitle,
|
|
187
|
+
messageCount: fileSummary.messageCount,
|
|
188
|
+
firstMessageAt: fileSummary.firstMessageAt,
|
|
189
|
+
lastMessageAt: fileSummary.lastMessageAt,
|
|
190
|
+
preview: fileSummary.preview,
|
|
191
|
+
workspace: fileSummary.workspace,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
existing.messageCount += fileSummary.messageCount;
|
|
196
|
+
if (!existing.firstMessageAt || fileSummary.firstMessageAt < existing.firstMessageAt) {
|
|
197
|
+
existing.firstMessageAt = fileSummary.firstMessageAt;
|
|
198
|
+
}
|
|
199
|
+
if (fileSummary.lastMessageAt >= existing.lastMessageAt) {
|
|
200
|
+
existing.lastMessageAt = fileSummary.lastMessageAt;
|
|
201
|
+
if (fileSummary.sessionTitle) existing.sessionTitle = fileSummary.sessionTitle;
|
|
202
|
+
if (fileSummary.preview) existing.preview = fileSummary.preview;
|
|
203
|
+
}
|
|
204
|
+
if (!existing.workspace && fileSummary.workspace) {
|
|
205
|
+
existing.workspace = fileSummary.workspace;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return Object.fromEntries(sortSavedHistorySessionSummaries(Array.from(summaries.values())).map((summary) => [summary.historySessionId, summary]));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readPersistedSavedHistorySessionSummaries(dir: string): SavedHistorySessionSummary[] | null {
|
|
213
|
+
try {
|
|
214
|
+
const filePath = getSavedHistoryIndexFilePath(dir);
|
|
215
|
+
if (!fs.existsSync(filePath)) return null;
|
|
216
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as PersistedSavedHistoryIndexFile;
|
|
217
|
+
if (!raw || raw.version !== SAVED_HISTORY_INDEX_VERSION || !raw.sessions || typeof raw.sessions !== 'object') {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return sortSavedHistorySessionSummaries(
|
|
221
|
+
Object.values(raw.sessions)
|
|
222
|
+
.filter((summary) => !!summary && typeof summary.historySessionId === 'string' && summary.messageCount > 0 && summary.lastMessageAt > 0)
|
|
223
|
+
.map((summary) => ({
|
|
224
|
+
historySessionId: summary.historySessionId,
|
|
225
|
+
sessionTitle: summary.sessionTitle,
|
|
226
|
+
messageCount: summary.messageCount,
|
|
227
|
+
firstMessageAt: summary.firstMessageAt,
|
|
228
|
+
lastMessageAt: summary.lastMessageAt,
|
|
229
|
+
preview: summary.preview,
|
|
230
|
+
workspace: summary.workspace,
|
|
231
|
+
})),
|
|
232
|
+
);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function shouldScheduleSavedHistoryRollup(totalBytes: number): boolean {
|
|
239
|
+
return Number.isFinite(totalBytes) && totalBytes >= SAVED_HISTORY_ROLLUP_THRESHOLD_BYTES;
|
|
240
|
+
}
|
|
241
|
+
|
|
138
242
|
function sanitizeHistoryFileSegment(value?: string): string {
|
|
139
243
|
return String(value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
140
244
|
}
|
|
@@ -153,76 +257,459 @@ function listHistoryFiles(dir: string, historySessionId?: string): string[] {
|
|
|
153
257
|
.reverse();
|
|
154
258
|
}
|
|
155
259
|
|
|
156
|
-
function
|
|
157
|
-
|
|
260
|
+
function normalizeSavedHistorySessionId(agentType: string, historySessionId: string): string {
|
|
261
|
+
const normalizedId = String(historySessionId || '').trim();
|
|
262
|
+
if (!normalizedId) return '';
|
|
263
|
+
const strictProviderId = normalizeProviderSessionId(agentType, normalizedId);
|
|
264
|
+
if (strictProviderId) return strictProviderId;
|
|
265
|
+
return agentType === 'hermes-cli' ? '' : normalizedId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractSavedHistorySessionIdFromFile(agentType: string, file: string): string {
|
|
269
|
+
const match = file.match(/^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/);
|
|
270
|
+
return normalizeSavedHistorySessionId(agentType, match?.[1] || '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildSavedHistoryFileSignatureMap(dir: string, files: string[]): Map<string, string> {
|
|
274
|
+
return new Map(files.map((file) => {
|
|
158
275
|
try {
|
|
159
276
|
const stat = fs.statSync(path.join(dir, file));
|
|
160
|
-
return `${file}:${stat.size}:${Math.trunc(stat.mtimeMs)}
|
|
277
|
+
return [file, `${file}:${stat.size}:${Math.trunc(stat.mtimeMs)}`] as const;
|
|
161
278
|
} catch {
|
|
162
|
-
return `${file}:missing
|
|
279
|
+
return [file, `${file}:missing`] as const;
|
|
163
280
|
}
|
|
164
|
-
})
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildSavedHistoryCacheSignature(files: string[], fileSignatures: Map<string, string>): string {
|
|
285
|
+
return files.map((file) => fileSignatures.get(file) || `${file}:missing`).join('|');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getSavedHistoryIndexFilePath(dir: string): string {
|
|
289
|
+
return path.join(dir, SAVED_HISTORY_INDEX_FILE);
|
|
165
290
|
}
|
|
166
291
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
292
|
+
function getSavedHistoryIndexLockPath(dir: string): string {
|
|
293
|
+
return `${getSavedHistoryIndexFilePath(dir)}${SAVED_HISTORY_INDEX_LOCK_SUFFIX}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sleepBlocking(ms: number): void {
|
|
297
|
+
if (ms <= 0) return;
|
|
298
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function loadPersistedSavedHistoryIndexFromFile(dir: string): Map<string, SavedHistoryFileSummaryCacheEntry> {
|
|
302
|
+
try {
|
|
303
|
+
const filePath = getSavedHistoryIndexFilePath(dir);
|
|
304
|
+
if (!fs.existsSync(filePath)) return new Map();
|
|
305
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as PersistedSavedHistoryIndexFile;
|
|
306
|
+
if (!raw || raw.version !== SAVED_HISTORY_INDEX_VERSION || !raw.files || typeof raw.files !== 'object') {
|
|
307
|
+
return new Map();
|
|
308
|
+
}
|
|
309
|
+
return new Map(
|
|
310
|
+
Object.entries(raw.files)
|
|
311
|
+
.filter(([file, entry]) => !!file && !!entry && typeof entry.signature === 'string')
|
|
312
|
+
.map(([file, entry]) => [file, {
|
|
313
|
+
signature: entry.signature,
|
|
314
|
+
summary: entry.summary || null,
|
|
315
|
+
}]),
|
|
316
|
+
);
|
|
317
|
+
} catch {
|
|
318
|
+
return new Map();
|
|
177
319
|
}
|
|
320
|
+
}
|
|
178
321
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
322
|
+
function writePersistedSavedHistoryIndexFile(dir: string, entries: Map<string, SavedHistoryFileSummaryCacheEntry>): void {
|
|
323
|
+
const filePath = getSavedHistoryIndexFilePath(dir);
|
|
324
|
+
const tempPath = `${filePath}.tmp`;
|
|
325
|
+
const payload: PersistedSavedHistoryIndexFile = {
|
|
326
|
+
version: SAVED_HISTORY_INDEX_VERSION,
|
|
327
|
+
files: Object.fromEntries(entries.entries()),
|
|
328
|
+
sessions: buildSavedHistorySessionSummaryMapFromEntries(entries),
|
|
329
|
+
};
|
|
330
|
+
fs.writeFileSync(tempPath, JSON.stringify(payload), 'utf-8');
|
|
331
|
+
fs.renameSync(tempPath, filePath);
|
|
332
|
+
}
|
|
187
333
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
334
|
+
function acquireSavedHistoryIndexLock(dir: string): (() => void) | null {
|
|
335
|
+
const lockPath = getSavedHistoryIndexLockPath(dir);
|
|
336
|
+
const deadline = Date.now() + SAVED_HISTORY_INDEX_LOCK_WAIT_MS;
|
|
337
|
+
|
|
338
|
+
while (Date.now() <= deadline) {
|
|
339
|
+
try {
|
|
340
|
+
fs.mkdirSync(lockPath);
|
|
341
|
+
return () => {
|
|
194
342
|
try {
|
|
195
|
-
|
|
343
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
196
344
|
} catch {
|
|
197
|
-
|
|
345
|
+
// Ignore lock cleanup failures.
|
|
198
346
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
347
|
+
};
|
|
348
|
+
} catch (error: any) {
|
|
349
|
+
if (error?.code !== 'EEXIST') return null;
|
|
350
|
+
try {
|
|
351
|
+
const stat = fs.statSync(lockPath);
|
|
352
|
+
if (Date.now() - stat.mtimeMs > SAVED_HISTORY_INDEX_LOCK_STALE_MS) {
|
|
353
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
202
354
|
continue;
|
|
203
355
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
|
|
208
|
-
if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
|
|
356
|
+
} catch {
|
|
357
|
+
// Lock disappeared between stat attempts; retry immediately.
|
|
358
|
+
continue;
|
|
209
359
|
}
|
|
360
|
+
sleepBlocking(SAVED_HISTORY_INDEX_LOCK_POLL_MS);
|
|
210
361
|
}
|
|
362
|
+
}
|
|
211
363
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function withLockedPersistedSavedHistoryIndex<T>(
|
|
368
|
+
dir: string,
|
|
369
|
+
callback: (entries: Map<string, SavedHistoryFileSummaryCacheEntry>) => T,
|
|
370
|
+
): T | null {
|
|
371
|
+
const release = acquireSavedHistoryIndexLock(dir);
|
|
372
|
+
if (!release) return null;
|
|
373
|
+
try {
|
|
374
|
+
const entries = loadPersistedSavedHistoryIndexFromFile(dir);
|
|
375
|
+
const result = callback(entries);
|
|
376
|
+
writePersistedSavedHistoryIndexFile(dir, entries);
|
|
377
|
+
return result;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
} finally {
|
|
381
|
+
release();
|
|
222
382
|
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function loadPersistedSavedHistoryIndex(dir: string): Map<string, SavedHistoryFileSummaryCacheEntry> {
|
|
386
|
+
return loadPersistedSavedHistoryIndexFromFile(dir);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function savePersistedSavedHistoryIndex(dir: string, entries: Map<string, SavedHistoryFileSummaryCacheEntry>): void {
|
|
390
|
+
withLockedPersistedSavedHistoryIndex(dir, (currentEntries) => {
|
|
391
|
+
const incomingFiles = new Set(Array.from(entries.keys()));
|
|
392
|
+
for (const [file, entry] of Array.from(entries.entries())) {
|
|
393
|
+
const liveSignature = buildSavedHistoryFileSignature(dir, file);
|
|
394
|
+
const existingEntry = currentEntries.get(file);
|
|
395
|
+
if (existingEntry && existingEntry.signature !== liveSignature && entry.signature !== liveSignature) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (entry.signature !== liveSignature && (!existingEntry || existingEntry.signature !== liveSignature)) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
currentEntries.set(file, entry.signature === liveSignature ? entry : {
|
|
402
|
+
signature: liveSignature,
|
|
403
|
+
summary: existingEntry?.summary || entry.summary,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
for (const file of Array.from(currentEntries.keys())) {
|
|
407
|
+
if (incomingFiles.has(file)) continue;
|
|
408
|
+
if (!fs.existsSync(path.join(dir, file))) {
|
|
409
|
+
currentEntries.delete(file);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
223
414
|
|
|
224
|
-
|
|
225
|
-
|
|
415
|
+
function invalidatePersistedSavedHistoryIndex(agentType: string, dir: string): void {
|
|
416
|
+
try {
|
|
417
|
+
fs.rmSync(getSavedHistoryIndexFilePath(dir), { force: true });
|
|
418
|
+
} catch {
|
|
419
|
+
// Ignore persisted index cleanup failures.
|
|
420
|
+
}
|
|
421
|
+
savedHistorySessionCache.delete(agentType.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function getSavedHistoryFileSummaryCacheEntry(dir: string, file: string): SavedHistoryFileSummaryCacheEntry | null {
|
|
425
|
+
const filePath = path.join(dir, file);
|
|
426
|
+
const cached = savedHistoryFileSummaryCache.get(filePath);
|
|
427
|
+
if (cached) return cached;
|
|
428
|
+
const persisted = loadPersistedSavedHistoryIndex(dir).get(file) || null;
|
|
429
|
+
if (persisted) {
|
|
430
|
+
savedHistoryFileSummaryCache.set(filePath, persisted);
|
|
431
|
+
}
|
|
432
|
+
return persisted;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function buildSavedHistoryIndexFileSignature(dir: string): string {
|
|
436
|
+
try {
|
|
437
|
+
const stat = fs.statSync(getSavedHistoryIndexFilePath(dir));
|
|
438
|
+
return `index:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
439
|
+
} catch {
|
|
440
|
+
return 'index:missing';
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function historyDirectoryHasFilesNewerThanIndex(dir: string): boolean {
|
|
445
|
+
try {
|
|
446
|
+
const indexStat = fs.statSync(getSavedHistoryIndexFilePath(dir));
|
|
447
|
+
const files = listHistoryFiles(dir);
|
|
448
|
+
for (const file of files) {
|
|
449
|
+
const stat = fs.statSync(path.join(dir, file));
|
|
450
|
+
if (stat.mtimeMs > indexStat.mtimeMs) return true;
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
453
|
+
} catch {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function buildSavedHistoryFileSignature(dir: string, file: string): string {
|
|
459
|
+
try {
|
|
460
|
+
const stat = fs.statSync(path.join(dir, file));
|
|
461
|
+
return `${file}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
462
|
+
} catch {
|
|
463
|
+
return `${file}:missing`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function persistSavedHistoryFileSummaryEntry(agentType: string, dir: string, file: string, updater: (currentSummary: SavedHistoryFileSummary | null) => SavedHistoryFileSummary | null): void {
|
|
468
|
+
const filePath = path.join(dir, file);
|
|
469
|
+
const result = withLockedPersistedSavedHistoryIndex(dir, (entries) => {
|
|
470
|
+
const currentEntry = entries.get(file) || null;
|
|
471
|
+
const nextSummary = updater(currentEntry?.summary || null);
|
|
472
|
+
const nextEntry: SavedHistoryFileSummaryCacheEntry = {
|
|
473
|
+
signature: buildSavedHistoryFileSignature(dir, file),
|
|
474
|
+
summary: nextSummary,
|
|
475
|
+
};
|
|
476
|
+
entries.set(file, nextEntry);
|
|
477
|
+
savedHistoryFileSummaryCache.set(filePath, nextEntry);
|
|
478
|
+
return nextEntry;
|
|
479
|
+
});
|
|
480
|
+
if (!result) return;
|
|
481
|
+
if (result.summary?.historySessionId && shouldScheduleSavedHistoryRollupForSignature(result.signature)) {
|
|
482
|
+
scheduleSavedHistoryRollup(agentType, result.summary.historySessionId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function updateSavedHistoryIndexForSessionStart(agentType: string, dir: string, file: string, historySessionId: string, workspace: string): void {
|
|
487
|
+
const normalizedSessionId = normalizeSavedHistorySessionId(agentType, historySessionId);
|
|
488
|
+
const normalizedWorkspace = String(workspace || '').trim();
|
|
489
|
+
if (!normalizedSessionId || !normalizedWorkspace) return;
|
|
490
|
+
persistSavedHistoryFileSummaryEntry(agentType, dir, file, (currentSummary) => ({
|
|
491
|
+
file,
|
|
492
|
+
historySessionId: normalizedSessionId,
|
|
493
|
+
messageCount: currentSummary?.messageCount || 0,
|
|
494
|
+
firstMessageAt: currentSummary?.firstMessageAt || 0,
|
|
495
|
+
lastMessageAt: currentSummary?.lastMessageAt || 0,
|
|
496
|
+
sessionTitle: currentSummary?.sessionTitle,
|
|
497
|
+
preview: currentSummary?.preview,
|
|
498
|
+
workspace: normalizedWorkspace,
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function updateSavedHistoryIndexForAppendedMessages(
|
|
503
|
+
agentType: string,
|
|
504
|
+
dir: string,
|
|
505
|
+
file: string,
|
|
506
|
+
historySessionId: string | undefined,
|
|
507
|
+
messages: HistoryMessage[],
|
|
508
|
+
): void {
|
|
509
|
+
const normalizedSessionId = normalizeSavedHistorySessionId(agentType, historySessionId || '');
|
|
510
|
+
if (!normalizedSessionId || messages.length === 0) return;
|
|
511
|
+
persistSavedHistoryFileSummaryEntry(agentType, dir, file, (currentSummary) => {
|
|
512
|
+
const nextSummary: SavedHistoryFileSummary = {
|
|
513
|
+
file,
|
|
514
|
+
historySessionId: normalizedSessionId,
|
|
515
|
+
messageCount: currentSummary?.messageCount || 0,
|
|
516
|
+
firstMessageAt: currentSummary?.firstMessageAt || 0,
|
|
517
|
+
lastMessageAt: currentSummary?.lastMessageAt || 0,
|
|
518
|
+
sessionTitle: currentSummary?.sessionTitle,
|
|
519
|
+
preview: currentSummary?.preview,
|
|
520
|
+
workspace: currentSummary?.workspace,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
for (const message of messages) {
|
|
524
|
+
if (!message || message.historySessionId !== historySessionId) continue;
|
|
525
|
+
if (message.kind === 'session_start') {
|
|
526
|
+
if (message.workspace) nextSummary.workspace = message.workspace;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
nextSummary.messageCount += 1;
|
|
530
|
+
if (!nextSummary.firstMessageAt || message.receivedAt < nextSummary.firstMessageAt) {
|
|
531
|
+
nextSummary.firstMessageAt = message.receivedAt;
|
|
532
|
+
}
|
|
533
|
+
if (!nextSummary.lastMessageAt || message.receivedAt >= nextSummary.lastMessageAt) {
|
|
534
|
+
nextSummary.lastMessageAt = message.receivedAt;
|
|
535
|
+
if (message.sessionTitle) nextSummary.sessionTitle = message.sessionTitle;
|
|
536
|
+
if (message.role !== 'system' && message.content.trim()) nextSummary.preview = message.content.trim();
|
|
537
|
+
} else if (message.sessionTitle) {
|
|
538
|
+
nextSummary.sessionTitle = message.sessionTitle;
|
|
539
|
+
}
|
|
540
|
+
if (!nextSummary.preview && message.role !== 'system' && message.content.trim()) {
|
|
541
|
+
nextSummary.preview = message.content.trim();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return nextSummary;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function computeSavedHistoryFileSummary(agentType: string, dir: string, file: string): SavedHistoryFileSummary | null {
|
|
550
|
+
const historySessionId = extractSavedHistorySessionIdFromFile(agentType, file);
|
|
551
|
+
if (!historySessionId) return null;
|
|
552
|
+
|
|
553
|
+
const filePath = path.join(dir, file);
|
|
554
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
555
|
+
const lines = content.split('\n').filter(Boolean);
|
|
556
|
+
let messageCount = 0;
|
|
557
|
+
let firstMessageAt = 0;
|
|
558
|
+
let lastMessageAt = 0;
|
|
559
|
+
let sessionTitle = '';
|
|
560
|
+
let preview = '';
|
|
561
|
+
let workspace = '';
|
|
562
|
+
|
|
563
|
+
for (const line of lines) {
|
|
564
|
+
let parsed: HistoryMessage | null = null;
|
|
565
|
+
try {
|
|
566
|
+
parsed = JSON.parse(line) as HistoryMessage;
|
|
567
|
+
} catch {
|
|
568
|
+
parsed = null;
|
|
569
|
+
}
|
|
570
|
+
if (!parsed || parsed.historySessionId !== historySessionId) continue;
|
|
571
|
+
if (parsed.kind === 'session_start') {
|
|
572
|
+
if (!workspace && parsed.workspace) workspace = parsed.workspace;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
messageCount += 1;
|
|
576
|
+
if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
|
|
577
|
+
if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
|
|
578
|
+
if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
|
|
579
|
+
if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (messageCount === 0 || !lastMessageAt) return null;
|
|
583
|
+
return {
|
|
584
|
+
file,
|
|
585
|
+
historySessionId,
|
|
586
|
+
messageCount,
|
|
587
|
+
firstMessageAt,
|
|
588
|
+
lastMessageAt,
|
|
589
|
+
sessionTitle: sessionTitle || undefined,
|
|
590
|
+
preview: preview || undefined,
|
|
591
|
+
workspace: workspace || undefined,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function shouldScheduleSavedHistoryRollupForSignature(signature: string): boolean {
|
|
596
|
+
const parts = String(signature || '').split(':');
|
|
597
|
+
const size = Number(parts[1] || 0);
|
|
598
|
+
return shouldScheduleSavedHistoryRollup(size);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function scheduleSavedHistoryRollup(agentType: string, historySessionId: string): void {
|
|
602
|
+
const key = `${agentType}:${historySessionId}`;
|
|
603
|
+
if (!historySessionId || savedHistoryRollupInFlight.has(key)) return;
|
|
604
|
+
savedHistoryRollupInFlight.add(key);
|
|
605
|
+
setTimeout(() => {
|
|
606
|
+
try {
|
|
607
|
+
new ChatHistoryWriter().compactHistorySession(agentType, historySessionId);
|
|
608
|
+
} finally {
|
|
609
|
+
savedHistoryRollupInFlight.delete(key);
|
|
610
|
+
}
|
|
611
|
+
}, 0);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function scheduleSavedHistoryBackgroundRefresh(agentType: string, dir: string): void {
|
|
615
|
+
const key = `${agentType}:${dir}`;
|
|
616
|
+
if (savedHistoryBackgroundRefresh.has(key)) return;
|
|
617
|
+
savedHistoryBackgroundRefresh.add(key);
|
|
618
|
+
setTimeout(() => {
|
|
619
|
+
try {
|
|
620
|
+
if (!fs.existsSync(dir)) return;
|
|
621
|
+
const files = listHistoryFiles(dir);
|
|
622
|
+
const fileSignatures = buildSavedHistoryFileSignatureMap(dir, files);
|
|
623
|
+
const persistedEntries = loadPersistedSavedHistoryIndex(dir);
|
|
624
|
+
const computed = computeSavedHistorySessionSummaries(agentType, dir, files, fileSignatures, persistedEntries);
|
|
625
|
+
savePersistedSavedHistoryIndex(dir, computed.persistedEntries || new Map());
|
|
626
|
+
const refreshedIndexSignature = buildSavedHistoryIndexFileSignature(dir);
|
|
627
|
+
savedHistorySessionCache.set(agentType.replace(/[^a-zA-Z0-9_-]/g, '_'), {
|
|
628
|
+
signature: refreshedIndexSignature,
|
|
629
|
+
summaries: computed.summaries || [],
|
|
630
|
+
});
|
|
631
|
+
for (const [file, entry] of Array.from(computed.persistedEntries.entries())) {
|
|
632
|
+
if (!entry?.summary || !shouldScheduleSavedHistoryRollupForSignature(entry.signature)) continue;
|
|
633
|
+
scheduleSavedHistoryRollup(agentType, entry.summary.historySessionId);
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
// Ignore background refresh failures.
|
|
637
|
+
} finally {
|
|
638
|
+
savedHistoryBackgroundRefresh.delete(key);
|
|
639
|
+
}
|
|
640
|
+
}, 0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function computeSavedHistorySessionSummaries(
|
|
644
|
+
agentType: string,
|
|
645
|
+
dir: string,
|
|
646
|
+
files: string[],
|
|
647
|
+
fileSignatures: Map<string, string>,
|
|
648
|
+
persistedEntries: Map<string, SavedHistoryFileSummaryCacheEntry>,
|
|
649
|
+
): { summaries: SavedHistorySessionSummary[]; persistedEntries: Map<string, SavedHistoryFileSummaryCacheEntry> } {
|
|
650
|
+
const summaryBySessionId = new Map<string, SavedHistorySessionSummary>();
|
|
651
|
+
const nextPersistedEntries = new Map<string, SavedHistoryFileSummaryCacheEntry>();
|
|
652
|
+
|
|
653
|
+
for (const file of files.slice().sort()) {
|
|
654
|
+
const filePath = path.join(dir, file);
|
|
655
|
+
const signature = fileSignatures.get(file) || `${file}:missing`;
|
|
656
|
+
const cached = savedHistoryFileSummaryCache.get(filePath);
|
|
657
|
+
const persisted = persistedEntries.get(file);
|
|
658
|
+
const reusableEntry = cached?.signature === signature
|
|
659
|
+
? cached
|
|
660
|
+
: persisted?.signature === signature
|
|
661
|
+
? persisted
|
|
662
|
+
: null;
|
|
663
|
+
const fileSummary = reusableEntry?.summary || computeSavedHistoryFileSummary(agentType, dir, file);
|
|
664
|
+
const nextEntry: SavedHistoryFileSummaryCacheEntry = reusableEntry || {
|
|
665
|
+
signature,
|
|
666
|
+
summary: fileSummary,
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
if (!reusableEntry) {
|
|
670
|
+
nextEntry.signature = signature;
|
|
671
|
+
nextEntry.summary = fileSummary;
|
|
672
|
+
}
|
|
673
|
+
savedHistoryFileSummaryCache.set(filePath, nextEntry);
|
|
674
|
+
nextPersistedEntries.set(file, nextEntry);
|
|
675
|
+
|
|
676
|
+
if (!fileSummary) continue;
|
|
677
|
+
const existing = summaryBySessionId.get(fileSummary.historySessionId);
|
|
678
|
+
if (fileSummary.messageCount <= 0 || !fileSummary.lastMessageAt) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (!existing) {
|
|
682
|
+
summaryBySessionId.set(fileSummary.historySessionId, {
|
|
683
|
+
historySessionId: fileSummary.historySessionId,
|
|
684
|
+
sessionTitle: fileSummary.sessionTitle,
|
|
685
|
+
messageCount: fileSummary.messageCount,
|
|
686
|
+
firstMessageAt: fileSummary.firstMessageAt,
|
|
687
|
+
lastMessageAt: fileSummary.lastMessageAt,
|
|
688
|
+
preview: fileSummary.preview,
|
|
689
|
+
workspace: fileSummary.workspace,
|
|
690
|
+
});
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
existing.messageCount += fileSummary.messageCount;
|
|
695
|
+
if (!existing.firstMessageAt || fileSummary.firstMessageAt < existing.firstMessageAt) {
|
|
696
|
+
existing.firstMessageAt = fileSummary.firstMessageAt;
|
|
697
|
+
}
|
|
698
|
+
if (fileSummary.lastMessageAt >= existing.lastMessageAt) {
|
|
699
|
+
existing.lastMessageAt = fileSummary.lastMessageAt;
|
|
700
|
+
if (fileSummary.sessionTitle) existing.sessionTitle = fileSummary.sessionTitle;
|
|
701
|
+
if (fileSummary.preview) existing.preview = fileSummary.preview;
|
|
702
|
+
}
|
|
703
|
+
if (!existing.workspace && fileSummary.workspace) {
|
|
704
|
+
existing.workspace = fileSummary.workspace;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
summaries: Array.from(summaryBySessionId.values())
|
|
710
|
+
.sort((a, b) => b.lastMessageAt - a.lastMessageAt),
|
|
711
|
+
persistedEntries: nextPersistedEntries,
|
|
712
|
+
};
|
|
226
713
|
}
|
|
227
714
|
|
|
228
715
|
export class ChatHistoryWriter {
|
|
@@ -313,9 +800,11 @@ export class ChatHistoryWriter {
|
|
|
313
800
|
|
|
314
801
|
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
315
802
|
const filePrefix = effectiveHistoryKey ? `${this.sanitize(effectiveHistoryKey)}_` : '';
|
|
316
|
-
const
|
|
803
|
+
const fileName = `${filePrefix}${date}.jsonl`;
|
|
804
|
+
const filePath = path.join(dir, fileName);
|
|
317
805
|
const lines = newMessages.map(m => JSON.stringify(m)).join('\n') + '\n';
|
|
318
806
|
fs.appendFileSync(filePath, lines, 'utf-8');
|
|
807
|
+
updateSavedHistoryIndexForAppendedMessages(agentType, dir, fileName, effectiveHistoryKey, newMessages);
|
|
319
808
|
|
|
320
809
|
// Detect session switch — only for unstable runtime-only histories.
|
|
321
810
|
// When we have a persistent history session key, replayed read_chat payloads
|
|
@@ -438,7 +927,8 @@ export class ChatHistoryWriter {
|
|
|
438
927
|
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
439
928
|
fs.mkdirSync(dir, { recursive: true });
|
|
440
929
|
const date = new Date().toISOString().slice(0, 10);
|
|
441
|
-
const
|
|
930
|
+
const fileName = `${this.sanitize(id)}_${date}.jsonl`;
|
|
931
|
+
const filePath = path.join(dir, fileName);
|
|
442
932
|
const record: HistoryMessage = {
|
|
443
933
|
ts: new Date().toISOString(),
|
|
444
934
|
receivedAt: Date.now(),
|
|
@@ -451,6 +941,7 @@ export class ChatHistoryWriter {
|
|
|
451
941
|
workspace: ws,
|
|
452
942
|
};
|
|
453
943
|
fs.appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf-8');
|
|
944
|
+
updateSavedHistoryIndexForSessionStart(agentType, dir, fileName, id, ws);
|
|
454
945
|
} catch {
|
|
455
946
|
// Ignore — must not affect main functionality
|
|
456
947
|
}
|
|
@@ -530,6 +1021,7 @@ export class ChatHistoryWriter {
|
|
|
530
1021
|
}
|
|
531
1022
|
fs.unlinkSync(sourcePath);
|
|
532
1023
|
}
|
|
1024
|
+
invalidatePersistedSavedHistoryIndex(agentType, dir);
|
|
533
1025
|
} catch {
|
|
534
1026
|
// Ignore promotion failure; future messages will still write to the new session key.
|
|
535
1027
|
}
|
|
@@ -587,6 +1079,7 @@ export class ChatHistoryWriter {
|
|
|
587
1079
|
}
|
|
588
1080
|
fs.writeFileSync(filePath, `${collapsed.map((entry) => JSON.stringify(entry)).join('\n')}\n`, 'utf-8');
|
|
589
1081
|
}
|
|
1082
|
+
invalidatePersistedSavedHistoryIndex(agentType, dir);
|
|
590
1083
|
} catch {
|
|
591
1084
|
// Ignore compaction failure.
|
|
592
1085
|
}
|
|
@@ -613,14 +1106,19 @@ export class ChatHistoryWriter {
|
|
|
613
1106
|
const dirPath = path.join(HISTORY_DIR, dir.name);
|
|
614
1107
|
const files = fs.readdirSync(dirPath)
|
|
615
1108
|
.filter(f => f.endsWith('.jsonl') || f.endsWith('.terminal.log'));
|
|
1109
|
+
let removedAny = false;
|
|
616
1110
|
|
|
617
1111
|
for (const file of files) {
|
|
618
1112
|
const filePath = path.join(dirPath, file);
|
|
619
1113
|
const stat = fs.statSync(filePath);
|
|
620
1114
|
if (stat.mtimeMs < cutoff) {
|
|
621
1115
|
fs.unlinkSync(filePath);
|
|
1116
|
+
removedAny = true;
|
|
622
1117
|
}
|
|
623
1118
|
}
|
|
1119
|
+
if (removedAny) {
|
|
1120
|
+
invalidatePersistedSavedHistoryIndex(dir.name, dirPath);
|
|
1121
|
+
}
|
|
624
1122
|
}
|
|
625
1123
|
} catch {
|
|
626
1124
|
// Ignore rotate failure
|
|
@@ -717,21 +1215,56 @@ export function listSavedHistorySessions(
|
|
|
717
1215
|
return { sessions: [], hasMore: false };
|
|
718
1216
|
}
|
|
719
1217
|
|
|
720
|
-
const files = listHistoryFiles(dir);
|
|
721
|
-
const signature = buildSavedHistoryCacheSignature(dir, files);
|
|
722
1218
|
const cached = savedHistorySessionCache.get(sanitized);
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1219
|
+
const offset = Math.max(0, options.offset || 0);
|
|
1220
|
+
const limit = Math.max(1, options.limit || 30);
|
|
1221
|
+
const indexSignature = buildSavedHistoryIndexFileSignature(dir);
|
|
1222
|
+
let cacheWasInvalidated = false;
|
|
1223
|
+
if (cached) {
|
|
1224
|
+
const cacheLooksPersisted = cached.signature.startsWith('index:');
|
|
1225
|
+
const cacheStillValid = cacheLooksPersisted
|
|
1226
|
+
? cached.signature === indexSignature
|
|
1227
|
+
: (() => {
|
|
1228
|
+
const files = listHistoryFiles(dir);
|
|
1229
|
+
const fileSignatures = buildSavedHistoryFileSignatureMap(dir, files);
|
|
1230
|
+
return cached.signature === buildSavedHistoryCacheSignature(files, fileSignatures);
|
|
1231
|
+
})();
|
|
1232
|
+
if (cacheStillValid) {
|
|
1233
|
+
const sliced = cached.summaries.slice(offset, offset + limit);
|
|
1234
|
+
return {
|
|
1235
|
+
sessions: sliced,
|
|
1236
|
+
hasMore: cached.summaries.length > offset + limit,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
cacheWasInvalidated = true;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const persistedSessions = readPersistedSavedHistorySessionSummaries(dir);
|
|
1243
|
+
if (!cacheWasInvalidated && persistedSessions?.length && !historyDirectoryHasFilesNewerThanIndex(dir)) {
|
|
727
1244
|
savedHistorySessionCache.set(sanitized, {
|
|
728
|
-
signature,
|
|
729
|
-
summaries,
|
|
1245
|
+
signature: indexSignature,
|
|
1246
|
+
summaries: persistedSessions,
|
|
730
1247
|
});
|
|
1248
|
+
scheduleSavedHistoryBackgroundRefresh(agentType, dir);
|
|
1249
|
+
const sliced = persistedSessions.slice(offset, offset + limit);
|
|
1250
|
+
return {
|
|
1251
|
+
sessions: sliced,
|
|
1252
|
+
hasMore: persistedSessions.length > offset + limit,
|
|
1253
|
+
};
|
|
731
1254
|
}
|
|
732
1255
|
|
|
733
|
-
const
|
|
734
|
-
const
|
|
1256
|
+
const files = listHistoryFiles(dir);
|
|
1257
|
+
const fileSignatures = buildSavedHistoryFileSignatureMap(dir, files);
|
|
1258
|
+
const signature = buildSavedHistoryCacheSignature(files, fileSignatures);
|
|
1259
|
+
const persistedEntries = loadPersistedSavedHistoryIndex(dir);
|
|
1260
|
+
const computed = computeSavedHistorySessionSummaries(agentType, dir, files, fileSignatures, persistedEntries);
|
|
1261
|
+
const summaries = computed.summaries || [];
|
|
1262
|
+
savePersistedSavedHistoryIndex(dir, computed.persistedEntries || new Map());
|
|
1263
|
+
savedHistorySessionCache.set(sanitized, {
|
|
1264
|
+
signature,
|
|
1265
|
+
summaries,
|
|
1266
|
+
});
|
|
1267
|
+
|
|
735
1268
|
const sliced = summaries.slice(offset, offset + limit);
|
|
736
1269
|
return {
|
|
737
1270
|
sessions: sliced,
|