@myagentroam/node 0.1.2 → 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 (205) 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 -353
  14. package/dist/connector.js +614 -5936
  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 +281 -157
  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-command-engine.d.ts +9 -0
  52. package/dist/runner-command-engine.js +24 -0
  53. package/dist/runner-command-engine.js.map +1 -1
  54. package/dist/runner-profiles.js +12 -0
  55. package/dist/runner-profiles.js.map +1 -1
  56. package/dist/runner-usage.d.ts +2 -2
  57. package/dist/runner-usage.js +3 -5
  58. package/dist/runner-usage.js.map +1 -1
  59. package/dist/runtime-state.js +6 -5
  60. package/dist/runtime-state.js.map +1 -1
  61. package/dist/service/attachment-domain-state.d.ts +5 -0
  62. package/dist/service/attachment-domain-state.js +6 -0
  63. package/dist/service/attachment-domain-state.js.map +1 -0
  64. package/dist/service/conversation-history-service.d.ts +34 -0
  65. package/dist/service/conversation-history-service.js +159 -0
  66. package/dist/service/conversation-history-service.js.map +1 -0
  67. package/dist/service/external-session-resume-service.d.ts +32 -0
  68. package/dist/service/external-session-resume-service.js +99 -0
  69. package/dist/service/external-session-resume-service.js.map +1 -0
  70. package/dist/service/fake-managed-run-service.d.ts +29 -0
  71. package/dist/service/fake-managed-run-service.js +63 -0
  72. package/dist/service/fake-managed-run-service.js.map +1 -0
  73. package/dist/service/managed-runner-session-service.d.ts +10 -0
  74. package/dist/service/managed-runner-session-service.js +31 -0
  75. package/dist/service/managed-runner-session-service.js.map +1 -0
  76. package/dist/service/native-session-projection-service.d.ts +50 -0
  77. package/dist/service/native-session-projection-service.js +268 -0
  78. package/dist/service/native-session-projection-service.js.map +1 -0
  79. package/dist/service/native-session-watch-service.d.ts +37 -0
  80. package/dist/service/native-session-watch-service.js +151 -0
  81. package/dist/service/native-session-watch-service.js.map +1 -0
  82. package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
  83. package/dist/service/node-connection-lifecycle-service.js +84 -0
  84. package/dist/service/node-connection-lifecycle-service.js.map +1 -0
  85. package/dist/service/node-control-channel.d.ts +32 -0
  86. package/dist/service/node-control-channel.js +84 -0
  87. package/dist/service/node-control-channel.js.map +1 -0
  88. package/dist/service/node-control-message-service.d.ts +28 -0
  89. package/dist/service/node-control-message-service.js +105 -0
  90. package/dist/service/node-control-message-service.js.map +1 -0
  91. package/dist/service/node-request-service.d.ts +14 -0
  92. package/dist/service/node-request-service.js +51 -0
  93. package/dist/service/node-request-service.js.map +1 -0
  94. package/dist/service/node-run-event-bridge.d.ts +38 -0
  95. package/dist/service/node-run-event-bridge.js +80 -0
  96. package/dist/service/node-run-event-bridge.js.map +1 -0
  97. package/dist/service/node-terminal-channel.d.ts +29 -0
  98. package/dist/service/node-terminal-channel.js +176 -0
  99. package/dist/service/node-terminal-channel.js.map +1 -0
  100. package/dist/service/node-workspace-coordinator.d.ts +32 -0
  101. package/dist/service/node-workspace-coordinator.js +81 -0
  102. package/dist/service/node-workspace-coordinator.js.map +1 -0
  103. package/dist/service/run-attachment-service.d.ts +37 -0
  104. package/dist/service/run-attachment-service.js +161 -0
  105. package/dist/service/run-attachment-service.js.map +1 -0
  106. package/dist/service/run-domain-state.d.ts +4 -0
  107. package/dist/service/run-domain-state.js +5 -0
  108. package/dist/service/run-domain-state.js.map +1 -0
  109. package/dist/service/run-event-service.d.ts +31 -0
  110. package/dist/service/run-event-service.js +103 -0
  111. package/dist/service/run-event-service.js.map +1 -0
  112. package/dist/service/run-workbench-service.d.ts +27 -0
  113. package/dist/service/run-workbench-service.js +114 -0
  114. package/dist/service/run-workbench-service.js.map +1 -0
  115. package/dist/service/runner-channel-message-service.d.ts +13 -0
  116. package/dist/service/runner-channel-message-service.js +21 -0
  117. package/dist/service/runner-channel-message-service.js.map +1 -0
  118. package/dist/service/runner-interaction-service.d.ts +40 -0
  119. package/dist/service/runner-interaction-service.js +245 -0
  120. package/dist/service/runner-interaction-service.js.map +1 -0
  121. package/dist/service/runner-service.d.ts +43 -0
  122. package/dist/service/runner-service.js +55 -0
  123. package/dist/service/runner-service.js.map +1 -0
  124. package/dist/service/session-catalog-service.d.ts +30 -0
  125. package/dist/service/session-catalog-service.js +145 -0
  126. package/dist/service/session-catalog-service.js.map +1 -0
  127. package/dist/service/session-command-service.d.ts +23 -0
  128. package/dist/service/session-command-service.js +70 -0
  129. package/dist/service/session-command-service.js.map +1 -0
  130. package/dist/service/session-context-service.d.ts +34 -0
  131. package/dist/service/session-context-service.js +88 -0
  132. package/dist/service/session-context-service.js.map +1 -0
  133. package/dist/service/session-domain-state.d.ts +8 -0
  134. package/dist/service/session-domain-state.js +9 -0
  135. package/dist/service/session-domain-state.js.map +1 -0
  136. package/dist/service/session-identity-service.d.ts +24 -0
  137. package/dist/service/session-identity-service.js +87 -0
  138. package/dist/service/session-identity-service.js.map +1 -0
  139. package/dist/service/session-lifecycle-service.d.ts +31 -0
  140. package/dist/service/session-lifecycle-service.js +236 -0
  141. package/dist/service/session-lifecycle-service.js.map +1 -0
  142. package/dist/service/session-message-service.d.ts +33 -0
  143. package/dist/service/session-message-service.js +158 -0
  144. package/dist/service/session-message-service.js.map +1 -0
  145. package/dist/service/session-presentation-service.d.ts +33 -0
  146. package/dist/service/session-presentation-service.js +37 -0
  147. package/dist/service/session-presentation-service.js.map +1 -0
  148. package/dist/service/session-query-service.d.ts +31 -0
  149. package/dist/service/session-query-service.js +78 -0
  150. package/dist/service/session-query-service.js.map +1 -0
  151. package/dist/service/terminal-relay-state.d.ts +6 -0
  152. package/dist/service/terminal-relay-state.js +4 -0
  153. package/dist/service/terminal-relay-state.js.map +1 -0
  154. package/dist/service/terminal-service.d.ts +12 -0
  155. package/dist/service/terminal-service.js +53 -0
  156. package/dist/service/terminal-service.js.map +1 -0
  157. package/dist/service/workbench-event-service.d.ts +21 -0
  158. package/dist/service/workbench-event-service.js +54 -0
  159. package/dist/service/workbench-event-service.js.map +1 -0
  160. package/dist/service/workbench-manifest-service.d.ts +88 -0
  161. package/dist/service/workbench-manifest-service.js +69 -0
  162. package/dist/service/workbench-manifest-service.js.map +1 -0
  163. package/dist/service/workspace-change-service.d.ts +9 -0
  164. package/dist/service/workspace-change-service.js +86 -0
  165. package/dist/service/workspace-change-service.js.map +1 -0
  166. package/dist/service/workspace-domain-state.d.ts +75 -0
  167. package/dist/service/workspace-domain-state.js +41 -0
  168. package/dist/service/workspace-domain-state.js.map +1 -0
  169. package/dist/service/workspace-file-service.d.ts +14 -0
  170. package/dist/service/workspace-file-service.js +77 -0
  171. package/dist/service/workspace-file-service.js.map +1 -0
  172. package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
  173. package/dist/service/workspace-queue-workbench-service.js +227 -0
  174. package/dist/service/workspace-queue-workbench-service.js.map +1 -0
  175. package/dist/service/workspace-service.d.ts +15 -0
  176. package/dist/service/workspace-service.js +54 -0
  177. package/dist/service/workspace-service.js.map +1 -0
  178. package/dist/service/workspace-session-service.d.ts +23 -0
  179. package/dist/service/workspace-session-service.js +140 -0
  180. package/dist/service/workspace-session-service.js.map +1 -0
  181. package/dist/service/workspace-upload-service.d.ts +37 -0
  182. package/dist/service/workspace-upload-service.js +241 -0
  183. package/dist/service/workspace-upload-service.js.map +1 -0
  184. package/dist/service/workspace-watch-service.d.ts +18 -0
  185. package/dist/service/workspace-watch-service.js +121 -0
  186. package/dist/service/workspace-watch-service.js.map +1 -0
  187. package/dist/service/workspace-workbench-service.d.ts +38 -0
  188. package/dist/service/workspace-workbench-service.js +156 -0
  189. package/dist/service/workspace-workbench-service.js.map +1 -0
  190. package/dist/terminal.d.ts +1 -0
  191. package/dist/terminal.js +1 -1
  192. package/dist/terminal.js.map +1 -1
  193. package/dist/util/node-operation-parsers.d.ts +91 -0
  194. package/dist/util/node-operation-parsers.js +380 -0
  195. package/dist/util/node-operation-parsers.js.map +1 -0
  196. package/dist/util/runner-native-session-parsers.d.ts +72 -0
  197. package/dist/util/runner-native-session-parsers.js +337 -0
  198. package/dist/util/runner-native-session-parsers.js.map +1 -0
  199. package/dist/util/secret-environment.d.ts +1 -0
  200. package/dist/util/secret-environment.js +26 -0
  201. package/dist/util/secret-environment.js.map +1 -0
  202. package/dist/workspace.d.ts +9 -2
  203. package/dist/workspace.js +41 -0
  204. package/dist/workspace.js.map +1 -1
  205. package/package.json +2 -2
