@myagentroam/node 0.1.3 → 0.1.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 (197) hide show
  1. package/dist/claude-agent-sdk.d.ts +1 -15
  2. package/dist/claude-agent-sdk.js +9 -77
  3. package/dist/claude-agent-sdk.js.map +1 -1
  4. package/dist/codex-app-server.d.ts +4 -0
  5. package/dist/codex-app-server.js +18 -4
  6. package/dist/codex-app-server.js.map +1 -1
  7. package/dist/connector/node-connector-options.d.ts +23 -0
  8. package/dist/connector/node-connector-options.js +2 -0
  9. package/dist/connector/node-connector-options.js.map +1 -0
  10. package/dist/connector/node-operation-router.d.ts +9 -0
  11. package/dist/connector/node-operation-router.js +23 -0
  12. package/dist/connector/node-operation-router.js.map +1 -0
  13. package/dist/connector.d.ts +59 -356
  14. package/dist/connector.js +611 -6175
  15. package/dist/connector.js.map +1 -1
  16. package/dist/control-outbound-scheduler.d.ts +26 -0
  17. package/dist/control-outbound-scheduler.js +122 -0
  18. package/dist/control-outbound-scheduler.js.map +1 -0
  19. package/dist/database.js +1 -1
  20. package/dist/database.js.map +1 -1
  21. package/dist/native-jsonl-reader.d.ts +14 -0
  22. package/dist/native-jsonl-reader.js +98 -0
  23. package/dist/native-jsonl-reader.js.map +1 -0
  24. package/dist/native-session-history.d.ts +22 -3
  25. package/dist/native-session-history.js +275 -156
  26. package/dist/native-session-history.js.map +1 -1
  27. package/dist/runner/abstract-runner.d.ts +148 -0
  28. package/dist/runner/abstract-runner.js +139 -0
  29. package/dist/runner/abstract-runner.js.map +1 -0
  30. package/dist/runner/claude/managed-run-controller.d.ts +52 -0
  31. package/dist/runner/claude/managed-run-controller.js +307 -0
  32. package/dist/runner/claude/managed-run-controller.js.map +1 -0
  33. package/dist/runner/claude-code-runner.d.ts +68 -0
  34. package/dist/runner/claude-code-runner.js +268 -0
  35. package/dist/runner/claude-code-runner.js.map +1 -0
  36. package/dist/runner/codex/conversation-parser.d.ts +61 -0
  37. package/dist/runner/codex/conversation-parser.js +676 -0
  38. package/dist/runner/codex/conversation-parser.js.map +1 -0
  39. package/dist/runner/codex/managed-run-controller.d.ts +79 -0
  40. package/dist/runner/codex/managed-run-controller.js +379 -0
  41. package/dist/runner/codex/managed-run-controller.js.map +1 -0
  42. package/dist/runner/codex-runner.d.ts +81 -0
  43. package/dist/runner/codex-runner.js +342 -0
  44. package/dist/runner/codex-runner.js.map +1 -0
  45. package/dist/runner/fake-test-runner.d.ts +26 -0
  46. package/dist/runner/fake-test-runner.js +48 -0
  47. package/dist/runner/fake-test-runner.js.map +1 -0
  48. package/dist/runner/runner-registry.d.ts +17 -0
  49. package/dist/runner/runner-registry.js +66 -0
  50. package/dist/runner/runner-registry.js.map +1 -0
  51. package/dist/runner-usage.d.ts +2 -2
  52. package/dist/runner-usage.js +3 -5
  53. package/dist/runner-usage.js.map +1 -1
  54. package/dist/runtime-state.js +6 -5
  55. package/dist/runtime-state.js.map +1 -1
  56. package/dist/service/attachment-domain-state.d.ts +5 -0
  57. package/dist/service/attachment-domain-state.js +6 -0
  58. package/dist/service/attachment-domain-state.js.map +1 -0
  59. package/dist/service/conversation-history-service.d.ts +34 -0
  60. package/dist/service/conversation-history-service.js +159 -0
  61. package/dist/service/conversation-history-service.js.map +1 -0
  62. package/dist/service/external-session-resume-service.d.ts +32 -0
  63. package/dist/service/external-session-resume-service.js +99 -0
  64. package/dist/service/external-session-resume-service.js.map +1 -0
  65. package/dist/service/fake-managed-run-service.d.ts +29 -0
  66. package/dist/service/fake-managed-run-service.js +63 -0
  67. package/dist/service/fake-managed-run-service.js.map +1 -0
  68. package/dist/service/managed-runner-session-service.d.ts +10 -0
  69. package/dist/service/managed-runner-session-service.js +31 -0
  70. package/dist/service/managed-runner-session-service.js.map +1 -0
  71. package/dist/service/native-session-projection-service.d.ts +50 -0
  72. package/dist/service/native-session-projection-service.js +268 -0
  73. package/dist/service/native-session-projection-service.js.map +1 -0
  74. package/dist/service/native-session-watch-service.d.ts +37 -0
  75. package/dist/service/native-session-watch-service.js +151 -0
  76. package/dist/service/native-session-watch-service.js.map +1 -0
  77. package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
  78. package/dist/service/node-connection-lifecycle-service.js +84 -0
  79. package/dist/service/node-connection-lifecycle-service.js.map +1 -0
  80. package/dist/service/node-control-channel.d.ts +32 -0
  81. package/dist/service/node-control-channel.js +84 -0
  82. package/dist/service/node-control-channel.js.map +1 -0
  83. package/dist/service/node-control-message-service.d.ts +28 -0
  84. package/dist/service/node-control-message-service.js +105 -0
  85. package/dist/service/node-control-message-service.js.map +1 -0
  86. package/dist/service/node-request-service.d.ts +14 -0
  87. package/dist/service/node-request-service.js +51 -0
  88. package/dist/service/node-request-service.js.map +1 -0
  89. package/dist/service/node-run-event-bridge.d.ts +38 -0
  90. package/dist/service/node-run-event-bridge.js +80 -0
  91. package/dist/service/node-run-event-bridge.js.map +1 -0
  92. package/dist/service/node-terminal-channel.d.ts +29 -0
  93. package/dist/service/node-terminal-channel.js +176 -0
  94. package/dist/service/node-terminal-channel.js.map +1 -0
  95. package/dist/service/node-workspace-coordinator.d.ts +32 -0
  96. package/dist/service/node-workspace-coordinator.js +81 -0
  97. package/dist/service/node-workspace-coordinator.js.map +1 -0
  98. package/dist/service/run-attachment-service.d.ts +37 -0
  99. package/dist/service/run-attachment-service.js +161 -0
  100. package/dist/service/run-attachment-service.js.map +1 -0
  101. package/dist/service/run-domain-state.d.ts +4 -0
  102. package/dist/service/run-domain-state.js +5 -0
  103. package/dist/service/run-domain-state.js.map +1 -0
  104. package/dist/service/run-event-service.d.ts +31 -0
  105. package/dist/service/run-event-service.js +103 -0
  106. package/dist/service/run-event-service.js.map +1 -0
  107. package/dist/service/run-workbench-service.d.ts +27 -0
  108. package/dist/service/run-workbench-service.js +114 -0
  109. package/dist/service/run-workbench-service.js.map +1 -0
  110. package/dist/service/runner-channel-message-service.d.ts +13 -0
  111. package/dist/service/runner-channel-message-service.js +21 -0
  112. package/dist/service/runner-channel-message-service.js.map +1 -0
  113. package/dist/service/runner-interaction-service.d.ts +40 -0
  114. package/dist/service/runner-interaction-service.js +245 -0
  115. package/dist/service/runner-interaction-service.js.map +1 -0
  116. package/dist/service/runner-service.d.ts +43 -0
  117. package/dist/service/runner-service.js +55 -0
  118. package/dist/service/runner-service.js.map +1 -0
  119. package/dist/service/session-catalog-service.d.ts +30 -0
  120. package/dist/service/session-catalog-service.js +145 -0
  121. package/dist/service/session-catalog-service.js.map +1 -0
  122. package/dist/service/session-command-service.d.ts +23 -0
  123. package/dist/service/session-command-service.js +70 -0
  124. package/dist/service/session-command-service.js.map +1 -0
  125. package/dist/service/session-context-service.d.ts +34 -0
  126. package/dist/service/session-context-service.js +88 -0
  127. package/dist/service/session-context-service.js.map +1 -0
  128. package/dist/service/session-domain-state.d.ts +8 -0
  129. package/dist/service/session-domain-state.js +9 -0
  130. package/dist/service/session-domain-state.js.map +1 -0
  131. package/dist/service/session-identity-service.d.ts +24 -0
  132. package/dist/service/session-identity-service.js +87 -0
  133. package/dist/service/session-identity-service.js.map +1 -0
  134. package/dist/service/session-lifecycle-service.d.ts +31 -0
  135. package/dist/service/session-lifecycle-service.js +236 -0
  136. package/dist/service/session-lifecycle-service.js.map +1 -0
  137. package/dist/service/session-message-service.d.ts +33 -0
  138. package/dist/service/session-message-service.js +158 -0
  139. package/dist/service/session-message-service.js.map +1 -0
  140. package/dist/service/session-presentation-service.d.ts +33 -0
  141. package/dist/service/session-presentation-service.js +37 -0
  142. package/dist/service/session-presentation-service.js.map +1 -0
  143. package/dist/service/session-query-service.d.ts +31 -0
  144. package/dist/service/session-query-service.js +78 -0
  145. package/dist/service/session-query-service.js.map +1 -0
  146. package/dist/service/terminal-relay-state.d.ts +6 -0
  147. package/dist/service/terminal-relay-state.js +4 -0
  148. package/dist/service/terminal-relay-state.js.map +1 -0
  149. package/dist/service/terminal-service.d.ts +12 -0
  150. package/dist/service/terminal-service.js +53 -0
  151. package/dist/service/terminal-service.js.map +1 -0
  152. package/dist/service/workbench-event-service.d.ts +21 -0
  153. package/dist/service/workbench-event-service.js +54 -0
  154. package/dist/service/workbench-event-service.js.map +1 -0
  155. package/dist/service/workbench-manifest-service.d.ts +88 -0
  156. package/dist/service/workbench-manifest-service.js +69 -0
  157. package/dist/service/workbench-manifest-service.js.map +1 -0
  158. package/dist/service/workspace-change-service.d.ts +9 -0
  159. package/dist/service/workspace-change-service.js +86 -0
  160. package/dist/service/workspace-change-service.js.map +1 -0
  161. package/dist/service/workspace-domain-state.d.ts +75 -0
  162. package/dist/service/workspace-domain-state.js +41 -0
  163. package/dist/service/workspace-domain-state.js.map +1 -0
  164. package/dist/service/workspace-file-service.d.ts +14 -0
  165. package/dist/service/workspace-file-service.js +77 -0
  166. package/dist/service/workspace-file-service.js.map +1 -0
  167. package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
  168. package/dist/service/workspace-queue-workbench-service.js +227 -0
  169. package/dist/service/workspace-queue-workbench-service.js.map +1 -0
  170. package/dist/service/workspace-service.d.ts +15 -0
  171. package/dist/service/workspace-service.js +54 -0
  172. package/dist/service/workspace-service.js.map +1 -0
  173. package/dist/service/workspace-session-service.d.ts +23 -0
  174. package/dist/service/workspace-session-service.js +140 -0
  175. package/dist/service/workspace-session-service.js.map +1 -0
  176. package/dist/service/workspace-upload-service.d.ts +37 -0
  177. package/dist/service/workspace-upload-service.js +241 -0
  178. package/dist/service/workspace-upload-service.js.map +1 -0
  179. package/dist/service/workspace-watch-service.d.ts +18 -0
  180. package/dist/service/workspace-watch-service.js +121 -0
  181. package/dist/service/workspace-watch-service.js.map +1 -0
  182. package/dist/service/workspace-workbench-service.d.ts +38 -0
  183. package/dist/service/workspace-workbench-service.js +156 -0
  184. package/dist/service/workspace-workbench-service.js.map +1 -0
  185. package/dist/terminal.d.ts +1 -0
  186. package/dist/terminal.js +1 -1
  187. package/dist/terminal.js.map +1 -1
  188. package/dist/util/node-operation-parsers.d.ts +91 -0
  189. package/dist/util/node-operation-parsers.js +380 -0
  190. package/dist/util/node-operation-parsers.js.map +1 -0
  191. package/dist/util/runner-native-session-parsers.d.ts +72 -0
  192. package/dist/util/runner-native-session-parsers.js +337 -0
  193. package/dist/util/runner-native-session-parsers.js.map +1 -0
  194. package/dist/util/secret-environment.d.ts +1 -0
  195. package/dist/util/secret-environment.js +26 -0
  196. package/dist/util/secret-environment.js.map +1 -0
  197. package/package.json +2 -2
