nterminal 1.3.2 → 1.3.4

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.
Files changed (62) hide show
  1. package/README.md +4 -2
  2. package/bin/nterminal.js +5 -0
  3. package/dist/client/assets/MarkdownPreview-DjDO4lEl.js +1 -0
  4. package/dist/client/assets/TranscriptMarkdownBody-H8ZsISc6.js +3 -0
  5. package/dist/client/assets/{index-lQ2cyEWO.css → index-CIYazAGv.css} +1 -1
  6. package/dist/client/assets/index-DwvLN_WJ.js +56 -0
  7. package/dist/client/assets/{index-CosXAR2f.js → index-DxfBq-4P.js} +1 -1
  8. package/dist/client/index.html +2 -2
  9. package/dist/scripts/onboarding.js +9 -0
  10. package/dist/scripts/onboarding.js.map +1 -1
  11. package/dist/server/agent/agentProxy.d.ts +1 -0
  12. package/dist/server/agent/agentProxy.js +3 -1
  13. package/dist/server/agent/agentProxy.js.map +1 -1
  14. package/dist/server/agent/agentRoutes.js +16 -2
  15. package/dist/server/agent/agentRoutes.js.map +1 -1
  16. package/dist/server/index.js +2 -0
  17. package/dist/server/index.js.map +1 -1
  18. package/dist/server/routes/agentManagementRoutes.js +13 -2
  19. package/dist/server/routes/agentManagementRoutes.js.map +1 -1
  20. package/dist/server/routes/terminalRoutes.js +19 -2
  21. package/dist/server/routes/terminalRoutes.js.map +1 -1
  22. package/dist/server/routes/transcriptQuery.d.ts +14 -0
  23. package/dist/server/routes/transcriptQuery.js +57 -0
  24. package/dist/server/routes/transcriptQuery.js.map +1 -0
  25. package/dist/server/terminal/PtyAdapter.d.ts +12 -0
  26. package/dist/server/terminal/PtyAdapter.js.map +1 -1
  27. package/dist/server/terminal/TerminalManager.d.ts +6 -7
  28. package/dist/server/terminal/TerminalManager.js +57 -11
  29. package/dist/server/terminal/TerminalManager.js.map +1 -1
  30. package/dist/server/terminal/TmuxPtyAdapter.d.ts +15 -1
  31. package/dist/server/terminal/TmuxPtyAdapter.js +288 -13
  32. package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
  33. package/dist/server/terminal/codexTranscriptSource.d.ts +18 -1
  34. package/dist/server/terminal/codexTranscriptSource.js +273 -75
  35. package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
  36. package/dist/server/terminal/sqliteReader.d.ts +21 -0
  37. package/dist/server/terminal/sqliteReader.js +158 -0
  38. package/dist/server/terminal/sqliteReader.js.map +1 -0
  39. package/dist/server/terminal/subagentSources.js +3 -2
  40. package/dist/server/terminal/subagentSources.js.map +1 -1
  41. package/dist/server/terminal/transcriptHistory.d.ts +34 -0
  42. package/dist/server/terminal/transcriptHistory.js +81 -36
  43. package/dist/server/terminal/transcriptHistory.js.map +1 -1
  44. package/dist/server/terminal/transcriptStore.d.ts +64 -0
  45. package/dist/server/terminal/transcriptStore.js +779 -0
  46. package/dist/server/terminal/transcriptStore.js.map +1 -0
  47. package/dist/shared/nodeVersion.d.ts +3 -0
  48. package/dist/shared/nodeVersion.js +12 -0
  49. package/dist/shared/nodeVersion.js.map +1 -0
  50. package/dist/shared/protocol.d.ts +33 -0
  51. package/dist/shared/protocol.js.map +1 -1
  52. package/dist/shared/transcriptSearch.d.ts +2 -0
  53. package/dist/shared/transcriptSearch.js +12 -0
  54. package/dist/shared/transcriptSearch.js.map +1 -0
  55. package/dist/src/shared/nodeVersion.js +12 -0
  56. package/dist/src/shared/nodeVersion.js.map +1 -0
  57. package/docs/terminal-history.md +22 -4
  58. package/package.json +2 -2
  59. package/scripts/nterminalctl +8 -4
  60. package/dist/client/assets/MarkdownPreview-CzA5ZyZ3.js +0 -1
  61. package/dist/client/assets/TranscriptMarkdownBody-Cu7KK3bx.js +0 -3
  62. package/dist/client/assets/index-wiyTLXQg.js +0 -56
