nterminal 1.3.3 → 1.3.5

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 (114) hide show
  1. package/bin/nterminal.js +8 -3
  2. package/dist/client/assets/MarkdownPreview-BIrwUAaa.js +1 -0
  3. package/dist/client/assets/TranscriptMarkdownBody-CI0ztvMX.js +3 -0
  4. package/dist/client/assets/{index-DDCpqDNb.js → index-EN8ngMgv.js} +1 -1
  5. package/dist/client/assets/index-JQJ1Eet1.js +58 -0
  6. package/dist/client/assets/index-xkL4h3qt.css +1 -0
  7. package/dist/client/index.html +2 -2
  8. package/dist/scripts/hooks.js +83 -0
  9. package/dist/scripts/hooks.js.map +1 -0
  10. package/dist/server/accounts/accountService.d.ts +23 -1
  11. package/dist/server/accounts/accountService.js +235 -103
  12. package/dist/server/accounts/accountService.js.map +1 -1
  13. package/dist/server/accounts/providerRequests.d.ts +12 -0
  14. package/dist/server/accounts/providerRequests.js +159 -0
  15. package/dist/server/accounts/providerRequests.js.map +1 -0
  16. package/dist/server/agent/agentRoutes.js +11 -0
  17. package/dist/server/agent/agentRoutes.js.map +1 -1
  18. package/dist/server/hooks/helperJournal.d.ts +16 -0
  19. package/dist/server/hooks/helperJournal.js +74 -0
  20. package/dist/server/hooks/helperJournal.js.map +1 -0
  21. package/dist/server/hooks/hookBinding.d.ts +39 -0
  22. package/dist/server/hooks/hookBinding.js +91 -0
  23. package/dist/server/hooks/hookBinding.js.map +1 -0
  24. package/dist/server/hooks/hookFiles.d.ts +24 -0
  25. package/dist/server/hooks/hookFiles.js +160 -0
  26. package/dist/server/hooks/hookFiles.js.map +1 -0
  27. package/dist/server/hooks/hookInstallation.d.ts +79 -0
  28. package/dist/server/hooks/hookInstallation.js +978 -0
  29. package/dist/server/hooks/hookInstallation.js.map +1 -0
  30. package/dist/server/hooks/hookProcess.d.ts +5 -0
  31. package/dist/server/hooks/hookProcess.js +85 -0
  32. package/dist/server/hooks/hookProcess.js.map +1 -0
  33. package/dist/server/hooks/hookReceiver.d.ts +66 -0
  34. package/dist/server/hooks/hookReceiver.js +549 -0
  35. package/dist/server/hooks/hookReceiver.js.map +1 -0
  36. package/dist/server/hooks/lifecycle.d.ts +40 -0
  37. package/dist/server/hooks/lifecycle.js +200 -0
  38. package/dist/server/hooks/lifecycle.js.map +1 -0
  39. package/dist/server/hooks/publicationLock.d.ts +3 -0
  40. package/dist/server/hooks/publicationLock.js +137 -0
  41. package/dist/server/hooks/publicationLock.js.map +1 -0
  42. package/dist/server/hooks/verifiedLock.d.ts +24 -0
  43. package/dist/server/hooks/verifiedLock.js +207 -0
  44. package/dist/server/hooks/verifiedLock.js.map +1 -0
  45. package/dist/server/http.d.ts +4 -0
  46. package/dist/server/http.js +6 -0
  47. package/dist/server/http.js.map +1 -1
  48. package/dist/server/index.js +69 -2
  49. package/dist/server/index.js.map +1 -1
  50. package/dist/server/notifications/agentCompletionMonitor.d.ts +16 -1
  51. package/dist/server/notifications/agentCompletionMonitor.js +261 -47
  52. package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
  53. package/dist/server/notifications/completionNotificationRouter.d.ts +2 -0
  54. package/dist/server/notifications/completionNotificationRouter.js +9 -0
  55. package/dist/server/notifications/completionNotificationRouter.js.map +1 -1
  56. package/dist/server/routes/accountRoutes.js +11 -0
  57. package/dist/server/routes/accountRoutes.js.map +1 -1
  58. package/dist/server/routes/activityRoutes.d.ts +13 -0
  59. package/dist/server/routes/activityRoutes.js +165 -0
  60. package/dist/server/routes/activityRoutes.js.map +1 -0
  61. package/dist/server/routes/agentManagementRoutes.js +8 -1
  62. package/dist/server/routes/agentManagementRoutes.js.map +1 -1
  63. package/dist/server/routes/hookRoutes.d.ts +16 -0
  64. package/dist/server/routes/hookRoutes.js +77 -0
  65. package/dist/server/routes/hookRoutes.js.map +1 -0
  66. package/dist/server/routes/terminalRoutes.js +4 -0
  67. package/dist/server/routes/terminalRoutes.js.map +1 -1
  68. package/dist/server/routes/transcriptQuery.d.ts +2 -1
  69. package/dist/server/routes/transcriptQuery.js +3 -1
  70. package/dist/server/routes/transcriptQuery.js.map +1 -1
  71. package/dist/server/terminal/PtyAdapter.d.ts +18 -0
  72. package/dist/server/terminal/PtyAdapter.js.map +1 -1
  73. package/dist/server/terminal/TerminalManager.d.ts +61 -0
  74. package/dist/server/terminal/TerminalManager.js +665 -12
  75. package/dist/server/terminal/TerminalManager.js.map +1 -1
  76. package/dist/server/terminal/TmuxPtyAdapter.d.ts +14 -2
  77. package/dist/server/terminal/TmuxPtyAdapter.js +104 -16
  78. package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
  79. package/dist/server/terminal/activityChannel.d.ts +57 -0
  80. package/dist/server/terminal/activityChannel.js +166 -0
  81. package/dist/server/terminal/activityChannel.js.map +1 -0
  82. package/dist/server/terminal/agentTranscriptStatus.d.ts +12 -0
  83. package/dist/server/terminal/agentTranscriptStatus.js +78 -7
  84. package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
  85. package/dist/server/terminal/codexTranscriptSource.d.ts +2 -0
  86. package/dist/server/terminal/codexTranscriptSource.js +53 -46
  87. package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
  88. package/dist/server/terminal/subagentSources.js +2 -1
  89. package/dist/server/terminal/subagentSources.js.map +1 -1
  90. package/dist/server/terminal/transcriptHeader.d.ts +85 -0
  91. package/dist/server/terminal/transcriptHeader.js +376 -0
  92. package/dist/server/terminal/transcriptHeader.js.map +1 -0
  93. package/dist/server/terminal/transcriptHistory.d.ts +16 -2
  94. package/dist/server/terminal/transcriptHistory.js +120 -21
  95. package/dist/server/terminal/transcriptHistory.js.map +1 -1
  96. package/dist/server/terminal/transcriptStore.d.ts +6 -0
  97. package/dist/server/terminal/transcriptStore.js +222 -26
  98. package/dist/server/terminal/transcriptStore.js.map +1 -1
  99. package/dist/shared/activityProbe.d.ts +10 -0
  100. package/dist/shared/activityProbe.js +24 -0
  101. package/dist/shared/activityProbe.js.map +1 -0
  102. package/dist/shared/protocol.d.ts +45 -0
  103. package/dist/shared/protocol.js.map +1 -1
  104. package/dist/shared/types.d.ts +9 -1
  105. package/docs/features.md +10 -0
  106. package/docs/provider-hooks.md +287 -0
  107. package/docs/terminal-history.md +10 -0
  108. package/package.json +2 -1
  109. package/scripts/nterminal-hook.cjs +171 -0
  110. package/scripts/nterminalctl +26 -2
  111. package/dist/client/assets/MarkdownPreview-CWK2qIZp.js +0 -1
  112. package/dist/client/assets/TranscriptMarkdownBody-Ddi-rw6X.js +0 -3
  113. package/dist/client/assets/index-CcAQThtj.js +0 -56
  114. package/dist/client/assets/index-DTTX24tu.css +0 -1
