@adhdev/daemon-core 0.9.45 → 0.9.47

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.
@@ -1288,34 +1288,6 @@ export function listSavedHistorySessions(
1288
1288
  }
1289
1289
  }
1290
1290
 
1291
- function normalizeCanonicalHermesMessageContent(content: unknown): string {
1292
- if (typeof content === 'string') return content.trim();
1293
- if (content == null) return '';
1294
- try {
1295
- return JSON.stringify(content).trim();
1296
- } catch {
1297
- return String(content).trim();
1298
- }
1299
- }
1300
-
1301
- function extractCanonicalHermesMessageTimestamp(message: Record<string, unknown>, fallbackTs: number): number {
1302
- const numericTimestamp = Number(message.receivedAt || message.timestamp || message.ts || 0);
1303
- if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
1304
- const stringTimestamp = typeof message.ts === 'string'
1305
- ? Date.parse(message.ts)
1306
- : (typeof message.timestamp === 'string' ? Date.parse(message.timestamp) : NaN);
1307
- if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
1308
- return fallbackTs;
1309
- }
1310
-
1311
- function extractTimestampValue(value: unknown): number {
1312
- const numericTimestamp = Number(value || 0);
1313
- if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
1314
- const stringTimestamp = typeof value === 'string' ? Date.parse(value) : NaN;
1315
- if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
1316
- return 0;
1317
- }
1318
-
1319
1291
  function readExistingSessionStartRecord(agentType: string, historySessionId: string): HistoryMessage | null {
1320
1292
  try {
1321
1293
  const dir = path.join(HISTORY_DIR, agentType);
@@ -1363,610 +1335,119 @@ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: strin
1363
1335
  }
1364
1336
  }
1365
1337
 
1366
- function buildHermesNativeHistoryRecords(historySessionId: string): HistoryMessage[] | null {
1367
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1368
- if (!normalizedSessionId) return null;
1369
- try {
1370
- const sessionFilePath = path.join(os.homedir(), '.hermes', 'sessions', `session_${normalizedSessionId}.json`);
1371
- if (!fs.existsSync(sessionFilePath)) return null;
1372
- const raw = JSON.parse(fs.readFileSync(sessionFilePath, 'utf-8')) as {
1373
- session_start?: string;
1374
- last_updated?: string;
1375
- messages?: Array<Record<string, unknown>>;
1376
- };
1377
- const canonicalMessages = Array.isArray(raw.messages) ? raw.messages : [];
1378
- const records: HistoryMessage[] = [];
1379
- let fallbackTs = Date.parse(raw.session_start || raw.last_updated || '') || Date.now();
1380
- for (const message of canonicalMessages) {
1381
- const role = String(message.role || '').trim();
1382
- const content = normalizeCanonicalHermesMessageContent(message.content);
1383
- if (!content) continue;
1384
- const receivedAt = extractCanonicalHermesMessageTimestamp(message, fallbackTs);
1385
- fallbackTs = receivedAt + 1;
1386
- if (role === 'user' || role === 'assistant') {
1387
- records.push({
1388
- ts: new Date(receivedAt).toISOString(),
1389
- receivedAt,
1390
- role,
1391
- content,
1392
- kind: 'standard',
1393
- agent: 'hermes-cli',
1394
- historySessionId: normalizedSessionId,
1395
- });
1396
- continue;
1397
- }
1398
- if (role === 'tool') {
1399
- records.push({
1400
- ts: new Date(receivedAt).toISOString(),
1401
- receivedAt,
1402
- role: 'assistant',
1403
- content,
1404
- kind: 'tool',
1405
- senderName: 'Tool',
1406
- agent: 'hermes-cli',
1407
- historySessionId: normalizedSessionId,
1408
- });
1409
- }
1410
- }
1411
- return records;
1412
- } catch {
1413
- return null;
1414
- }
1415
- }
1338
+ export type ProviderNativeHistoryScripts = Record<string, ((input: any) => any) | undefined>;
1416
1339
 
1417
- export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
1418
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1419
- if (!normalizedSessionId) return false;
1340
+ type ProviderNativeHistoryReadResult = { records: HistoryMessage[]; sourcePath: string; sourceMtimeMs: number };
1420
1341
 