@@ -1,13 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
3
  import { existsSync } from 'node:fs';
4
- import { access, open as openFile, readFile, readdir, realpath, stat } from 'node:fs/promises';
4
+ import { access, open as openFile, opendir, readFile, readdir, readlink, realpath, stat } from 'node:fs/promises';
5
5
  import os from 'node:os';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import * as nodePty from 'node-pty';
8
8
  import path from 'node:path';
9
9
  import { normalizeTerminalTitle, PtySessionUnavailableError } from './PtyAdapter.js';
10
- import { resolveCodexTranscriptFromDaemon, resolveCodexTranscriptFromOpenFds, resolveCodexTranscriptFromProcessLogs } from './codexTranscriptSource.js';
10
+ import { lookupCodexTranscriptFromDaemonOrLogs, lookupCodexTranscriptFromOpenFds, resolveCodexTranscriptCandidates, processBinding, TRANSCRIPT_CANDIDATE_LIMIT } from './codexTranscriptSource.js';
11
11
  import { isSessionId } from './sessionId.js';
12
12
  import { processStartTicksToSecondsAsync, readProcessCwdAsync, readProcessEnvValuesAsync, readProcessStartTicksAsync } from './procfs.js';
13
13
  import { TtlCache } from './ttlCache.js';
@@ -26,6 +26,7 @@ const TRANSCRIPT_FILE_NEGATIVE_CACHE_TTL_MS = 20_000;
26
26
  // letting a history-paging burst (several page requests within seconds)
27
27
  // reuse one resolution instead of re-forking tmux/ps per page.
28
28
  const TRANSCRIPT_RESOLUTION_CACHE_TTL_MS = 3_000;
29
+ const TRANSCRIPT_CANDIDATE_CACHE_TTL_MS = 500;
29
30
  const PROCESS_TREE_CACHE_TTL_MS = 1_000;
30
31
  const CLAUDE_TRANSCRIPT_START_SKEW_MS = 10_000;
31
32
  const CLAUDE_TRANSCRIPT_SAMPLE_BYTES = 512 * 1024;
@@ -75,6 +76,7 @@ export class TmuxPtyAdapter {
75
76
  ptySpawner;
76
77
  transcriptResolutionCache = new TtlCache();
77
78
  transcriptResolutionInflight = new KeyedSingleFlight();
79
+ transcriptCandidateInflight = new KeyedSingleFlight();
78
80
  processTreeCache = new TtlCache();
79
81
  processTreeInflight = new KeyedSingleFlight();
80
82
  historyTokenGeneration = randomBytes(16).toString('hex');
@@ -1047,6 +1049,76 @@ export class TmuxPtyAdapter {
1047
1049
  return source;
1048
1050
  });
1049
1051
  }
