@btatum5/codex-bridge 0.1.0 → 1.3.3
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 +473 -23
- package/bin/codex-bridge.js +136 -100
- package/bin/phodex.js +8 -0
- package/bin/remodex.js +8 -0
- package/package.json +38 -24
- package/src/bridge.js +622 -0
- package/src/codex-desktop-refresher.js +776 -0
- package/src/codex-transport.js +238 -0
- package/src/daemon-state.js +170 -0
- package/src/desktop-handler.js +407 -0
- package/src/git-handler.js +1267 -0
- package/src/index.js +35 -0
- package/src/macos-launch-agent.js +457 -0
- package/src/notifications-handler.js +95 -0
- package/src/push-notification-completion-dedupe.js +147 -0
- package/src/push-notification-service-client.js +151 -0
- package/src/push-notification-tracker.js +688 -0
- package/src/qr.js +19 -0
- package/src/rollout-live-mirror.js +730 -0
- package/src/rollout-watch.js +853 -0
- package/src/scripts/codex-handoff.applescript +100 -0
- package/src/scripts/codex-refresh.applescript +51 -0
- package/src/secure-device-state.js +430 -0
- package/src/secure-transport.js +738 -0
- package/src/session-state.js +62 -0
- package/src/thread-context-handler.js +80 -0
- package/src/workspace-handler.js +464 -0
- package/server.mjs +0 -290
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
// FILE: rollout-watch.js
|
|
2
|
+
// Purpose: Shared rollout-file lookup/watch helpers for CLI inspection, desktop refresh, and usage fallbacks.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: watchThreadRollout, createThreadRolloutActivityWatcher
|
|
5
|
+
// Depends on: fs, os, path, ./session-state
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { readLastActiveThread } = require("./session-state");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_WATCH_INTERVAL_MS = 1_000;
|
|
13
|
+
const DEFAULT_LOOKUP_TIMEOUT_MS = 5_000;
|
|
14
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 10_000;
|
|
15
|
+
const DEFAULT_TRANSIENT_ERROR_RETRY_LIMIT = 2;
|
|
16
|
+
const DEFAULT_INITIAL_USAGE_SCAN_BYTES = 128 * 1024;
|
|
17
|
+
const DEFAULT_TURN_LOOKUP_SCAN_BYTES = 16 * 1024;
|
|
18
|
+
const DEFAULT_THREAD_LOOKUP_SCAN_BYTES = 512 * 1024;
|
|
19
|
+
const DEFAULT_CONTEXT_READ_SCAN_BYTES = 512 * 1024;
|
|
20
|
+
const DEFAULT_CONTEXT_READ_CANDIDATE_LIMIT = 128;
|
|
21
|
+
const DEFAULT_RECENT_ROLLOUT_CANDIDATE_LIMIT = 24;
|
|
22
|
+
const DEFAULT_RECENT_ROLLOUT_LOOKBACK_MS = 15 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
// Polls one rollout file until it materializes and then reports size growth.
|
|
25
|
+
function createThreadRolloutActivityWatcher({
|
|
26
|
+
threadId,
|
|
27
|
+
turnId = "",
|
|
28
|
+
intervalMs = DEFAULT_WATCH_INTERVAL_MS,
|
|
29
|
+
lookupTimeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS,
|
|
30
|
+
idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
31
|
+
initialUsageScanBytes = DEFAULT_INITIAL_USAGE_SCAN_BYTES,
|
|
32
|
+
now = () => Date.now(),
|
|
33
|
+
fsModule = fs,
|
|
34
|
+
transientErrorRetryLimit = DEFAULT_TRANSIENT_ERROR_RETRY_LIMIT,
|
|
35
|
+
onEvent = () => {},
|
|
36
|
+
onUsage = () => {},
|
|
37
|
+
onIdle = () => {},
|
|
38
|
+
onTimeout = () => {},
|
|
39
|
+
onError = () => {},
|
|
40
|
+
} = {}) {
|
|
41
|
+
const resolvedThreadId = resolveThreadId(threadId);
|
|
42
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
43
|
+
const startedAt = now();
|
|
44
|
+
|
|
45
|
+
let isStopped = false;
|
|
46
|
+
let rolloutPath = null;
|
|
47
|
+
let lastSize = null;
|
|
48
|
+
let lastGrowthAt = startedAt;
|
|
49
|
+
let transientErrorCount = 0;
|
|
50
|
+
let usageScanOffset = 0;
|
|
51
|
+
let partialUsageLine = "";
|
|
52
|
+
let lastUsageSignature = null;
|
|
53
|
+
|
|
54
|
+
const tick = () => {
|
|
55
|
+
if (isStopped) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const currentTime = now();
|
|
61
|
+
|
|
62
|
+
if (!rolloutPath) {
|
|
63
|
+
if (currentTime - startedAt >= lookupTimeoutMs) {
|
|
64
|
+
onTimeout({ threadId: resolvedThreadId });
|
|
65
|
+
stop();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rolloutPath = findRecentRolloutFileForWatch(sessionsRoot, {
|
|
70
|
+
threadId: resolvedThreadId,
|
|
71
|
+
fsModule,
|
|
72
|
+
startedAt,
|
|
73
|
+
turnId,
|
|
74
|
+
});
|
|
75
|
+
if (!rolloutPath) {
|
|
76
|
+
transientErrorCount = 0;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lastSize = readFileSize(rolloutPath, fsModule);
|
|
81
|
+
lastGrowthAt = currentTime;
|
|
82
|
+
transientErrorCount = 0;
|
|
83
|
+
const initialScanStart = Math.max(0, lastSize - initialUsageScanBytes);
|
|
84
|
+
const initialUsageResult = readRolloutUsageChunk({
|
|
85
|
+
filePath: rolloutPath,
|
|
86
|
+
start: initialScanStart,
|
|
87
|
+
endExclusive: lastSize,
|
|
88
|
+
carry: "",
|
|
89
|
+
fsModule,
|
|
90
|
+
skipLeadingPartial: initialScanStart > 0,
|
|
91
|
+
});
|
|
92
|
+
usageScanOffset = lastSize;
|
|
93
|
+
partialUsageLine = initialUsageResult.partialLine;
|
|
94
|
+
emitUsageIfChanged(initialUsageResult.usage, "materialized");
|
|
95
|
+
onEvent({
|
|
96
|
+
reason: "materialized",
|
|
97
|
+
threadId: resolvedThreadId,
|
|
98
|
+
rolloutPath,
|
|
99
|
+
size: lastSize,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const nextSize = readFileSize(rolloutPath, fsModule);
|
|
105
|
+
transientErrorCount = 0;
|
|
106
|
+
if (nextSize > lastSize) {
|
|
107
|
+
lastSize = nextSize;
|
|
108
|
+
lastGrowthAt = currentTime;
|
|
109
|
+
const usageResult = readRolloutUsageChunk({
|
|
110
|
+
filePath: rolloutPath,
|
|
111
|
+
start: usageScanOffset,
|
|
112
|
+
endExclusive: nextSize,
|
|
113
|
+
carry: partialUsageLine,
|
|
114
|
+
fsModule,
|
|
115
|
+
});
|
|
116
|
+
usageScanOffset = nextSize;
|
|
117
|
+
partialUsageLine = usageResult.partialLine;
|
|
118
|
+
emitUsageIfChanged(usageResult.usage, "growth");
|
|
119
|
+
onEvent({
|
|
120
|
+
reason: "growth",
|
|
121
|
+
threadId: resolvedThreadId,
|
|
122
|
+
rolloutPath,
|
|
123
|
+
size: nextSize,
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (currentTime - lastGrowthAt >= idleTimeoutMs) {
|
|
129
|
+
onIdle({
|
|
130
|
+
threadId: resolvedThreadId,
|
|
131
|
+
rolloutPath,
|
|
132
|
+
size: lastSize,
|
|
133
|
+
});
|
|
134
|
+
stop();
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (isRetryableFilesystemError(error) && transientErrorCount < transientErrorRetryLimit) {
|
|
138
|
+
transientErrorCount += 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onError(error);
|
|
143
|
+
stop();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const intervalId = setInterval(tick, intervalMs);
|
|
148
|
+
tick();
|
|
149
|
+
|
|
150
|
+
// Emits only when the rollout produced a newer token-count snapshot.
|
|
151
|
+
function emitUsageIfChanged(usage, reason) {
|
|
152
|
+
if (!usage) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const nextSignature = `${usage.tokensUsed}|${usage.tokenLimit}`;
|
|
157
|
+
if (nextSignature === lastUsageSignature) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lastUsageSignature = nextSignature;
|
|
162
|
+
onUsage({
|
|
163
|
+
reason,
|
|
164
|
+
threadId: resolvedThreadId,
|
|
165
|
+
rolloutPath,
|
|
166
|
+
usage,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stop() {
|
|
171
|
+
if (isStopped) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isStopped = true;
|
|
176
|
+
clearInterval(intervalId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
stop,
|
|
181
|
+
get threadId() {
|
|
182
|
+
return resolvedThreadId;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function watchThreadRollout(threadId = "") {
|
|
188
|
+
const resolvedThreadId = resolveThreadId(threadId);
|
|
189
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
190
|
+
const rolloutPath = findRolloutFileForThread(sessionsRoot, resolvedThreadId);
|
|
191
|
+
|
|
192
|
+
if (!rolloutPath) {
|
|
193
|
+
throw new Error(`No rollout file found for thread ${resolvedThreadId}.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let offset = fs.statSync(rolloutPath).size;
|
|
197
|
+
let partialLine = "";
|
|
198
|
+
|
|
199
|
+
console.log(`[codex-bridge] Watching thread ${resolvedThreadId}`);
|
|
200
|
+
console.log(`[codex-bridge] Rollout file: ${rolloutPath}`);
|
|
201
|
+
console.log("[codex-bridge] Waiting for new persisted events... (Ctrl+C to stop)");
|
|
202
|
+
|
|
203
|
+
const onChange = (current, previous) => {
|
|
204
|
+
if (current.size <= previous.size) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const stream = fs.createReadStream(rolloutPath, {
|
|
209
|
+
start: offset,
|
|
210
|
+
end: current.size - 1,
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
let chunkBuffer = "";
|
|
215
|
+
stream.on("data", (chunk) => {
|
|
216
|
+
chunkBuffer += chunk;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
stream.on("end", () => {
|
|
220
|
+
offset = current.size;
|
|
221
|
+
const combined = partialLine + chunkBuffer;
|
|
222
|
+
const lines = combined.split("\n");
|
|
223
|
+
partialLine = lines.pop() || "";
|
|
224
|
+
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const formatted = formatRolloutLine(line);
|
|
227
|
+
if (formatted) {
|
|
228
|
+
console.log(formatted);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
fs.watchFile(rolloutPath, { interval: 700 }, onChange);
|
|
235
|
+
|
|
236
|
+
const cleanup = () => {
|
|
237
|
+
fs.unwatchFile(rolloutPath, onChange);
|
|
238
|
+
process.exit(0);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
process.on("SIGINT", cleanup);
|
|
242
|
+
process.on("SIGTERM", cleanup);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveThreadId(threadId) {
|
|
246
|
+
if (threadId && typeof threadId === "string") {
|
|
247
|
+
return threadId;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const last = readLastActiveThread();
|
|
251
|
+
if (last?.threadId) {
|
|
252
|
+
return last.threadId;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
throw new Error("No thread id provided and no remembered Codex thread found.");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveSessionsRoot() {
|
|
259
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
260
|
+
return path.join(codexHome, "sessions");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function findRolloutFileForThread(root, threadId, { fsModule = fs } = {}) {
|
|
264
|
+
if (!fsModule.existsSync(root)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const stack = [root];
|
|
269
|
+
|
|
270
|
+
while (stack.length > 0) {
|
|
271
|
+
const current = stack.pop();
|
|
272
|
+
const entries = fsModule.readdirSync(current, { withFileTypes: true });
|
|
273
|
+
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const fullPath = path.join(current, entry.name);
|
|
276
|
+
if (entry.isDirectory()) {
|
|
277
|
+
stack.push(fullPath);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!entry.isFile()) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (entry.name.includes(threadId) && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
286
|
+
return fullPath;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Chooses the rollout file for the active bridge turn, preferring turn_id and then the thread-scoped file.
|
|
295
|
+
function findRecentRolloutFileForWatch(
|
|
296
|
+
root,
|
|
297
|
+
{
|
|
298
|
+
threadId = "",
|
|
299
|
+
turnId = "",
|
|
300
|
+
startedAt = 0,
|
|
301
|
+
fsModule = fs,
|
|
302
|
+
candidateLimit = DEFAULT_RECENT_ROLLOUT_CANDIDATE_LIMIT,
|
|
303
|
+
lookbackMs = DEFAULT_RECENT_ROLLOUT_LOOKBACK_MS,
|
|
304
|
+
turnLookupScanBytes = DEFAULT_TURN_LOOKUP_SCAN_BYTES,
|
|
305
|
+
} = {}
|
|
306
|
+
) {
|
|
307
|
+
const candidates = collectRecentRolloutFiles(root, {
|
|
308
|
+
fsModule,
|
|
309
|
+
candidateLimit,
|
|
310
|
+
modifiedAfterMs: startedAt > 0 ? (startedAt - lookbackMs) : 0,
|
|
311
|
+
});
|
|
312
|
+
if (candidates.length === 0) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (turnId) {
|
|
317
|
+
for (const candidate of candidates) {
|
|
318
|
+
if (rolloutFileContainsTurnId(candidate.filePath, turnId, {
|
|
319
|
+
fsModule,
|
|
320
|
+
scanBytes: turnLookupScanBytes,
|
|
321
|
+
})) {
|
|
322
|
+
return candidate.filePath;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (threadId) {
|
|
328
|
+
const threadScopedRollout = findPreferredRolloutFileForThread(root, candidates, threadId, {
|
|
329
|
+
fsModule,
|
|
330
|
+
});
|
|
331
|
+
if (threadScopedRollout) {
|
|
332
|
+
return threadScopedRollout;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Picks the rollout file tied back to a thread/turn for on-demand reads without crossing into another thread.
|
|
340
|
+
function findRecentRolloutFileForContextRead(
|
|
341
|
+
root,
|
|
342
|
+
{
|
|
343
|
+
threadId = "",
|
|
344
|
+
turnId = "",
|
|
345
|
+
fsModule = fs,
|
|
346
|
+
candidateLimit = DEFAULT_CONTEXT_READ_CANDIDATE_LIMIT,
|
|
347
|
+
lookbackMs = DEFAULT_RECENT_ROLLOUT_LOOKBACK_MS,
|
|
348
|
+
now = () => Date.now(),
|
|
349
|
+
turnLookupScanBytes = DEFAULT_TURN_LOOKUP_SCAN_BYTES,
|
|
350
|
+
threadLookupScanBytes = DEFAULT_THREAD_LOOKUP_SCAN_BYTES,
|
|
351
|
+
} = {}
|
|
352
|
+
) {
|
|
353
|
+
const candidates = collectRecentRolloutFiles(root, {
|
|
354
|
+
fsModule,
|
|
355
|
+
candidateLimit,
|
|
356
|
+
modifiedAfterMs: 0,
|
|
357
|
+
});
|
|
358
|
+
if (candidates.length === 0) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (turnId) {
|
|
363
|
+
for (const candidate of candidates) {
|
|
364
|
+
if (rolloutFileContainsTurnId(candidate.filePath, turnId, {
|
|
365
|
+
fsModule,
|
|
366
|
+
scanBytes: turnLookupScanBytes,
|
|
367
|
+
})) {
|
|
368
|
+
return candidate.filePath;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (threadId) {
|
|
374
|
+
const threadScopedRollout = findPreferredRolloutFileForThread(root, candidates, threadId, {
|
|
375
|
+
fsModule,
|
|
376
|
+
});
|
|
377
|
+
if (threadScopedRollout) {
|
|
378
|
+
return threadScopedRollout;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (const candidate of candidates) {
|
|
382
|
+
if (rolloutFileContainsThreadId(candidate.filePath, threadId, {
|
|
383
|
+
fsModule,
|
|
384
|
+
scanBytes: threadLookupScanBytes,
|
|
385
|
+
})) {
|
|
386
|
+
return candidate.filePath;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Keeps the fast "recent files first" path, but falls back to a full-tree scan
|
|
395
|
+
// so older valid thread rollouts still recover after many newer sessions exist.
|
|
396
|
+
function findPreferredRolloutFileForThread(root, candidates, threadId, { fsModule = fs } = {}) {
|
|
397
|
+
const recentMatch = findMostRecentRolloutFileForThread(candidates, threadId);
|
|
398
|
+
if (recentMatch) {
|
|
399
|
+
return recentMatch;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return findNewestRolloutFileForThread(root, threadId, { fsModule });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Prefers the newest filename-scoped rollout for a thread instead of the first
|
|
406
|
+
// filesystem hit, which can be an older stale session for the same thread.
|
|
407
|
+
function findMostRecentRolloutFileForThread(candidates, threadId) {
|
|
408
|
+
if (!Array.isArray(candidates) || !threadId) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const match = candidates.find(({ filePath }) => path.basename(filePath).includes(threadId));
|
|
413
|
+
return match?.filePath || null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Scans the whole sessions tree only when the recent candidate window missed the
|
|
417
|
+
// thread, still preferring the newest matching rollout instead of the first hit.
|
|
418
|
+
function findNewestRolloutFileForThread(root, threadId, { fsModule = fs } = {}) {
|
|
419
|
+
if (!threadId || !fsModule.existsSync(root)) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const stack = [root];
|
|
424
|
+
let newestMatch = null;
|
|
425
|
+
|
|
426
|
+
while (stack.length > 0) {
|
|
427
|
+
const current = stack.pop();
|
|
428
|
+
const entries = fsModule.readdirSync(current, { withFileTypes: true });
|
|
429
|
+
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
const fullPath = path.join(current, entry.name);
|
|
432
|
+
if (entry.isDirectory()) {
|
|
433
|
+
stack.push(fullPath);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!entry.isFile()
|
|
438
|
+
|| !entry.name.startsWith("rollout-")
|
|
439
|
+
|| !entry.name.endsWith(".jsonl")
|
|
440
|
+
|| !entry.name.includes(threadId)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const stat = fsModule.statSync(fullPath);
|
|
445
|
+
const candidate = {
|
|
446
|
+
filePath: fullPath,
|
|
447
|
+
mtimeMs: stat.mtimeMs,
|
|
448
|
+
};
|
|
449
|
+
if (!newestMatch || compareRolloutCandidates(candidate, newestMatch) < 0) {
|
|
450
|
+
newestMatch = {
|
|
451
|
+
filePath: candidate.filePath,
|
|
452
|
+
mtimeMs: candidate.mtimeMs,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return newestMatch?.filePath || null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function collectRecentRolloutFiles(
|
|
462
|
+
root,
|
|
463
|
+
{
|
|
464
|
+
fsModule = fs,
|
|
465
|
+
candidateLimit = DEFAULT_RECENT_ROLLOUT_CANDIDATE_LIMIT,
|
|
466
|
+
modifiedAfterMs = 0,
|
|
467
|
+
} = {}
|
|
468
|
+
) {
|
|
469
|
+
if (!fsModule.existsSync(root)) {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const stack = [root];
|
|
474
|
+
const candidates = [];
|
|
475
|
+
|
|
476
|
+
while (stack.length > 0) {
|
|
477
|
+
const current = stack.pop();
|
|
478
|
+
const entries = fsModule.readdirSync(current, { withFileTypes: true });
|
|
479
|
+
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
const fullPath = path.join(current, entry.name);
|
|
482
|
+
if (entry.isDirectory()) {
|
|
483
|
+
stack.push(fullPath);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!entry.isFile()
|
|
488
|
+
|| !entry.name.startsWith("rollout-")
|
|
489
|
+
|| !entry.name.endsWith(".jsonl")) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const stat = fsModule.statSync(fullPath);
|
|
494
|
+
if (modifiedAfterMs > 0 && stat.mtimeMs < modifiedAfterMs) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
candidates.push({
|
|
499
|
+
filePath: fullPath,
|
|
500
|
+
mtimeMs: stat.mtimeMs,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
candidates.sort(compareRolloutCandidates);
|
|
506
|
+
return candidates.slice(0, candidateLimit);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function compareRolloutCandidates(lhs, rhs) {
|
|
510
|
+
if (lhs.mtimeMs !== rhs.mtimeMs) {
|
|
511
|
+
return rhs.mtimeMs - lhs.mtimeMs;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const lhsBase = path.basename(lhs.filePath);
|
|
515
|
+
const rhsBase = path.basename(rhs.filePath);
|
|
516
|
+
const baseCompare = rhsBase.localeCompare(lhsBase);
|
|
517
|
+
if (baseCompare !== 0) {
|
|
518
|
+
return baseCompare;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return rhs.filePath.localeCompare(lhs.filePath);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function rolloutFileContainsTurnId(
|
|
525
|
+
filePath,
|
|
526
|
+
turnId,
|
|
527
|
+
{
|
|
528
|
+
fsModule = fs,
|
|
529
|
+
scanBytes = DEFAULT_TURN_LOOKUP_SCAN_BYTES,
|
|
530
|
+
} = {}
|
|
531
|
+
) {
|
|
532
|
+
if (!filePath || !turnId) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const stat = fsModule.statSync(filePath);
|
|
537
|
+
const chunk = readFileSlice(
|
|
538
|
+
filePath,
|
|
539
|
+
0,
|
|
540
|
+
Math.min(stat.size, scanBytes),
|
|
541
|
+
fsModule
|
|
542
|
+
);
|
|
543
|
+
if (!chunk) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return chunk.includes(`"turn_id":"${turnId}"`) || chunk.includes(`"turnId":"${turnId}"`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function rolloutFileContainsThreadId(
|
|
551
|
+
filePath,
|
|
552
|
+
threadId,
|
|
553
|
+
{
|
|
554
|
+
fsModule = fs,
|
|
555
|
+
scanBytes = DEFAULT_THREAD_LOOKUP_SCAN_BYTES,
|
|
556
|
+
} = {}
|
|
557
|
+
) {
|
|
558
|
+
if (!filePath || !threadId) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const stat = fsModule.statSync(filePath);
|
|
563
|
+
const chunk = readFileSlice(
|
|
564
|
+
filePath,
|
|
565
|
+
Math.max(0, stat.size - Math.min(stat.size, scanBytes)),
|
|
566
|
+
stat.size,
|
|
567
|
+
fsModule
|
|
568
|
+
);
|
|
569
|
+
if (!chunk) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
chunk.includes(`"thread_id":"${threadId}"`)
|
|
575
|
+
|| chunk.includes(`"threadId":"${threadId}"`)
|
|
576
|
+
|| chunk.includes(`"conversation_id":"${threadId}"`)
|
|
577
|
+
|| chunk.includes(`"conversationId":"${threadId}"`)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function formatRolloutLine(rawLine) {
|
|
582
|
+
const trimmed = rawLine.trim();
|
|
583
|
+
if (!trimmed) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let parsed = null;
|
|
588
|
+
try {
|
|
589
|
+
parsed = JSON.parse(trimmed);
|
|
590
|
+
} catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const timestamp = formatTimestamp(parsed.timestamp);
|
|
595
|
+
const payload = parsed.payload || {};
|
|
596
|
+
|
|
597
|
+
if (parsed.type === "event_msg") {
|
|
598
|
+
const eventType = payload.type;
|
|
599
|
+
if (eventType === "user_message") {
|
|
600
|
+
return `${timestamp} Phone: ${previewText(payload.message)}`;
|
|
601
|
+
}
|
|
602
|
+
if (eventType === "agent_message") {
|
|
603
|
+
return `${timestamp} Codex: ${previewText(payload.message)}`;
|
|
604
|
+
}
|
|
605
|
+
if (eventType === "task_started") {
|
|
606
|
+
return `${timestamp} Task started`;
|
|
607
|
+
}
|
|
608
|
+
if (eventType === "task_complete") {
|
|
609
|
+
return `${timestamp} Task complete`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Extracts the latest usable context-window numbers from persisted token_count lines.
|
|
617
|
+
function readRolloutUsageChunk({
|
|
618
|
+
filePath,
|
|
619
|
+
start,
|
|
620
|
+
endExclusive,
|
|
621
|
+
carry = "",
|
|
622
|
+
fsModule = fs,
|
|
623
|
+
skipLeadingPartial = false,
|
|
624
|
+
} = {}) {
|
|
625
|
+
if (!filePath || endExclusive <= start) {
|
|
626
|
+
return { partialLine: carry, usage: null };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const chunk = readFileSlice(filePath, start, endExclusive, fsModule);
|
|
630
|
+
if (!chunk) {
|
|
631
|
+
return { partialLine: carry, usage: null };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const combined = `${carry}${chunk}`;
|
|
635
|
+
const lines = combined.split("\n");
|
|
636
|
+
const partialLine = lines.pop() || "";
|
|
637
|
+
|
|
638
|
+
if (skipLeadingPartial && lines.length > 0) {
|
|
639
|
+
lines.shift();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let latestUsage = null;
|
|
643
|
+
for (const line of lines) {
|
|
644
|
+
const usage = extractContextUsageFromRolloutLine(line);
|
|
645
|
+
if (usage) {
|
|
646
|
+
latestUsage = usage;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
partialLine,
|
|
652
|
+
usage: latestUsage,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function readFileSlice(filePath, start, endExclusive, fsModule = fs) {
|
|
657
|
+
const length = Math.max(0, endExclusive - start);
|
|
658
|
+
if (length === 0) {
|
|
659
|
+
return "";
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const fileHandle = fsModule.openSync(filePath, "r");
|
|
663
|
+
try {
|
|
664
|
+
const buffer = Buffer.alloc(length);
|
|
665
|
+
const bytesRead = fsModule.readSync(fileHandle, buffer, 0, length, start);
|
|
666
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
667
|
+
} finally {
|
|
668
|
+
fsModule.closeSync(fileHandle);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function extractContextUsageFromRolloutLine(rawLine) {
|
|
673
|
+
const trimmed = rawLine.trim();
|
|
674
|
+
if (!trimmed) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
let parsed = null;
|
|
679
|
+
try {
|
|
680
|
+
parsed = JSON.parse(trimmed);
|
|
681
|
+
} catch {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (parsed?.type !== "event_msg") {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const payload = parsed.payload;
|
|
690
|
+
if (!payload || typeof payload !== "object" || payload.type !== "token_count") {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return contextUsageFromTokenCountPayload(payload);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function contextUsageFromTokenCountPayload(payload) {
|
|
698
|
+
const info = payload?.info;
|
|
699
|
+
if (!info || typeof info !== "object") {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Prefer the last-turn snapshot over cumulative totals so the UI shows the
|
|
704
|
+
// active context load, not the lifetime token count of the whole session file.
|
|
705
|
+
const usageRoot = info.last_token_usage || info.lastTokenUsage || info.total_token_usage || info.totalTokenUsage;
|
|
706
|
+
const tokenLimit = readPositiveInteger(
|
|
707
|
+
info.model_context_window ?? info.modelContextWindow ?? info.context_window ?? info.contextWindow
|
|
708
|
+
);
|
|
709
|
+
if (!tokenLimit) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const tokensUsed = readPositiveInteger(usageRoot?.total_tokens ?? usageRoot?.totalTokens)
|
|
714
|
+
?? sumPositiveIntegers([
|
|
715
|
+
usageRoot?.input_tokens ?? usageRoot?.inputTokens,
|
|
716
|
+
usageRoot?.output_tokens ?? usageRoot?.outputTokens,
|
|
717
|
+
usageRoot?.reasoning_output_tokens ?? usageRoot?.reasoningOutputTokens,
|
|
718
|
+
]);
|
|
719
|
+
if (tokensUsed == null) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
tokensUsed: Math.min(tokensUsed, tokenLimit),
|
|
725
|
+
tokenLimit,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function readPositiveInteger(value) {
|
|
730
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
731
|
+
return Math.max(0, Math.trunc(value));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (typeof value === "string") {
|
|
735
|
+
const parsed = Number.parseInt(value, 10);
|
|
736
|
+
if (Number.isFinite(parsed)) {
|
|
737
|
+
return Math.max(0, parsed);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function sumPositiveIntegers(values) {
|
|
745
|
+
let total = 0;
|
|
746
|
+
let foundValue = false;
|
|
747
|
+
|
|
748
|
+
for (const value of values) {
|
|
749
|
+
const parsed = readPositiveInteger(value);
|
|
750
|
+
if (parsed == null) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
foundValue = true;
|
|
755
|
+
total += parsed;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return foundValue ? total : null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Reads the newest usable token-count snapshot for a specific thread/turn from recent rollout files.
|
|
762
|
+
function readLatestContextWindowUsage({
|
|
763
|
+
threadId = "",
|
|
764
|
+
turnId = "",
|
|
765
|
+
fsModule = fs,
|
|
766
|
+
scanBytes = DEFAULT_CONTEXT_READ_SCAN_BYTES,
|
|
767
|
+
candidateLimit = DEFAULT_CONTEXT_READ_CANDIDATE_LIMIT,
|
|
768
|
+
lookbackMs = DEFAULT_RECENT_ROLLOUT_LOOKBACK_MS,
|
|
769
|
+
now = () => Date.now(),
|
|
770
|
+
} = {}) {
|
|
771
|
+
const rolloutRoot = resolveSessionsRoot();
|
|
772
|
+
const rolloutPath = findRecentRolloutFileForContextRead(rolloutRoot, {
|
|
773
|
+
threadId,
|
|
774
|
+
turnId,
|
|
775
|
+
fsModule,
|
|
776
|
+
candidateLimit,
|
|
777
|
+
lookbackMs,
|
|
778
|
+
now,
|
|
779
|
+
});
|
|
780
|
+
if (!rolloutPath) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const stat = fsModule.statSync(rolloutPath);
|
|
785
|
+
const boundedStart = Math.max(0, stat.size - Math.min(stat.size, scanBytes));
|
|
786
|
+
let result = readRolloutUsageChunk({
|
|
787
|
+
filePath: rolloutPath,
|
|
788
|
+
start: boundedStart,
|
|
789
|
+
endExclusive: stat.size,
|
|
790
|
+
fsModule,
|
|
791
|
+
skipLeadingPartial: boundedStart > 0,
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
if (!result.usage && boundedStart > 0) {
|
|
795
|
+
result = readRolloutUsageChunk({
|
|
796
|
+
filePath: rolloutPath,
|
|
797
|
+
start: 0,
|
|
798
|
+
endExclusive: stat.size,
|
|
799
|
+
fsModule,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return result.usage
|
|
804
|
+
? {
|
|
805
|
+
rolloutPath,
|
|
806
|
+
usage: result.usage,
|
|
807
|
+
}
|
|
808
|
+
: null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function formatTimestamp(value) {
|
|
812
|
+
if (!value || typeof value !== "string") {
|
|
813
|
+
return "[time?]";
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const date = new Date(value);
|
|
817
|
+
if (Number.isNaN(date.getTime())) {
|
|
818
|
+
return "[time?]";
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return `[${date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}]`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function previewText(value) {
|
|
825
|
+
if (typeof value !== "string") {
|
|
826
|
+
return "";
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
830
|
+
if (normalized.length <= 120) {
|
|
831
|
+
return normalized;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return `${normalized.slice(0, 117)}...`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function readFileSize(filePath, fsModule = fs) {
|
|
838
|
+
return fsModule.statSync(filePath).size;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function isRetryableFilesystemError(error) {
|
|
842
|
+
return ["ENOENT", "EACCES", "EPERM", "EBUSY"].includes(error?.code);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
module.exports = {
|
|
846
|
+
watchThreadRollout,
|
|
847
|
+
createThreadRolloutActivityWatcher,
|
|
848
|
+
contextUsageFromTokenCountPayload,
|
|
849
|
+
readLatestContextWindowUsage,
|
|
850
|
+
resolveSessionsRoot,
|
|
851
|
+
findRolloutFileForThread,
|
|
852
|
+
findRecentRolloutFileForContextRead,
|
|
853
|
+
};
|