@adhdev/daemon-core 0.9.44 → 0.9.46

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.
@@ -13,7 +13,7 @@ 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 type { ProviderHistoryBehavior } from '../providers/contracts.js';
16
+ import type { ProviderCanonicalHistoryConfig, ProviderHistoryBehavior } from '../providers/contracts.js';
17
17
 
18
18
  const HISTORY_DIR = path.join(os.homedir(), '.adhdev', 'history');
19
19
  const RETAIN_DAYS = 30;
@@ -170,6 +170,9 @@ export interface SavedHistorySessionSummary {
170
170
  lastMessageAt: number;
171
171
  preview?: string;
172
172
  workspace?: string;
173
+ source?: 'adhdev-mirror' | 'provider-native';
174
+ sourcePath?: string;
175
+ sourceMtimeMs?: number;
173
176
  }
174
177
 
175
178
  function sortSavedHistorySessionSummaries(summaries: SavedHistorySessionSummary[]): SavedHistorySessionSummary[] {
@@ -1138,6 +1141,37 @@ export class ChatHistoryWriter {
1138
1141
  * the newest N messages are skipped so older-history pagination can avoid
1139
1142
  * duplicating the live transcript tail already shown in the UI.
1140
1143
  */
1144
+ function pageHistoryRecords(
1145
+ agentType: string,
1146
+ records: HistoryMessage[],
1147
+ offset: number = 0,
1148
+ limit: number = 30,
1149
+ excludeRecentCount: number = 0,
1150
+ historyBehavior?: ProviderHistoryBehavior,
1151
+ ): { messages: HistoryMessage[]; hasMore: boolean } {
1152
+ const allMessages = records
1153
+ .map((message) => sanitizeHistoryMessage(agentType, message))
1154
+ .filter(Boolean) as HistoryMessage[];
1155
+ allMessages.sort((a, b) => a.receivedAt - b.receivedAt);
1156
+ const chronological: HistoryMessage[] = [];
1157
+ let lastTurn: HistoryMessage | null = null;
1158
+ for (const message of allMessages) {
1159
+ const previous = chronological[chronological.length - 1];
1160
+ if (isAdjacentHistoryDuplicate(agentType, previous, message)) continue;
1161
+ if (message.role !== 'system' && isAdjacentHistoryDuplicate(agentType, lastTurn, message)) continue;
1162
+ chronological.push(message);
1163
+ if (message.role !== 'system') lastTurn = message;
1164
+ }
1165
+ const collapsed = collapseReplayAssistantTurns(chronological, historyBehavior);
1166
+ const boundedLimit = Math.max(1, limit);
1167
+ const boundedOffset = Math.max(0, offset);
1168
+ const boundedExclude = Math.max(0, Math.min(excludeRecentCount, collapsed.length));
1169
+ const endExclusive = Math.max(0, collapsed.length - boundedExclude - boundedOffset);
1170
+ const startInclusive = Math.max(0, endExclusive - boundedLimit);
1171
+ const sliced = collapsed.slice(startInclusive, endExclusive);
1172
+ return { messages: sliced, hasMore: startInclusive > 0 };
1173
+ }
1174
+
1141
1175
  export function readChatHistory(
1142
1176
  agentType: string,
1143
1177
  offset: number = 0,
@@ -1175,29 +1209,7 @@ export function readChatHistory(
1175
1209
  }
1176
1210
  }
1177
1211
 
1178
- allMessages.sort((a, b) => a.receivedAt - b.receivedAt);
1179
- const chronological: HistoryMessage[] = [];
1180
- let lastTurn: HistoryMessage | null = null;
1181
- for (const message of allMessages) {
1182
- const previous = chronological[chronological.length - 1];
1183
- if (isAdjacentHistoryDuplicate(agentType, previous, message)) continue;
1184
- if (message.role !== 'system' && isAdjacentHistoryDuplicate(agentType, lastTurn, message)) continue;
1185
- chronological.push(message);
1186
- if (message.role !== 'system') lastTurn = message;
1187
- }
1188
- const collapsed = collapseReplayAssistantTurns(chronological, historyBehavior);
1189
-
1190
- // Page backwards from the newest saved messages while keeping the returned
1191
- // slice in chronological order for prepend-based UI rendering.
1192
- const boundedLimit = Math.max(1, limit);
1193
- const boundedOffset = Math.max(0, offset);
1194
- const boundedExclude = Math.max(0, Math.min(excludeRecentCount, collapsed.length));
1195
- const endExclusive = Math.max(0, collapsed.length - boundedExclude - boundedOffset);
1196
- const startInclusive = Math.max(0, endExclusive - boundedLimit);
1197
- const sliced = collapsed.slice(startInclusive, endExclusive);
1198
- const hasMore = startInclusive > 0;
1199
-
1200
- return { messages: sliced, hasMore };
1212
+ return pageHistoryRecords(agentType, allMessages, offset, limit, excludeRecentCount, historyBehavior);
1201
1213
  } catch {
1202
1214
  return { messages: [], hasMore: false };
1203
1215
  }
@@ -1276,34 +1288,6 @@ export function listSavedHistorySessions(
1276
1288
  }
1277
1289
  }
