@adhdev/daemon-core 0.8.83 → 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.
@@ -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 {};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.83",
3
+ "version": "0.8.84",
4
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.83",
3
+ "version": "0.8.84",
4
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",
@@ -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(
@@ -363,15 +416,24 @@ export class CliProviderInstance implements ProviderInstance {
363
416
  messagesToSave = messagesToSave.slice(0, lastIdx);
364
417
  }
365
418
  }
366
- if (!shouldSkipReplayPersist && messagesToSave.length > 0) {
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);
367
428
  this.historyWriter.appendNewMessages(
368
429
  this.type,
369
- messagesToSave,
430
+ incrementalMessages,
370
431
  parsedStatus?.title || dirName,
371
432
  this.instanceId,
372
433
  this.providerSessionId,
373
434
  );
374
435
  }
436
+ this.lastPersistedHistoryMessages = normalizedMessagesToSave;
375
437
  }
376
438
 
377
439
  this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
@@ -640,6 +702,7 @@ export class CliProviderInstance implements ProviderInstance {
640
702
 
641
703
  if (data.sessionEvent === 'new_session') {
642
704
  this.runtimeMessages = [];
705
+ this.lastPersistedHistoryMessages = [];
643
706
  this.suppressIdleHistoryReplay = false;
644
707
  this.adapter.clearHistory();
645
708
  }