@codedeck/codedeck 2026.3.38 → 2026.3.39
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/dist/agent/session-manager.js.map +1 -1
- package/dist/daemon/codex-watcher.d.ts +8 -35
- package/dist/daemon/codex-watcher.d.ts.map +1 -1
- package/dist/daemon/codex-watcher.js +161 -378
- package/dist/daemon/codex-watcher.js.map +1 -1
- package/dist/daemon/gemini-watcher.d.ts +15 -35
- package/dist/daemon/gemini-watcher.d.ts.map +1 -1
- package/dist/daemon/gemini-watcher.js +105 -305
- package/dist/daemon/gemini-watcher.js.map +1 -1
- package/dist/daemon/subsession-manager.d.ts +1 -12
- package/dist/daemon/subsession-manager.d.ts.map +1 -1
- package/dist/daemon/subsession-manager.js +53 -180
- package/dist/daemon/subsession-manager.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Watches Codex JSONL rollout files for structured events.
|
|
4
|
-
*
|
|
5
|
-
* Codex writes per-session rollout files to:
|
|
6
|
-
* ~/.codex/sessions/YYYY/MM/DD/rollout-YYYY-MM-DDThh-mm-ss-<uuid>.jsonl
|
|
7
|
-
*
|
|
8
|
-
* The first line of each file is a "session_meta" record whose payload.cwd
|
|
9
|
-
* identifies the project directory. We match files to codedeck sessions by
|
|
10
|
-
* comparing payload.cwd to the session's workDir.
|
|
11
|
-
*
|
|
12
|
-
* Events emitted:
|
|
13
|
-
* - user.message ← event_msg { type: "user_message", message: "..." }
|
|
14
|
-
* - assistant.text ← event_msg { type: "agent_message", phase: "final_answer", message: "..." }
|
|
15
|
-
*
|
|
16
|
-
* Integration:
|
|
17
|
-
* - startWatching(sessionName, workDir) when a codex session starts
|
|
18
|
-
* - stopWatching(sessionName) when it stops
|
|
19
4
|
*/
|
|
20
5
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -24,26 +9,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
24
9
|
exports.readCwd = readCwd;
|
|
25
10
|
exports.parseLine = parseLine;
|
|
26
11
|
exports.preClaimFile = preClaimFile;
|
|
12
|
+
exports.isFileClaimedByOther = isFileClaimedByOther;
|
|
13
|
+
exports.extractUuidFromPath = extractUuidFromPath;
|
|
27
14
|
exports.extractNewRolloutUuid = extractNewRolloutUuid;
|
|
28
15
|
exports.findRolloutPathByUuid = findRolloutPathByUuid;
|
|
29
16
|
exports.startWatching = startWatching;
|
|
30
|
-
exports.isWatching = isWatching;
|
|
31
17
|
exports.startWatchingSpecificFile = startWatchingSpecificFile;
|
|
18
|
+
exports.startWatchingById = startWatchingById;
|
|
32
19
|
exports.stopWatching = stopWatching;
|
|
20
|
+
exports.isWatching = isWatching;
|
|
33
21
|
const promises_1 = require("fs/promises");
|
|
34
22
|
const path_1 = require("path");
|
|
35
23
|
const os_1 = require("os");
|
|
36
24
|
const timeline_emitter_js_1 = require("./timeline-emitter.js");
|
|
37
25
|
const logger_js_1 = __importDefault(require("../util/logger.js"));
|
|
38
26
|
// ── Path helpers ───────────────────────────────────────────────────────────────
|
|
39
|
-
/** Return ~/.codex/sessions/YYYY/MM/DD for a given Date. */
|
|
40
27
|
function codexSessionDir(d) {
|
|
41
28
|
const yyyy = d.getUTCFullYear();
|
|
42
29
|
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
43
30
|
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
44
31
|
return (0, path_1.join)((0, os_1.homedir)(), '.codex', 'sessions', String(yyyy), mm, dd);
|
|
45
32
|
}
|
|
46
|
-
/** Return the last 30 days of session dirs (newest first). */
|
|
47
33
|
function recentSessionDirs() {
|
|
48
34
|
const dirs = [];
|
|
49
35
|
for (let i = 0; i < 30; i++) {
|
|
@@ -53,27 +39,17 @@ function recentSessionDirs() {
|
|
|
53
39
|
return dirs;
|
|
54
40
|
}
|
|
55
41
|
// ── JSONL matching ─────────────────────────────────────────────────────────────
|
|
56
|
-
/**
|
|
57
|
-
* Read the first line of a rollout file and return payload.cwd if it's a
|
|
58
|
-
* session_meta record, otherwise null.
|
|
59
|
-
* Exported for testing.
|
|
60
|
-
*/
|
|
61
42
|
async function readCwd(filePath) {
|
|
62
43
|
let fh = null;
|
|
63
44
|
try {
|
|
64
45
|
fh = await (0, promises_1.open)(filePath, 'r');
|
|
65
|
-
// The session_meta first line can be very large (includes full conversation context).
|
|
66
|
-
// Read only the first 4KB — enough to find the "cwd" field which appears early.
|
|
67
|
-
// We extract cwd via regex instead of full JSON.parse to avoid truncation issues.
|
|
68
46
|
const buf = Buffer.allocUnsafe(4096);
|
|
69
47
|
const { bytesRead } = await fh.read(buf, 0, 4096, 0);
|
|
70
48
|
if (bytesRead === 0)
|
|
71
49
|
return null;
|
|
72
50
|
const snippet = buf.subarray(0, bytesRead).toString('utf8');
|
|
73
|
-
// Verify this is a session_meta line
|
|
74
51
|
if (!snippet.includes('"session_meta"'))
|
|
75
52
|
return null;
|
|
76
|
-
// Extract "cwd":"..." value — cwd paths don't contain quotes or backslashes
|
|
77
53
|
const m = /"cwd"\s*:\s*"([^"]+)"/.exec(snippet);
|
|
78
54
|
return m ? m[1] : null;
|
|
79
55
|
}
|
|
@@ -85,10 +61,6 @@ async function readCwd(filePath) {
|
|
|
85
61
|
await fh.close().catch(() => { });
|
|
86
62
|
}
|
|
87
63
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Find the most recent rollout-*.jsonl in dir whose session_meta.cwd matches workDir.
|
|
90
|
-
* Returns the file path, or null if none found.
|
|
91
|
-
*/
|
|
92
64
|
async function findLatestRollout(dir, workDir, excludeClaimed = true) {
|
|
93
65
|
let entries;
|
|
94
66
|
try {
|
|
@@ -97,32 +69,22 @@ async function findLatestRollout(dir, workDir, excludeClaimed = true) {
|
|
|
97
69
|
catch {
|
|
98
70
|
return null;
|
|
99
71
|
}
|
|
100
|
-
const rollouts = entries.filter((e) => e.startsWith('rollout-') && e.endsWith('.jsonl'));
|
|
101
|
-
if (rollouts.length === 0)
|
|
102
|
-
return null;
|
|
103
|
-
// Sort newest first by filename (timestamps embedded in name)
|
|
104
|
-
rollouts.sort((a, b) => b.localeCompare(a));
|
|
72
|
+
const rollouts = entries.filter((e) => e.startsWith('rollout-') && e.endsWith('.jsonl')).sort().reverse();
|
|
105
73
|
for (const name of rollouts) {
|
|
106
74
|
const fpath = (0, path_1.join)(dir, name);
|
|
107
|
-
// Skip if claimed by someone else
|
|
108
75
|
if (excludeClaimed) {
|
|
109
76
|
const owner = claimedFiles.get(fpath);
|
|
110
77
|
if (owner && owner !== 'UNKNOWN')
|
|
111
78
|
continue;
|
|
112
79
|
}
|
|
113
80
|
const cwd = await readCwd(fpath);
|
|
114
|
-
if (cwd && normalizePath(cwd) === normalizePath(workDir))
|
|
81
|
+
if (cwd && normalizePath(cwd) === normalizePath(workDir))
|
|
115
82
|
return fpath;
|
|
116
|
-
}
|
|
117
83
|
}
|
|
118
84
|
return null;
|
|
119
85
|
}
|
|
120
|
-
function normalizePath(p) {
|
|
121
|
-
return p.replace(/\/+$/, '');
|
|
122
|
-
}
|
|
86
|
+
function normalizePath(p) { return p.replace(/\/+$/, ''); }
|
|
123
87
|
// ── JSONL parsing ──────────────────────────────────────────────────────────────
|
|
124
|
-
// Debounce buffers for streaming final_answer events.
|
|
125
|
-
// Codex emits a new final_answer snapshot on every token; we only want the last one.
|
|
126
88
|
const finalAnswerBuffers = new Map();
|
|
127
89
|
const FINAL_ANSWER_DEBOUNCE_MS = 600;
|
|
128
90
|
function flushFinalAnswer(sessionName) {
|
|
@@ -132,7 +94,6 @@ function flushFinalAnswer(sessionName) {
|
|
|
132
94
|
finalAnswerBuffers.delete(sessionName);
|
|
133
95
|
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'assistant.text', { text: buf.text, streaming: false }, { source: 'daemon', confidence: 'high' });
|
|
134
96
|
}
|
|
135
|
-
/** Exported for testing. */
|
|
136
97
|
function parseLine(sessionName, line, model) {
|
|
137
98
|
if (!line.trim())
|
|
138
99
|
return;
|
|
@@ -143,94 +104,67 @@ function parseLine(sessionName, line, model) {
|
|
|
143
104
|
catch {
|
|
144
105
|
return;
|
|
145
106
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const pl = raw['payload'];
|
|
107
|
+
if (raw.type === 'response_item') {
|
|
108
|
+
const pl = raw.payload;
|
|
149
109
|
if (!pl)
|
|
150
110
|
return;
|
|
151
|
-
if (pl
|
|
152
|
-
const name = String(pl
|
|
153
|
-
|
|
154
|
-
let input = argsStr ?? '';
|
|
111
|
+
if (pl.type === 'function_call') {
|
|
112
|
+
const name = String(pl.name ?? 'tool');
|
|
113
|
+
let input = pl.arguments ?? '';
|
|
155
114
|
try {
|
|
156
|
-
const args = JSON.parse(
|
|
157
|
-
|
|
158
|
-
const summary = args['cmd'] ?? args['command'] ?? args['path'] ?? args['query'] ?? args['input'];
|
|
115
|
+
const args = JSON.parse(pl.arguments ?? '{}');
|
|
116
|
+
const summary = args.cmd ?? args.command ?? args.path ?? args.query ?? args.input;
|
|
159
117
|
if (summary !== undefined)
|
|
160
118
|
input = String(summary);
|
|
161
119
|
}
|
|
162
|
-
catch {
|
|
163
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'tool.call', {
|
|
164
|
-
tool: name, ...(input ? { input } : {}),
|
|
165
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
120
|
+
catch { }
|
|
121
|
+
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'tool.call', { tool: name, ...(input ? { input } : {}) }, { source: 'daemon', confidence: 'high' });
|
|
166
122
|
}
|
|
167
|
-
else if (pl
|
|
168
|
-
const errMsg = pl
|
|
123
|
+
else if (pl.type === 'function_call_output') {
|
|
124
|
+
const errMsg = pl.error;
|
|
169
125
|
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'tool.result', { ...(errMsg ? { error: errMsg } : {}) }, { source: 'daemon', confidence: 'high' });
|
|
170
126
|
}
|
|
171
127
|
return;
|
|
172
128
|
}
|
|
173
|
-
if (raw
|
|
129
|
+
if (raw.type !== 'event_msg')
|
|
174
130
|
return;
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
131
|
+
const pl = raw.payload;
|
|
132
|
+
if (!pl)
|
|
177
133
|
return;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const last = info?.['last_token_usage'];
|
|
182
|
-
const ctxWin = info?.['model_context_window'] ?? 1_000_000;
|
|
183
|
-
if (last && typeof last['input_tokens'] === 'number') {
|
|
134
|
+
if (pl.type === 'token_count') {
|
|
135
|
+
const last = pl.info?.last_token_usage;
|
|
136
|
+
if (last && typeof last.input_tokens === 'number') {
|
|
184
137
|
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'usage.update', {
|
|
185
|
-
inputTokens: last
|
|
186
|
-
cacheTokens: last
|
|
187
|
-
contextWindow:
|
|
138
|
+
inputTokens: last.input_tokens,
|
|
139
|
+
cacheTokens: last.cached_input_tokens ?? 0,
|
|
140
|
+
contextWindow: pl.info.model_context_window ?? 1000000,
|
|
188
141
|
...(model ? { model } : {}),
|
|
189
142
|
}, { source: 'daemon', confidence: 'high' });
|
|
190
143
|
}
|
|
191
|
-
return;
|
|
192
144
|
}
|
|
193
|
-
if (
|
|
194
|
-
// Flush any pending assistant text before a new user message
|
|
145
|
+
else if (pl.type === 'user_message') {
|
|
195
146
|
flushFinalAnswer(sessionName);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'user.message', { text }, { source: 'daemon', confidence: 'high' });
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
147
|
+
if (pl.message?.trim())
|
|
148
|
+
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'user.message', { text: pl.message }, { source: 'daemon', confidence: 'high' });
|
|
201
149
|
}
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
const text = payload['message'];
|
|
150
|
+
else if (pl.type === 'agent_message') {
|
|
151
|
+
const text = pl.message;
|
|
205
152
|
if (!text?.trim())
|
|
206
153
|
return;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (evtType === 'agent_message' && payload['phase'] === 'final_answer') {
|
|
219
|
-
const text = payload['message'];
|
|
220
|
-
if (!text?.trim())
|
|
221
|
-
return;
|
|
222
|
-
// Emit immediately as streaming update, debounce the final non-streaming emit
|
|
223
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'assistant.text', { text, streaming: true }, { source: 'daemon', confidence: 'high' });
|
|
224
|
-
// Debounce: buffer the latest snapshot and reset the timer
|
|
225
|
-
const existing = finalAnswerBuffers.get(sessionName);
|
|
226
|
-
if (existing)
|
|
227
|
-
clearTimeout(existing.timer);
|
|
228
|
-
const timer = setTimeout(() => flushFinalAnswer(sessionName), FINAL_ANSWER_DEBOUNCE_MS);
|
|
229
|
-
finalAnswerBuffers.set(sessionName, { text, timer });
|
|
154
|
+
if (pl.phase === 'final_answer') {
|
|
155
|
+
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'assistant.text', { text, streaming: true }, { source: 'daemon', confidence: 'high' });
|
|
156
|
+
const existing = finalAnswerBuffers.get(sessionName);
|
|
157
|
+
if (existing)
|
|
158
|
+
clearTimeout(existing.timer);
|
|
159
|
+
const timer = setTimeout(() => flushFinalAnswer(sessionName), FINAL_ANSWER_DEBOUNCE_MS);
|
|
160
|
+
finalAnswerBuffers.set(sessionName, { text, timer });
|
|
161
|
+
}
|
|
162
|
+
else if (pl.phase === 'commentary') {
|
|
163
|
+
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'assistant.text', { text: `_${text}_`, streaming: true }, { source: 'daemon', confidence: 'high' });
|
|
164
|
+
}
|
|
230
165
|
}
|
|
231
166
|
}
|
|
232
167
|
// ── History replay ─────────────────────────────────────────────────────────────
|
|
233
|
-
const HISTORY_LINES = 200;
|
|
234
168
|
async function emitRecentHistory(sessionName, filePath, model) {
|
|
235
169
|
let fh = null;
|
|
236
170
|
try {
|
|
@@ -241,13 +175,9 @@ async function emitRecentHistory(sessionName, filePath, model) {
|
|
|
241
175
|
const readSize = Math.min(size, 256 * 1024);
|
|
242
176
|
const buf = Buffer.allocUnsafe(readSize);
|
|
243
177
|
const { bytesRead } = await fh.read(buf, 0, readSize, size - readSize);
|
|
244
|
-
if (bytesRead === 0)
|
|
245
|
-
return;
|
|
246
178
|
const chunk = buf.subarray(0, bytesRead).toString('utf8');
|
|
247
179
|
const lines = chunk.split('\n');
|
|
248
|
-
const startIdx = size > readSize ? 1 : 0;
|
|
249
|
-
const historyEvents = [];
|
|
250
|
-
let lastTokenPayload = null;
|
|
180
|
+
const startIdx = size > readSize ? 1 : 0;
|
|
251
181
|
let bytePos = size - readSize;
|
|
252
182
|
for (let i = 0; i < startIdx; i++)
|
|
253
183
|
bytePos += Buffer.byteLength(lines[i], 'utf8') + 1;
|
|
@@ -257,101 +187,10 @@ async function emitRecentHistory(sessionName, filePath, model) {
|
|
|
257
187
|
const line = lines[i];
|
|
258
188
|
if (!line.trim())
|
|
259
189
|
continue;
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
raw = JSON.parse(line);
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
// Tool calls (response_item)
|
|
268
|
-
if (raw['type'] === 'response_item') {
|
|
269
|
-
const pl = raw['payload'];
|
|
270
|
-
if (!pl)
|
|
271
|
-
continue;
|
|
272
|
-
if (pl['type'] === 'function_call') {
|
|
273
|
-
const name = String(pl['name'] ?? 'tool');
|
|
274
|
-
const argsStr = pl['arguments'];
|
|
275
|
-
let input = argsStr ?? '';
|
|
276
|
-
try {
|
|
277
|
-
const args = JSON.parse(argsStr ?? '{}');
|
|
278
|
-
const summary = args['cmd'] ?? args['command'] ?? args['path'] ?? args['query'] ?? args['input'];
|
|
279
|
-
if (summary !== undefined)
|
|
280
|
-
input = String(summary);
|
|
281
|
-
}
|
|
282
|
-
catch { /* keep raw */ }
|
|
283
|
-
historyEvents.push({ type: 'tool_call', name, input, callId: String(pl['call_id'] ?? ''), stableId: `cx:${sessionName}:${lineBytePos}:tc` });
|
|
284
|
-
}
|
|
285
|
-
else if (pl['type'] === 'function_call_output') {
|
|
286
|
-
const output = String(pl['output'] ?? '');
|
|
287
|
-
const errMsg = pl['error'];
|
|
288
|
-
historyEvents.push({ type: 'tool_result', output: output.length > 400 ? output.slice(0, 400) + '…' : output, callId: String(pl['call_id'] ?? ''), stableId: `cx:${sessionName}:${lineBytePos}:tr`, ...(errMsg ? { error: errMsg } : {}) });
|
|
289
|
-
}
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (raw['type'] !== 'event_msg')
|
|
293
|
-
continue;
|
|
294
|
-
const payload = raw['payload'];
|
|
295
|
-
if (!payload)
|
|
296
|
-
continue;
|
|
297
|
-
const evtType = payload['type'];
|
|
298
|
-
if (evtType === 'user_message') {
|
|
299
|
-
const text = payload['message'];
|
|
300
|
-
if (text?.trim())
|
|
301
|
-
historyEvents.push({ type: 'user', text, stableId: `cx:${sessionName}:${lineBytePos}:um` });
|
|
302
|
-
}
|
|
303
|
-
else if (evtType === 'agent_message' && payload['phase'] === 'final_answer') {
|
|
304
|
-
const text = payload['message'];
|
|
305
|
-
if (!text?.trim())
|
|
306
|
-
continue;
|
|
307
|
-
const last = historyEvents[historyEvents.length - 1];
|
|
308
|
-
if (last?.type === 'assistant') {
|
|
309
|
-
last.text = text;
|
|
310
|
-
last.stableId = `cx:${sessionName}:${lineBytePos}:at`; // update to last final_answer position
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
historyEvents.push({ type: 'assistant', text, stableId: `cx:${sessionName}:${lineBytePos}:at` });
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
else if (evtType === 'token_count') {
|
|
317
|
-
const info = payload['info'];
|
|
318
|
-
const last = info?.['last_token_usage'];
|
|
319
|
-
const ctxWin = info?.['model_context_window'] ?? 1_000_000;
|
|
320
|
-
if (last && typeof last['input_tokens'] === 'number') {
|
|
321
|
-
lastTokenPayload = {
|
|
322
|
-
inputTokens: last['input_tokens'],
|
|
323
|
-
cacheTokens: last['cached_input_tokens'] ?? 0,
|
|
324
|
-
contextWindow: ctxWin,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// Emit deduplicated history (most recent HISTORY_LINES events)
|
|
330
|
-
// Stable eventIds ensure duplicate re-emissions across daemon restarts are
|
|
331
|
-
// deduplicated by the browser's mergeEvents.
|
|
332
|
-
const slice = historyEvents.slice(-HISTORY_LINES);
|
|
333
|
-
for (const ev of slice) {
|
|
334
|
-
if (ev.type === 'user') {
|
|
335
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'user.message', { text: ev.text }, { source: 'daemon', confidence: 'high', eventId: ev.stableId });
|
|
336
|
-
}
|
|
337
|
-
else if (ev.type === 'assistant') {
|
|
338
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'assistant.text', { text: ev.text, streaming: false }, { source: 'daemon', confidence: 'high', eventId: ev.stableId });
|
|
339
|
-
}
|
|
340
|
-
else if (ev.type === 'tool_call') {
|
|
341
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'tool.call', { tool: ev.name, ...(ev.input ? { input: ev.input } : {}) }, { source: 'daemon', confidence: 'high', eventId: ev.stableId });
|
|
342
|
-
}
|
|
343
|
-
else if (ev.type === 'tool_result') {
|
|
344
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'tool.result', { ...(ev.error ? { error: ev.error } : {}) }, { source: 'daemon', confidence: 'high', eventId: ev.stableId });
|
|
345
|
-
}
|
|
190
|
+
parseLine(sessionName, line, model); // Simplified for this restoration fix
|
|
346
191
|
}
|
|
347
|
-
// Emit last usage snapshot so the context bar populates on load
|
|
348
|
-
if (lastTokenPayload) {
|
|
349
|
-
timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'usage.update', { ...lastTokenPayload, ...(model ? { model } : {}) }, { source: 'daemon', confidence: 'high' });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
// best-effort
|
|
354
192
|
}
|
|
193
|
+
catch { }
|
|
355
194
|
finally {
|
|
356
195
|
if (fh)
|
|
357
196
|
await fh.close().catch(() => { });
|
|
@@ -359,9 +198,7 @@ async function emitRecentHistory(sessionName, filePath, model) {
|
|
|
359
198
|
}
|
|
360
199
|
const watchers = new Map();
|
|
361
200
|
const claimedFiles = new Map(); // filePath → sessionName
|
|
362
|
-
/** Manually claim a file for a session (prevents directory scan from stealing it). */
|
|
363
201
|
function preClaimFile(sessionName, filePath) {
|
|
364
|
-
// Clear any existing claim by this session
|
|
365
202
|
for (const [fp, sn] of claimedFiles) {
|
|
366
203
|
if (sn === sessionName) {
|
|
367
204
|
claimedFiles.delete(fp);
|
|
@@ -370,15 +207,21 @@ function preClaimFile(sessionName, filePath) {
|
|
|
370
207
|
}
|
|
371
208
|
claimedFiles.set(filePath, sessionName);
|
|
372
209
|
}
|
|
373
|
-
|
|
374
|
-
const
|
|
210
|
+
function isFileClaimedByOther(sessionName, filePath) {
|
|
211
|
+
const owner = claimedFiles.get(filePath);
|
|
212
|
+
return !!(owner && owner !== sessionName && owner !== 'UNKNOWN');
|
|
213
|
+
}
|
|
214
|
+
function extractUuidFromPath(p) {
|
|
215
|
+
const m = /rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(p);
|
|
216
|
+
return m ? m[1] : null;
|
|
217
|
+
}
|
|
375
218
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* Polls every 1s for up to 60s.
|
|
219
|
+
* Wait for a new rollout file to appear for the given workDir after launchTime.
|
|
220
|
+
* Returns the UUID extracted from the filename, or null if not found within timeout.
|
|
379
221
|
*/
|
|
380
|
-
async function extractNewRolloutUuid(workDir,
|
|
381
|
-
|
|
222
|
+
async function extractNewRolloutUuid(workDir, launchTime, timeoutMs = 5000) {
|
|
223
|
+
const deadline = Date.now() + timeoutMs;
|
|
224
|
+
while (Date.now() < deadline) {
|
|
382
225
|
for (const dir of recentSessionDirs()) {
|
|
383
226
|
let entries;
|
|
384
227
|
try {
|
|
@@ -387,15 +230,13 @@ async function extractNewRolloutUuid(workDir, since) {
|
|
|
387
230
|
catch {
|
|
388
231
|
continue;
|
|
389
232
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
const uuidMatch = UUID_RE.exec(filename);
|
|
393
|
-
if (!uuidMatch)
|
|
233
|
+
for (const name of entries) {
|
|
234
|
+
if (!name.startsWith('rollout-') || !name.endsWith('.jsonl'))
|
|
394
235
|
continue;
|
|
395
|
-
const fpath = (0, path_1.join)(dir,
|
|
236
|
+
const fpath = (0, path_1.join)(dir, name);
|
|
396
237
|
try {
|
|
397
238
|
const s = await (0, promises_1.stat)(fpath);
|
|
398
|
-
if (s.mtimeMs
|
|
239
|
+
if (s.mtimeMs < launchTime)
|
|
399
240
|
continue;
|
|
400
241
|
}
|
|
401
242
|
catch {
|
|
@@ -403,18 +244,17 @@ async function extractNewRolloutUuid(workDir, since) {
|
|
|
403
244
|
}
|
|
404
245
|
const cwd = await readCwd(fpath);
|
|
405
246
|
if (cwd && normalizePath(cwd) === normalizePath(workDir)) {
|
|
406
|
-
|
|
247
|
+
const uuid = extractUuidFromPath(fpath);
|
|
248
|
+
if (uuid)
|
|
249
|
+
return uuid;
|
|
407
250
|
}
|
|
408
251
|
}
|
|
409
252
|
}
|
|
410
|
-
await new Promise(
|
|
253
|
+
await new Promise(r => setTimeout(r, 200));
|
|
411
254
|
}
|
|
412
255
|
return null;
|
|
413
256
|
}
|
|
414
|
-
/**
|
|
415
|
-
* Find the full path of a rollout file by UUID, scanning the last 30 days.
|
|
416
|
-
* Returns null if not found.
|
|
417
|
-
*/
|
|
257
|
+
/** Search recent session dirs for the rollout file containing the given UUID. */
|
|
418
258
|
async function findRolloutPathByUuid(uuid) {
|
|
419
259
|
for (const dir of recentSessionDirs()) {
|
|
420
260
|
let entries;
|
|
@@ -424,108 +264,111 @@ async function findRolloutPathByUuid(uuid) {
|
|
|
424
264
|
catch {
|
|
425
265
|
continue;
|
|
426
266
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const uuidMatch = UUID_RE.exec(filename);
|
|
431
|
-
if (uuidMatch && uuidMatch[1] === uuid) {
|
|
432
|
-
return (0, path_1.join)(dir, filename);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
267
|
+
const match = entries.find(e => e.includes(uuid) && e.endsWith('.jsonl'));
|
|
268
|
+
if (match)
|
|
269
|
+
return (0, path_1.join)(dir, match);
|
|
435
270
|
}
|
|
436
271
|
return null;
|
|
437
272
|
}
|
|
438
273
|
// ── Public API ─────────────────────────────────────────────────────────────────
|
|
439
274
|
async function startWatching(sessionName, workDir, model) {
|
|
440
|
-
if (watchers.has(sessionName))
|
|
275
|
+
if (watchers.has(sessionName))
|
|
441
276
|
stopWatching(sessionName);
|
|
442
|
-
}
|
|
443
|
-
const state = {
|
|
444
|
-
workDir,
|
|
445
|
-
activeFile: null,
|
|
446
|
-
fileOffset: 0,
|
|
447
|
-
abort: new AbortController(),
|
|
448
|
-
stopped: false,
|
|
449
|
-
...(model ? { model } : {}),
|
|
450
|
-
};
|
|
277
|
+
const state = { workDir, activeFile: null, fileOffset: 0, abort: new AbortController(), stopped: false, model };
|
|
451
278
|
watchers.set(sessionName, state);
|
|
452
|
-
// Search recent dirs for existing rollout matching workDir
|
|
453
279
|
for (const dir of recentSessionDirs()) {
|
|
454
280
|
const found = await findLatestRollout(dir, workDir);
|
|
455
281
|
if (found) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
await emitRecentHistory(sessionName, found, state.model);
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
state.activeFile = found;
|
|
465
|
-
state.fileOffset = 0;
|
|
466
|
-
claimedFiles.set(found, sessionName);
|
|
467
|
-
}
|
|
282
|
+
const s = await (0, promises_1.stat)(found);
|
|
283
|
+
state.activeFile = found;
|
|
284
|
+
state.fileOffset = s.size;
|
|
285
|
+
claimedFiles.set(found, sessionName);
|
|
286
|
+
await emitRecentHistory(sessionName, found, model);
|
|
468
287
|
break;
|
|
469
288
|
}
|
|
470
289
|
}
|
|
471
|
-
|
|
472
|
-
state.
|
|
473
|
-
void drainNewLines(sessionName, state);
|
|
474
|
-
}, 2000);
|
|
475
|
-
// Watch all recent dirs for new/modified rollout files.
|
|
476
|
-
// Only start a watcher for dirs that exist (or today's dir which Codex may create soon).
|
|
477
|
-
const todayDir = codexSessionDir(new Date());
|
|
478
|
-
for (const dir of recentSessionDirs()) {
|
|
479
|
-
const isToday = dir === todayDir;
|
|
480
|
-
if (!isToday) {
|
|
481
|
-
// Skip non-existent historical dirs to avoid WARN spam
|
|
482
|
-
try {
|
|
483
|
-
await (0, promises_1.stat)(dir);
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
void watchDir(sessionName, state, dir);
|
|
490
|
-
}
|
|
290
|
+
startPoll(sessionName, state);
|
|
291
|
+
void watchDir(sessionName, state, state.workDir || codexSessionDir(new Date()));
|
|
491
292
|
}
|
|
492
|
-
function isWatching(sessionName) {
|
|
493
|
-
return watchers.has(sessionName);
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Watch a specific rollout file directly (used when UUID is already known).
|
|
497
|
-
* The file is expected to already exist.
|
|
498
|
-
*/
|
|
499
293
|
async function startWatchingSpecificFile(sessionName, filePath, model) {
|
|
500
|
-
if (watchers.has(sessionName))
|
|
294
|
+
if (watchers.has(sessionName))
|
|
501
295
|
stopWatching(sessionName);
|
|
502
|
-
|
|
503
|
-
let fileSize = 0;
|
|
296
|
+
let size = 0;
|
|
504
297
|
try {
|
|
505
|
-
|
|
506
|
-
fileSize = s.size;
|
|
507
|
-
}
|
|
508
|
-
catch {
|
|
509
|
-
// file may not exist yet — start from 0
|
|
298
|
+
size = (await (0, promises_1.stat)(filePath)).size;
|
|
510
299
|
}
|
|
300
|
+
catch { }
|
|
511
301
|
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
512
|
-
const state = {
|
|
513
|
-
workDir: dir,
|
|
514
|
-
activeFile: filePath,
|
|
515
|
-
fileOffset: fileSize,
|
|
516
|
-
abort: new AbortController(),
|
|
517
|
-
stopped: false,
|
|
518
|
-
...(model ? { model } : {}),
|
|
519
|
-
};
|
|
302
|
+
const state = { workDir: dir, activeFile: filePath, fileOffset: size, abort: new AbortController(), stopped: false, model };
|
|
520
303
|
watchers.set(sessionName, state);
|
|
521
304
|
claimedFiles.set(filePath, sessionName);
|
|
522
|
-
await emitRecentHistory(sessionName, filePath,
|
|
523
|
-
|
|
305
|
+
await emitRecentHistory(sessionName, filePath, model);
|
|
306
|
+
startPoll(sessionName, state);
|
|
307
|
+
void watchDir(sessionName, state, dir);
|
|
308
|
+
}
|
|
309
|
+
async function startWatchingById(sessionName, uuid, model) {
|
|
310
|
+
if (watchers.has(sessionName))
|
|
311
|
+
stopWatching(sessionName);
|
|
312
|
+
const state = { workDir: '', activeFile: null, fileOffset: 0, abort: new AbortController(), stopped: false, model };
|
|
313
|
+
watchers.set(sessionName, state);
|
|
314
|
+
for (let i = 0; i < 60 && !state.stopped; i++) {
|
|
315
|
+
for (const dir of recentSessionDirs()) {
|
|
316
|
+
try {
|
|
317
|
+
const entries = await (0, promises_1.readdir)(dir);
|
|
318
|
+
const match = entries.find(e => e.includes(uuid));
|
|
319
|
+
if (match) {
|
|
320
|
+
const found = (0, path_1.join)(dir, match);
|
|
321
|
+
state.activeFile = found;
|
|
322
|
+
state.workDir = dir;
|
|
323
|
+
claimedFiles.set(found, sessionName);
|
|
324
|
+
startPoll(sessionName, state);
|
|
325
|
+
void watchDir(sessionName, state, dir);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
}
|
|
331
|
+
await new Promise(r => setTimeout(r, 500));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function startPoll(sessionName, state) {
|
|
524
335
|
state.pollTimer = setInterval(() => {
|
|
525
|
-
void
|
|
336
|
+
void (async () => {
|
|
337
|
+
await drainNewLines(sessionName, state);
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
if (now - (state._lastRotationCheck || 0) > 30000) {
|
|
340
|
+
state._lastRotationCheck = now;
|
|
341
|
+
const uuid = state.activeFile ? extractUuidFromPath(state.activeFile) : null;
|
|
342
|
+
if (uuid) {
|
|
343
|
+
for (const dir of recentSessionDirs()) {
|
|
344
|
+
if (dir === state.workDir)
|
|
345
|
+
continue;
|
|
346
|
+
try {
|
|
347
|
+
const entries = await (0, promises_1.readdir)(dir);
|
|
348
|
+
const match = entries.find(e => e.includes(uuid));
|
|
349
|
+
if (match) {
|
|
350
|
+
const newPath = (0, path_1.join)(dir, match);
|
|
351
|
+
if (await checkNewer(newPath, state.activeFile)) {
|
|
352
|
+
logger_js_1.default.info({ sessionName, new: newPath }, 'codex-watcher: date rotation detected');
|
|
353
|
+
if (state.activeFile)
|
|
354
|
+
claimedFiles.delete(state.activeFile);
|
|
355
|
+
state.activeFile = newPath;
|
|
356
|
+
state.workDir = dir;
|
|
357
|
+
state.fileOffset = 0;
|
|
358
|
+
claimedFiles.set(newPath, sessionName);
|
|
359
|
+
void watchDir(sessionName, state, dir);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
526
371
|
}, 2000);
|
|
527
|
-
// Watch the parent dir for changes to this specific file
|
|
528
|
-
void watchDir(sessionName, state, dir);
|
|
529
372
|
}
|
|
530
373
|
function stopWatching(sessionName) {
|
|
531
374
|
const state = watchers.get(sessionName);
|
|
@@ -536,83 +379,29 @@ function stopWatching(sessionName) {
|
|
|
536
379
|
if (state.pollTimer)
|
|
537
380
|
clearInterval(state.pollTimer);
|
|
538
381
|
watchers.delete(sessionName);
|
|
539
|
-
// Remove claims for this session
|
|
540
382
|
for (const [fp, sn] of claimedFiles) {
|
|
541
383
|
if (sn === sessionName)
|
|
542
384
|
claimedFiles.delete(fp);
|
|
543
385
|
}
|
|
544
|
-
// Flush any buffered final_answer on stop
|
|
545
|
-
flushFinalAnswer(sessionName);
|
|
546
|
-
const buf = finalAnswerBuffers.get(sessionName);
|
|
547
|
-
if (buf) {
|
|
548
|
-
clearTimeout(buf.timer);
|
|
549
|
-
finalAnswerBuffers.delete(sessionName);
|
|
550
|
-
}
|
|
551
386
|
}
|
|
552
|
-
|
|
387
|
+
function isWatching(sessionName) { return watchers.has(sessionName); }
|
|
553
388
|
async function watchDir(sessionName, state, dir) {
|
|
554
|
-
// Wait for dir to exist (Codex may not have created it yet)
|
|
555
|
-
for (let i = 0; i < 60; i++) {
|
|
556
|
-
if (state.stopped)
|
|
557
|
-
return;
|
|
558
|
-
try {
|
|
559
|
-
await (0, promises_1.stat)(dir);
|
|
560
|
-
break;
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (state.stopped)
|
|
567
|
-
return;
|
|
568
389
|
try {
|
|
569
390
|
const watcher = (0, promises_1.watch)(dir, { persistent: false, signal: state.abort.signal });
|
|
570
391
|
for await (const event of watcher) {
|
|
571
392
|
if (state.stopped)
|
|
572
393
|
break;
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
if (!event.filename.startsWith('rollout-') || !event.filename.endsWith('.jsonl'))
|
|
576
|
-
continue;
|
|
577
|
-
const changedFile = (0, path_1.join)(dir, event.filename);
|
|
578
|
-
if (changedFile !== state.activeFile) {
|
|
579
|
-
// New file — check if it matches our workDir and is newer
|
|
580
|
-
const cwd = await readCwd(changedFile);
|
|
581
|
-
if (!cwd || normalizePath(cwd) !== normalizePath(state.workDir))
|
|
582
|
-
continue;
|
|
583
|
-
// Skip if claimed by someone else
|
|
584
|
-
const owner = claimedFiles.get(changedFile);
|
|
585
|
-
if (owner && owner !== sessionName && owner !== 'UNKNOWN')
|
|
586
|
-
continue;
|
|
587
|
-
const isNewer = await checkNewer(changedFile, state.activeFile);
|
|
588
|
-
if (isNewer || !state.activeFile) {
|
|
589
|
-
logger_js_1.default.debug({ sessionName, file: event.filename }, 'codex-watcher: switching to new rollout file');
|
|
590
|
-
// Release old claim
|
|
591
|
-
if (state.activeFile)
|
|
592
|
-
claimedFiles.delete(state.activeFile);
|
|
593
|
-
state.activeFile = changedFile;
|
|
594
|
-
state.fileOffset = 0;
|
|
595
|
-
claimedFiles.set(changedFile, sessionName);
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
await drainNewLines(sessionName, state);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
catch (err) {
|
|
605
|
-
if (!state.stopped) {
|
|
606
|
-
logger_js_1.default.warn({ sessionName, dir, err }, 'codex-watcher: dir watch error');
|
|
394
|
+
if (event.filename?.startsWith('rollout-'))
|
|
395
|
+
await drainNewLines(sessionName, state);
|
|
607
396
|
}
|
|
608
397
|
}
|
|
398
|
+
catch { }
|
|
609
399
|
}
|
|
610
|
-
async function checkNewer(
|
|
611
|
-
if (!
|
|
400
|
+
async function checkNewer(a, b) {
|
|
401
|
+
if (!b)
|
|
612
402
|
return true;
|
|
613
403
|
try {
|
|
614
|
-
|
|
615
|
-
return cs.mtimeMs > curS.mtimeMs;
|
|
404
|
+
return (await (0, promises_1.stat)(a)).mtimeMs > (await (0, promises_1.stat)(b)).mtimeMs;
|
|
616
405
|
}
|
|
617
406
|
catch {
|
|
618
407
|
return false;
|
|
@@ -624,13 +413,11 @@ async function drainNewLines(sessionName, state) {
|
|
|
624
413
|
let fh = null;
|
|
625
414
|
try {
|
|
626
415
|
fh = await (0, promises_1.open)(state.activeFile, 'r');
|
|
627
|
-
const
|
|
628
|
-
if (
|
|
416
|
+
const s = await fh.stat();
|
|
417
|
+
if (s.size <= state.fileOffset)
|
|
629
418
|
return;
|
|
630
|
-
const buf = Buffer.allocUnsafe(
|
|
419
|
+
const buf = Buffer.allocUnsafe(s.size - state.fileOffset);
|
|
631
420
|
const { bytesRead } = await fh.read(buf, 0, buf.length, state.fileOffset);
|
|
632
|
-
if (bytesRead === 0)
|
|
633
|
-
return;
|
|
634
421
|
state.fileOffset += bytesRead;
|
|
635
422
|
const chunk = buf.subarray(0, bytesRead).toString('utf8');
|
|
636
423
|
for (const line of chunk.split('\n')) {
|
|
@@ -639,11 +426,7 @@ async function drainNewLines(sessionName, state) {
|
|
|
639
426
|
parseLine(sessionName, line, state.model);
|
|
640
427
|
}
|
|
641
428
|
}
|
|
642
|
-
catch
|
|
643
|
-
if (!state.stopped) {
|
|
644
|
-
logger_js_1.default.debug({ sessionName, err }, 'codex-watcher: drain error');
|
|
645
|
-
}
|
|
646
|
-
}
|
|
429
|
+
catch { }
|
|
647
430
|
finally {
|
|
648
431
|
if (fh)
|
|
649
432
|
await fh.close().catch(() => { });
|