1278
1290
 
1279
- function normalizeCanonicalHermesMessageContent(content: unknown): string {
1280
- if (typeof content === 'string') return content.trim();
1281
- if (content == null) return '';
1282
- try {
1283
- return JSON.stringify(content).trim();
1284
- } catch {
1285
- return String(content).trim();
1286
- }
1287
- }
1288
-
1289
- function extractCanonicalHermesMessageTimestamp(message: Record<string, unknown>, fallbackTs: number): number {
1290
- const numericTimestamp = Number(message.receivedAt || message.timestamp || message.ts || 0);
1291
- if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
1292
- const stringTimestamp = typeof message.ts === 'string'
1293
- ? Date.parse(message.ts)
1294
- : (typeof message.timestamp === 'string' ? Date.parse(message.timestamp) : NaN);
1295
- if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
1296
- return fallbackTs;
1297
- }
1298
-
1299
- function extractTimestampValue(value: unknown): number {
1300
- const numericTimestamp = Number(value || 0);
1301
- if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
1302
- const stringTimestamp = typeof value === 'string' ? Date.parse(value) : NaN;
1303
- if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
1304
- return 0;
1305
- }
1306
-
1307
1291
  function readExistingSessionStartRecord(agentType: string, historySessionId: string): HistoryMessage | null {
1308
1292
  try {
1309
1293
  const dir = path.join(HISTORY_DIR, agentType);
@@ -1351,257 +1335,271 @@ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: strin
1351
1335
  }
1352
1336
  }
1353
1337
 