@@ -1,59 +1,18 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { open, readdir, unlink } from 'node:fs/promises';
2
+ import { readFile, readdir, unlink } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, join, resolve } from 'node:path';
5
+ import { forEachJsonlRecord, listJsonlFiles, mapConcurrent, readJsonlWindow } from './native-jsonl-reader.js';
6
+ import { nodeLog } from './operational.js';
5
7
  import { stripClaudePlanTag } from './runner-command-engine.js';
6
- const DISCOVERY_READ_BYTES = 128 * 1024;
8
+ const DISCOVERY_READ_BYTES = 32 * 1024;
7
9
  const TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
8
10
  const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
9
- const TASK_ACTIVITY_READ_BYTES = 32 * 1024 * 1024;
11
+ const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
10
12
  const transcriptIndex = new Map();
11
- /** Reads the latest durable Codex task lifecycle event without exposing it. */
12
- export async function readCodexNativeTaskActivity(workspacePath, externalSessionId, now = Date.now()) {
13
- const wanted = canonical(workspacePath);
14
- let path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
15
- if (path === undefined) {
16
- await discoverNativeSessions('codex', workspacePath);
17
- path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
18
- }
19
- if (path === undefined)
20
- return undefined;
21
- let content;
22
- try {
23
- const handle = await open(path, 'r');
24
- try {
25
- const metadata = await handle.stat();
26
- const start = Math.max(0, metadata.size - TASK_ACTIVITY_READ_BYTES);
27
- const buffer = Buffer.alloc(metadata.size - start);
28
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
29
- content = buffer.subarray(0, bytesRead).toString('utf8');
30
- }
31
- finally {
32
- await handle.close();
33
- }
34
- }
35
- catch {
36
- return undefined;
37
- }
38
- let latest;
39
- for (const line of content.split('\n')) {
40
- try {
41
- const value = JSON.parse(line);
42
- const payload = value.type === 'event_msg' && isRecord(value.payload) ? value.payload : undefined;
43
- if (payload?.type !== 'task_started' && payload?.type !== 'task_complete')
44
- continue;
45
- const seconds = payload.type === 'task_started' ? payload.started_at : payload.completed_at;
46
- const at = typeof seconds === 'number' ? seconds * 1000 : 0;
47
- latest = { state: payload.type === 'task_started' ? 'ACTIVE' : 'IDLE', at };
48
- }
49
- catch {
50
- // 单条损坏的 JSONL 记录不影响后续活动状态记录。
51
- }
52
- }
53
- if (latest === undefined)
54
- return undefined;
55
- return latest.state === 'ACTIVE' && now - latest.at > 60 * 60_000 ? 'STALE' : latest.state;
56
- }
13
+ const discoveryReads = new Map();
14
+ const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000;
15
+ const CLAUDE_CONTEXT_WINDOW_ENV = 'MAR_CLAUDE_CONTEXT_WINDOW_TOKENS';
57
16
  /**
58
17
  * Read-only adapter for the supported vendor JSONL stores. It requires an
59
18
  * explicit cwd on the record and returns normalized visible messages and tool
@@ -69,8 +28,23 @@ export async function discoverNativeSessions(runner, workspacePath) {
69
28
  * hide older sessions from a project with a large global history.
70
29
  */