1421
- try {
1422
- const sessionFilePath = path.join(os.homedir(), '.hermes', 'sessions', `session_${normalizedSessionId}.json`);
1423
- if (!fs.existsSync(sessionFilePath)) return false;
1424
- const raw = JSON.parse(fs.readFileSync(sessionFilePath, 'utf-8')) as {
1425
- session_start?: string;
1426
- last_updated?: string;
1427
- messages?: Array<Record<string, unknown>>;
1428
- };
1429
- const canonicalMessages = Array.isArray(raw.messages) ? raw.messages : [];
1430
- const dir = path.join(HISTORY_DIR, 'hermes-cli');
1431
- fs.mkdirSync(dir, { recursive: true });
1432
- const existingSessionStart = readExistingSessionStartRecord('hermes-cli', normalizedSessionId);
1433
- const records: HistoryMessage[] = [];
1434
- if (existingSessionStart) {
1435
- records.push({
1436
- ...existingSessionStart,
1437
- historySessionId: normalizedSessionId,
1438
- });
1439
- }
1440
-
1441
- let fallbackTs = Date.parse(raw.session_start || raw.last_updated || '') || Date.now();
1442
- for (const message of canonicalMessages) {
1443
- const role = String(message.role || '').trim();
1444
- const content = normalizeCanonicalHermesMessageContent(message.content);
1445
- if (!content) continue;
1446
- const receivedAt = extractCanonicalHermesMessageTimestamp(message, fallbackTs);
1447
- fallbackTs = receivedAt + 1;
1448
-
1449
- if (role === 'user') {
1450
- records.push({
1451
- ts: new Date(receivedAt).toISOString(),
1452
- receivedAt,
1453
- role: 'user',
1454
- content,
1455
- kind: 'standard',
1456
- agent: 'hermes-cli',
1457
- historySessionId: normalizedSessionId,
1458
- });
1459
- continue;
1460
- }
1461
-
1462
- if (role === 'assistant') {
1463
- records.push({
1464
- ts: new Date(receivedAt).toISOString(),
1465
- receivedAt,
1466
- role: 'assistant',
1467
- content,
1468
- kind: 'standard',
1469
- agent: 'hermes-cli',
1470
- historySessionId: normalizedSessionId,
1471
- });
1472
- continue;
1473
- }
1474
-
1475
- if (role === 'tool') {
1476
- records.push({
1477
- ts: new Date(receivedAt).toISOString(),
1478
- receivedAt,
1479
- role: 'assistant',
1480
- content,
1481
- kind: 'tool',
1482
- senderName: 'Tool',
1483
- agent: 'hermes-cli',
1484
- historySessionId: normalizedSessionId,
1485
- });
1486
- }
1487
- }
1488
-
1489
- return rewriteCanonicalSavedHistory('hermes-cli', normalizedSessionId, records);
1490
- } catch {
1491
- return false;
1492
- }
1493
- }
1494
-
1495
- function resolveClaudeProjectTranscriptPath(historySessionId: string, workspace?: string): string | null {
1496
- const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
1497
- if (!fs.existsSync(claudeProjectsDir)) return null;
1498
- const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1499
- if (normalizedWorkspace) {
1500
- const directPath = path.join(claudeProjectsDir, normalizedWorkspace.replace(/[\\/]/g, '-'), `${historySessionId}.jsonl`);
1501
- if (fs.existsSync(directPath)) return directPath;
1502
- }
1503
- const stack = [claudeProjectsDir];
1504
- while (stack.length > 0) {
1505
- const current = stack.pop();
1506
- if (!current) continue;
1507
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
1508
- const entryPath = path.join(current, entry.name);
1509
- if (entry.isDirectory()) {
1510
- stack.push(entryPath);
1511
- continue;
1512
- }
1513
- if (entry.isFile() && entry.name === `${historySessionId}.jsonl`) {
1514
- return entryPath;
1515
- }
1516
- }
1517
- }
1518
- return null;
1519
- }
1520
-
1521
- function extractClaudeAssistantContentParts(content: unknown): Array<{ content: string; kind: 'standard' | 'tool'; senderName?: string; role?: 'assistant' }> {
1522
- if (typeof content === 'string') {
1523
- const trimmed = content.trim();
1524
- return trimmed ? [{ content: trimmed, kind: 'standard', role: 'assistant' }] : [];
1525
- }
1526
- if (!Array.isArray(content)) return [];
1527
- const parts: Array<{ content: string; kind: 'standard' | 'tool'; senderName?: string; role?: 'assistant' }> = [];
1528
- for (const block of content) {
1529
- if (!block || typeof block !== 'object') continue;
1530
- const record = block as Record<string, unknown>;
1531
- const type = String(record.type || '').trim();
1532
- if (type === 'text') {
1533
- const text = String(record.text || '').trim();
1534
- if (text) parts.push({ content: text, kind: 'standard', role: 'assistant' });
1535
- continue;
1536
- }
1537
- if (type === 'tool_use') {
1538
- const name = String(record.name || '').trim() || 'Tool';
1539
- const input = record.input && typeof record.input === 'object'
1540
- ? record.input as Record<string, unknown>
1541
- : null;
1542
- const command = input ? String(input.command || '').trim() : '';
1543
- const summary = command ? `${name}: ${command}` : name;
1544
- if (summary) parts.push({ content: summary, kind: 'tool', senderName: 'Tool', role: 'assistant' });
1545
- }
1546
- }
1547
- return parts;
1548
- }
1549
-
1550
- function extractClaudeUserContentParts(content: unknown): Array<{ role: 'user' | 'assistant'; content: string; kind: 'standard' | 'tool'; senderName?: string }> {
1551
- if (typeof content === 'string') {
1552
- const trimmed = content.trim();
1553
- return trimmed ? [{ role: 'user', content: trimmed, kind: 'standard' }] : [];
1554
- }
1555
- if (!Array.isArray(content)) return [];
1556
- const parts: Array<{ role: 'user' | 'assistant'; content: string; kind: 'standard' | 'tool'; senderName?: string }> = [];
1557
- for (const block of content) {
1558
- if (!block || typeof block !== 'object') continue;
1559
- const record = block as Record<string, unknown>;
1560
- const type = String(record.type || '').trim();
1561
- if (type === 'text') {
1562
- const text = String(record.text || '').trim();
1563
- if (text) parts.push({ role: 'user', content: text, kind: 'standard' });
1564
- continue;
1565
- }
1566
- if (type === 'tool_result') {
1567
- const rawContent = record.content;
1568
- const text = typeof rawContent === 'string'
1569
- ? rawContent.trim()
1570
- : Array.isArray(rawContent)
1571
- ? rawContent
1572
- .map((entry) => {
1573
- if (typeof entry === 'string') return entry.trim();
1574
- if (!entry || typeof entry !== 'object') return '';
1575
- const nested = entry as Record<string, unknown>;
1576
- if (typeof nested.text === 'string') return nested.text.trim();
1577
- if (typeof nested.content === 'string') return nested.content.trim();
1578
- return '';
1579
- })
1580
- .filter(Boolean)
1581
- .join('\n')
1582
- : '';
1583
- if (text) parts.push({ role: 'assistant', content: text, kind: 'tool', senderName: 'Tool' });
1584
- }
1585
- }
1586
- return parts;
1587
- }
1588
-
1589
- function buildClaudeNativeHistoryRecords(historySessionId: string, workspace?: string): HistoryMessage[] | null {
1590
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1591
- if (!normalizedSessionId) return null;
1592
- try {
1593
- const transcriptPath = resolveClaudeProjectTranscriptPath(normalizedSessionId, workspace);
1594
- if (!transcriptPath) return null;
1595
- const lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
1596
- const records: HistoryMessage[] = [];
1597
- let fallbackTs = Date.now();
1598
- for (const line of lines) {
1599
- let parsed: Record<string, unknown> | null = null;
1600
- try { parsed = JSON.parse(line) as Record<string, unknown>; } catch { parsed = null; }
1601
- if (!parsed) continue;
1602
- const parsedSessionId = String(parsed.sessionId || '').trim();
1603
- if (parsedSessionId && parsedSessionId !== normalizedSessionId) continue;
1604
- const receivedAt = extractTimestampValue(parsed.timestamp) || fallbackTs;
1605
- fallbackTs = receivedAt + 1;
1606
- const parsedWorkspace = String(parsed.cwd || workspace || '').trim();
1607
- if (records.length === 0 && parsedWorkspace) {
1608
- records.push({
1609
- ts: new Date(receivedAt).toISOString(),
1610
- receivedAt,
1611
- role: 'system',
1612
- kind: 'session_start',
1613
- content: parsedWorkspace,
1614
- agent: 'claude-cli',
1615
- historySessionId: normalizedSessionId,
1616
- workspace: parsedWorkspace,
1617
- });
1618
- }
1619
- const type = String(parsed.type || '').trim();
1620
- const message = parsed.message && typeof parsed.message === 'object'
1621
- ? parsed.message as Record<string, unknown>
1622
- : null;
1623
- if (type === 'user' && message) {
1624
- for (const part of extractClaudeUserContentParts(message.content)) {
1625
- records.push({
1626
- ts: new Date(receivedAt).toISOString(),
1627
- receivedAt,
1628
- role: part.role,
1629
- content: part.content,
1630
- kind: part.kind,
1631
- senderName: part.senderName,
1632
- agent: 'claude-cli',
1633
- historySessionId: normalizedSessionId,
1634
- });
1635
- }
1636
- continue;
1637
- }
1638
- if (type === 'assistant' && message) {
1639
- for (const part of extractClaudeAssistantContentParts(message.content)) {
1640
- records.push({
1641
- ts: new Date(receivedAt).toISOString(),
1642
- receivedAt,
1643
- role: 'assistant',
1644
- content: part.content,
1645
- kind: part.kind,
1646
- senderName: part.senderName,
1647
- agent: 'claude-cli',
1648
- historySessionId: normalizedSessionId,
1649
- });
1650
- }
1651
- }
1652
- }
1653
- return records;
1654
- } catch {
1655
- return null;
1656
- }
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';
1657
1346
  }