1354
- export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
1338
+ export type ProviderNativeHistoryScripts = Record<string, ((input: any) => any) | undefined>;
1339
+
1340
+ type ProviderNativeHistoryReadResult = { records: HistoryMessage[]; sourcePath: string; sourceMtimeMs: number };
1341
+
1342
+ function getNativeHistoryScriptName(canonicalHistory: ProviderCanonicalHistoryConfig | undefined, key: 'readSession' | 'listSessions'): string {
1343
+ const configured = canonicalHistory?.scripts?.[key];
1344
+ if (typeof configured === 'string' && configured.trim()) return configured.trim();
1345
+ return key === 'readSession' ? 'readNativeHistory' : 'listNativeHistory';
1346
+ }
1347
+
1348
+ function getProviderNativeHistoryScript(
1349
+ scripts: ProviderNativeHistoryScripts | undefined,
1350
+ canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1351
+ key: 'readSession' | 'listSessions',
1352
+ ): ((input: any) => any) | null {
1353
+ if (!canonicalHistory?.scripts) return null;
1354
+ const fn = scripts?.[getNativeHistoryScriptName(canonicalHistory, key)];
1355
+ return typeof fn === 'function' ? fn : null;
1356
+ }
1357
+
1358
+ function normalizeProviderNativeHistoryRecords(agentType: string, historySessionId: string, records: unknown): HistoryMessage[] {
1359
+ if (!Array.isArray(records)) return [];
1355
1360
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1356
- if (!normalizedSessionId) return false;
1361
+ return records
1362
+ .map((record: any) => sanitizeHistoryMessage(agentType, {
1363
+ ts: typeof record?.ts === 'string' ? record.ts : new Date(Number(record?.receivedAt) || Date.now()).toISOString(),
1364
+ receivedAt: Number(record?.receivedAt) || Date.parse(record?.ts || '') || Date.now(),
1365
+ role: record?.role,
1366
+ content: String(record?.content || ''),
1367
+ kind: record?.kind || (record?.role === 'system' ? 'session_start' : 'standard'),
1368
+ senderName: record?.senderName,
1369
+ agent: agentType,
1370
+ instanceId: record?.instanceId,
1371
+ historySessionId: normalizeSavedHistorySessionId(record?.historySessionId || normalizedSessionId),
1372
+ sessionTitle: record?.sessionTitle,
1373
+ workspace: record?.workspace,
1374
+ } as HistoryMessage))
1375
+ .filter(Boolean) as HistoryMessage[];
1376
+ }
1357
1377
 
