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