1658
1347
 
1659
- export function rebuildClaudeSavedHistoryFromNativeProject(historySessionId: string, workspace?: string): boolean {
1660
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1661
- if (!normalizedSessionId) return false;
1662
-
1663
- try {
1664
- const transcriptPath = resolveClaudeProjectTranscriptPath(normalizedSessionId, workspace);
1665
- if (!transcriptPath) return false;
1666
- const lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
1667
- const records: HistoryMessage[] = [];
1668
- const existingSessionStart = readExistingSessionStartRecord('claude-cli', normalizedSessionId);
1669
- if (existingSessionStart) {
1670
- records.push({
1671
- ...existingSessionStart,
1672
- historySessionId: normalizedSessionId,
1673
- });
1674
- }
1675
- let fallbackTs = Date.now();
1676
- for (const line of lines) {
1677
- let parsed: Record<string, unknown> | null = null;
1678
- try {
1679
- parsed = JSON.parse(line) as Record<string, unknown>;
1680
- } catch {
1681
- parsed = null;
1682
- }
1683
- if (!parsed) continue;
1684
- const parsedSessionId = String(parsed.sessionId || '').trim();
1685
- if (parsedSessionId && parsedSessionId !== normalizedSessionId) continue;
1686
- const receivedAt = extractTimestampValue(parsed.timestamp) || fallbackTs;
1687
- fallbackTs = receivedAt + 1;
1688
- const parsedWorkspace = String(parsed.cwd || workspace || '').trim();
1689
- if (records.length === 0 && parsedWorkspace) {
1690
- records.push({
1691
- ts: new Date(receivedAt).toISOString(),
1692
- receivedAt,
1693
- role: 'system',
1694
- kind: 'session_start',
1695
- content: parsedWorkspace,
1696
- agent: 'claude-cli',
1697
- historySessionId: normalizedSessionId,
1698
- workspace: parsedWorkspace,
1699
- });
1700
- }
1701
- const type = String(parsed.type || '').trim();
1702
- const message = parsed.message && typeof parsed.message === 'object'
1703
- ? parsed.message as Record<string, unknown>
1704
- : null;
1705
- if (type === 'user' && message) {
1706
- for (const part of extractClaudeUserContentParts(message.content)) {
1707
- records.push({
1708
- ts: new Date(receivedAt).toISOString(),
1709
- receivedAt,
1710
- role: part.role,
1711
- content: part.content,
1712
- kind: part.kind,
1713
- senderName: part.senderName,
1714
- agent: 'claude-cli',
1715
- historySessionId: normalizedSessionId,
1716
- });
1717
- }
1718
- continue;
1719
- }
1720
- if (type === 'assistant' && message) {
1721
- for (const part of extractClaudeAssistantContentParts(message.content)) {
1722
- records.push({
1723
- ts: new Date(receivedAt).toISOString(),
1724
- receivedAt,
1725
- role: 'assistant',
1726
- content: part.content,
1727
- kind: part.kind,
1728
- senderName: part.senderName,
1729
- agent: 'claude-cli',
1730
- historySessionId: normalizedSessionId,
1731
- });
1732
- }
1733
- }
1734
- }
1735
-
1736
- return rewriteCanonicalSavedHistory('claude-cli', normalizedSessionId, records);
1737
- } catch {
1738
- return false;
1739
- }
1740
- }
1741
-
1742
- function isUuidLikeSessionId(sessionId: string): boolean {
1743
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
1744
- }
1745
-
1746
- function readCodexSessionMeta(filePath: string): Record<string, unknown> | null {
1747
- try {
1748
- const firstLine = fs.readFileSync(filePath, 'utf-8').split('\n').find(Boolean);
1749
- if (!firstLine) return null;
1750
- const parsed = JSON.parse(firstLine) as Record<string, unknown>;
1751
- if (String(parsed.type || '') !== 'session_meta') return null;
1752
- const payload = parsed.payload && typeof parsed.payload === 'object'
1753
- ? parsed.payload as Record<string, unknown>
1754
- : null;
1755
- return payload;
1756
- } catch {
1757
- return null;
1758
- }
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;
1759
1356
  }