@@ -1,58 +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
- const DISCOVERY_READ_BYTES = 128 * 1024;
5
+ import { forEachJsonlRecord, listJsonlFiles, mapConcurrent, readJsonlWindow } from './native-jsonl-reader.js';
6
+ import { nodeLog } from './operational.js';
7
+ import { stripClaudePlanTag } from './runner-command-engine.js';
8
+ const DISCOVERY_READ_BYTES = 32 * 1024;
6
9
  const TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
7
10
  const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
8
- const TASK_ACTIVITY_READ_BYTES = 32 * 1024 * 1024;
11
+ const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
9
12
  const transcriptIndex = new Map();
10
- /** Reads the latest durable Codex task lifecycle event without exposing it. */
11
- export async function readCodexNativeTaskActivity(workspacePath, externalSessionId, now = Date.now()) {
12
- const wanted = canonical(workspacePath);
13
- let path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
14
- if (path === undefined) {
15
- await discoverNativeSessions('codex', workspacePath);
16
- path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
17
- }
18
- if (path === undefined)
19
- return undefined;
20
- let content;
21
- try {
22
- const handle = await open(path, 'r');
23
- try {
24
- const metadata = await handle.stat();
25
- const start = Math.max(0, metadata.size - TASK_ACTIVITY_READ_BYTES);
26
- const buffer = Buffer.alloc(metadata.size - start);
27
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
28
- content = buffer.subarray(0, bytesRead).toString('utf8');
29
- }
30
- finally {
31
- await handle.close();
32
- }
33
- }
34
- catch {
35
- return undefined;
36
- }
37
- let latest;
38
- for (const line of content.split('\n')) {
39
- try {
40
- const value = JSON.parse(line);
41
- const payload = value.type === 'event_msg' && isRecord(value.payload) ? value.payload : undefined;
42
- if (payload?.type !== 'task_started' && payload?.type !== 'task_complete')
43
- continue;
44
- const seconds = payload.type === 'task_started' ? payload.started_at : payload.completed_at;
45
- const at = typeof seconds === 'number' ? seconds * 1000 : 0;
46
- latest = { state: payload.type === 'task_started' ? 'ACTIVE' : 'IDLE', at };
47
- }
48
- catch {
49
- // 单条损坏的 JSONL 记录不影响后续活动状态记录。
50
- }
51
- }
52
- if (latest === undefined)
53
- return undefined;
54
- return latest.state === 'ACTIVE' && now - latest.at > 60 * 60_000 ? 'STALE' : latest.state;
55
- }
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';
56
16
  /**
57
17
  * Read-only adapter for the supported vendor JSONL stores. It requires an
58
18
  * explicit cwd on the record and returns normalized visible messages and tool
@@ -68,8 +28,23 @@ export async function discoverNativeSessions(runner, workspacePath) {
68
28
  * hide older sessions from a project with a large global history.
69
29
  */