@@ -7,7 +7,7 @@ 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 { lookupCodexTranscriptFromDaemonOrLogs, lookupCodexTranscriptFromOpenFds, resolveCodexTranscriptCandidates, processBinding, TRANSCRIPT_CANDIDATE_LIMIT } from './codexTranscriptSource.js';
10
+ import { lookupCodexTranscriptFromDaemon, lookupCodexTranscriptFromProcessLogs, 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,8 @@ 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
+ // Diagnostics outlive a few status polls, not the pane's next resolution.
30
+ const TRANSCRIPT_AMBIGUITY_TTL_MS = 90_000;
29
31
  const TRANSCRIPT_CANDIDATE_CACHE_TTL_MS = 500;
30
32
  const PROCESS_TREE_CACHE_TTL_MS = 1_000;
31
33
  const CLAUDE_TRANSCRIPT_START_SKEW_MS = 10_000;
@@ -75,6 +77,9 @@ export class TmuxPtyAdapter {
75
77
  commandRunner;
76
78
  ptySpawner;
77
79
  transcriptResolutionCache = new TtlCache();
80
+ transcriptResolutionGeneration = new Map();
81
+ // Last discovery outcome per pane, for status reporting only (bounded by panes).
82
+ transcriptAmbiguity = new Map();
78
83
  transcriptResolutionInflight = new KeyedSingleFlight();
79
84
  transcriptCandidateInflight = new KeyedSingleFlight();
80
85
  processTreeCache = new TtlCache();
@@ -397,6 +402,8 @@ export class TmuxPtyAdapter {
397
402
  this.historyOutputGenerationBySession.delete(sessionId);
398
403
  this.historyOutputUnreliableSessions.delete(sessionId);
399
404
  this.transcriptResolutionCache.delete(sessionId);
405
+ this.transcriptResolutionGeneration.delete(sessionId);
406
+ this.transcriptAmbiguity.delete(sessionId);
400
407
  }
401
408
  isInflightFinalCreation(sessionId, marker) {
402
409
  const inflightMarker = this.inflightFinalCreationMarkers.get(sessionId);
@@ -1039,16 +1046,50 @@ export class TmuxPtyAdapter {
1039
1046
  }
1040
1047
  }
1041
1048
  async resolveTranscriptSource(sessionId) {
1049
+ return (await this.resolveTranscriptSourceEvidence(sessionId))?.source ?? null;
1050
+ }
1051
+ async resolveTranscriptSourceEvidence(sessionId, options = {}) {
1052
+ const resolved = options.fresh ? await this.resolveTranscriptSourceUncached(sessionId, true)
1053
+ : await this.resolveTranscriptSourceCached(sessionId);
1054
+ if (!resolved)
1055
+ return null;
1056
+ const { evidence = 'inferred', client, ...source } = resolved;
1057
+ return { source, evidence, ...(client ? { client } : {}) };
1058
+ }
1059
+ getTranscriptAmbiguity(sessionId) {
1060
+ const entry = this.transcriptAmbiguity.get(sessionId);
1061
+ if (!entry)
1062
+ return null;
1063
+ if (Date.now() - entry.at > TRANSCRIPT_AMBIGUITY_TTL_MS) {
1064
+ this.transcriptAmbiguity.delete(sessionId);
1065
+ return null;
1066
+ }
1067
+ return entry.reason;
1068
+ }
1069
+ noteTranscriptAmbiguity(sessionId, ambiguous) {
1070
+ if (ambiguous)
1071
+ this.transcriptAmbiguity.set(sessionId, { reason: 'codex_shared_daemon_same_cwd', at: Date.now() });
1072
+ else
1073
+ this.transcriptAmbiguity.delete(sessionId);
1074
+ }
1075
+ async resolveTranscriptSourceCached(sessionId) {
1042
1076
  const cached = this.transcriptResolutionCache.get(sessionId);
1043
1077
  if (cached) {
1044
1078
  return cached.value;
1045
1079
  }
1046
- return this.transcriptResolutionInflight.run(sessionId, async () => {
1080
+ const generation = this.transcriptResolutionGeneration.get(sessionId) ?? 0;
1081
+ return this.transcriptResolutionInflight.run(`${sessionId}:${generation}`, async () => {
1047
1082
  const source = await this.resolveTranscriptSourceUncached(sessionId);
1048
- this.transcriptResolutionCache.set(sessionId, source, TRANSCRIPT_RESOLUTION_CACHE_TTL_MS);
1083
+ if ((this.transcriptResolutionGeneration.get(sessionId) ?? 0) === generation) {
1084
+ this.transcriptResolutionCache.set(sessionId, source, TRANSCRIPT_RESOLUTION_CACHE_TTL_MS);
1085
+ }
1049
1086
  return source;
1050
1087
  });
1051
1088
  }
1089
+ invalidateTranscriptSource(sessionId) {
1090
+ this.transcriptResolutionCache.delete(sessionId);
1091
+ this.transcriptResolutionGeneration.set(sessionId, (this.transcriptResolutionGeneration.get(sessionId) ?? 0) + 1);
1092
+ }
1052
1093
  async resolveTranscriptCandidates(sessionId) {
1053
1094
  return this.transcriptCandidateInflight.run(sessionId, async () => {
1054
1095
  const result = await this.resolveTranscriptCandidatesUncached(sessionId);
@@ -1071,6 +1112,10 @@ export class TmuxPtyAdapter {
1071
1112
  await mapWithConcurrency(processes, 8, async ({ pid }) => argsByPid.set(pid, await readProcessArgsAsync(pid)));
1072
1113
  const discoveries = [];
1073
1114
  for (const { pid, depth } of processes) {
1115
+ // Breadth-first traversal: deeper workers cannot affect the selected
1116
+ // owner, including a worker exiting while its procfs identity is read.
1117
+ if (discoveries.length && depth > discoveries[0].depth)
1118
+ continue;
1074
1119
  const args = argsByPid.get(pid);
1075
1120
  if (findCommandArg(args, 'claude') !== -1) {
1076
1121
  if (isNonInteractiveClaudeInvocation(args))
@@ -1111,11 +1156,17 @@ export class TmuxPtyAdapter {
1111
1156
  }
1112
1157
  }
1113
1158
  return {
1114
- binding: `pane:${panePid}:${paneStart}|${discoveries.map(({ result }) => result.binding).sort().join('|')}`,
1159
+ binding: `pane:${panePid}:${paneStart}|${discoveries.filter(({ depth }) => depth === closest).map(({ result }) => result.binding).sort().join('|')}`,
1115
1160
  candidates: [...candidates.values()].slice(0, TRANSCRIPT_CANDIDATE_LIMIT)
1116
1161
  };
1117
1162
  }
1118
- async resolveTranscriptSourceUncached(sessionId) {
1163
+ async resolveTranscriptSourceUncached(sessionId, fresh = false) {
1164
+ const state = { daemonAmbiguous: false };
1165
+ const resolved = await this.resolveTranscriptSourceUncachedInner(sessionId, fresh, state);
1166
+ this.noteTranscriptAmbiguity(sessionId, !resolved && state.daemonAmbiguous);
1167
+ return resolved;
1168
+ }
1169
+ async resolveTranscriptSourceUncachedInner(sessionId, fresh, state) {
1119
1170
  const panePid = this.panePidBySession.get(sessionId) ?? await this.readPanePidAsync(sessionId);
1120
1171
  if (panePid === null) {
1121
1172
  return null;
@@ -1136,6 +1187,7 @@ export class TmuxPtyAdapter {
1136
1187
  }
1137
1188
  });
1138
1189
  let entries = [];
1190
+ const pendingCodexDepths = [];
1139
1191
  let claudeSessionPending = false;
1140
1192
  for (const [pid, args] of argsByPid) {
1141
1193
  const processDepth = processDepthByPid.get(pid);
@@ -1166,8 +1218,10 @@ export class TmuxPtyAdapter {
1166
1218
  entries.push({ candidate: live.source, rank: 'live', processDepth });
1167
1219
  continue;
1168
1220
  }
1169
- if (live.status === 'pending' || live.status === 'ambiguous')
1170
- return null;
1221
+ if (live.status === 'pending' || live.status === 'ambiguous') {
1222
+ pendingCodexDepths.push(processDepth);
1223
+ continue;
1224
+ }
1171
1225
  // Launcher pids (the npm node wrapper) merely relay the binary's
1172
1226
  // argv; after /new their stale `resume <id>` args would conflict
1173
1227
  // with the live session the binary itself reports, so only the
@@ -1177,13 +1231,21 @@ export class TmuxPtyAdapter {
1177
1231
  }
1178
1232
  // A connected daemon is authoritative, even when its current
1179
1233
  // thread is pending/ambiguous. Never replace that with stale argv.
1180
- const lookup = await lookupCodexTranscriptFromDaemonOrLogs(pid);
1181
- if (lookup.status === 'pending' || lookup.status === 'ambiguous')
1182
- return null;
1234
+ const daemon = await lookupCodexTranscriptFromDaemon(pid, fresh ? { bypassCache: true } : undefined);
1235
+ const lookup = daemon.status === 'absent' || daemon.status === 'unavailable'
1236
+ ? await lookupCodexTranscriptFromProcessLogs(pid) : daemon;
1237
+ if (lookup.status === 'pending' || lookup.status === 'ambiguous') {
1238
+ if (daemon.status === 'ambiguous')
1239
+ state.daemonAmbiguous = true;
1240
+ pendingCodexDepths.push(processDepth);
1241
+ continue;
1242
+ }
1183
1243
  const stated = lookup.status === 'matched' ? lookup.source
1184
1244
  : lookup.status === 'absent' ? extractCodexArgsCandidate(args) : null;
1185
1245
  if (stated) {
1186
- entries.push({ candidate: stated, rank: 'stated', processDepth });
1246
+ const client = daemon.status === 'matched' ? await describeTranscriptClient(pid, args) : null;
1247
+ entries.push({ candidate: stated, rank: daemon.status === 'matched' ? 'daemon' : 'stated', processDepth,
1248
+ ...(client ? { client } : {}) });
1187
1249
  }
1188
1250
  }
1189
1251
  }
@@ -1194,15 +1256,23 @@ export class TmuxPtyAdapter {
1194
1256
  // other session's file) is wrong by construction.
1195
1257
  entries = entries.filter((entry) => entry.candidate.provider !== 'claude' || entry.rank === 'live');
1196
1258
  }
1197
- const candidate = selectTranscriptCandidate(entries);
1198
- if (!candidate) {
1259
+ // Uncertain child jobs cannot veto a closer write-open TUI owner. At
1260
+ // the same or shallower depth uncertainty still blocks all fallback.
1261
+ const closestLiveCodex = Math.min(...entries.filter(entry => entry.rank === 'live' && entry.candidate.provider === 'codex')
1262
+ .map(entry => entry.processDepth ?? Infinity));
1263
+ if (pendingCodexDepths.some(depth => depth <= closestLiveCodex))
1264
+ return null;
1265
+ const selected = selectTranscriptCandidateEntry(entries);
1266
+ if (!selected) {
1199
1267
  return null;
1200
1268
  }
1269
+ const candidate = selected.candidate;
1201
1270
  const matches = candidate.filePath ? [candidate.filePath] : await findTranscriptFilesAsync(candidate.provider, candidate.sessionId);
1202
1271
  if (matches.length !== 1) {
1203
1272
  return null;
1204
1273
  }
1205
- return { provider: candidate.provider, sessionId: candidate.sessionId, filePath: matches[0] };
1274
+ return { provider: candidate.provider, sessionId: candidate.sessionId, filePath: matches[0], evidence: selected.rank,
1275
+ ...(selected.client ? { client: selected.client } : {}) };
1206
1276
  }
1207
1277
  async readPaneMetricsAsync(sessionId) {
1208
1278
  try {
@@ -1292,6 +1362,10 @@ export class TmuxPtyAdapter {
1292
1362
  return false;
1293
1363
  }
1294
1364
  }
1365
+ getSessionPid(sessionId) {
1366
+ const cached = this.panePidBySession.get(sessionId);
1367
+ return cached ? Promise.resolve(cached) : this.readPanePidAsync(sessionId);
1368
+ }
1295
1369
  async readPanePidAsync(sessionId) {
1296
1370
  try {
1297
1371
  const result = await this.commandRunner('tmux', [
@@ -1714,6 +1788,17 @@ async function readProcessArgsAsync(pid) {
1714
1788
  return [];
1715
1789
  }
1716
1790
  }
1791
+ // Launch facts for the TUI behind a daemon association. Missing facts stay
1792
+ // missing (never guessed), so corroboration fails closed.
1793
+ async function describeTranscriptClient(pid, args) {
1794
+ const startTicks = await readProcessStartTicksAsync(pid);
1795
+ if (startTicks === null)
1796
+ return null;
1797
+ const startedAtSeconds = await processStartTicksToSecondsAsync(startTicks);
1798
+ const stated = extractCodexArgsCandidate(args)?.sessionId;
1799
+ return { pid, startTicks, startedAtMs: startedAtSeconds === null ? null : Math.round(startedAtSeconds * 1000),
1800
+ ...(stated ? { statedSessionId: stated } : {}) };
1801
+ }
1717
1802
  function extractCodexArgsCandidate(args) {
1718
1803
  const codexIndex = findCommandArg(args, 'codex');
1719
1804
  if (codexIndex === -1) {
@@ -1822,7 +1907,10 @@ function hasCodexDescendant(tree, root, argsByPid) {
1822
1907
  // tier returns null — falling back to a lower tier would resolve precisely
1823
1908
  // when the best information says the answer is uncertain.
1824
1909
  export function selectTranscriptCandidate(entries) {
1825
- for (const rank of ['live', 'stated', 'inferred']) {
1910
+ return selectTranscriptCandidateEntry(entries)?.candidate ?? null;
1911
+ }
1912
+ function selectTranscriptCandidateEntry(entries) {
1913
+ for (const rank of ['live', 'daemon', 'stated', 'inferred']) {
1826
1914
  let pool = entries.filter((entry) => entry.rank === rank);
1827
1915
  if (pool.length === 0) {
1828
1916
  continue;
@@ -1848,7 +1936,7 @@ export function selectTranscriptCandidate(entries) {
1848
1936
  if (unique.size !== 1) {
1849
1937
  return null;
1850
1938
  }
1851
- return [...unique.values()][0];
1939
+ return { candidate: [...unique.values()][0], rank };
1852
1940
  }
1853
1941
  return null;
1854
1942
  }