71
30
  export async function discoverAllNativeSessions(runner) {
31
+ const current = discoveryReads.get(runner);
32
+ if (current !== undefined)
33
+ return current;
34
+ const discovery = performNativeSessionDiscovery(runner).finally(() => discoveryReads.delete(runner));
35
+ discoveryReads.set(runner, discovery);
36
+ return discovery;
37
+ }
38
+ async function performNativeSessionDiscovery(runner) {
39
+ const startedAt = performance.now();
72
40
  const found = new Map();
73
- const files = await listJsonl(transcriptRoot(runner));
41
+ nodeLog('native.session.discovery.started', { runner });
42
+ const files = await listJsonlFiles(transcriptRoot(runner));
43
+ nodeLog('native.session.discovery.files-listed', {
44
+ runner,
45
+ files: files.length,
46
+ durationMs: Math.round(performance.now() - startedAt)
47
+ });
74
48
  const parsedFiles = await readDiscoveryHeaders(files, runner);
75
49
  for (const [index, parsed] of parsedFiles.entries()) {
76
50
  // Discovery only needs a session identity, cwd and an optional early
@@ -82,24 +56,29 @@ export async function discoverAllNativeSessions(runner) {
82
56
  transcriptIndex.set(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
83
57
  found.set(`${cwd}\u0000${parsed.externalSessionId}`, parsed);
84
58
  }
59
+ nodeLog('native.session.discovery.completed', {
60
+ runner,
61
+ files: files.length,
62
+ sessions: found.size,
63
+ durationMs: Math.round(performance.now() - startedAt)
64
+ });
85
65
  return [...found.values()];
86
66
  }
87
67
  async function readDiscoveryHeaders(files, runner) {
88
- const values = Array.from({ length: files.length });
89
- let next = 0;
90
- const worker = async () => {
91
- while (true) {
92
- const index = next;
93
- next += 1;
94
- if (index >= files.length)
95
- return;
96
- values[index] = await readTranscript(files[index], runner, DISCOVERY_READ_BYTES);
97
- }
98
- };
99
- // Header reads are independent. Bounded concurrency avoids serial startup
100
- // delays without opening an unbounded number of descriptors on large homes.
101
- await Promise.all(Array.from({ length: Math.min(32, files.length) }, worker));
102
- return values;
68
+ let completed = 0;
69
+ const startedAt = performance.now();
70
+ return mapConcurrent(files, 32, async (path) => {
71
+ const value = await readTranscript(path, runner, DISCOVERY_READ_BYTES);
72
+ completed += 1;
73
+ if (completed % 250 === 0)
74
+ nodeLog('native.session.discovery.progress', {
75
+ runner,
76
+ completed,
77
+ files: files.length,
78
+ durationMs: Math.round(performance.now() - startedAt)
79
+ });
80
+ return value;
81
+ });
103
82
  }
104
83
  /** Reads one already-discovered external session again to follow appended JSONL records. */
105
84
  export async function readNativeSession(runner, workspacePath, externalSessionId) {
@@ -131,37 +110,94 @@ export async function readCodexNativeContextUsage(workspacePath, externalSession
131
110
  }
132
111
  if (path === undefined)
133
112
  return undefined;
134
- let content;
113
+ const window = await readJsonlWindow(path, { tailBytes: CONTEXT_USAGE_READ_BYTES });
114
+ if (window === undefined)
115
+ return undefined;
116
+ let usage;
117
+ forEachJsonlRecord(window.content, (record) => {
118
+ const next = codexContextUsage(record);
119
+ if (next !== undefined)
120
+ usage = next;
121
+ });
122
+ return usage;
123
+ }
124
+ /** Reads the latest actual Claude prompt occupancy without starting a CLI/SDK control stream. */
125
+ export async function readClaudeNativeContextUsage(workspacePath, externalSessionId, options = {}) {
126
+ const wanted = canonical(workspacePath);
127
+ let path = transcriptIndex.get(indexKey('claude-code', wanted, externalSessionId));
128
+ path ??= await locateClaudeTranscript(wanted, externalSessionId);
129
+ if (path === undefined) {
130
+ await discoverNativeSessions('claude-code', workspacePath);
131
+ path = transcriptIndex.get(indexKey('claude-code', wanted, externalSessionId));
132
+ }
133
+ if (path === undefined)
134
+ return undefined;
135
+ const latest = await readLatestClaudeContextUsage(path);
136
+ if (latest === undefined)
137
+ return undefined;
138
+ return {
139
+ ...latest,
140
+ maxTokens: await claudeContextWindowTokens(options)
141
+ };
142
+ }
143
+ async function readLatestClaudeContextUsage(path) {
144
+ for (const tailBytes of CLAUDE_CONTEXT_USAGE_READ_BYTES) {
145
+ const window = await readJsonlWindow(path, { tailBytes });
146
+ if (window === undefined)
147
+ return undefined;
148
+ let latest;
149
+ forEachJsonlRecord(window.content, (record) => {
150
+ const usage = claudeContextUsage(record);
151
+ if (usage !== undefined)
152
+ latest = usage;
153
+ });
154
+ if (latest !== undefined)
155
+ return latest;
156
+ if (!window.truncated)
157
+ return undefined;
158
+ }
159
+ return undefined;
160
+ }
161
+ /** Claude main transcripts use the native session ID as their filename. */
162
+ async function locateClaudeTranscript(wantedCwd, externalSessionId) {
163
+ const root = transcriptRoot('claude-code');
164
+ let entries;
135
165
  try {
136
- const handle = await open(path, 'r');
137
- try {
138
- const metadata = await handle.stat();
139
- const start = Math.max(0, metadata.size - CONTEXT_USAGE_READ_BYTES);
140
- const buffer = Buffer.alloc(metadata.size - start);
141
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
142
- content = buffer.subarray(0, bytesRead).toString('utf8');
143
- }
144
- finally {
145
- await handle.close();
146
- }
166
+ entries = await readdir(root, { withFileTypes: true });
147
167
  }
148
168
  catch {
149
169
  return undefined;
150
170
  }
151
- let usage;
152
- for (const line of content.split('\n')) {
153
- let record;
154
- try {
155
- record = JSON.parse(line);
156
- }
157
- catch {
158
- continue;
171
+ const candidates = entries
172
+ .filter((entry) => entry.isDirectory())
173
+ .map((entry) => join(root, entry.name, `${externalSessionId}.jsonl`));
174
+ const parsed = await mapConcurrent(candidates, 16, async (path) => ({
175
+ path,
176
+ history: await readTranscript(path, 'claude-code', DISCOVERY_READ_BYTES)
177
+ }));
178
+ const match = parsed.find(({ history }) => history?.externalSessionId === externalSessionId && canonical(history.cwd) === wantedCwd);
179
+ if (match === undefined)
180
+ return undefined;
181
+ transcriptIndex.set(indexKey('claude-code', wantedCwd, externalSessionId), match.path);
182
+ return match.path;
183
+ }
184
+ export async function claudeContextWindowTokens(options = {}) {
185
+ const environment = options.environment ?? process.env;
186
+ const direct = positiveInteger(environment[CLAUDE_CONTEXT_WINDOW_ENV]);
187
+ if (direct !== undefined)
188
+ return direct;
189
+ try {
190
+ const settings = JSON.parse(await readFile(options.settingsPath ?? join(homedir(), '.claude', 'settings.json'), 'utf8'));
191
+ if (isRecord(settings) && isRecord(settings.env)) {
192
+ const configured = positiveInteger(settings.env[CLAUDE_CONTEXT_WINDOW_ENV]);
193
+ if (configured !== undefined)
194
+ return configured;
159
195
  }
160
- const next = codexContextUsage(record);
161
- if (next !== undefined)
162
- usage = next;
163
196
  }
164
- return usage;
197
+ catch {
198
+ // Missing or malformed optional Claude settings use the stable default.
199
+ }
200
+ return DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS;
165
201
  }
166
202
  /**
167
203
  * Permanently removes exactly one previously discovered transcript. The
@@ -200,78 +236,18 @@ function transcriptRoot(runner) {
200
236
  ? join(homedir(), '.codex', 'sessions')
201
237
  : join(homedir(), '.claude', 'projects');
202
238
  }
203
- async function listJsonl(directory) {
204
- const files = [];
205
- const walk = async (current) => {
206
- let entries;
207
- try {
208
- entries = await readdir(current, { withFileTypes: true });
209
- }
210
- catch {
211
- return;
212
- }
213
- for (const entry of entries) {
214
- const path = join(current, entry.name);
215
- if (entry.isDirectory())
216
- await walk(path);
217
- else if (entry.isFile() && entry.name.endsWith('.jsonl'))
218
- files.push(path);
219
- }
220
- };
221
- await walk(directory);
222
- return files;
223
- }
224
239
  async function readTranscript(path, runner, byteLimit) {
225
- let content;
226
- let truncated = false;
227
- try {
228
- const handle = await open(path, 'r');
229
- try {
230
- const metadata = await handle.stat();
231
- const headSize = Math.min(byteLimit, metadata.size);
232
- const head = Buffer.alloc(headSize);
233
- const { bytesRead: headBytesRead } = await handle.read(head, 0, head.length, 0);
234
- if (headSize === metadata.size) {
235
- content = head.subarray(0, headBytesRead).toString('utf8');
236
- }
237
- else {
238
- // Discovery needs identity from the header and genuine recency from
239
- // the latest visible records. Keep both bounded instead of falling
240
- // back to mtime, whose changes do not necessarily mean a new chat.
241
- const tailSize = Math.min(byteLimit, metadata.size - headSize);
242
- truncated = headSize + tailSize < metadata.size;
243
- const tail = Buffer.alloc(tailSize);
244
- const { bytesRead: tailBytesRead } = await handle.read(tail, 0, tail.length, metadata.size - tailSize);
245
- content = `${head.subarray(0, headBytesRead).toString('utf8')}\n${tail
246
- .subarray(0, tailBytesRead)
247
- .toString('utf8')}`;
248
- }
249
- }
250
- finally {
251
- await handle.close();
252
- }
253
- }
254
- catch {
240
+ const window = await readJsonlWindow(path, { headBytes: byteLimit, tailBytes: byteLimit });
241
+ if (window === undefined)
255
242
  return undefined;
256
- }
243
+ const { content, truncated } = window;
257
244
  let cwd;
258
245
  let id;
259
246
  const items = [];
260
247
  const toolIndexes = new Map();
261
248
  const pendingToolResults = new Map();
262
249
  const metaRecordIds = new Set();
263
- for (const line of content.split('\n')) {
264
- if (line.length === 0)
265
- continue;
266
- let value;
267
- try {
268
- value = JSON.parse(line);
269
- }
270
- catch {
271
- continue;
272
- }
273
- if (!isRecord(value))
274
- continue;
250
+ forEachJsonlRecord(content, (value) => {
275
251
  cwd ??= workspacePath(value);
276
252
  id ??= sessionId(value);
277
253
  const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
@@ -280,8 +256,11 @@ async function readTranscript(path, runner, byteLimit) {
280
256
  metaRecordIds.add(recordId);
281
257
  if (isHiddenTranscriptRecord(value) ||
282
258
  (parentId !== undefined && metaRecordIds.has(parentId))) {
283
- continue;
259
+ return;
284
260
  }
261
+ const generatedImage = codexGeneratedImageEvent(value);
262
+ if (generatedImage !== undefined)
263
+ items.push(generatedImage);
285
264
  for (const item of transcriptItems(value)) {
286
265
  if (item.kind === 'tool_result') {
287
266
  const toolIndex = toolIndexes.get(item.toolUseId);
@@ -301,7 +280,7 @@ async function readTranscript(path, runner, byteLimit) {
301
280
  }
302
281
  items.push(item);
303
282
  }
304
- }
283
+ });
305
284
  for (const result of pendingToolResults.values())
306
285
  items.push({
307
286
  kind: 'tool_call',
@@ -312,6 +291,9 @@ async function readTranscript(path, runner, byteLimit) {
312
291
  inputTruncated: false,
313
292
  outputSummary: result.output,
314
293
  outputTruncated: result.truncated,
294
+ ...(result.imageAttachments === undefined
295
+ ? {}
296
+ : { imageAttachments: result.imageAttachments }),
315
297
  failed: result.failed,
316
298
  createdAt: result.createdAt
317
299
  });
@@ -375,6 +357,37 @@ function codexContextUsage(value) {
375
357
  return undefined;
376
358
  return { usedTokens: promptUsage.input_tokens, maxTokens };
377
359
  }
360
+ function claudeContextUsage(value) {
361
+ if (value.type !== 'assistant' || !isRecord(value.message))
362
+ return undefined;
363
+ const model = value.message.model;
364
+ const usage = value.message.usage;
365
+ if (typeof model !== 'string' ||
366
+ model.length === 0 ||
367
+ model === '<synthetic>' ||
368
+ !isRecord(usage))
369
+ return undefined;
370
+ const inputTokens = nonNegativeInteger(usage.input_tokens);
371
+ const cacheCreationTokens = optionalNonNegativeInteger(usage.cache_creation_input_tokens);
372
+ const cacheReadTokens = optionalNonNegativeInteger(usage.cache_read_input_tokens);
373
+ if (inputTokens === undefined ||
374
+ cacheCreationTokens === undefined ||
375
+ cacheReadTokens === undefined)
376
+ return undefined;
377
+ return { model, usedTokens: inputTokens + cacheCreationTokens + cacheReadTokens };
378
+ }
379
+ function optionalNonNegativeInteger(value) {
380
+ return value === undefined ? 0 : nonNegativeInteger(value);
381
+ }
382
+ function nonNegativeInteger(value) {
383
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
384
+ }
385
+ function positiveInteger(value) {
386
+ if (typeof value !== 'string' || !/^\d+$/.test(value.trim()))
387
+ return undefined;
388
+ const parsed = Number(value);
389
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
390
+ }
378
391
  function transcriptItems(record) {
379
392
  const createdAt = timestampOf(record);
380
393
  const results = [...toolResultsOf(record, createdAt), ...codexToolResultsOf(record, createdAt)];
@@ -479,6 +492,7 @@ function codexToolResultsOf(record, createdAt) {
479
492
  if (toolUseId === undefined)
480
493
  return [];
481
494
  const output = compactValue(codexToolOutput(payload['output'] ?? payload['contentItems']), 10_000);
495
+ const imageAttachments = codexImagePlaceholders(payload['output'] ?? payload['contentItems']);
482
496
  return [
483
497
  {
484
498
  kind: 'tool_result',
@@ -486,6 +500,7 @@ function codexToolResultsOf(record, createdAt) {
486
500
  output: output.text,
487
501
  failed: payload['status'] === 'failed' || payload['error'] !== undefined,
488
502
  truncated: output.truncated,
503
+ ...(imageAttachments.length === 0 ? {} : { imageAttachments }),
489
504
  createdAt
490
505
  }
491
506
  ];
@@ -495,6 +510,31 @@ function codexToolPayload(record) {
495
510
  ? record['payload']
496
511
  : undefined;
497
512
  }
513
+ function codexGeneratedImageEvent(record) {
514
+ if (record['type'] !== 'event_msg' || !isRecord(record['payload']))
515
+ return undefined;
516
+ const payload = record['payload'];
517
+ if (payload['type'] !== 'image_generation_end' ||
518
+ typeof payload['call_id'] !== 'string' ||
519
+ typeof payload['saved_path'] !== 'string')
520
+ return undefined;
521
+ const localPath = payload['saved_path'];
522
+ if (!localPath.includes('/.codex/generated_images/') || !/\.(?:png|jpe?g|webp)$/i.test(localPath))
523
+ return undefined;
524
+ return {
525
+ kind: 'tool_call',
526
+ toolUseId: payload['call_id'],
527
+ toolName: 'imagegen',
528
+ input: null,
529
+ inputSummary: null,
530
+ inputTruncated: false,
531
+ outputSummary: payload['status'] === 'completed' ? '图片生成完成' : '图片生成失败',
532
+ outputTruncated: false,
533
+ imageAttachments: [{ name: basename(localPath), mime: imageMime(localPath), localPath }],
534
+ failed: payload['status'] !== 'completed',
535
+ createdAt: timestampOf(record)
536
+ };
537
+ }
498
538
  /** Extract Codex output text blocks instead of rendering their JSON envelopes. */
499
539
  function codexToolOutput(value) {
500
540
  if (!Array.isArray(value))
@@ -505,15 +545,94 @@ function codexToolOutput(value) {
505
545
  if (!isRecord(block))
506
546
  return [];
507
547
  const direct = firstString(block, ['text', 'content']);
508
- return direct === undefined ? [] : [direct];
548
+ return direct === undefined ? [] : [codexVisibleToolText(direct)];
509
549
  });
510
550
  return text.length > 0 ? text.join('') : value;
511
551
  }
552
+ function codexVisibleToolText(value) {
553
+ const marker = 'data:image/';
554
+ let cursor = 0;
555
+ let visible = '';
556
+ while (cursor < value.length) {
557
+ const start = value.indexOf(marker, cursor);
558
+ if (start < 0)
559
+ return visible + value.slice(cursor);
560
+ visible += `${value.slice(cursor, start)}[图片内容已过滤]`;
561
+ const doubleQuote = value.indexOf('"', start);
562
+ const singleQuote = value.indexOf("'", start);
563
+ const end = doubleQuote < 0
564
+ ? singleQuote
565
+ : singleQuote < 0
566
+ ? doubleQuote
567
+ : Math.min(doubleQuote, singleQuote);
568
+ if (end < 0)
569
+ return visible;
570
+ cursor = end;
571
+ }
572
+ return visible;
573
+ }
574
+ /** Extract only image placeholder metadata from native Codex output. */
575
+ function codexImagePlaceholders(value) {
576
+ const paths = [];
577
+ let visited = 0;
578
+ const visit = (current, depth) => {
579
+ if (paths.length >= 5 || visited >= 128 || depth > 5 || current == null)
580
+ return;
581
+ visited += 1;
582
+ if (typeof current === 'string') {
583
+ collectGeneratedImagePaths(current, paths);
584
+ return;
585
+ }
586
+ if (Array.isArray(current)) {
587
+ for (const entry of current)
588
+ visit(entry, depth + 1);
589
+ return;
590
+ }
591
+ if (!isRecord(current))
592
+ return;
593
+ for (const child of Object.values(current))
594
+ visit(child, depth + 1);
595
+ };
596
+ visit(value, 0);
597
+ return paths.map((localPath) => ({
598
+ name: basename(localPath),
599
+ mime: imageMime(localPath),
600
+ localPath
601
+ }));
602
+ }
603
+ function collectGeneratedImagePaths(value, paths) {
604
+ const marker = '/.codex/generated_images/';
605
+ let cursor = 0;
606
+ while (paths.length < 5) {
607
+ const markerIndex = value.indexOf(marker, cursor);
608
+ if (markerIndex < 0)
609
+ return;
610
+ let start = markerIndex;
611
+ while (start > 0 && !/[\s"']/.test(value[start - 1]))
612
+ start -= 1;
613
+ let end = markerIndex + marker.length;
614
+ while (end < value.length && !/[\s"']/.test(value[end]))
615
+ end += 1;
616
+ const path = value.slice(start, end).replace(/[),.;:]+$/, '');
617
+ if (/\.(?:png|jpe?g|webp)$/i.test(path) && !paths.includes(path))
618
+ paths.push(path);
619
+ cursor = end;
620
+ }
621
+ }
622
+ function imageMime(path) {
623
+ const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
624
+ if (extension === 'jpg' || extension === 'jpeg')
625
+ return 'image/jpeg';
626
+ if (extension === 'webp')
627
+ return 'image/webp';
628
+ return 'image/png';
629
+ }
512
630
  function withToolResult(call, result) {
513
631
  return {
514
632
  ...call,
515
633
  outputSummary: result.output,
516
634
  outputTruncated: result.truncated,
635
+ ...(result.imageAttachments === undefined ? {} : { imageAttachments: result.imageAttachments }),
517
636
  failed: result.failed
518
637
  };
519
638
  }