70
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();
71
40
  const found = new Map();
72
- 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
+ });
73
48
  const parsedFiles = await readDiscoveryHeaders(files, runner);
74
49
  for (const [index, parsed] of parsedFiles.entries()) {
75
50
  // Discovery only needs a session identity, cwd and an optional early
@@ -81,24 +56,29 @@ export async function discoverAllNativeSessions(runner) {
81
56
  transcriptIndex.set(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
82
57
  found.set(`${cwd}\u0000${parsed.externalSessionId}`, parsed);
83
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
+ });
84
65
  return [...found.values()];
85
66
  }
86
67
  async function readDiscoveryHeaders(files, runner) {
87
- const values = Array.from({ length: files.length });
88
- let next = 0;
89
- const worker = async () => {
90
- while (true) {
91
- const index = next;
92
- next += 1;
93
- if (index >= files.length)
94
- return;
95
- values[index] = await readTranscript(files[index], runner, DISCOVERY_READ_BYTES);
96
- }
97
- };
98
- // Header reads are independent. Bounded concurrency avoids serial startup
99
- // delays without opening an unbounded number of descriptors on large homes.
100
- await Promise.all(Array.from({ length: Math.min(32, files.length) }, worker));
101
- 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
+ });
102
82
  }
103
83
  /** Reads one already-discovered external session again to follow appended JSONL records. */
