@adhdev/daemon-core 0.8.82 → 0.8.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +1 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.js +176 -79
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +176 -79
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +10 -0
- package/dist/shared-types.d.ts +2 -0
- package/dist/status/chat-tail-hot-sessions.d.ts +2 -0
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +94 -26
- package/src/cli-adapters/terminal-screen.ts +6 -4
- package/src/config/chat-history.ts +137 -0
- package/src/providers/cli-provider-instance.ts +66 -2
- package/src/shared-types.d.ts +3 -0
- package/src/shared-types.ts +2 -0
- package/src/status/chat-tail-hot-sessions.ts +15 -1
- package/src/status/snapshot.ts +2 -0
|
@@ -8,6 +8,14 @@ import { type ProviderModule } from './contracts.js';
|
|
|
8
8
|
import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
|
|
9
9
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
10
10
|
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
11
|
+
type PersistableCliHistoryMessage = {
|
|
12
|
+
role: string;
|
|
13
|
+
content: string;
|
|
14
|
+
kind?: string;
|
|
15
|
+
senderName?: string;
|
|
16
|
+
receivedAt?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function buildIncrementalHistoryAppendMessages(previousMessages: PersistableCliHistoryMessage[], currentMessages: PersistableCliHistoryMessage[]): PersistableCliHistoryMessage[];
|
|
11
19
|
export declare function getForcedNewSessionScriptName(provider: ProviderModule | undefined, launchMode: 'new' | 'resume' | 'manual'): string | null;
|
|
12
20
|
export declare function waitForCliAdapterReady(adapter: {
|
|
13
21
|
isReady?: () => boolean;
|
|
@@ -39,6 +47,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
39
47
|
private appliedEffectKeys;
|
|
40
48
|
private historyWriter;
|
|
41
49
|
private runtimeMessages;
|
|
50
|
+
private lastPersistedHistoryMessages;
|
|
42
51
|
readonly instanceId: string;
|
|
43
52
|
private suppressIdleHistoryReplay;
|
|
44
53
|
private errorMessage;
|
|
@@ -99,3 +108,4 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
99
108
|
private buildSqlPlaceholderList;
|
|
100
109
|
private querySqliteText;
|
|
101
110
|
}
|
|
111
|
+
export {};
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare const DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS = 8000;
|
|
|
3
3
|
export interface HotChatSessionLike {
|
|
4
4
|
id?: string | null;
|
|
5
5
|
status?: unknown;
|
|
6
|
+
unread?: unknown;
|
|
7
|
+
inboxBucket?: unknown;
|
|
6
8
|
lastMessageAt?: unknown;
|
|
7
9
|
runtimeLifecycle?: unknown;
|
|
8
10
|
runtimeSurfaceKind?: unknown;
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import * as os from 'os';
|
|
18
18
|
import type { CliAdapter } from '../cli-adapter-types.js';
|
|
19
19
|
import { LOG } from '../logging/logger.js';
|
|
20
|
+
import { getDebugRuntimeConfig } from '../logging/debug-config.js';
|
|
20
21
|
import { TerminalScreen } from './terminal-screen.js';
|
|
21
22
|
import {
|
|
22
23
|
NodePtyTransportFactory,
|
|
@@ -182,8 +183,23 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
182
183
|
private currentTurnScope: TurnParseScope | null = null;
|
|
183
184
|
private traceEntries: CliTraceEntry[] = [];
|
|
184
185
|
private traceSeq = 0;
|
|
185
|
-
private traceSessionId =
|
|
186
|
+
private traceSessionId = '';
|
|
187
|
+
private parsedStatusCache: {
|
|
188
|
+
committedMessagesRef: CliChatMessage[];
|
|
189
|
+
responseBuffer: string;
|
|
190
|
+
currentTurnScope: TurnParseScope | null;
|
|
191
|
+
recentOutputBuffer: string;
|
|
192
|
+
accumulatedBuffer: string;
|
|
193
|
+
accumulatedRawBuffer: string;
|
|
194
|
+
screenText: string;
|
|
195
|
+
currentStatus: CliSessionStatus['status'];
|
|
196
|
+
activeModal: { message: string; buttons: string[] } | null;
|
|
197
|
+
cliName: string;
|
|
198
|
+
lastOutputAt: number;
|
|
199
|
+
result: any;
|
|
200
|
+
} | null = null;
|
|
186
201
|
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
202
|
+
|
|
187
203
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
188
204
|
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
189
205
|
private static readonly MAX_FINISH_RETRIES = 2;
|
|
@@ -323,7 +339,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
323
339
|
`[${this.cliType}] Provider resolution: providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'} source=${this.providerResolutionMeta.scriptsSource || '-'} version=${this.providerResolutionMeta.resolvedVersion || '-'}`
|
|
324
340
|
);
|
|
325
341
|
} else {
|
|
326
|
-
|
|
342
|
+
const resolutionSummary = `providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'} source=${this.providerResolutionMeta.scriptsSource || '-'} version=${this.providerResolutionMeta.resolvedVersion || '-'}`;
|
|
343
|
+
const hasResolvedProviderScripts = Boolean(
|
|
344
|
+
this.providerResolutionMeta.providerDir
|
|
345
|
+
|| this.providerResolutionMeta.scriptDir
|
|
346
|
+
|| this.providerResolutionMeta.scriptsPath
|
|
347
|
+
|| this.providerResolutionMeta.scriptsSource
|
|
348
|
+
|| this.providerResolutionMeta.resolvedVersion,
|
|
349
|
+
);
|
|
350
|
+
if (hasResolvedProviderScripts) {
|
|
351
|
+
LOG.warn('CLI', `[${this.cliType}] ⚠ No CLI scripts loaded! Provider needs scripts/{version}/scripts.js (${resolutionSummary})`);
|
|
352
|
+
} else {
|
|
353
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts not yet resolved (${resolutionSummary})`);
|
|
354
|
+
}
|
|
327
355
|
}
|
|
328
356
|
}
|
|
329
357
|
|
|
@@ -485,7 +513,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
485
513
|
this.terminalScreen.write(rawData);
|
|
486
514
|
const cleanData = sanitizeTerminalText(rawData);
|
|
487
515
|
const now = Date.now();
|
|
488
|
-
const
|
|
516
|
+
const screenText = this.terminalScreen.getText();
|
|
517
|
+
const normalizedScreenSnapshot = normalizeScreenSnapshot(screenText);
|
|
489
518
|
this.lastOutputAt = now;
|
|
490
519
|
if (cleanData.trim()) this.lastNonEmptyOutputAt = now;
|
|
491
520
|
if (normalizedScreenSnapshot !== this.lastScreenSnapshot) {
|
|
@@ -498,13 +527,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
498
527
|
if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
|
|
499
528
|
this.clearIdleFinishCandidate('new_output');
|
|
500
529
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
530
|
+
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
531
|
+
this.recordTrace('output', {
|
|
532
|
+
rawLength: rawData.length,
|
|
533
|
+
cleanLength: cleanData.length,
|
|
534
|
+
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
535
|
+
cleanPreview: summarizeCliTraceText(cleanData, 300),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
508
538
|
|
|
509
539
|
if (this.startupParseGate) {
|
|
510
540
|
this.scheduleStartupSettleCheck();
|
|
@@ -1304,15 +1334,36 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1304
1334
|
* Called by command handler / dashboard for rich content rendering.
|
|
1305
1335
|
*/
|
|
1306
1336
|
getScriptParsedStatus(): any {
|
|
1337
|
+
const screenText = this.terminalScreen.getText();
|
|
1338
|
+
const cached = this.parsedStatusCache;
|
|
1339
|
+
if (
|
|
1340
|
+
cached
|
|
1341
|
+
&& cached.committedMessagesRef === this.committedMessages
|
|
1342
|
+
&& cached.responseBuffer === this.responseBuffer
|
|
1343
|
+
&& cached.currentTurnScope === this.currentTurnScope
|
|
1344
|
+
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1345
|
+
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1346
|
+
&& cached.accumulatedRawBuffer === this.accumulatedRawBuffer
|
|
1347
|
+
&& cached.screenText === screenText
|
|
1348
|
+
&& cached.currentStatus === this.currentStatus
|
|
1349
|
+
&& cached.activeModal === this.activeModal
|
|
1350
|
+
&& cached.cliName === this.cliName
|
|
1351
|
+
&& cached.lastOutputAt === this.lastOutputAt
|
|
1352
|
+
) {
|
|
1353
|
+
return cached.result;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1307
1356
|
const parsed = this.parseCurrentTranscript(
|
|
1308
1357
|
this.committedMessages,
|
|
1309
1358
|
this.responseBuffer,
|
|
1310
1359
|
this.currentTurnScope,
|
|
1360
|
+
screenText,
|
|
1311
1361
|
);
|
|
1312
1362
|
const shouldPreferCommittedMessages =
|
|
1313
1363
|
!this.currentTurnScope
|
|
1314
1364
|
&& this.currentStatus === 'idle'
|
|
1315
1365
|
&& !this.activeModal;
|
|
1366
|
+
let result: any;
|
|
1316
1367
|
if (parsed && Array.isArray(parsed.messages)) {
|
|
1317
1368
|
const hydratedMessages = shouldPreferCommittedMessages
|
|
1318
1369
|
? this.committedMessages.map((message, index) => buildChatMessage({
|
|
@@ -1328,7 +1379,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1328
1379
|
scope: this.currentTurnScope,
|
|
1329
1380
|
lastOutputAt: this.lastOutputAt,
|
|
1330
1381
|
});
|
|
1331
|
-
|
|
1382
|
+
result = {
|
|
1332
1383
|
id: parsed.id || 'cli_session',
|
|
1333
1384
|
status: parsed.status || this.currentStatus,
|
|
1334
1385
|
title: parsed.title || this.cliName,
|
|
@@ -1336,23 +1387,39 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1336
1387
|
activeModal: parsed.activeModal ?? this.activeModal,
|
|
1337
1388
|
providerSessionId: typeof parsed.providerSessionId === 'string' ? parsed.providerSessionId : undefined,
|
|
1338
1389
|
};
|
|
1390
|
+
} else {
|
|
1391
|
+
const messages = [...this.committedMessages];
|
|
1392
|
+
result = {
|
|
1393
|
+
id: 'cli_session',
|
|
1394
|
+
status: this.currentStatus,
|
|
1395
|
+
title: this.cliName,
|
|
1396
|
+
messages: messages.map((message, index) => buildChatMessage({
|
|
1397
|
+
...message,
|
|
1398
|
+
id: message.id || `msg_${index}`,
|
|
1399
|
+
index: typeof message.index === 'number' ? message.index : index,
|
|
1400
|
+
receivedAt: typeof message.receivedAt === 'number'
|
|
1401
|
+
? message.receivedAt
|
|
1402
|
+
: message.timestamp,
|
|
1403
|
+
})),
|
|
1404
|
+
activeModal: this.activeModal,
|
|
1405
|
+
};
|
|
1339
1406
|
}
|
|
1340
1407
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
receivedAt: typeof message.receivedAt === 'number'
|
|
1351
|
-
? message.receivedAt
|
|
1352
|
-
: message.timestamp,
|
|
1353
|
-
})),
|
|
1408
|
+
this.parsedStatusCache = {
|
|
1409
|
+
committedMessagesRef: this.committedMessages,
|
|
1410
|
+
responseBuffer: this.responseBuffer,
|
|
1411
|
+
currentTurnScope: this.currentTurnScope,
|
|
1412
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1413
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1414
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1415
|
+
screenText,
|
|
1416
|
+
currentStatus: this.currentStatus,
|
|
1354
1417
|
activeModal: this.activeModal,
|
|
1418
|
+
cliName: this.cliName,
|
|
1419
|
+
lastOutputAt: this.lastOutputAt,
|
|
1420
|
+
result,
|
|
1355
1421
|
};
|
|
1422
|
+
return result;
|
|
1356
1423
|
}
|
|
1357
1424
|
|
|
1358
1425
|
async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
|
|
@@ -1377,17 +1444,18 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1377
1444
|
}));
|
|
1378
1445
|
}
|
|
1379
1446
|
|
|
1380
|
-
private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): any {
|
|
1447
|
+
private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null, screenTextOverride?: string): any {
|
|
1381
1448
|
if (!this.cliScripts?.parseOutput) {
|
|
1382
1449
|
this.parseErrorMessage = null;
|
|
1383
1450
|
return null;
|
|
1384
1451
|
}
|
|
1385
1452
|
try {
|
|
1453
|
+
const screenText = typeof screenTextOverride === 'string' ? screenTextOverride : this.terminalScreen.getText();
|
|
1386
1454
|
const input = buildCliParseInput({
|
|
1387
1455
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1388
1456
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1389
1457
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1390
|
-
terminalScreenText:
|
|
1458
|
+
terminalScreenText: screenText,
|
|
1391
1459
|
baseMessages,
|
|
1392
1460
|
partialResponse,
|
|
1393
1461
|
isWaitingForResponse: this.isWaitingForResponse,
|
|
@@ -67,10 +67,12 @@ function logTerminalBackendSelection(
|
|
|
67
67
|
if (loggedTerminalBackends.has(key)) return;
|
|
68
68
|
loggedTerminalBackends.add(key);
|
|
69
69
|
if (backendKind === 'xterm' && preference !== 'xterm' && !ghosttyAvailable) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
const message = `[terminal-screen] ghostty-vt unavailable; using xterm fallback (preference=${preference})`;
|
|
71
|
+
if (preference === 'auto') {
|
|
72
|
+
LOG.info('Terminal', message);
|
|
73
|
+
} else {
|
|
74
|
+
LOG.warn('Terminal', message);
|
|
75
|
+
}
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
78
|
LOG.info(
|
|
@@ -1274,3 +1274,140 @@ export function listSavedHistorySessions(
|
|
|
1274
1274
|
return { sessions: [], hasMore: false };
|
|
1275
1275
|
}
|
|
1276
1276
|
}
|
|
1277
|
+
|
|
1278
|
+
function normalizeCanonicalHermesMessageContent(content: unknown): string {
|
|
1279
|
+
if (typeof content === 'string') return content.trim();
|
|
1280
|
+
if (content == null) return '';
|
|
1281
|
+
try {
|
|
1282
|
+
return JSON.stringify(content).trim();
|
|
1283
|
+
} catch {
|
|
1284
|
+
return String(content).trim();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function extractCanonicalHermesMessageTimestamp(message: Record<string, unknown>, fallbackTs: number): number {
|
|
1289
|
+
const numericTimestamp = Number(message.receivedAt || message.timestamp || message.ts || 0);
|
|
1290
|
+
if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
|
|
1291
|
+
const stringTimestamp = typeof message.ts === 'string'
|
|
1292
|
+
? Date.parse(message.ts)
|
|
1293
|
+
: (typeof message.timestamp === 'string' ? Date.parse(message.timestamp) : NaN);
|
|
1294
|
+
if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
|
|
1295
|
+
return fallbackTs;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function readExistingHermesSessionStartRecord(historySessionId: string): HistoryMessage | null {
|
|
1299
|
+
try {
|
|
1300
|
+
const dir = path.join(HISTORY_DIR, 'hermes-cli');
|
|
1301
|
+
if (!fs.existsSync(dir)) return null;
|
|
1302
|
+
const files = listHistoryFiles(dir, historySessionId).sort();
|
|
1303
|
+
for (const file of files) {
|
|
1304
|
+
const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
|
|
1305
|
+
for (const line of lines) {
|
|
1306
|
+
try {
|
|
1307
|
+
const parsed = JSON.parse(line) as HistoryMessage;
|
|
1308
|
+
if (parsed.historySessionId !== historySessionId) continue;
|
|
1309
|
+
if (parsed.kind === 'session_start' && parsed.role === 'system') {
|
|
1310
|
+
return parsed;
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
// Ignore malformed lines while probing for the original session_start marker.
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return null;
|
|
1318
|
+
} catch {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
|
|
1324
|
+
const normalizedSessionId = normalizeSavedHistorySessionId('hermes-cli', historySessionId);
|
|
1325
|
+
if (!normalizedSessionId) return false;
|
|
1326
|
+
|
|
1327
|
+
try {
|
|
1328
|
+
const sessionFilePath = path.join(os.homedir(), '.hermes', 'sessions', `session_${normalizedSessionId}.json`);
|
|
1329
|
+
if (!fs.existsSync(sessionFilePath)) return false;
|
|
1330
|
+
const raw = JSON.parse(fs.readFileSync(sessionFilePath, 'utf-8')) as {
|
|
1331
|
+
session_start?: string;
|
|
1332
|
+
last_updated?: string;
|
|
1333
|
+
messages?: Array<Record<string, unknown>>;
|
|
1334
|
+
};
|
|
1335
|
+
const canonicalMessages = Array.isArray(raw.messages) ? raw.messages : [];
|
|
1336
|
+
const dir = path.join(HISTORY_DIR, 'hermes-cli');
|
|
1337
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1338
|
+
const existingSessionStart = readExistingHermesSessionStartRecord(normalizedSessionId);
|
|
1339
|
+
const records: HistoryMessage[] = [];
|
|
1340
|
+
if (existingSessionStart) {
|
|
1341
|
+
records.push({
|
|
1342
|
+
...existingSessionStart,
|
|
1343
|
+
historySessionId: normalizedSessionId,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let fallbackTs = Date.parse(raw.session_start || raw.last_updated || '') || Date.now();
|
|
1348
|
+
for (const message of canonicalMessages) {
|
|
1349
|
+
const role = String(message.role || '').trim();
|
|
1350
|
+
const content = normalizeCanonicalHermesMessageContent(message.content);
|
|
1351
|
+
if (!content) continue;
|
|
1352
|
+
const receivedAt = extractCanonicalHermesMessageTimestamp(message, fallbackTs);
|
|
1353
|
+
fallbackTs = receivedAt + 1;
|
|
1354
|
+
|
|
1355
|
+
if (role === 'user') {
|
|
1356
|
+
records.push({
|
|
1357
|
+
ts: new Date(receivedAt).toISOString(),
|
|
1358
|
+
receivedAt,
|
|
1359
|
+
role: 'user',
|
|
1360
|
+
content,
|
|
1361
|
+
kind: 'standard',
|
|
1362
|
+
agent: 'hermes-cli',
|
|
1363
|
+
historySessionId: normalizedSessionId,
|
|
1364
|
+
});
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (role === 'assistant') {
|
|
1369
|
+
records.push({
|
|
1370
|
+
ts: new Date(receivedAt).toISOString(),
|
|
1371
|
+
receivedAt,
|
|
1372
|
+
role: 'assistant',
|
|
1373
|
+
content,
|
|
1374
|
+
kind: 'standard',
|
|
1375
|
+
agent: 'hermes-cli',
|
|
1376
|
+
historySessionId: normalizedSessionId,
|
|
1377
|
+
});
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (role === 'tool') {
|
|
1382
|
+
records.push({
|
|
1383
|
+
ts: new Date(receivedAt).toISOString(),
|
|
1384
|
+
receivedAt,
|
|
1385
|
+
role: 'assistant',
|
|
1386
|
+
content,
|
|
1387
|
+
kind: 'tool',
|
|
1388
|
+
senderName: 'Tool',
|
|
1389
|
+
agent: 'hermes-cli',
|
|
1390
|
+
historySessionId: normalizedSessionId,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (records.length === 0) return false;
|
|
1396
|
+
|
|
1397
|
+
const prefix = `${normalizedSessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}_`;
|
|
1398
|
+
for (const file of fs.readdirSync(dir)) {
|
|
1399
|
+
if (file.startsWith(prefix) && file.endsWith('.jsonl')) {
|
|
1400
|
+
fs.unlinkSync(path.join(dir, file));
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const targetDate = new Date(records[records.length - 1].receivedAt || Date.now()).toISOString().slice(0, 10);
|
|
1405
|
+
const filePath = path.join(dir, `${prefix}${targetDate}.jsonl`);
|
|
1406
|
+
fs.writeFileSync(filePath, `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, 'utf-8');
|
|
1407
|
+
invalidatePersistedSavedHistoryIndex('hermes-cli', dir);
|
|
1408
|
+
savedHistorySessionCache.delete('hermes-cli');
|
|
1409
|
+
return true;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
@@ -27,6 +27,51 @@ import { mergeProviderPatchState, resolveProviderStateSurface } from './provider
|
|
|
27
27
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
28
28
|
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
|
|
29
29
|
|
|
30
|
+
type PersistableCliHistoryMessage = {
|
|
31
|
+
role: string;
|
|
32
|
+
content: string;
|
|
33
|
+
kind?: string;
|
|
34
|
+
senderName?: string;
|
|
35
|
+
receivedAt?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function normalizePersistableCliHistoryContent(content: unknown): string {
|
|
39
|
+
return flattenContent(content as any).replace(/\s+/g, ' ').trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildPersistableCliHistorySignature(message: PersistableCliHistoryMessage): string {
|
|
43
|
+
return [
|
|
44
|
+
String(message.role || ''),
|
|
45
|
+
String(message.kind || ''),
|
|
46
|
+
String(message.senderName || ''),
|
|
47
|
+
normalizePersistableCliHistoryContent(message.content),
|
|
48
|
+
].join('|');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildIncrementalHistoryAppendMessages(
|
|
52
|
+
previousMessages: PersistableCliHistoryMessage[],
|
|
53
|
+
currentMessages: PersistableCliHistoryMessage[],
|
|
54
|
+
): PersistableCliHistoryMessage[] {
|
|
55
|
+
if (!Array.isArray(currentMessages) || currentMessages.length === 0) return [];
|
|
56
|
+
if (!Array.isArray(previousMessages) || previousMessages.length === 0) return currentMessages;
|
|
57
|
+
|
|
58
|
+
const previousSignatures = previousMessages.map(buildPersistableCliHistorySignature);
|
|
59
|
+
const currentSignatures = currentMessages.map(buildPersistableCliHistorySignature);
|
|
60
|
+
|
|
61
|
+
let sharedPrefixLength = 0;
|
|
62
|
+
while (
|
|
63
|
+
sharedPrefixLength < previousSignatures.length
|
|
64
|
+
&& sharedPrefixLength < currentSignatures.length
|
|
65
|
+
&& previousSignatures[sharedPrefixLength] === currentSignatures[sharedPrefixLength]
|
|
66
|
+
) {
|
|
67
|
+
sharedPrefixLength += 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sharedPrefixLength === currentSignatures.length) return [];
|
|
71
|
+
if (sharedPrefixLength === previousSignatures.length) return currentMessages.slice(sharedPrefixLength);
|
|
72
|
+
return currentMessages;
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
|
|
31
76
|
prepare(sql: string): { get(...params: Array<string | number>): unknown };
|
|
32
77
|
close(): void;
|
|
@@ -112,6 +157,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
112
157
|
private appliedEffectKeys = new Set<string>();
|
|
113
158
|
private historyWriter: ChatHistoryWriter;
|
|
114
159
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
160
|
+
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
115
161
|
readonly instanceId: string;
|
|
116
162
|
private suppressIdleHistoryReplay = false;
|
|
117
163
|
private errorMessage: string | undefined = undefined;
|
|
@@ -200,6 +246,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
200
246
|
this.providerSessionId,
|
|
201
247
|
this.instanceId,
|
|
202
248
|
);
|
|
249
|
+
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
250
|
+
role: message.role,
|
|
251
|
+
content: message.content,
|
|
252
|
+
kind: message.kind,
|
|
253
|
+
senderName: message.senderName,
|
|
254
|
+
receivedAt: message.receivedAt,
|
|
255
|
+
}));
|
|
203
256
|
this.suppressIdleHistoryReplay = restoredHistory.messages.length > 0;
|
|
204
257
|
if (restoredHistory.messages.length > 0) {
|
|
205
258
|
this.adapter.seedCommittedMessages(
|
|
@@ -231,6 +284,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
231
284
|
|
|
232
285
|
async onTick(): Promise<void> {
|
|
233
286
|
if (this.providerSessionId) return;
|
|
287
|
+
if (this.type === 'hermes-cli' && this.launchMode === 'new') return;
|
|
234
288
|
|
|
235
289
|
let probedSessionId: string | null = null;
|
|
236
290
|
|
|
@@ -362,15 +416,24 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
362
416
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
363
417
|
}
|
|
364
418
|
}
|
|
365
|
-
|
|
419
|
+
const normalizedMessagesToSave = messagesToSave.map((message: PersistableCliHistoryMessage & { timestamp?: number }) => ({
|
|
420
|
+
role: message.role,
|
|
421
|
+
content: flattenContent(message.content),
|
|
422
|
+
kind: typeof message.kind === 'string' ? message.kind : undefined,
|
|
423
|
+
senderName: typeof message.senderName === 'string' ? message.senderName : undefined,
|
|
424
|
+
receivedAt: typeof message.receivedAt === 'number' ? message.receivedAt : message.timestamp,
|
|
425
|
+
}));
|
|
426
|
+
if (!shouldSkipReplayPersist && normalizedMessagesToSave.length > 0) {
|
|
427
|
+
const incrementalMessages = buildIncrementalHistoryAppendMessages(this.lastPersistedHistoryMessages, normalizedMessagesToSave);
|
|
366
428
|
this.historyWriter.appendNewMessages(
|
|
367
429
|
this.type,
|
|
368
|
-
|
|
430
|
+
incrementalMessages,
|
|
369
431
|
parsedStatus?.title || dirName,
|
|
370
432
|
this.instanceId,
|
|
371
433
|
this.providerSessionId,
|
|
372
434
|
);
|
|
373
435
|
}
|
|
436
|
+
this.lastPersistedHistoryMessages = normalizedMessagesToSave;
|
|
374
437
|
}
|
|
375
438
|
|
|
376
439
|
this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
|
|
@@ -639,6 +702,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
639
702
|
|
|
640
703
|
if (data.sessionEvent === 'new_session') {
|
|
641
704
|
this.runtimeMessages = [];
|
|
705
|
+
this.lastPersistedHistoryMessages = [];
|
|
642
706
|
this.suppressIdleHistoryReplay = false;
|
|
643
707
|
this.adapter.clearHistory();
|
|
644
708
|
}
|
package/src/shared-types.d.ts
CHANGED
|
@@ -190,10 +190,13 @@ export interface SessionEntry {
|
|
|
190
190
|
summaryMetadata?: ProviderSummaryMetadata;
|
|
191
191
|
errorMessage?: string;
|
|
192
192
|
errorReason?: _ProviderErrorReason;
|
|
193
|
+
lastMessageHash?: string;
|
|
193
194
|
lastUpdated?: number;
|
|
194
195
|
unread?: boolean;
|
|
195
196
|
lastSeenAt?: number;
|
|
196
197
|
inboxBucket?: RecentSessionBucket;
|
|
198
|
+
completionMarker?: string;
|
|
199
|
+
seenCompletionMarker?: string;
|
|
197
200
|
surfaceHidden?: boolean;
|
|
198
201
|
}
|
|
199
202
|
/**
|
package/src/shared-types.ts
CHANGED
|
@@ -13,6 +13,8 @@ const LIVE_RUNTIME_LIFECYCLES = new Set(['starting', 'running', 'stopping', 'int
|
|
|
13
13
|
export interface HotChatSessionLike {
|
|
14
14
|
id?: string | null;
|
|
15
15
|
status?: unknown;
|
|
16
|
+
unread?: unknown;
|
|
17
|
+
inboxBucket?: unknown;
|
|
16
18
|
lastMessageAt?: unknown;
|
|
17
19
|
runtimeLifecycle?: unknown;
|
|
18
20
|
runtimeSurfaceKind?: unknown;
|
|
@@ -79,10 +81,22 @@ export function classifyHotChatSessionsForSubscriptionFlush(
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
const status = String(session?.status || '').toLowerCase();
|
|
84
|
+
const unread = session?.unread === true;
|
|
85
|
+
const inboxBucket = String(session?.inboxBucket || '').toLowerCase();
|
|
86
|
+
const runtimeSurfaceKind = String(session?.runtimeSurfaceKind || '').toLowerCase();
|
|
87
|
+
const runtimeLifecycle = String(session?.runtimeLifecycle || '').toLowerCase();
|
|
88
|
+
const isLiveRuntime = runtimeSurfaceKind === 'live_runtime' || LIVE_RUNTIME_LIFECYCLES.has(runtimeLifecycle);
|
|
82
89
|
const lastMessageAt = parseMessageTimestamp(session?.lastMessageAt);
|
|
83
90
|
const recentlyUpdated = lastMessageAt > 0 && (now - lastMessageAt) <= recentMessageGraceMs;
|
|
91
|
+
const shouldKeepRecentTailHot = recentlyUpdated && (
|
|
92
|
+
unread
|
|
93
|
+
|| inboxBucket === 'task_complete'
|
|
94
|
+
|| inboxBucket === 'needs_attention'
|
|
95
|
+
|| isLiveRuntime
|
|
96
|
+
|| activeStatuses.has(status)
|
|
97
|
+
);
|
|
84
98
|
|
|
85
|
-
if (activeStatuses.has(status) ||
|
|
99
|
+
if (activeStatuses.has(status) || shouldKeepRecentTailHot) {
|
|
86
100
|
active.add(sessionId);
|
|
87
101
|
}
|
|
88
102
|
}
|
package/src/status/snapshot.ts
CHANGED
|
@@ -408,6 +408,8 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
|
|
|
408
408
|
session.lastSeenAt = lastSeenAt;
|
|
409
409
|
session.unread = overlayUnread;
|
|
410
410
|
session.inboxBucket = overlayInboxBucket;
|
|
411
|
+
session.completionMarker = completionMarker;
|
|
412
|
+
session.seenCompletionMarker = seenCompletionMarker;
|
|
411
413
|
if (READ_DEBUG_ENABLED && (session.unread || session.inboxBucket !== 'idle' || session.providerType.includes('codex'))) {
|
|
412
414
|
const recentReadSnapshot: RecentReadDebugSnapshot = {
|
|
413
415
|
sessionId: session.id,
|