1760
1357
 
1761
- export function resolveCodexSessionTranscriptPath(historySessionId: string, workspace?: string): string | null {
1358
+ function normalizeProviderNativeHistoryRecords(agentType: string, historySessionId: string, records: unknown): HistoryMessage[] {
1359
+ if (!Array.isArray(records)) return [];
1762
1360
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1763
- if (!normalizedSessionId || !isUuidLikeSessionId(normalizedSessionId)) return null;
1764
- const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1765
- if (!fs.existsSync(sessionsDir)) return null;
1766
- const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1767
- const candidates: Array<{ path: string; mtimeMs: number; workspaceMatches: boolean; metaMatches: boolean }> = [];
1768
- const stack = [sessionsDir];
1769
- while (stack.length > 0) {
1770
- const current = stack.pop();
1771
- if (!current) continue;
1772
- let entries: fs.Dirent[] = [];
1773
- try {
1774
- entries = fs.readdirSync(current, { withFileTypes: true });
1775
- } catch {
1776
- continue;
1777
- }
1778
- for (const entry of entries) {
1779
- const entryPath = path.join(current, entry.name);
1780
- if (entry.isDirectory()) {
1781
- stack.push(entryPath);
1782
- continue;
1783
- }
1784
- if (!entry.isFile() || !entry.name.endsWith('.jsonl') || !entry.name.includes(normalizedSessionId)) continue;
1785
- const meta = readCodexSessionMeta(entryPath);
1786
- const metaSessionId = String(meta?.id || '').trim();
1787
- if (metaSessionId && metaSessionId !== normalizedSessionId) continue;
1788
- const metaWorkspace = String(meta?.cwd || '').trim();
1789
- let mtimeMs = 0;
1790
- try { mtimeMs = fs.statSync(entryPath).mtimeMs; } catch { /* ignore */ }
1791
- candidates.push({
1792
- path: entryPath,
1793
- mtimeMs,
1794
- workspaceMatches: !!normalizedWorkspace && metaWorkspace === normalizedWorkspace,
1795
- metaMatches: metaSessionId === normalizedSessionId,
1796
- });
1797
- }
1798
- }
1799
- candidates.sort((a, b) => Number(b.workspaceMatches) - Number(a.workspaceMatches)
1800
- || Number(b.metaMatches) - Number(a.metaMatches)
1801
- || b.mtimeMs - a.mtimeMs);
1802
- return candidates[0]?.path || null;
1803
- }
1804
-
1805
- function flattenCodexContent(content: unknown): string {
1806
- if (typeof content === 'string') return content.trim();
1807
- if (content == null) return '';
1808
- if (Array.isArray(content)) {
1809
- return content
1810
- .map((entry) => flattenCodexContent(entry))
1811
- .filter(Boolean)
1812
- .join('\n')
1813
- .trim();
1814
- }
1815
- if (typeof content === 'object') {
1816
- const record = content as Record<string, unknown>;
1817
- if (typeof record.text === 'string') return record.text.trim();
1818
- if (typeof record.content === 'string' || Array.isArray(record.content)) return flattenCodexContent(record.content);
1819
- if (typeof record.output === 'string') return record.output.trim();
1820
- if (typeof record.message === 'string') return record.message.trim();
1821
- }
1822
- return '';
1823
- }
1824
-
1825
- function summarizeCodexToolCall(payload: Record<string, unknown>): string {
1826
- const name = String(payload.name || payload.type || 'tool').trim() || 'tool';
1827
- const rawArguments = payload.arguments ?? payload.input;
1828
- let argumentValue = '';
1829
- if (typeof rawArguments === 'string') {
1830
- const trimmed = rawArguments.trim();
1831
- try {
1832
- const parsed = JSON.parse(trimmed) as unknown;
1833
- argumentValue = summarizeCodexToolArguments(parsed);
1834
- } catch {
1835
- argumentValue = trimmed;
1836
- }
1837
- } else {
1838
- argumentValue = summarizeCodexToolArguments(rawArguments);
1839
- }
1840
- return argumentValue ? `${name}: ${argumentValue}` : name;
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[];
1841
1376
  }
1842
1377
 
1843
- function summarizeCodexToolArguments(value: unknown): string {
1844
- if (typeof value === 'string') return value.trim();
1845
- if (Array.isArray(value)) return value.map((entry) => String(entry)).join(' ').trim();
1846
- if (!value || typeof value !== 'object') return '';
1847
- const record = value as Record<string, unknown>;
1848
- const direct = record.command || record.cmd || record.query || record.path || record.prompt;
1849
- if (typeof direct === 'string') return direct.trim();
1850
- if (Array.isArray(direct)) return direct.map((entry) => String(entry)).join(' ').trim();
1851
- try {
1852
- return JSON.stringify(record).trim();
1853
- } catch {
1854
- return '';
1855
- }
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
+ };
1856
1404
  }