104
84
  export async function readNativeSession(runner, workspacePath, externalSessionId) {
@@ -130,37 +110,94 @@ export async function readCodexNativeContextUsage(workspacePath, externalSession
130
110
  }
131
111
  if (path === undefined)
132
112
  return undefined;
133
- 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;
134
165
  try {
135
- const handle = await open(path, 'r');
136
- try {
137
- const metadata = await handle.stat();
138
- const start = Math.max(0, metadata.size - CONTEXT_USAGE_READ_BYTES);
139
- const buffer = Buffer.alloc(metadata.size - start);
140
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
141
- content = buffer.subarray(0, bytesRead).toString('utf8');
142
- }
143
- finally {
144
- await handle.close();
145
- }
166
+ entries = await readdir(root, { withFileTypes: true });
146
167
  }
147
168
  catch {
148
169
  return undefined;
149
170
  }
150
- let usage;
151
- for (const line of content.split('\n')) {
152
- let record;
153
- try {
154
- record = JSON.parse(line);
155
- }
156
- catch {
157
- 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;
158
195
  }
159
- const next = codexContextUsage(record);
160
- if (next !== undefined)
161
- usage = next;
162
196
  }
163
- return usage;
197
+ catch {
198
+ // Missing or malformed optional Claude settings use the stable default.
199
+ }
200
+ return DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS;
164
201
  }