1052
+ async resolveTranscriptCandidates(sessionId) {
1053
+ return this.transcriptCandidateInflight.run(sessionId, async () => {
1054
+ const result = await this.resolveTranscriptCandidatesUncached(sessionId);
1055
+ return { binding: result.binding, candidates: result.candidates.map(candidate => ({ ...candidate })) };
1056
+ });
1057
+ }
1058
+ async resolveTranscriptCandidatesUncached(sessionId) {
1059
+ const empty = { binding: '', candidates: [] };
1060
+ const panePid = await this.readPanePidAsync(sessionId);
1061
+ if (panePid === null)
1062
+ return empty;
1063
+ const paneStart = await readProcessStartTicksAsync(panePid);
1064
+ if (paneStart === null)
1065
+ return empty;
1066
+ const tree = await this.readProcessTreeSnapshot();
1067
+ if (!tree)
1068
+ return empty;
1069
+ const argsByPid = new Map();
1070
+ const processes = listProcessPidsWithDepth(tree, panePid);
1071
+ await mapWithConcurrency(processes, 8, async ({ pid }) => argsByPid.set(pid, await readProcessArgsAsync(pid)));
1072
+ const discoveries = [];
1073
+ for (const { pid, depth } of processes) {
1074
+ // Breadth-first traversal: deeper workers cannot affect the selected
1075
+ // owner, including a worker exiting while its procfs identity is read.
1076
+ if (discoveries.length && depth > discoveries[0].depth)
1077
+ continue;
1078
+ const args = argsByPid.get(pid);
1079
+ if (findCommandArg(args, 'claude') !== -1) {
1080
+ if (isNonInteractiveClaudeInvocation(args))
1081
+ continue;
1082
+ const result = await resolveClaudeTranscriptCandidates(pid, { args, reuseCache: true });
1083
+ if (!result.binding)
1084
+ return empty;
1085
+ if (result.rank !== 'live' && listProcessPidsWithDepth(tree, pid).some(child => child.pid !== pid && findCommandArg(argsByPid.get(child.pid) ?? [], 'claude') !== -1 &&
1086
+ !isNonInteractiveClaudeInvocation(argsByPid.get(child.pid) ?? [])))
1087
+ continue;
1088
+ discoveries.push({ depth, result });
1089
+ continue;
1090
+ }
1091
+ if (findCommandArg(args, 'codex') === -1 || isNonInteractiveCodexInvocation(args))
1092
+ continue;
1093
+ if (hasCodexDescendant(tree, pid, argsByPid) &&
1094
+ (await lookupCodexTranscriptFromOpenFds(pid)).status === 'absent')
1095
+ continue;
1096
+ const startTicks = await readProcessStartTicksAsync(pid);
1097
+ if (startTicks === null)
1098
+ return empty;
1099
+ const result = await resolveCodexTranscriptCandidates(pid, {
1100
+ processStartedAtSeconds: await processStartTicksToSecondsAsync(startTicks)
1101
+ });
1102
+ if (!result.binding || startTicks !== await readProcessStartTicksAsync(pid))
1103
+ return empty;
1104
+ discoveries.push({ depth, result });
1105
+ }
1106
+ if (paneStart !== await readProcessStartTicksAsync(panePid) || panePid !== await this.readPanePidAsync(sessionId))
1107
+ return empty;
1108
+ const closest = Math.min(...discoveries.map(({ depth }) => depth));
1109
+ const candidates = new Map();
1110
+ for (const { depth, result } of discoveries) {
1111
+ if (depth !== closest)
1112
+ continue;
1113
+ for (const candidate of result.candidates) {
1114
+ candidates.set(`${candidate.provider}:${candidate.sessionId}`, candidate);
1115
+ }
1116
+ }
1117
+ return {
1118
+ binding: `pane:${panePid}:${paneStart}|${discoveries.filter(({ depth }) => depth === closest).map(({ result }) => result.binding).sort().join('|')}`,
1119
+ candidates: [...candidates.values()].slice(0, TRANSCRIPT_CANDIDATE_LIMIT)
1120
+ };
1121
+ }
1050
1122
  async resolveTranscriptSourceUncached(sessionId) {
1051
1123
  const panePid = this.panePidBySession.get(sessionId) ?? await this.readPanePidAsync(sessionId);
1052
1124
  if (panePid === null) {
@@ -1093,11 +1165,13 @@ export class TmuxPtyAdapter {
1093
1165
  // The append-open rollout is the authoritative owner signal. Check
1094
1166
  // it before descendant suppression: recent Codex releases spawn
1095
1167
  // helpers such as codex-code-mode-host beneath the real process.
1096
- const live = await resolveCodexTranscriptFromOpenFds(pid);
1097
- if (live) {
1098
- entries.push({ candidate: live, rank: 'live', processDepth });
1168
+ const live = await lookupCodexTranscriptFromOpenFds(pid);
1169
+ if (live.status === 'matched') {
1170
+ entries.push({ candidate: live.source, rank: 'live', processDepth });
1099
1171
  continue;
1100
1172
  }
1173
+ if (live.status === 'pending' || live.status === 'ambiguous')
1174
+ return null;
1101
1175
  // Launcher pids (the npm node wrapper) merely relay the binary's
1102
1176
  // argv; after /new their stale `resume <id>` args would conflict
1103
1177
  // with the live session the binary itself reports, so only the
@@ -1105,14 +1179,13 @@ export class TmuxPtyAdapter {
1105
1179
  if (hasCodexDescendant(tree, pid, argsByPid)) {
1106
1180
  continue;
1107
1181
  }
1108
- // Process logs reflect the thread the process wrote to most
1109
- // recently, so they outrank launch args, which freeze at startup.
1110
- // Daemon-backed TUIs (Codex >= 0.157) leave neither an fd nor
1111
- // thread-tagged logs of their own; their daemon's open rollouts come
1112
- // next, still ahead of args that go stale after /new or /resume.
1113
- const stated = (await resolveCodexTranscriptFromProcessLogs(pid)) ??
1114
- (await resolveCodexTranscriptFromDaemon(pid)) ??
1115
- extractCodexArgsCandidate(args);
1182
+ // A connected daemon is authoritative, even when its current
1183
+ // thread is pending/ambiguous. Never replace that with stale argv.
1184
+ const lookup = await lookupCodexTranscriptFromDaemonOrLogs(pid);
1185
+ if (lookup.status === 'pending' || lookup.status === 'ambiguous')
1186
+ return null;
1187
+ const stated = lookup.status === 'matched' ? lookup.source
1188
+ : lookup.status === 'absent' ? extractCodexArgsCandidate(args) : null;
1116
1189
  if (stated) {
1117
1190
  entries.push({ candidate: stated, rank: 'stated', processDepth });
1118
1191
  }
@@ -1815,6 +1888,208 @@ async function resolveClaudeCandidateForPidAsync(pid, args) {
1815
1888
  });
1816
1889
  return { entry: inferred ? { candidate: inferred, rank: 'inferred' } : null, pending: false };
1817
1890
  }
1891
+ const claudeCandidateCache = new TtlCache();
1892
+ const claudeCandidateInflight = new KeyedSingleFlight();
1893
+ const CLAUDE_CANDIDATE_DIRECTORY_LIMIT = 500;
1894
+ const CLAUDE_CANDIDATE_SAMPLE_BYTES = 32 * 1024;
1895
+ export function isNonInteractiveClaudeInvocation(args) {
1896
+ const index = findCommandArg(args, 'claude');
1897
+ if (index === -1)
1898
+ return true;
1899
+ const rest = args.slice(index + 1);
1900
+ return rest.some(arg => arg === '-p' || arg === '--print' || arg.startsWith('--print=')) ||
1901
+ ['mcp', 'auth', 'doctor', 'install', 'update', 'setup-token'].includes(rest[0] ?? '');
1902
+ }
1903
+ export async function resolveClaudeTranscriptCandidates(pid, options = {}) {
1904
+ const empty = { binding: '', candidates: [], exact: null, rank: 'inferred' };
1905
+ if (!Number.isInteger(pid) || pid <= 0)
1906
+ return empty;
1907
+ const procRoot = options.procRoot ?? '/proc';
1908
+ const processDir = path.join(procRoot, String(pid));
1909
+ const binding = await processBinding(procRoot, [pid]);
1910
+ if (!binding)
1911
+ return empty;
1912
+ let args, cwd, env;
1913
+ try {
1914
+ args = options.args ?? (await readFile(path.join(processDir, 'cmdline'), 'utf8')).split('\0').filter(Boolean);
1915
+ cwd = await readlink(path.join(processDir, 'cwd'));
1916
+ env = new Map((await readFile(path.join(processDir, 'environ'), 'utf8')).split('\0').map(entry => {
1917
+ const separator = entry.indexOf('=');
1918
+ return [entry.slice(0, separator), entry.slice(separator + 1)];
1919
+ }));
1920
+ }
1921
+ catch {
1922
+ return empty;
1923
+ }
1924
+ if (isNonInteractiveClaudeInvocation(args))
1925
+ return { ...empty, binding };
1926
+ const configRoot = env.get('CLAUDE_CONFIG_DIR') || path.join(env.get('HOME') || os.homedir(), '.claude');
1927
+ const registryPath = path.join(configRoot, 'sessions', `${pid}.json`);
1928
+ const version = async (filePath) => {
1929
+ try {
1930
+ const info = await stat(filePath);
1931
+ return [info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs];
1932
+ }
1933
+ catch {
1934
+ return null;
1935
+ }
1936
+ };
1937
+ // Registry switches and project membership changes invalidate even the brief
1938
+ // metadata cache. Appending a transcript does not change its process binding.
1939
+ const evidenceVersion = options.reuseCache ? await Promise.all([
1940
+ version(registryPath), version(path.join(configRoot, 'projects', encodeClaudeProjectPath(cwd)))
1941
+ ]) : null;
1942
+ const key = JSON.stringify([procRoot, binding, configRoot, cwd, args, options.processStartedAtMs, evidenceVersion]);
1943
+ const discover = async () => {
1944
+ const cached = options.reuseCache ? claudeCandidateCache.get(key)?.value : undefined;
1945
+ if (cached)
1946
+ return cached;
1947
+ const startTicks = binding.slice(binding.indexOf(':') + 1);
1948
+ // Fake procfs never consults the host clock or invokes getconf.
1949
+ const startMs = options.processStartedAtMs !== undefined ? options.processStartedAtMs
1950
+ : options.procRoot ? null : await processStartTicksToSecondsAsync(startTicks).then(value => value === null ? null : value * 1000);
1951
+ const result = await discoverClaudeProjectCandidates({ pid, binding, configRoot, cwd, args, startMs, startTicks });
1952
+ if (options.reuseCache)
1953
+ claudeCandidateCache.set(key, result, TRANSCRIPT_CANDIDATE_CACHE_TTL_MS);
1954
+ return result;
1955
+ };
1956
+ const result = options.reuseCache ? await claudeCandidateInflight.run(key, discover) : await discover();
1957
+ try {
1958
+ if (binding !== await processBinding(procRoot, [pid]) || cwd !== await readlink(path.join(processDir, 'cwd')))
1959
+ return empty;
1960
+ }
1961
+ catch {
1962
+ return empty;
1963
+ }
1964
+ return { ...result, candidates: result.candidates.map(candidate => ({ ...candidate })) };
1965
+ }
1966
+ async function discoverClaudeProjectCandidates(options) {
1967
+ const result = { binding: options.binding, candidates: [], exact: null, rank: 'inferred' };
1968
+ let cwds = await normalizeClaudeCwdCandidatesAsync([options.cwd]);
1969
+ let sessionId;
1970
+ try {
1971
+ const file = await openFile(path.join(options.configRoot, 'sessions', `${options.pid}.json`), 'r');
1972
+ try {
1973
+ const info = await file.stat();
1974
+ if (info.size > 16 * 1024)
1975
+ return { ...result, rank: 'live' };
1976
+ const buffer = Buffer.alloc(info.size);
1977
+ await readExactly(file, buffer, 0);
1978
+ const record = JSON.parse(buffer.toString('utf8'));
1979
+ if (record && Number(record.pid) === options.pid && String(record.procStart) === options.startTicks) {
1980
+ if (record.kind !== undefined && record.kind !== 'interactive')
1981
+ return result;
1982
+ result.rank = 'live';
1983
+ if (!isSessionId(record.sessionId))
1984
+ return result;
1985
+ const registryCwds = typeof record.cwd === 'string' ? await normalizeClaudeCwdCandidatesAsync([record.cwd]) : [];
1986
+ if (registryCwds.length === 0)
1987
+ return result;
1988
+ // A live process can change cwd after opening its conversation. The
1989
+ // start-tick-matched registry is stronger evidence than current cwd.
1990
+ cwds = registryCwds;
1991
+ sessionId = record.sessionId;
1992
+ }
1993
+ }
1994
+ finally {
1995
+ await file.close();
1996
+ }
1997
+ }
1998
+ catch { /* Missing/stale registry permits bounded current-project evidence. */ }
1999
+ if (!sessionId) {
2000
+ sessionId = extractClaudeArgsCandidate(options.args)?.sessionId;
2001
+ if (sessionId)
2002
+ result.rank = 'stated';
2003
+ }
2004
+ const projectDirs = [...new Set(cwds.map(cwd => path.join(options.configRoot, 'projects', encodeClaudeProjectPath(cwd))))];
2005
+ let incomplete = false;
2006
+ if (sessionId) {
2007
+ // A confirmed pending registry must not fall back to another session.
2008
+ for (const projectDir of projectDirs) {
2009
+ const candidate = await describeClaudeCandidate(path.join(projectDir, `${sessionId}.jsonl`), sessionId, cwds, undefined, false);
2010
+ if (candidate)
2011
+ result.candidates.push(candidate);
2012
+ }
2013
+ }
2014
+ else if (options.startMs !== null) {
2015
+ let inspected = 0;
2016
+ for (const projectDir of projectDirs) {
2017
+ try {
2018
+ const directory = await opendir(projectDir);
2019
+ for await (const entry of directory) {
2020
+ if (++inspected > CLAUDE_CANDIDATE_DIRECTORY_LIMIT || result.candidates.length >= TRANSCRIPT_CANDIDATE_LIMIT) {
2021
+ incomplete = true;
2022
+ break;
2023
+ }
2024
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
2025
+ continue;
2026
+ const id = entry.name.slice(0, -6);
2027
+ if (!isSessionId(id))
2028
+ continue;
2029
+ const candidate = await describeClaudeCandidate(path.join(projectDir, entry.name), id, cwds, options.startMs);
2030
+ if (candidate)
2031
+ result.candidates.push(candidate);
2032
+ }
2033
+ }
2034
+ catch {
2035
+ incomplete = true;
2036
+ }
2037
+ if (incomplete)
2038
+ break;
2039
+ }
2040
+ }
2041
+ // Never rank by mtime. Unknown/unreadable peers also veto uniqueness.
2042
+ if (!incomplete && result.candidates.length === 1 && result.candidates[0].readable &&
2043
+ result.candidates[0].verification === 'verified')
2044
+ result.exact = result.candidates[0];
2045
+ return result;
2046
+ }
2047
+ async function describeClaudeCandidate(filePath, sessionId, cwds, processStartedAtMs, preserveMissing = true) {
2048
+ const candidate = {
2049
+ provider: 'claude', sessionId, filePath, ...(cwds[0] ? { cwd: cwds[0] } : {}), verification: 'unknown', readable: false
2050
+ };
2051
+ let file;
2052
+ try {
2053
+ file = await openFile(filePath, 'r');
2054
+ const info = await file.stat();
2055
+ if (!info.isFile() || (processStartedAtMs !== undefined && info.mtimeMs + CLAUDE_TRANSCRIPT_START_SKEW_MS < processStartedAtMs))
2056
+ return null;
2057
+ candidate.updatedAt = info.mtime.toISOString();
2058
+ candidate.readable = true;
2059
+ const offsets = info.size > CLAUDE_CANDIDATE_SAMPLE_BYTES ? [0, Math.max(CLAUDE_CANDIDATE_SAMPLE_BYTES, info.size - CLAUDE_CANDIDATE_SAMPLE_BYTES)] : [0];
2060
+ let otherCwd = false;
2061
+ let sidechain = false;
2062
+ for (const offset of offsets) {
2063
+ const buffer = Buffer.alloc(Math.min(CLAUDE_CANDIDATE_SAMPLE_BYTES, info.size - offset));
2064
+ const count = await readExactly(file, buffer, offset);
2065
+ for (const line of buffer.subarray(0, count).toString('utf8').split('\n')) {
2066
+ try {
2067
+ const row = JSON.parse(line);
2068
+ if (row.sessionId !== sessionId || typeof row.cwd !== 'string')
2069
+ continue;
2070
+ if (row.isSidechain === true) {
2071
+ sidechain = true;
2072
+ continue;
2073
+ }
2074
+ if (cwds.includes(row.cwd))
2075
+ candidate.verification = 'verified';
2076
+ else
2077
+ otherCwd = true;
2078
+ }
2079
+ catch { /* Partial sample lines are unknown, never negative evidence. */ }
2080
+ }
2081
+ }
2082
+ if ((otherCwd || sidechain) && candidate.verification !== 'verified')
2083
+ return null;
2084
+ return candidate;
2085
+ }
2086
+ catch (error) {
2087
+ return !preserveMissing && error.code === 'ENOENT' ? null : candidate;
2088
+ }
2089
+ finally {
2090
+ await file?.close().catch(() => undefined);
2091
+ }
2092
+ }
1818
2093
  export async function resolveClaudeTranscriptFromSessionRegistry(options) {
1819
2094
  if (!options.procStartTicks)
1820
2095
  return { kind: 'unavailable' };