@myagentroam/node 0.1.0
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.
- package/README.md +17 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +101 -0
- package/dist/claude-agent-sdk.js +341 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/claude-channel.d.ts +28 -0
- package/dist/claude-channel.js +45 -0
- package/dist/claude-channel.js.map +1 -0
- package/dist/codex-app-server.d.ts +96 -0
- package/dist/codex-app-server.js +361 -0
- package/dist/codex-app-server.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -0
- package/dist/connector.d.ts +343 -0
- package/dist/connector.js +5525 -0
- package/dist/connector.js.map +1 -0
- package/dist/database.d.ts +282 -0
- package/dist/database.js +1347 -0
- package/dist/database.js.map +1 -0
- package/dist/event-buffer.d.ts +12 -0
- package/dist/event-buffer.js +29 -0
- package/dist/event-buffer.js.map +1 -0
- package/dist/fake-runner.d.ts +42 -0
- package/dist/fake-runner.js +130 -0
- package/dist/fake-runner.js.map +1 -0
- package/dist/health.d.ts +4 -0
- package/dist/health.js +4 -0
- package/dist/health.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +47 -0
- package/dist/main.js.map +1 -0
- package/dist/native-session-history.d.ts +72 -0
- package/dist/native-session-history.js +647 -0
- package/dist/native-session-history.js.map +1 -0
- package/dist/operational.d.ts +30 -0
- package/dist/operational.js +82 -0
- package/dist/operational.js.map +1 -0
- package/dist/process-tree.d.ts +9 -0
- package/dist/process-tree.js +19 -0
- package/dist/process-tree.js.map +1 -0
- package/dist/runner-command-engine.d.ts +19 -0
- package/dist/runner-command-engine.js +47 -0
- package/dist/runner-command-engine.js.map +1 -0
- package/dist/runner-profiles.d.ts +11 -0
- package/dist/runner-profiles.js +138 -0
- package/dist/runner-profiles.js.map +1 -0
- package/dist/runner-usage.d.ts +33 -0
- package/dist/runner-usage.js +193 -0
- package/dist/runner-usage.js.map +1 -0
- package/dist/runtime-state.d.ts +174 -0
- package/dist/runtime-state.js +957 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/service.d.ts +4 -0
- package/dist/service.js +39 -0
- package/dist/service.js.map +1 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +33 -0
- package/dist/storage.js.map +1 -0
- package/dist/terminal.d.ts +132 -0
- package/dist/terminal.js +417 -0
- package/dist/terminal.js.map +1 -0
- package/dist/workspace.d.ts +116 -0
- package/dist/workspace.js +732 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { open, readdir, unlink } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, join, resolve } from 'node:path';
|
|
5
|
+
const DISCOVERY_READ_BYTES = 128 * 1024;
|
|
6
|
+
const TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
|
|
8
|
+
const TASK_ACTIVITY_READ_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
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
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read-only adapter for the supported vendor JSONL stores. It requires an
|
|
58
|
+
* explicit cwd on the record and returns normalized visible messages and tool
|
|
59
|
+
* cards; no raw vendor events or hidden reasoning are exposed to the Workbench.
|
|
60
|
+
*/
|
|
61
|
+
export async function discoverNativeSessions(runner, workspacePath) {
|
|
62
|
+
const wanted = canonical(workspacePath);
|
|
63
|
+
return (await discoverAllNativeSessions(runner)).filter((value) => canonical(value.cwd) === wanted);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads every supported transcript header before applying a Workspace filter.
|
|
67
|
+
* The caller owns in-process caching; a fixed file-count cutoff would silently
|
|
68
|
+
* hide older sessions from a project with a large global history.
|
|
69
|
+
*/
|
|
70
|
+
export async function discoverAllNativeSessions(runner) {
|
|
71
|
+
const found = new Map();
|
|
72
|
+
const files = await listJsonl(transcriptRoot(runner));
|
|
73
|
+
const parsedFiles = await readDiscoveryHeaders(files, runner);
|
|
74
|
+
for (const [index, parsed] of parsedFiles.entries()) {
|
|
75
|
+
// Discovery only needs a session identity, cwd and an optional early
|
|
76
|
+
// title. Loading entire JSONL files here made opening a Workspace depend
|
|
77
|
+
// on the total history of every other project on the Node.
|
|
78
|
+
if (parsed === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
const cwd = canonical(parsed.cwd);
|
|
81
|
+
transcriptIndex.set(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
|
|
82
|
+
found.set(`${cwd}\u0000${parsed.externalSessionId}`, parsed);
|
|
83
|
+
}
|
|
84
|
+
return [...found.values()];
|
|
85
|
+
}
|
|
86
|
+
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;
|
|
102
|
+
}
|
|
103
|
+
/** Reads one already-discovered external session again to follow appended JSONL records. */
|
|
104
|
+
export async function readNativeSession(runner, workspacePath, externalSessionId) {
|
|
105
|
+
const wanted = canonical(workspacePath);
|
|
106
|
+
const path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
|
|
107
|
+
if (path === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
const parsed = await readTranscript(path, runner, TRANSCRIPT_READ_BYTES);
|
|
110
|
+
if (parsed === undefined ||
|
|
111
|
+
parsed.externalSessionId !== externalSessionId ||
|
|
112
|
+
canonical(parsed.cwd) !== wanted) {
|
|
113
|
+
transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Codex records the most recent prompt token count alongside the model context
|
|
120
|
+
* window in `event_msg/token_count`. Read only the tail of an already
|
|
121
|
+
* discovered transcript: the count is append-only and never needs a costly
|
|
122
|
+
* full-history scan.
|
|
123
|
+
*/
|
|
124
|
+
export async function readCodexNativeContextUsage(workspacePath, externalSessionId) {
|
|
125
|
+
const wanted = canonical(workspacePath);
|
|
126
|
+
let path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
|
|
127
|
+
if (path === undefined) {
|
|
128
|
+
await discoverNativeSessions('codex', workspacePath);
|
|
129
|
+
path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
|
|
130
|
+
}
|
|
131
|
+
if (path === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
let content;
|
|
134
|
+
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
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
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;
|
|
158
|
+
}
|
|
159
|
+
const next = codexContextUsage(record);
|
|
160
|
+
if (next !== undefined)
|
|
161
|
+
usage = next;
|
|
162
|
+
}
|
|
163
|
+
return usage;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Permanently removes exactly one previously discovered transcript. The
|
|
167
|
+
* caller supplies no path: the in-process discovery index resolves it, then
|
|
168
|
+
* the record is re-read and its runner/cwd/native ID are checked again before
|
|
169
|
+
* unlinking. This keeps a destructive operation bounded to one known file.
|
|
170
|
+
*/
|
|
171
|
+
export async function removeNativeSession(runner, workspacePath, externalSessionId) {
|
|
172
|
+
const wanted = canonical(workspacePath);
|
|
173
|
+
let path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
|
|
174
|
+
if (path === undefined) {
|
|
175
|
+
await discoverNativeSessions(runner, workspacePath);
|
|
176
|
+
path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
|
|
177
|
+
}
|
|
178
|
+
if (path === undefined)
|
|
179
|
+
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
180
|
+
const parsed = await readTranscript(path, runner, TRANSCRIPT_READ_BYTES);
|
|
181
|
+
if (parsed === undefined ||
|
|
182
|
+
parsed.externalSessionId !== externalSessionId ||
|
|
183
|
+
canonical(parsed.cwd) !== wanted) {
|
|
184
|
+
transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
|
|
185
|
+
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await unlink(path);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error.code === 'ENOENT')
|
|
192
|
+
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
|
|
196
|
+
}
|
|
197
|
+
function transcriptRoot(runner) {
|
|
198
|
+
return runner === 'codex'
|
|
199
|
+
? join(homedir(), '.codex', 'sessions')
|
|
200
|
+
: join(homedir(), '.claude', 'projects');
|
|
201
|
+
}
|
|
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
|
+
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 {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
let cwd;
|
|
257
|
+
let id;
|
|
258
|
+
const items = [];
|
|
259
|
+
const toolIndexes = new Map();
|
|
260
|
+
const pendingToolResults = new Map();
|
|
261
|
+
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;
|
|
274
|
+
cwd ??= workspacePath(value);
|
|
275
|
+
id ??= sessionId(value);
|
|
276
|
+
const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
|
|
277
|
+
const parentId = typeof value['parentUuid'] === 'string' ? value['parentUuid'] : undefined;
|
|
278
|
+
if (value['isMeta'] === true && recordId !== undefined)
|
|
279
|
+
metaRecordIds.add(recordId);
|
|
280
|
+
if (isHiddenTranscriptRecord(value) ||
|
|
281
|
+
(parentId !== undefined && metaRecordIds.has(parentId))) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for (const item of transcriptItems(value)) {
|
|
285
|
+
if (item.kind === 'tool_result') {
|
|
286
|
+
const toolIndex = toolIndexes.get(item.toolUseId);
|
|
287
|
+
if (toolIndex === undefined)
|
|
288
|
+
pendingToolResults.set(item.toolUseId, item);
|
|
289
|
+
else
|
|
290
|
+
items[toolIndex] = withToolResult(items[toolIndex], item);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (item.kind === 'tool_call') {
|
|
294
|
+
const pending = pendingToolResults.get(item.toolUseId);
|
|
295
|
+
const completed = pending === undefined ? item : withToolResult(item, pending);
|
|
296
|
+
pendingToolResults.delete(item.toolUseId);
|
|
297
|
+
toolIndexes.set(item.toolUseId, items.length);
|
|
298
|
+
items.push(completed);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
items.push(item);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const result of pendingToolResults.values())
|
|
305
|
+
items.push({
|
|
306
|
+
kind: 'tool_call',
|
|
307
|
+
toolUseId: result.toolUseId,
|
|
308
|
+
toolName: '工具',
|
|
309
|
+
input: null,
|
|
310
|
+
inputSummary: null,
|
|
311
|
+
inputTruncated: false,
|
|
312
|
+
outputSummary: result.output,
|
|
313
|
+
outputTruncated: result.truncated,
|
|
314
|
+
failed: result.failed,
|
|
315
|
+
createdAt: result.createdAt
|
|
316
|
+
});
|
|
317
|
+
if (cwd === undefined)
|
|
318
|
+
return undefined;
|
|
319
|
+
const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
|
|
320
|
+
return {
|
|
321
|
+
runner,
|
|
322
|
+
externalSessionId: id ?? basename(path, '.jsonl'),
|
|
323
|
+
cwd,
|
|
324
|
+
...(title === undefined ? {} : { title: compact(title) }),
|
|
325
|
+
digest: createHash('sha256').update(content).digest('hex'),
|
|
326
|
+
items,
|
|
327
|
+
lastActivityAt: Math.max(0, ...items.map((item) => item.createdAt ?? 0)),
|
|
328
|
+
truncated
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function workspacePath(record) {
|
|
332
|
+
return (firstString(record, ['cwd', 'working_directory', 'workspacePath']) ??
|
|
333
|
+
nestedString(record, ['payload', 'cwd']) ??
|
|
334
|
+
nestedString(record, ['message', 'cwd']));
|
|
335
|
+
}
|
|
336
|
+
function sessionId(record) {
|
|
337
|
+
// A Codex sub-agent transcript records its own thread in `payload.id`, but
|
|
338
|
+
// retains the parent thread in `payload.session_id`. Treating the latter
|
|
339
|
+
// as this file's identity makes the child file overwrite the parent thread
|
|
340
|
+
// in the discovery index, which then shows unrelated messages for the
|
|
341
|
+
// parent Chat.
|
|
342
|
+
if (record['type'] === 'session_meta') {
|
|
343
|
+
const transcriptId = nestedString(record, ['payload', 'id']);
|
|
344
|
+
if (transcriptId !== undefined)
|
|
345
|
+
return transcriptId;
|
|
346
|
+
}
|
|
347
|
+
return (firstString(record, ['sessionId', 'session_id', 'thread_id', 'conversationId']) ??
|
|
348
|
+
nestedString(record, ['payload', 'id']) ??
|
|
349
|
+
nestedString(record, ['payload', 'session_id']) ??
|
|
350
|
+
nestedString(record, ['payload', 'thread_id']));
|
|
351
|
+
}
|
|
352
|
+
function codexContextUsage(value) {
|
|
353
|
+
if (!isRecord(value) || value.type !== 'event_msg' || !isRecord(value.payload))
|
|
354
|
+
return undefined;
|
|
355
|
+
const payload = value.payload;
|
|
356
|
+
if (payload.type !== 'token_count' || !isRecord(payload.info))
|
|
357
|
+
return undefined;
|
|
358
|
+
const promptUsage = payload.info.last_token_usage;
|
|
359
|
+
const maxTokens = payload.info.model_context_window;
|
|
360
|
+
if (!isRecord(promptUsage) ||
|
|
361
|
+
typeof promptUsage.input_tokens !== 'number' ||
|
|
362
|
+
typeof maxTokens !== 'number')
|
|
363
|
+
return undefined;
|
|
364
|
+
if (!Number.isFinite(promptUsage.input_tokens) ||
|
|
365
|
+
!Number.isInteger(promptUsage.input_tokens) ||
|
|
366
|
+
!Number.isFinite(maxTokens) ||
|
|
367
|
+
!Number.isInteger(maxTokens) ||
|
|
368
|
+
maxTokens <= 0 ||
|
|
369
|
+
promptUsage.input_tokens < 0)
|
|
370
|
+
return undefined;
|
|
371
|
+
return { usedTokens: promptUsage.input_tokens, maxTokens };
|
|
372
|
+
}
|
|
373
|
+
function transcriptItems(record) {
|
|
374
|
+
const createdAt = timestampOf(record);
|
|
375
|
+
const results = [...toolResultsOf(record, createdAt), ...codexToolResultsOf(record, createdAt)];
|
|
376
|
+
if (results.length > 0)
|
|
377
|
+
return results;
|
|
378
|
+
const values = [
|
|
379
|
+
...toolUsesOf(record, createdAt),
|
|
380
|
+
...codexToolUsesOf(record, createdAt)
|
|
381
|
+
];
|
|
382
|
+
const role = roleOf(record);
|
|
383
|
+
if (role === undefined)
|
|
384
|
+
return values;
|
|
385
|
+
const text = messageText(textOf(record) ?? '');
|
|
386
|
+
if (text.length === 0)
|
|
387
|
+
return values;
|
|
388
|
+
// Codex transcripts can contain the host's injected AGENTS.md and runtime
|
|
389
|
+
// context as a `user` item before the first actual prompt. It is neither a
|
|
390
|
+
// user-visible chat turn nor a meaningful session title.
|
|
391
|
+
if (role === 'USER' && isInjectedContextPrompt(text))
|
|
392
|
+
return values;
|
|
393
|
+
values.unshift({ kind: 'message', role, text, createdAt });
|
|
394
|
+
return values;
|
|
395
|
+
}
|
|
396
|
+
function toolUsesOf(record, createdAt) {
|
|
397
|
+
return contentBlocks(record).flatMap((block) => {
|
|
398
|
+
if (block['type'] !== 'tool_use' ||
|
|
399
|
+
typeof block['id'] !== 'string' ||
|
|
400
|
+
typeof block['name'] !== 'string')
|
|
401
|
+
return [];
|
|
402
|
+
const input = block['input'];
|
|
403
|
+
const summary = compactValue(input, 10_000);
|
|
404
|
+
return [
|
|
405
|
+
{
|
|
406
|
+
kind: 'tool_call',
|
|
407
|
+
toolUseId: block['id'],
|
|
408
|
+
toolName: compact(block['name']).slice(0, 128),
|
|
409
|
+
input,
|
|
410
|
+
inputSummary: summary.text || null,
|
|
411
|
+
inputTruncated: summary.truncated,
|
|
412
|
+
outputSummary: null,
|
|
413
|
+
outputTruncated: false,
|
|
414
|
+
failed: false,
|
|
415
|
+
createdAt
|
|
416
|
+
}
|
|
417
|
+
];
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function toolResultsOf(record, createdAt) {
|
|
421
|
+
return contentBlocks(record).flatMap((block) => {
|
|
422
|
+
if (block['type'] !== 'tool_result' || typeof block['tool_use_id'] !== 'string')
|
|
423
|
+
return [];
|
|
424
|
+
const output = compactValue(block['content'], 10_000);
|
|
425
|
+
return [
|
|
426
|
+
{
|
|
427
|
+
kind: 'tool_result',
|
|
428
|
+
toolUseId: block['tool_use_id'],
|
|
429
|
+
output: output.text,
|
|
430
|
+
failed: block['is_error'] === true,
|
|
431
|
+
truncated: output.truncated,
|
|
432
|
+
createdAt
|
|
433
|
+
}
|
|
434
|
+
];
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Codex persists custom and standard function tools as paired `response_item`
|
|
439
|
+
* records. Their record IDs differ, while `call_id` is the stable identifier
|
|
440
|
+
* shared by invocation and result. Normalize on that key so JSONL fallback
|
|
441
|
+
* history has the same single-card shape as live events.
|
|
442
|
+
*/
|
|
443
|
+
function codexToolUsesOf(record, createdAt) {
|
|
444
|
+
const payload = codexToolPayload(record);
|
|
445
|
+
if (payload?.['type'] !== 'custom_tool_call' && payload?.['type'] !== 'function_call')
|
|
446
|
+
return [];
|
|
447
|
+
const toolUseId = firstString(payload, ['call_id', 'id']);
|
|
448
|
+
const toolName = firstString(payload, ['name', 'tool']);
|
|
449
|
+
if (toolUseId === undefined || toolName === undefined)
|
|
450
|
+
return [];
|
|
451
|
+
const input = payload['input'] ?? payload['arguments'];
|
|
452
|
+
const summary = compactValue(input, 10_000);
|
|
453
|
+
return [
|
|
454
|
+
{
|
|
455
|
+
kind: 'tool_call',
|
|
456
|
+
toolUseId,
|
|
457
|
+
toolName: compact(toolName).slice(0, 128),
|
|
458
|
+
input,
|
|
459
|
+
inputSummary: summary.text || null,
|
|
460
|
+
inputTruncated: summary.truncated,
|
|
461
|
+
outputSummary: null,
|
|
462
|
+
outputTruncated: false,
|
|
463
|
+
failed: false,
|
|
464
|
+
createdAt
|
|
465
|
+
}
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
function codexToolResultsOf(record, createdAt) {
|
|
469
|
+
const payload = codexToolPayload(record);
|
|
470
|
+
if (payload?.['type'] !== 'custom_tool_call_output' &&
|
|
471
|
+
payload?.['type'] !== 'function_call_output')
|
|
472
|
+
return [];
|
|
473
|
+
const toolUseId = firstString(payload, ['call_id', 'id']);
|
|
474
|
+
if (toolUseId === undefined)
|
|
475
|
+
return [];
|
|
476
|
+
const output = compactValue(codexToolOutput(payload['output'] ?? payload['contentItems']), 10_000);
|
|
477
|
+
return [
|
|
478
|
+
{
|
|
479
|
+
kind: 'tool_result',
|
|
480
|
+
toolUseId,
|
|
481
|
+
output: output.text,
|
|
482
|
+
failed: payload['status'] === 'failed' || payload['error'] !== undefined,
|
|
483
|
+
truncated: output.truncated,
|
|
484
|
+
createdAt
|
|
485
|
+
}
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
function codexToolPayload(record) {
|
|
489
|
+
return record['type'] === 'response_item' && isRecord(record['payload'])
|
|
490
|
+
? record['payload']
|
|
491
|
+
: undefined;
|
|
492
|
+
}
|
|
493
|
+
/** Extract Codex output text blocks instead of rendering their JSON envelopes. */
|
|
494
|
+
function codexToolOutput(value) {
|
|
495
|
+
if (!Array.isArray(value))
|
|
496
|
+
return value;
|
|
497
|
+
const text = value.flatMap((block) => {
|
|
498
|
+
if (typeof block === 'string')
|
|
499
|
+
return [block];
|
|
500
|
+
if (!isRecord(block))
|
|
501
|
+
return [];
|
|
502
|
+
const direct = firstString(block, ['text', 'content']);
|
|
503
|
+
return direct === undefined ? [] : [direct];
|
|
504
|
+
});
|
|
505
|
+
return text.length > 0 ? text.join('') : value;
|
|
506
|
+
}
|
|
507
|
+
function withToolResult(call, result) {
|
|
508
|
+
return {
|
|
509
|
+
...call,
|
|
510
|
+
outputSummary: result.output,
|
|
511
|
+
outputTruncated: result.truncated,
|
|
512
|
+
failed: result.failed
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function isInjectedContextPrompt(text) {
|
|
516
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
517
|
+
if (normalized.startsWith('<codex_internal_context'))
|
|
518
|
+
return true;
|
|
519
|
+
return (normalized.startsWith('# AGENTS.md instructions') &&
|
|
520
|
+
(normalized.includes('<INSTRUCTIONS>') || normalized.includes('<environment_context>')));
|
|
521
|
+
}
|
|
522
|
+
function isHiddenTranscriptRecord(record) {
|
|
523
|
+
if (contentBlocks(record).some((block) => block['type'] === 'tool_result'))
|
|
524
|
+
return false;
|
|
525
|
+
if (record['interruptedByShutdown'] === true ||
|
|
526
|
+
record['isMeta'] === true ||
|
|
527
|
+
record['isVisibleInTranscriptOnly'] === true ||
|
|
528
|
+
record['isCompactSummary'] === true)
|
|
529
|
+
return true;
|
|
530
|
+
const text = textOf(record)?.trim() ?? '';
|
|
531
|
+
return (text.startsWith('<local-command-') ||
|
|
532
|
+
text.startsWith('<command-name>') ||
|
|
533
|
+
text.startsWith('<command-message>'));
|
|
534
|
+
}
|
|
535
|
+
function roleOf(record) {
|
|
536
|
+
const candidates = [
|
|
537
|
+
firstString(record, ['role']),
|
|
538
|
+
nestedString(record, ['message', 'role']),
|
|
539
|
+
nestedString(record, ['payload', 'role']),
|
|
540
|
+
firstString(record, ['type']),
|
|
541
|
+
nestedString(record, ['payload', 'type'])
|
|
542
|
+
];
|
|
543
|
+
if (candidates.some((value) => value === 'user' || value === 'human' || value === 'input'))
|
|
544
|
+
return 'USER';
|
|
545
|
+
if (candidates.some((value) => value === 'assistant' || value === 'agent' || value === 'output'))
|
|
546
|
+
return 'ASSISTANT';
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
function textOf(record) {
|
|
550
|
+
return (firstString(record, ['content', 'text']) ??
|
|
551
|
+
nestedText(record['message']) ??
|
|
552
|
+
nestedText(record['payload']));
|
|
553
|
+
}
|
|
554
|
+
function nestedText(value) {
|
|
555
|
+
if (typeof value === 'string')
|
|
556
|
+
return value;
|
|
557
|
+
if (!isRecord(value))
|
|
558
|
+
return undefined;
|
|
559
|
+
const direct = firstString(value, ['content', 'text', 'message']);
|
|
560
|
+
if (direct !== undefined)
|
|
561
|
+
return direct;
|
|
562
|
+
if (Array.isArray(value['content'])) {
|
|
563
|
+
const parts = value['content'].flatMap((part) => {
|
|
564
|
+
if (typeof part === 'string')
|
|
565
|
+
return [part];
|
|
566
|
+
if (!isRecord(part))
|
|
567
|
+
return [];
|
|
568
|
+
return [firstString(part, ['text', 'content'])].filter((text) => text !== undefined);
|
|
569
|
+
});
|
|
570
|
+
return parts.join('\n');
|
|
571
|
+
}
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
function contentBlocks(record) {
|
|
575
|
+
const values = [record['message'], record['payload']];
|
|
576
|
+
for (const value of values)
|
|
577
|
+
if (isRecord(value) && Array.isArray(value['content']))
|
|
578
|
+
return value['content'].filter(isRecord);
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
function compactValue(value, limit) {
|
|
582
|
+
if (value === undefined || value === null)
|
|
583
|
+
return { text: '', truncated: false };
|
|
584
|
+
let text;
|
|
585
|
+
if (typeof value === 'string')
|
|
586
|
+
text = value;
|
|
587
|
+
else {
|
|
588
|
+
try {
|
|
589
|
+
text = JSON.stringify(value);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return { text: '(无法显示)', truncated: false };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
text: text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 1))}…`,
|
|
597
|
+
truncated: text.length > limit
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function timestampOf(record) {
|
|
601
|
+
const raw = record['timestamp'] ?? record['created_at'] ?? record['createdAt'];
|
|
602
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
603
|
+
return raw < 10_000_000_000 ? raw * 1_000 : raw;
|
|
604
|
+
if (typeof raw === 'string') {
|
|
605
|
+
const value = Date.parse(raw);
|
|
606
|
+
return Number.isNaN(value) ? null : value;
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
function firstString(value, keys) {
|
|
611
|
+
for (const key of keys)
|
|
612
|
+
if (typeof value[key] === 'string' && value[key].length > 0)
|
|
613
|
+
return value[key];
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
function nestedString(value, keys) {
|
|
617
|
+
let current = value;
|
|
618
|
+
for (const key of keys) {
|
|
619
|
+
if (!isRecord(current))
|
|
620
|
+
return undefined;
|
|
621
|
+
current = current[key];
|
|
622
|
+
}
|
|
623
|
+
return typeof current === 'string' && current.length > 0 ? current : undefined;
|
|
624
|
+
}
|
|
625
|
+
function compact(value) {
|
|
626
|
+
return value.replace(/\s+/g, ' ').trim().slice(0, 16_000);
|
|
627
|
+
}
|
|
628
|
+
/** 对话正文可能是 Markdown,不能像标题一样压缩内部空白。 */
|
|
629
|
+
function messageText(value) {
|
|
630
|
+
return value.trim().slice(0, 16_000);
|
|
631
|
+
}
|
|
632
|
+
function canonical(value) {
|
|
633
|
+
try {
|
|
634
|
+
const normalized = resolve(value).replace(/[\\/]+$/, '');
|
|
635
|
+
return process.platform === 'win32' ? normalized.toLocaleLowerCase() : normalized;
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
return value;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function indexKey(runner, cwd, externalSessionId) {
|
|
642
|
+
return `${runner}\u0000${cwd}\u0000${externalSessionId}`;
|
|
643
|
+
}
|
|
644
|
+
function isRecord(value) {
|
|
645
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
646
|
+
}
|
|
647
|
+
//# sourceMappingURL=native-session-history.js.map
|