context-carwash 0.2.0 → 0.2.1
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/CHANGELOG.md +7 -0
- package/README.md +21 -6
- package/package.json +3 -2
- package/src/cli.mjs +20 -8
- package/src/install/codex.mjs +256 -0
- package/src/install/doctor.mjs +52 -11
- package/src/install/install.mjs +91 -31
- package/src/paths.mjs +13 -2
- package/src/skill/codex/SKILL.md +47 -0
- package/src/skill/codex/agents/openai.yaml +3 -0
- package/src/stats/ledger.mjs +70 -9
- package/src/stats/report.mjs +10 -3
- package/src/tools.mjs +40 -0
- package/src/wash/brief.mjs +56 -17
- package/src/wash/hooks.mjs +138 -55
- package/src/wash/process.mjs +111 -0
- package/src/wash/rollout.mjs +111 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// Codex keeps each session in a "rollout" file: one JSON object per line. Codex
|
|
4
|
+
// says the format is not a stable interface, so everything here reads it
|
|
5
|
+
// defensively and anything unexpected is simply not counted.
|
|
6
|
+
//
|
|
7
|
+
// After each request to the model Codex writes a token_count event, whose
|
|
8
|
+
// last_token_usage.input_tokens is everything that request sent: the context
|
|
9
|
+
// in use at that point. The status line gives Claude Code's equivalent.
|
|
10
|
+
|
|
11
|
+
// Only the end of a long session file is read for the latest reading.
|
|
12
|
+
const TAIL_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
// { used, size } from one rollout line, or null if it is not a token reading.
|
|
15
|
+
export function tokenReading(line) {
|
|
16
|
+
const payload = line?.type === 'event_msg' ? line.payload : null;
|
|
17
|
+
if (payload?.type !== 'token_count') return null;
|
|
18
|
+
const used = payload.info?.last_token_usage?.input_tokens;
|
|
19
|
+
if (!Number.isFinite(used) || used <= 0) return null;
|
|
20
|
+
const size = payload.info.model_context_window;
|
|
21
|
+
return { used, size: Number.isFinite(size) && size > 0 ? size : null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The session file opens with a session_meta line, which can be long: it holds
|
|
25
|
+
// the model's base instructions.
|
|
26
|
+
const META_BYTES = 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
// The version of Codex that wrote a session file (its cli_version), or null.
|
|
29
|
+
export function readCodexVersion(file) {
|
|
30
|
+
if (typeof file !== 'string' || !file) return null;
|
|
31
|
+
let fd;
|
|
32
|
+
try {
|
|
33
|
+
fd = openSync(file, 'r');
|
|
34
|
+
const buffer = Buffer.alloc(Math.min(fstatSync(fd).size, META_BYTES));
|
|
35
|
+
readSync(fd, buffer, 0, buffer.length, 0);
|
|
36
|
+
const first = buffer.toString('utf8').split('\n', 1)[0];
|
|
37
|
+
const line = JSON.parse(first);
|
|
38
|
+
const version = line?.type === 'session_meta' ? line.payload?.cli_version : null;
|
|
39
|
+
return typeof version === 'string' ? version : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
} finally {
|
|
43
|
+
if (fd !== undefined) closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Whether a rollout line is a finished reply from the model.
|
|
48
|
+
function isReply(line) {
|
|
49
|
+
const payload = line?.payload;
|
|
50
|
+
if (line?.type === 'event_msg') return payload?.type === 'item_completed' && payload.item?.type === 'AgentMessage';
|
|
51
|
+
return line?.type === 'response_item' && payload?.type === 'message' && payload.role === 'assistant';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The latest token reading and whether it came after the latest reply, or null.
|
|
55
|
+
function latestReading(file, tailBytes) {
|
|
56
|
+
if (typeof file !== 'string' || !file) return null;
|
|
57
|
+
let fd;
|
|
58
|
+
try {
|
|
59
|
+
fd = openSync(file, 'r');
|
|
60
|
+
const { size } = fstatSync(fd);
|
|
61
|
+
const length = Math.min(size, tailBytes);
|
|
62
|
+
const buffer = Buffer.alloc(length);
|
|
63
|
+
readSync(fd, buffer, 0, length, size - length);
|
|
64
|
+
const lines = buffer.toString('utf8').split('\n');
|
|
65
|
+
let afterReply = true;
|
|
66
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
67
|
+
const text = lines[i];
|
|
68
|
+
const maybeReading = text.includes('token_count');
|
|
69
|
+
if (!maybeReading && !(afterReply && (text.includes('AgentMessage') || text.includes('"assistant"')))) continue;
|
|
70
|
+
let line;
|
|
71
|
+
try {
|
|
72
|
+
line = JSON.parse(text);
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const reading = maybeReading ? tokenReading(line) : null;
|
|
77
|
+
if (reading) return { reading, afterReply };
|
|
78
|
+
if (isReply(line)) afterReply = false;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
} finally {
|
|
84
|
+
if (fd !== undefined) closeSync(fd);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// How much context a Codex session is using, from its latest token reading, or
|
|
89
|
+
// null when it has none yet (or the file cannot be read).
|
|
90
|
+
export function readCodexUsage(file, { tailBytes = TAIL_BYTES } = {}) {
|
|
91
|
+
return latestReading(file, tailBytes)?.reading ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The reading for the reply that just ended, for the meter Codex shows after
|
|
95
|
+
// it. Codex writes that reply's token_count a few milliseconds before its Stop
|
|
96
|
+
// hook runs, close enough that the file may not have it yet. Until a reading
|
|
97
|
+
// follows the reply, it is read again, briefly; after that the older reading
|
|
98
|
+
// is used, which is only one request behind.
|
|
99
|
+
const SETTLE_MS = 250;
|
|
100
|
+
const RETRY_MS = 25;
|
|
101
|
+
|
|
102
|
+
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
103
|
+
|
|
104
|
+
export function readCodexReplyUsage(file, { tailBytes = TAIL_BYTES, settleMs = SETTLE_MS, wait = pause } = {}) {
|
|
105
|
+
let latest = latestReading(file, tailBytes);
|
|
106
|
+
for (let waited = 0; latest && !latest.afterReply && waited < settleMs; waited += RETRY_MS) {
|
|
107
|
+
wait(RETRY_MS);
|
|
108
|
+
latest = latestReading(file, tailBytes);
|
|
109
|
+
}
|
|
110
|
+
return latest?.reading ?? null;
|
|
111
|
+
}
|