@adhdev/daemon-core 0.9.43 → 0.9.45

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
  }
@@ -1351,6 +1363,57 @@ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: strin
1351
1363
  }
1352
1364
  }
1353
1365
 
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
+ }
1416
+
1354
1417
  export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
1355
1418
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1356
1419
  if (!normalizedSessionId) return false;
@@ -1523,6 +1586,76 @@ function extractClaudeUserContentParts(content: unknown): Array<{ role: 'user' |
1523
1586
  return parts;
1524
1587
  }
1525
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
+ }
1657
+ }
1658
+
1526
1659
  export function rebuildClaudeSavedHistoryFromNativeProject(historySessionId: string, workspace?: string): boolean {
1527
1660
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1528
1661
  if (!normalizedSessionId) return false;
@@ -1605,3 +1738,387 @@ export function rebuildClaudeSavedHistoryFromNativeProject(historySessionId: str
1605
1738
  return false;
1606
1739
  }
1607
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
+ }
1759
+ }
1760
+
1761
+ export function resolveCodexSessionTranscriptPath(historySessionId: string, workspace?: string): string | null {
1762
+ 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;
1841
+ }
1842
+
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
+ }
1856
+ }
1857
+
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 '';
1866
+ }
1867
+
1868
+ function buildCodexNativeHistoryRecords(historySessionId: string, workspace?: string): HistoryMessage[] | null {
1869
+ 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
+ }
1958
+ }
1959
+
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);
1970
+ }
1971
+
1972
+ export function isNativeSourceCanonicalHistory(canonicalHistory?: ProviderCanonicalHistoryConfig): boolean {
1973
+ if (!canonicalHistory) return false;
1974
+ if ((canonicalHistory as any).mode === 'disabled') return false;
1975
+ if ((canonicalHistory as any).mode === 'materialized-mirror') return false;
1976
+ return true;
1977
+ }
1978
+
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
+ export function readProviderChatHistory(
1993
+ agentType: string,
1994
+ options: {
1995
+ canonicalHistory?: ProviderCanonicalHistoryConfig;
1996
+ historySessionId?: string;
1997
+ workspace?: string;
1998
+ offset?: number;
1999
+ limit?: number;
2000
+ excludeRecentCount?: number;
2001
+ historyBehavior?: ProviderHistoryBehavior;
2002
+ } = {},
2003
+ ): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable' } {
2004
+ 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' };
2007
+ return {
2008
+ ...pageHistoryRecords(agentType, records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
2009
+ source: 'provider-native',
2010
+ };
2011
+ }
2012
+ return {
2013
+ ...readChatHistory(agentType, options.offset || 0, options.limit || 30, options.historySessionId, options.excludeRecentCount || 0, options.historyBehavior),
2014
+ source: 'adhdev-mirror',
2015
+ };
2016
+ }
2017
+
2018
+ function buildNativeSessionSummary(
2019
+ agentType: string,
2020
+ historySessionId: string,
2021
+ records: HistoryMessage[],
2022
+ sourcePath: string,
2023
+ ): SavedHistorySessionSummary | null {
2024
+ const visible = pageHistoryRecords(agentType, records, 0, Number.MAX_SAFE_INTEGER).messages;
2025
+ if (visible.length === 0) return null;
2026
+ let sourceMtimeMs = 0;
2027
+ try { sourceMtimeMs = fs.statSync(sourcePath).mtimeMs; } catch { /* ignore */ }
2028
+ const firstMessageAt = visible[0]?.receivedAt || sourceMtimeMs || Date.now();
2029
+ const lastMessageAt = visible[visible.length - 1]?.receivedAt || firstMessageAt;
2030
+ const lastNonSystem = [...visible].reverse().find((message) => message.role !== 'system') || visible[visible.length - 1];
2031
+ const firstSystem = visible.find((message) => message.kind === 'session_start');
2032
+ return {
2033
+ historySessionId,
2034
+ sessionTitle: lastNonSystem?.content,
2035
+ messageCount: visible.length,
2036
+ firstMessageAt,
2037
+ lastMessageAt,
2038
+ preview: lastNonSystem?.content,
2039
+ workspace: firstSystem?.workspace || (firstSystem?.kind === 'session_start' ? firstSystem.content : undefined),
2040
+ source: 'provider-native',
2041
+ sourcePath,
2042
+ sourceMtimeMs,
2043
+ };
2044
+ }
2045
+
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;
2065
+ }
2066
+
2067
+ function collectNativeHistorySessionSummaries(agentType: string, canonicalHistory: ProviderCanonicalHistoryConfig): SavedHistorySessionSummary[] {
2068
+ 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();
2092
+ 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);
2096
+ }
2097
+ }
2098
+ return sortSavedHistorySessionSummaries(summaries);
2099
+ }
2100
+
2101
+ export function listProviderHistorySessions(
2102
+ agentType: string,
2103
+ options: {
2104
+ canonicalHistory?: ProviderCanonicalHistoryConfig;
2105
+ offset?: number;
2106
+ limit?: number;
2107
+ historyBehavior?: ProviderHistoryBehavior;
2108
+ } = {},
2109
+ ): { sessions: SavedHistorySessionSummary[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' } {
2110
+ if (isNativeSourceCanonicalHistory(options.canonicalHistory)) {
2111
+ const offset = Math.max(0, options.offset || 0);
2112
+ const limit = Math.max(1, options.limit || 30);
2113
+ const summaries = collectNativeHistorySessionSummaries(agentType, options.canonicalHistory!);
2114
+ return {
2115
+ sessions: summaries.slice(offset, offset + limit),
2116
+ hasMore: offset + limit < summaries.length,
2117
+ source: 'provider-native',
2118
+ };
2119
+ }
2120
+ return {
2121
+ ...listSavedHistorySessions(agentType, { offset: options.offset, limit: options.limit }, options.historyBehavior),
2122
+ source: 'adhdev-mirror',
2123
+ };
2124
+ }
@@ -0,0 +1,37 @@
1
+ function normalizeMacAppPath(appPath: string): string | null {
2
+ const trimmed = String(appPath || '').trim()
3
+ if (!trimmed) return null
4
+ return trimmed.replace(/\/+$/, '')
5
+ }
6
+
7
+ function parsePsLine(line: string): { pid: number; args: string } | null {
8
+ const match = line.match(/^\s*(\d+)\s+(.+)$/)
9
+ if (!match) return null
10
+ const pid = Number.parseInt(match[1], 10)
11
+ if (!Number.isFinite(pid)) return null
12
+ return { pid, args: match[2] }
13
+ }
14
+
15
+ export function isMacAppProcessArgs(args: string, appPath: string): boolean {
16
+ const normalized = normalizeMacAppPath(appPath)
17
+ if (!normalized) return false
18
+ return String(args || '').startsWith(`${normalized}/`)
19
+ }
20
+
21
+ export function findMacAppProcessPids(psOutput: string, appPaths: readonly string[]): number[] {
22
+ const normalizedPaths = appPaths
23
+ .map(normalizeMacAppPath)
24
+ .filter((value): value is string => !!value)
25
+
26
+ if (normalizedPaths.length === 0) return []
27
+
28
+ const pids: number[] = []
29
+ for (const line of String(psOutput || '').split(/\r?\n/)) {
30
+ const parsed = parsePsLine(line)
31
+ if (!parsed) continue
32
+ if (normalizedPaths.some(appPath => isMacAppProcessArgs(parsed.args, appPath))) {
33
+ pids.push(parsed.pid)
34
+ }
35
+ }
36
+ return pids
37
+ }