1857
1405
 
1858
- function codexToolOutputContent(payload: Record<string, unknown>): string {
1859
- const output = payload.output ?? payload.result ?? payload.content;
1860
- const text = flattenCodexContent(output);
1861
- if (text) return text;
1862
- if (output && typeof output === 'object') {
1863
- try { return JSON.stringify(output).trim(); } catch { return ''; }
1864
- }
1865
- return '';
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);
1866
1416
  }
1867
1417
 
1868
- function buildCodexNativeHistoryRecords(historySessionId: string, workspace?: string): HistoryMessage[] | null {
1418
+ function materializeNativeHistoryToMirror(
1419
+ agentType: string,
1420
+ canonicalHistory: ProviderCanonicalHistoryConfig,
1421
+ historySessionId: string,
1422
+ workspace?: string,
1423
+ scripts?: ProviderNativeHistoryScripts,
1424
+ ): boolean {
1869
1425
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1870
- if (!normalizedSessionId || !isUuidLikeSessionId(normalizedSessionId)) return null;
1871
- try {
1872
- const transcriptPath = resolveCodexSessionTranscriptPath(normalizedSessionId, workspace);
1873
- if (!transcriptPath) return null;
1874
- const lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
1875
- const records: HistoryMessage[] = [];
1876
- let fallbackTs = Date.now();
1877
- for (const line of lines) {
1878
- let parsed: Record<string, unknown> | null = null;
1879
- try { parsed = JSON.parse(line) as Record<string, unknown>; } catch { parsed = null; }
1880
- if (!parsed) continue;
1881
- const receivedAt = extractTimestampValue(parsed.timestamp) || fallbackTs;
1882
- fallbackTs = receivedAt + 1;
1883
- const type = String(parsed.type || '').trim();
1884
- const payload = parsed.payload && typeof parsed.payload === 'object'
1885
- ? parsed.payload as Record<string, unknown>
1886
- : null;
1887
- if (!payload) continue;
1888
- if (type === 'session_meta') {
1889
- const parsedSessionId = String(payload.id || '').trim();
1890
- if (parsedSessionId && parsedSessionId !== normalizedSessionId) return null;
1891
- const parsedWorkspace = String(payload.cwd || workspace || '').trim();
1892
- if (records.length === 0 && parsedWorkspace) {
1893
- records.push({
1894
- ts: new Date(receivedAt).toISOString(),
1895
- receivedAt,
1896
- role: 'system',
1897
- kind: 'session_start',
1898
- content: parsedWorkspace,
1899
- agent: 'codex-cli',
1900
- historySessionId: normalizedSessionId,
1901
- workspace: parsedWorkspace,
1902
- });
1903
- }
1904
- continue;
1905
- }
1906
- if (type !== 'response_item') continue;
1907
- const payloadType = String(payload.type || '').trim();
1908
- if (payloadType === 'message') {
1909
- const role = String(payload.role || '').trim();
1910
- if (role !== 'user' && role !== 'assistant') continue;
1911
- const content = flattenCodexContent(payload.content);
1912
- if (!content) continue;
1913
- records.push({
1914
- ts: new Date(receivedAt).toISOString(),
1915
- receivedAt,
1916
- role,
1917
- content,
1918
- kind: 'standard',
1919
- agent: 'codex-cli',
1920
- historySessionId: normalizedSessionId,
1921
- });
1922
- continue;
1923
- }
1924
- if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
1925
- const content = summarizeCodexToolCall(payload);
1926
- if (!content) continue;
1927
- records.push({
1928
- ts: new Date(receivedAt).toISOString(),
1929
- receivedAt,
1930
- role: 'assistant',
1931
- content,
1932
- kind: 'tool',
1933
- senderName: 'Tool',
1934
- agent: 'codex-cli',
1935
- historySessionId: normalizedSessionId,
1936
- });
1937
- continue;
1938
- }
1939
- if (payloadType === 'function_call_output' || payloadType === 'custom_tool_call_output') {
1940
- const content = codexToolOutputContent(payload);
1941
- if (!content) continue;
1942
- records.push({
1943
- ts: new Date(receivedAt).toISOString(),
1944
- receivedAt,
1945
- role: 'assistant',
1946
- content,
1947
- kind: 'tool',
1948
- senderName: 'Tool',
1949
- agent: 'codex-cli',
1950
- historySessionId: normalizedSessionId,
1951
- });
1952
- }
1953
- }
1954
- return records;
1955
- } catch {
1956
- return null;
1957
- }
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);
1958
1440
  }