165
202
  /**
166
203
  * Permanently removes exactly one previously discovered transcript. The
@@ -199,78 +236,18 @@ function transcriptRoot(runner) {
199
236
  ? join(homedir(), '.codex', 'sessions')
200
237
  : join(homedir(), '.claude', 'projects');
201
238
  }
202
- async function listJsonl(directory) {
203
- const files = [];
204
- const walk = async (current) => {
205
- let entries;
206
- try {
207
- entries = await readdir(current, { withFileTypes: true });
208
- }
209
- catch {
210
- return;
211
- }
212
- for (const entry of entries) {
213
- const path = join(current, entry.name);
214
- if (entry.isDirectory())
215
- await walk(path);
216
- else if (entry.isFile() && entry.name.endsWith('.jsonl'))
217
- files.push(path);
218
- }
219
- };
220
- await walk(directory);
221
- return files;
222
- }
223
239
  async function readTranscript(path, runner, byteLimit) {
224
- let content;
225
- let truncated = false;
226
- try {
227
- const handle = await open(path, 'r');
228
- try {
229
- const metadata = await handle.stat();
230
- const headSize = Math.min(byteLimit, metadata.size);
231
- const head = Buffer.alloc(headSize);
232
- const { bytesRead: headBytesRead } = await handle.read(head, 0, head.length, 0);
233
- if (headSize === metadata.size) {
234
- content = head.subarray(0, headBytesRead).toString('utf8');
235
- }
236
- else {
237
- // Discovery needs identity from the header and genuine recency from
238
- // the latest visible records. Keep both bounded instead of falling
239
- // back to mtime, whose changes do not necessarily mean a new chat.
240
- const tailSize = Math.min(byteLimit, metadata.size - headSize);
241
- truncated = headSize + tailSize < metadata.size;
242
- const tail = Buffer.alloc(tailSize);
243
- const { bytesRead: tailBytesRead } = await handle.read(tail, 0, tail.length, metadata.size - tailSize);
244
- content = `${head.subarray(0, headBytesRead).toString('utf8')}\n${tail
245
- .subarray(0, tailBytesRead)
246
- .toString('utf8')}`;
247
- }
248
- }
249
- finally {
250
- await handle.close();
251
- }
252
- }
253
- catch {
240
+ const window = await readJsonlWindow(path, { headBytes: byteLimit, tailBytes: byteLimit });
241
+ if (window === undefined)
254
242
  return undefined;
255
- }
243
+ const { content, truncated } = window;
256
244
  let cwd;
257
245
  let id;
258
246
  const items = [];
259
247
  const toolIndexes = new Map();
260
248
  const pendingToolResults = new Map();
261
249
  const metaRecordIds = new Set();
262
- for (const line of content.split('\n')) {
263
- if (line.length === 0)
264
- continue;
265
- let value;
266
- try {
267
- value = JSON.parse(line);
268
- }
269
- catch {
270
- continue;
271
- }
272
- if (!isRecord(value))
273
- continue;
250
+ forEachJsonlRecord(content, (value) => {
274
251
  cwd ??= workspacePath(value);
275
252
  id ??= sessionId(value);
276
253
  const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
@@ -279,8 +256,11 @@ async function readTranscript(path, runner, byteLimit) {
279
256
  metaRecordIds.add(recordId);
280
257
  if (isHiddenTranscriptRecord(value) ||
281
258
  (parentId !== undefined && metaRecordIds.has(parentId))) {
282
- continue;
259
+ return;
283
260
  }
261
+ const generatedImage = codexGeneratedImageEvent(value);
262
+ if (generatedImage !== undefined)
263
+ items.push(generatedImage);
284
264
  for (const item of transcriptItems(value)) {
285
265
  if (item.kind === 'tool_result') {
286
266
  const toolIndex = toolIndexes.get(item.toolUseId);
@@ -300,7 +280,7 @@ async function readTranscript(path, runner, byteLimit) {
300
280
  }
301
281
  items.push(item);
302
282
  }
303
- }
283
+ });
304
284
  for (const result of pendingToolResults.values())
305
285
  items.push({
306
286
  kind: 'tool_call',
@@ -311,17 +291,24 @@ async function readTranscript(path, runner, byteLimit) {
311
291
  inputTruncated: false,
312
292
  outputSummary: result.output,
313
293
  outputTruncated: result.truncated,
294
+ ...(result.imageAttachments === undefined
295
+ ? {}
296
+ : { imageAttachments: result.imageAttachments }),
314
297
  failed: result.failed,
315
298
  createdAt: result.createdAt
316
299
  });
317
300
  if (cwd === undefined)
318
301
  return undefined;
319
302
  const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
303
+ // 标签式 Plan Mode 的指令标签不构成会话标题。
304
+ const strippedTitle = title === undefined ? undefined : stripClaudePlanTag(title);
320
305
  return {
321
306
  runner,
322
307
  externalSessionId: id ?? basename(path, '.jsonl'),
323
308
  cwd,
324
- ...(title === undefined ? {} : { title: compact(title) }),
309
+ ...(strippedTitle === undefined || strippedTitle.length === 0
310
+ ? {}
311
+ : { title: compact(strippedTitle) }),
325
312
  digest: createHash('sha256').update(content).digest('hex'),
326
313
  items,
327
314
  lastActivityAt: Math.max(0, ...items.map((item) => item.createdAt ?? 0)),
@@ -370,6 +357,37 @@ function codexContextUsage(value) {
370
357
  return undefined;
371
358
  return { usedTokens: promptUsage.input_tokens, maxTokens };
372
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
+ }
373
391
  function transcriptItems(record) {
374
392
  const createdAt = timestampOf(record);
375
393
  const results = [...toolResultsOf(record, createdAt), ...codexToolResultsOf(record, createdAt)];
@@ -474,6 +492,7 @@ function codexToolResultsOf(record, createdAt) {
474
492
  if (toolUseId === undefined)
475
493
  return [];
476
494
  const output = compactValue(codexToolOutput(payload['output'] ?? payload['contentItems']), 10_000);
495
+ const imageAttachments = codexImagePlaceholders(payload['output'] ?? payload['contentItems']);
477
496
  return [
478
497
  {
479
498
  kind: 'tool_result',
@@ -481,6 +500,7 @@ function codexToolResultsOf(record, createdAt) {
481
500
  output: output.text,
482
501
  failed: payload['status'] === 'failed' || payload['error'] !== undefined,
483
502
  truncated: output.truncated,
503
+ ...(imageAttachments.length === 0 ? {} : { imageAttachments }),
484
504
  createdAt
485
505
  }
486
506
  ];
@@ -490,6 +510,31 @@ function codexToolPayload(record) {
490
510
  ? record['payload']
491
511
  : undefined;
492
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
+ }
493
538
  /** Extract Codex output text blocks instead of rendering their JSON envelopes. */
494
539
  function codexToolOutput(value) {
495
540
  if (!Array.isArray(value))
@@ -500,15 +545,94 @@ function codexToolOutput(value) {
500
545
  if (!isRecord(block))
501
546
  return [];
502
547
  const direct = firstString(block, ['text', 'content']);
503
- return direct === undefined ? [] : [direct];
548
+ return direct === undefined ? [] : [codexVisibleToolText(direct)];
504
549
  });
505
550
  return text.length > 0 ? text.join('') : value;
506
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
+ }
507
630
  function withToolResult(call, result) {
508
631
  return {
509
632
  ...call,
510
633
  outputSummary: result.output,
511
634
  outputTruncated: result.truncated,
635
+ ...(result.imageAttachments === undefined ? {} : { imageAttachments: result.imageAttachments }),
512
636
  failed: result.failed
513
637
  };
514
638
  }