@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.
@@ -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 {};
@@ -270,6 +270,8 @@ export interface SessionEntry {
270
270
  unread?: boolean;
271
271
  lastSeenAt?: number;
272
272
  inboxBucket?: RecentSessionBucket;
273
+ completionMarker?: string;
274
+ seenCompletionMarker?: string;
273
275
  surfaceHidden?: boolean;
274
276
  }
275
277
  /**
@@ -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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.82",
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.82",
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",
@@ -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 = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
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
- LOG.warn('CLI', `[${this.cliType}] No CLI scripts loaded! Provider needs scripts/{version}/scripts.js`);
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 normalizedScreenSnapshot = normalizeScreenSnapshot(this.terminalScreen.getText());
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
- this.recordTrace('output', {
502
- rawLength: rawData.length,
503
- cleanLength: cleanData.length,
504
- rawPreview: summarizeCliTraceText(rawData, 300),
505
- cleanPreview: summarizeCliTraceText(cleanData, 300),
506
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 1200),
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
- return {
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
- const messages = [...this.committedMessages];
1342
- return {
1343
- id: 'cli_session',
1344
- status: this.currentStatus,
1345
- title: this.cliName,
1346
- messages: messages.map((message, index) => buildChatMessage({
1347
- ...message,
1348
- id: message.id || `msg_${index}`,
1349
- index: typeof message.index === 'number' ? message.index : index,
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: this.terminalScreen.getText(),
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
- LOG.warn(
71
- 'Terminal',
72
- `[terminal-screen] ghostty-vt unavailable; using xterm fallback (preference=${preference})`,
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
- 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);
366
428
  this.historyWriter.appendNewMessages(
367
429
  this.type,
368
- messagesToSave,
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
  }
@@ -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
  /**
@@ -339,6 +339,8 @@ export interface SessionEntry {
339
339
  unread?: boolean;
340
340
  lastSeenAt?: number;
341
341
  inboxBucket?: RecentSessionBucket;
342
+ completionMarker?: string;
343
+ seenCompletionMarker?: string;
342
344
  surfaceHidden?: boolean;
343
345
  }
344
346
 
@@ -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) || recentlyUpdated) {
99
+ if (activeStatuses.has(status) || shouldKeepRecentTailHot) {
86
100
  active.add(sessionId);
87
101
  }
88
102
  }
@@ -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,