1959
1441
 
1960
- export function rebuildCodexSavedHistoryFromNativeSession(historySessionId: string, workspace?: string): boolean {
1961
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1962
- if (!normalizedSessionId || !isUuidLikeSessionId(normalizedSessionId)) return false;
1963
- const records = buildCodexNativeHistoryRecords(normalizedSessionId, workspace);
1964
- if (!records || records.length === 0) return false;
1965
- const existingSessionStart = readExistingSessionStartRecord('codex-cli', normalizedSessionId);
1966
- const recordsToWrite = existingSessionStart && records[0]?.kind !== 'session_start'
1967
- ? [{ ...existingSessionStart, historySessionId: normalizedSessionId }, ...records]
1968
- : records;
1969
- return rewriteCanonicalSavedHistory('codex-cli', normalizedSessionId, recordsToWrite);
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);
1970
1451
  }
1971
1452
 
1972
1453
  export function isNativeSourceCanonicalHistory(canonicalHistory?: ProviderCanonicalHistoryConfig): boolean {
@@ -1976,19 +1457,6 @@ export function isNativeSourceCanonicalHistory(canonicalHistory?: ProviderCanoni
1976
1457
  return true;
1977
1458
  }
1978
1459
 
1979
- function buildNativeHistoryRecords(
1980
- canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1981
- historySessionId: string | undefined,
1982
- workspace?: string,
1983
- ): HistoryMessage[] | null {
1984
- const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1985
- if (!canonicalHistory || !normalizedSessionId || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1986
- if (canonicalHistory.format === 'hermes-json') return buildHermesNativeHistoryRecords(normalizedSessionId);
1987
- if (canonicalHistory.format === 'claude-jsonl') return buildClaudeNativeHistoryRecords(normalizedSessionId, workspace);
1988
- if (canonicalHistory.format === 'codex-jsonl') return buildCodexNativeHistoryRecords(normalizedSessionId, workspace);
1989
- return null;
1990
- }
1991
-
1992
1460
  export function readProviderChatHistory(
1993
1461
  agentType: string,
1994
1462
  options: {
@@ -1999,14 +1467,17 @@ export function readProviderChatHistory(
1999
1467
  limit?: number;
2000
1468
  excludeRecentCount?: number;
2001
1469
  historyBehavior?: ProviderHistoryBehavior;
1470
+ scripts?: ProviderNativeHistoryScripts;
2002
1471
  } = {},
2003
- ): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable' } {
1472
+ ): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable'; sourcePath?: string; sourceMtimeMs?: number } {
2004
1473
  if (isNativeSourceCanonicalHistory(options.canonicalHistory) && options.historySessionId) {
2005
- const records = buildNativeHistoryRecords(options.canonicalHistory, options.historySessionId, options.workspace);
2006
- if (!records) return { messages: [], hasMore: false, source: 'native-unavailable' };
1474
+ const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
1475
+ if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
2007
1476
  return {
2008
- ...pageHistoryRecords(agentType, records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
1477
+ ...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
2009
1478
  source: 'provider-native',
1479
+ sourcePath: nativeResult.sourcePath,
1480
+ sourceMtimeMs: nativeResult.sourceMtimeMs,
2010
1481
  };
2011
1482
  }
2012
1483
  return {
@@ -2043,61 +1514,70 @@ function buildNativeSessionSummary(
2043
1514
  };
2044
1515
  }
2045
1516
 
2046
- function listFilesRecursive(root: string, predicate: (entryPath: string, entry: fs.Dirent) => boolean): string[] {
2047
- if (!fs.existsSync(root)) return [];
2048
- const results: string[] = [];
2049
- const stack = [root];
2050
- while (stack.length > 0) {
2051
- const current = stack.pop();
2052
- if (!current) continue;
2053
- let entries: fs.Dirent[] = [];
2054
- try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
2055
- for (const entry of entries) {
2056
- const entryPath = path.join(current, entry.name);
2057
- if (entry.isDirectory()) {
2058
- stack.push(entryPath);
2059
- continue;
2060
- }
2061
- if (predicate(entryPath, entry)) results.push(entryPath);
2062
- }
2063
- }
2064
- return results;
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
+ };
2065
1537
  }
2066
1538
 
2067
- function collectNativeHistorySessionSummaries(agentType: string, canonicalHistory: ProviderCanonicalHistoryConfig): SavedHistorySessionSummary[] {
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 : [];
2068
1554
  const summaries: SavedHistorySessionSummary[] = [];
2069
- if (canonicalHistory.format === 'hermes-json') {
2070
- const root = path.join(os.homedir(), '.hermes', 'sessions');
2071
- for (const filePath of listFilesRecursive(root, (_entryPath, entry) => entry.isFile() && /^session_.+\.json$/.test(entry.name))) {
2072
- const fileName = path.basename(filePath);
2073
- const historySessionId = fileName.replace(/^session_/, '').replace(/\.json$/, '');
2074
- const records = buildHermesNativeHistoryRecords(historySessionId);
2075
- const summary = records ? buildNativeSessionSummary(agentType, historySessionId, records, filePath) : null;
2076
- if (summary) summaries.push(summary);
2077
- }
2078
- } else if (canonicalHistory.format === 'claude-jsonl') {
2079
- const root = path.join(os.homedir(), '.claude', 'projects');
2080
- for (const filePath of listFilesRecursive(root, (_entryPath, entry) => entry.isFile() && entry.name.endsWith('.jsonl'))) {
2081
- const historySessionId = path.basename(filePath, '.jsonl');
2082
- const records = buildClaudeNativeHistoryRecords(historySessionId);
2083
- const summary = records ? buildNativeSessionSummary(agentType, historySessionId, records, filePath) : null;
2084
- if (summary) summaries.push(summary);
2085
- }
2086
- } else if (canonicalHistory.format === 'codex-jsonl') {
2087
- const root = path.join(os.homedir(), '.codex', 'sessions');
2088
- const uuidPattern = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
2089
- for (const filePath of listFilesRecursive(root, (_entryPath, entry) => entry.isFile() && entry.name.endsWith('.jsonl'))) {
2090
- const meta = readCodexSessionMeta(filePath);
2091
- const historySessionId = String(meta?.id || path.basename(filePath).match(uuidPattern)?.[1] || '').trim();
1555
+ for (const item of sessions) {
1556
+ if (Array.isArray(item?.messages || item?.records)) {
1557
+ const historySessionId = normalizeSavedHistorySessionId(item?.historySessionId || item?.sessionId || '');
2092
1558
  if (!historySessionId) continue;
2093
- const records = buildCodexNativeHistoryRecords(historySessionId, String(meta?.cwd || '').trim() || undefined);
2094
- const summary = records ? buildNativeSessionSummary(agentType, historySessionId, records, filePath) : null;
2095
- if (summary) summaries.push(summary);
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
+ }
1565
+ continue;
2096
1566
  }
1567
+ const summary = normalizeProviderNativeHistorySessionSummary(agentType, item);
1568
+ if (summary) summaries.push(summary);
2097
1569
  }
2098
1570
  return sortSavedHistorySessionSummaries(summaries);
2099
1571
  }
2100
1572
 
1573
+ function collectNativeHistorySessionSummaries(
1574
+ agentType: string,
1575
+ canonicalHistory: ProviderCanonicalHistoryConfig,
1576
+ scripts?: ProviderNativeHistoryScripts,
1577
+ ): SavedHistorySessionSummary[] {
1578
+ return collectProviderScriptNativeHistorySessionSummaries(agentType, canonicalHistory, scripts) || [];
1579
+ }
1580
+
2101
1581
  export function listProviderHistorySessions(
2102
1582
  agentType: string,
2103
1583
  options: {
@@ -2105,12 +1585,13 @@ export function listProviderHistorySessions(
2105
1585
  offset?: number;
2106
1586
  limit?: number;
2107
1587
  historyBehavior?: ProviderHistoryBehavior;
1588
+ scripts?: ProviderNativeHistoryScripts;
2108
1589
  } = {},
2109
1590
  ): { sessions: SavedHistorySessionSummary[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' } {
2110
1591
  if (isNativeSourceCanonicalHistory(options.canonicalHistory)) {
2111
1592
  const offset = Math.max(0, options.offset || 0);
2112
1593
  const limit = Math.max(1, options.limit || 30);
2113
- const summaries = collectNativeHistorySessionSummaries(agentType, options.canonicalHistory!);
1594
+ const summaries = collectNativeHistorySessionSummaries(agentType, options.canonicalHistory!, options.scripts);
2114
1595
  return {
2115
1596
  sessions: summaries.slice(offset, offset + limit),
2116
1597
  hasMore: offset + limit < summaries.length,