1358
- try {
1359
- const sessionFilePath = path.join(os.homedir(), '.hermes', 'sessions', `session_${normalizedSessionId}.json`);
1360
- if (!fs.existsSync(sessionFilePath)) return false;
1361
- const raw = JSON.parse(fs.readFileSync(sessionFilePath, 'utf-8')) as {
1362
- session_start?: string;
1363
- last_updated?: string;
1364
- messages?: Array<Record<string, unknown>>;
1365
- };
1366
- const canonicalMessages = Array.isArray(raw.messages) ? raw.messages : [];
1367
- const dir = path.join(HISTORY_DIR, 'hermes-cli');
1368
- fs.mkdirSync(dir, { recursive: true });
1369
- const existingSessionStart = readExistingSessionStartRecord('hermes-cli', normalizedSessionId);
1370
- const records: HistoryMessage[] = [];
1371
- if (existingSessionStart) {
1372
- records.push({
1373
- ...existingSessionStart,
1374
- historySessionId: normalizedSessionId,
1375
- });
1376
- }
1378
+ function callProviderNativeHistoryRead(
1379
+ agentType: string,
1380
+ canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1381
+ scripts: ProviderNativeHistoryScripts | undefined,
1382
+ historySessionId: string,
1383
+ workspace?: string,
1384
+ ): ProviderNativeHistoryReadResult | null {
1385
+ const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
1386
+ if (!fn) return null;
1387
+ const result = fn({
1388
+ agentType,
1389
+ sessionId: historySessionId,
1390
+ historySessionId,
1391
+ workspace,
1392
+ format: canonicalHistory?.format,
1393
+ watchPath: canonicalHistory?.watchPath,
1394
+ args: { sessionId: historySessionId, historySessionId, workspace },
1395
+ });
1396
+ if (!result || typeof result !== 'object') return null;
1397
+ const records = normalizeProviderNativeHistoryRecords(agentType, historySessionId, (result as any).messages || (result as any).records);
1398
+ if (records.length === 0) return null;
1399
+ return {
1400
+ records,
1401
+ sourcePath: typeof (result as any).sourcePath === 'string' ? (result as any).sourcePath : '',
1402
+ sourceMtimeMs: Number((result as any).sourceMtimeMs) || 0,
1403
+ };
1404
+ }
1377
1405
 
1378
- let fallbackTs = Date.parse(raw.session_start || raw.last_updated || '') || Date.now();
1379
- for (const message of canonicalMessages) {
1380
- const role = String(message.role || '').trim();
1381
- const content = normalizeCanonicalHermesMessageContent(message.content);
1382
- if (!content) continue;
1383
- const receivedAt = extractCanonicalHermesMessageTimestamp(message, fallbackTs);
1384
- fallbackTs = receivedAt + 1;
1406
+ function buildNativeHistoryReadResult(
1407
+ agentType: string,
1408
+ canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1409
+ scripts: ProviderNativeHistoryScripts | undefined,
1410
+ historySessionId: string | undefined,
1411
+ workspace?: string,
1412
+ ): ProviderNativeHistoryReadResult | null {
1413
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1414
+ if (!canonicalHistory || !normalizedSessionId || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1415
+ return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
1416
+ }
1385
1417
 
1386
- if (role === 'user') {
1387
- records.push({
1388
- ts: new Date(receivedAt).toISOString(),
1389
- receivedAt,
1390
- role: 'user',
1391
- content,
1392
- kind: 'standard',
1393
- agent: 'hermes-cli',
1394
- historySessionId: normalizedSessionId,
1395
- });
1396
- continue;
1397
- }
1418
+ function materializeNativeHistoryToMirror(
1419
+ agentType: string,
1420
+ canonicalHistory: ProviderCanonicalHistoryConfig,
1421
+ historySessionId: string,
1422
+ workspace?: string,
1423
+ scripts?: ProviderNativeHistoryScripts,
1424
+ ): boolean {
1425
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1426
+ if (!normalizedSessionId) return false;
1427
+ const nativeResult = callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
1428
+ const nativeRecords = nativeResult?.records || [];
1429
+ if (nativeRecords.length === 0) return false;
1430
+ const normalizedRecords = nativeRecords.map((record) => ({
1431
+ ...record,
1432
+ agent: agentType,
1433
+ historySessionId: normalizedSessionId,
1434
+ }));
1435
+ const existingSessionStart = readExistingSessionStartRecord(agentType, normalizedSessionId);
1436
+ const records = existingSessionStart && normalizedRecords[0]?.kind !== 'session_start'
1437
+ ? [{ ...existingSessionStart, historySessionId: normalizedSessionId, agent: agentType }, ...normalizedRecords]
1438
+ : normalizedRecords;
1439
+ return rewriteCanonicalSavedHistory(agentType, normalizedSessionId, records);
1440
+ }
1398
1441
 
1399
- if (role === 'assistant') {
1400
- records.push({
1401
- ts: new Date(receivedAt).toISOString(),
1402
- receivedAt,
1403
- role: 'assistant',
1404
- content,
1405
- kind: 'standard',
1406
- agent: 'hermes-cli',
1407
- historySessionId: normalizedSessionId,
1408
- });
1409
- continue;
1410
- }
1442
+ export function materializeProviderNativeHistory(
1443
+ agentType: string,
1444
+ canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1445
+ historySessionId: string,
1446
+ workspace?: string,
1447
+ scripts?: ProviderNativeHistoryScripts,
1448
+ ): boolean {
1449
+ if (!canonicalHistory || canonicalHistory.mode !== 'materialized-mirror') return false;
1450
+ return materializeNativeHistoryToMirror(agentType, canonicalHistory, historySessionId, workspace, scripts);
1451
+ }
1411
1452
 
1412
- if (role === 'tool') {
1413
- records.push({
1414
- ts: new Date(receivedAt).toISOString(),
1415
- receivedAt,
1416
- role: 'assistant',
1417
- content,
1418
- kind: 'tool',
1419
- senderName: 'Tool',
1420
- agent: 'hermes-cli',
1421
- historySessionId: normalizedSessionId,
1422
- });
1423
- }
1424
- }
1453
+ export function isNativeSourceCanonicalHistory(canonicalHistory?: ProviderCanonicalHistoryConfig): boolean {
1454
+ if (!canonicalHistory) return false;
1455
+ if ((canonicalHistory as any).mode === 'disabled') return false;
1456
+ if ((canonicalHistory as any).mode === 'materialized-mirror') return false;
1457
+ return true;
1458
+ }
1425
1459
 
1426
- return rewriteCanonicalSavedHistory('hermes-cli', normalizedSessionId, records);
1427
- } catch {
1428
- return false;
1460
+ export function readProviderChatHistory(
1461
+ agentType: string,
1462
+ options: {
1463
+ canonicalHistory?: ProviderCanonicalHistoryConfig;
1464
+ historySessionId?: string;
1465
+ workspace?: string;
1466
+ offset?: number;
1467
+ limit?: number;
1468
+ excludeRecentCount?: number;
1469
+ historyBehavior?: ProviderHistoryBehavior;
1470
+ scripts?: ProviderNativeHistoryScripts;
1471
+ } = {},
1472
+ ): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable'; sourcePath?: string; sourceMtimeMs?: number } {
1473
+ if (isNativeSourceCanonicalHistory(options.canonicalHistory) && options.historySessionId) {
1474
+ const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
1475
+ if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
1476
+ return {
1477
+ ...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
1478
+ source: 'provider-native',
1479
+ sourcePath: nativeResult.sourcePath,
1480
+ sourceMtimeMs: nativeResult.sourceMtimeMs,
1481
+ };
1429
1482
  }
1483
+ return {
1484
+ ...readChatHistory(agentType, options.offset || 0, options.limit || 30, options.historySessionId, options.excludeRecentCount || 0, options.historyBehavior),
1485
+ source: 'adhdev-mirror',
1486
+ };
1430
1487
  }
1431
1488
 
1432
- function resolveClaudeProjectTranscriptPath(historySessionId: string, workspace?: string): string | null {
1433
- const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
1434
- if (!fs.existsSync(claudeProjectsDir)) return null;
1435
- const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1436
- if (normalizedWorkspace) {
1437
- const directPath = path.join(claudeProjectsDir, normalizedWorkspace.replace(/[\\/]/g, '-'), `${historySessionId}.jsonl`);
1438
- if (fs.existsSync(directPath)) return directPath;
1439
- }
1440
- const stack = [claudeProjectsDir];
1441
- while (stack.length > 0) {
1442
- const current = stack.pop();
1443
- if (!current) continue;
1444
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
1445
- const entryPath = path.join(current, entry.name);
1446
- if (entry.isDirectory()) {
1447
- stack.push(entryPath);
1448
- continue;
1449
- }
1450
- if (entry.isFile() && entry.name === `${historySessionId}.jsonl`) {
1451
- return entryPath;
1452
- }
1453
- }
1454
- }
1455
- return null;
1489
+ function buildNativeSessionSummary(
1490
+ agentType: string,
1491
+ historySessionId: string,
1492
+ records: HistoryMessage[],
1493
+ sourcePath: string,
1494
+ ): SavedHistorySessionSummary | null {
1495
+ const visible = pageHistoryRecords(agentType, records, 0, Number.MAX_SAFE_INTEGER).messages;
1496
+ if (visible.length === 0) return null;
1497
+ let sourceMtimeMs = 0;
1498
+ try { sourceMtimeMs = fs.statSync(sourcePath).mtimeMs; } catch { /* ignore */ }
1499
+ const firstMessageAt = visible[0]?.receivedAt || sourceMtimeMs || Date.now();
1500
+ const lastMessageAt = visible[visible.length - 1]?.receivedAt || firstMessageAt;
1501
+ const lastNonSystem = [...visible].reverse().find((message) => message.role !== 'system') || visible[visible.length - 1];
1502
+ const firstSystem = visible.find((message) => message.kind === 'session_start');
1503
+ return {
1504
+ historySessionId,
1505
+ sessionTitle: lastNonSystem?.content,
1506
+ messageCount: visible.length,
1507
+ firstMessageAt,
1508
+ lastMessageAt,
1509
+ preview: lastNonSystem?.content,
1510
+ workspace: firstSystem?.workspace || (firstSystem?.kind === 'session_start' ? firstSystem.content : undefined),
1511
+ source: 'provider-native',
1512
+ sourcePath,
1513
+ sourceMtimeMs,
1514
+ };
1456
1515
  }
1457
1516
 
1458
- function extractClaudeAssistantContentParts(content: unknown): Array<{ content: string; kind: 'standard' | 'tool'; senderName?: string; role?: 'assistant' }> {
1459
- if (typeof content === 'string') {
1460
- const trimmed = content.trim();
1461
- return trimmed ? [{ content: trimmed, kind: 'standard', role: 'assistant' }] : [];
1462
- }
1463
- if (!Array.isArray(content)) return [];
1464
- const parts: Array<{ content: string; kind: 'standard' | 'tool'; senderName?: string; role?: 'assistant' }> = [];
1465
- for (const block of content) {
1466
- if (!block || typeof block !== 'object') continue;
1467
- const record = block as Record<string, unknown>;
1468
- const type = String(record.type || '').trim();
1469
- if (type === 'text') {
1470
- const text = String(record.text || '').trim();
1471
- if (text) parts.push({ content: text, kind: 'standard', role: 'assistant' });
1472
- continue;
1473
- }
1474
- if (type === 'tool_use') {
1475
- const name = String(record.name || '').trim() || 'Tool';
1476
- const input = record.input && typeof record.input === 'object'
1477
- ? record.input as Record<string, unknown>
1478
- : null;
1479
- const command = input ? String(input.command || '').trim() : '';
1480
- const summary = command ? `${name}: ${command}` : name;
1481
- if (summary) parts.push({ content: summary, kind: 'tool', senderName: 'Tool', role: 'assistant' });
1482
- }
1483
- }
1484
- return parts;
1517
+ function normalizeProviderNativeHistorySessionSummary(agentType: string, item: any): SavedHistorySessionSummary | null {
1518
+ const historySessionId = normalizeSavedHistorySessionId(item?.historySessionId || item?.sessionId || '');
1519
+ if (!historySessionId) return null;
1520
+ const sourcePath = typeof item?.sourcePath === 'string' ? item.sourcePath : '';
1521
+ const sourceMtimeMs = Number(item?.sourceMtimeMs) || 0;
1522
+ const firstMessageAt = Number(item?.firstMessageAt) || sourceMtimeMs || Date.now();
1523
+ const lastMessageAt = Number(item?.lastMessageAt) || firstMessageAt;
1524
+ const messageCount = Math.max(0, Number(item?.messageCount) || 0);
1525
+ return {
1526
+ historySessionId,
1527
+ sessionTitle: typeof item?.sessionTitle === 'string' ? item.sessionTitle : undefined,
1528
+ messageCount,
1529
+ firstMessageAt,
1530
+ lastMessageAt,
1531
+ preview: typeof item?.preview === 'string' ? item.preview : undefined,
1532
+ workspace: typeof item?.workspace === 'string' ? item.workspace : undefined,
1533
+ source: 'provider-native',
1534
+ sourcePath,
1535
+ sourceMtimeMs,
1536
+ };
1485
1537
  }
1486
1538
 
1487
- function extractClaudeUserContentParts(content: unknown): Array<{ role: 'user' | 'assistant'; content: string; kind: 'standard' | 'tool'; senderName?: string }> {
1488
- if (typeof content === 'string') {
1489
- const trimmed = content.trim();
1490
- return trimmed ? [{ role: 'user', content: trimmed, kind: 'standard' }] : [];
1491
- }
1492
- if (!Array.isArray(content)) return [];
1493
- const parts: Array<{ role: 'user' | 'assistant'; content: string; kind: 'standard' | 'tool'; senderName?: string }> = [];
1494
- for (const block of content) {
1495
- if (!block || typeof block !== 'object') continue;
1496
- const record = block as Record<string, unknown>;
1497
- const type = String(record.type || '').trim();
1498
- if (type === 'text') {
1499
- const text = String(record.text || '').trim();
1500
- if (text) parts.push({ role: 'user', content: text, kind: 'standard' });
1539
+ function collectProviderScriptNativeHistorySessionSummaries(
1540
+ agentType: string,
1541
+ canonicalHistory: ProviderCanonicalHistoryConfig,
1542
+ scripts?: ProviderNativeHistoryScripts,
1543
+ ): SavedHistorySessionSummary[] | null {
1544
+ const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'listSessions');
1545
+ if (!fn) return null;
1546
+ const result = fn({
1547
+ agentType,
1548
+ format: canonicalHistory.format,
1549
+ watchPath: canonicalHistory.watchPath,
1550
+ args: {},
1551
+ });
1552
+ if (!result || typeof result !== 'object') return [];
1553
+ const sessions = Array.isArray((result as any).sessions) ? (result as any).sessions : [];
1554
+ const summaries: SavedHistorySessionSummary[] = [];
1555
+ for (const item of sessions) {
1556
+ if (Array.isArray(item?.messages || item?.records)) {
1557
+ const historySessionId = normalizeSavedHistorySessionId(item?.historySessionId || item?.sessionId || '');
1558
+ if (!historySessionId) continue;
1559
+ const records = normalizeProviderNativeHistoryRecords(agentType, historySessionId, item.messages || item.records);
1560
+ const summary = buildNativeSessionSummary(agentType, historySessionId, records, typeof item?.sourcePath === 'string' ? item.sourcePath : '');
1561
+ if (summary) {
1562
+ if (Number(item?.sourceMtimeMs)) summary.sourceMtimeMs = Number(item.sourceMtimeMs);
1563
+ summaries.push(summary);
1564
+ }
1501
1565
  continue;
1502
1566
  }
1503
- if (type === 'tool_result') {
1504
- const rawContent = record.content;
1505
- const text = typeof rawContent === 'string'
1506
- ? rawContent.trim()
1507
- : Array.isArray(rawContent)
1508
- ? rawContent
1509
- .map((entry) => {
1510
- if (typeof entry === 'string') return entry.trim();
1511
- if (!entry || typeof entry !== 'object') return '';
1512
- const nested = entry as Record<string, unknown>;
1513
- if (typeof nested.text === 'string') return nested.text.trim();
1514
- if (typeof nested.content === 'string') return nested.content.trim();
1515
- return '';
1516
- })
1517
- .filter(Boolean)
1518
- .join('\n')
1519
- : '';
1520
- if (text) parts.push({ role: 'assistant', content: text, kind: 'tool', senderName: 'Tool' });
1521
- }
1567
+ const summary = normalizeProviderNativeHistorySessionSummary(agentType, item);
1568
+ if (summary) summaries.push(summary);
1522
1569
  }
1523
- return parts;
1570
+ return sortSavedHistorySessionSummaries(summaries);
1524
1571
  }
1525
1572
 
1526
- export function rebuildClaudeSavedHistoryFromNativeProject(historySessionId: string, workspace?: string): boolean {
1527
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1528
- if (!normalizedSessionId) return false;
1529
-
1530
- try {
1531
- const transcriptPath = resolveClaudeProjectTranscriptPath(normalizedSessionId, workspace);
1532
- if (!transcriptPath) return false;
1533
- const lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
1534
- const records: HistoryMessage[] = [];
1535
- const existingSessionStart = readExistingSessionStartRecord('claude-cli', normalizedSessionId);
1536
- if (existingSessionStart) {
1537
- records.push({
1538
- ...existingSessionStart,
1539
- historySessionId: normalizedSessionId,
1540
- });
1541
- }
1542
- let fallbackTs = Date.now();
1543
- for (const line of lines) {
1544
- let parsed: Record<string, unknown> | null = null;
1545
- try {
1546
- parsed = JSON.parse(line) as Record<string, unknown>;
1547
- } catch {
1548
- parsed = null;
1549
- }
1550
- if (!parsed) continue;
1551
- const parsedSessionId = String(parsed.sessionId || '').trim();
1552
- if (parsedSessionId && parsedSessionId !== normalizedSessionId) continue;
1553
- const receivedAt = extractTimestampValue(parsed.timestamp) || fallbackTs;
1554
- fallbackTs = receivedAt + 1;
1555
- const parsedWorkspace = String(parsed.cwd || workspace || '').trim();
1556
- if (records.length === 0 && parsedWorkspace) {
1557
- records.push({
1558
- ts: new Date(receivedAt).toISOString(),
1559
- receivedAt,
1560
- role: 'system',
1561
- kind: 'session_start',
1562
- content: parsedWorkspace,
1563
- agent: 'claude-cli',
1564
- historySessionId: normalizedSessionId,
1565
- workspace: parsedWorkspace,
1566
- });
1567
- }
1568
- const type = String(parsed.type || '').trim();
1569
- const message = parsed.message && typeof parsed.message === 'object'
1570
- ? parsed.message as Record<string, unknown>
1571
- : null;
1572
- if (type === 'user' && message) {
1573
- for (const part of extractClaudeUserContentParts(message.content)) {
1574
- records.push({
1575
- ts: new Date(receivedAt).toISOString(),
1576
- receivedAt,
1577
- role: part.role,
1578
- content: part.content,
1579
- kind: part.kind,
1580
- senderName: part.senderName,
1581
- agent: 'claude-cli',
1582
- historySessionId: normalizedSessionId,
1583
- });
1584
- }
1585
- continue;
1586
- }
1587
- if (type === 'assistant' && message) {
1588
- for (const part of extractClaudeAssistantContentParts(message.content)) {
1589
- records.push({
1590
- ts: new Date(receivedAt).toISOString(),
1591
- receivedAt,
1592
- role: 'assistant',
1593
- content: part.content,
1594
- kind: part.kind,
1595
- senderName: part.senderName,
1596
- agent: 'claude-cli',
1597
- historySessionId: normalizedSessionId,
1598
- });
1599
- }
1600
- }
1601
- }
1573
+ function collectNativeHistorySessionSummaries(
1574
+ agentType: string,
1575
+ canonicalHistory: ProviderCanonicalHistoryConfig,
1576
+ scripts?: ProviderNativeHistoryScripts,
1577
+ ): SavedHistorySessionSummary[] {
1578
+ return collectProviderScriptNativeHistorySessionSummaries(agentType, canonicalHistory, scripts) || [];
1579
+ }
1602
1580
 
1603
- return rewriteCanonicalSavedHistory('claude-cli', normalizedSessionId, records);
1604
- } catch {
1605
- return false;
1581
+ export function listProviderHistorySessions(
1582
+ agentType: string,
1583
+ options: {
1584
+ canonicalHistory?: ProviderCanonicalHistoryConfig;
1585
+ offset?: number;
1586
+ limit?: number;
1587
+ historyBehavior?: ProviderHistoryBehavior;
1588
+ scripts?: ProviderNativeHistoryScripts;
1589
+ } = {},
1590
+ ): { sessions: SavedHistorySessionSummary[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' } {
1591
+ if (isNativeSourceCanonicalHistory(options.canonicalHistory)) {
1592
+ const offset = Math.max(0, options.offset || 0);
1593
+ const limit = Math.max(1, options.limit || 30);
1594
+ const summaries = collectNativeHistorySessionSummaries(agentType, options.canonicalHistory!, options.scripts);
1595
+ return {
1596
+ sessions: summaries.slice(offset, offset + limit),
1597
+ hasMore: offset + limit < summaries.length,
1598
+ source: 'provider-native',
1599
+ };
1606
1600
  }
1601
+ return {
1602
+ ...listSavedHistorySessions(agentType, { offset: options.offset, limit: options.limit }, options.historyBehavior),
1603
+ source: 'adhdev-mirror',
1604
+ };
1607
1605
  }