throughline 0.4.5 → 0.4.6
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 +9 -0
- package/README.ja.md +2 -0
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/codex-usage.mjs +26 -6
- package/src/codex-usage.test.mjs +83 -0
- package/src/token-monitor.mjs +4 -1
- package/src/token-monitor.test.mjs +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.6] — 2026-05-09
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Codex monitor usage now overlays transient `output_tokens` while a Codex turn
|
|
18
|
+
is open. During an in-flight turn the row displays `input_tokens +
|
|
19
|
+
output_tokens` and marks the model with `live+<tokens>`; after `task_complete`
|
|
20
|
+
the row drops back to verified `input_tokens` only.
|
|
21
|
+
|
|
13
22
|
## [0.4.5] — 2026-05-09
|
|
14
23
|
|
|
15
24
|
### Fixed
|
package/README.ja.md
CHANGED
|
@@ -216,6 +216,8 @@ throughline monitor --session <id-prefix>
|
|
|
216
216
|
監視中は Claude transcript / Codex rollout をライブに読み、Stop hook の state
|
|
217
217
|
snapshot はライブ usage が取れない場合の控えとして使います。これにより表示更新は
|
|
218
218
|
Stop 完了待ちではなくなります。
|
|
219
|
+
Codex は open turn 中だけ `input_tokens + output_tokens` を表示し、モデル欄に
|
|
220
|
+
`live+<tokens>` を付けます。`task_complete` 後は verified `input_tokens` のみに戻ります。
|
|
219
221
|
|
|
220
222
|
詳細仕様 (resize 追従、1M context 検出、ステイル隠し、Stop hook の非同期化など) は
|
|
221
223
|
[英語版 README](README.md#multi-session-token-monitor) を参照してください。
|
package/README.md
CHANGED
|
@@ -519,10 +519,12 @@ Example output:
|
|
|
519
519
|
- **Codex token counts use the rollout `token_count` event when present.** The
|
|
520
520
|
Codex Stop hook writes `codex:<thread_id>` monitor state with the rollout
|
|
521
521
|
path. While the monitor is running it reads the live rollout every tick and
|
|
522
|
-
prefers the latest verified `token_count` sample.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
522
|
+
prefers the latest verified `token_count` sample. During an open Codex turn,
|
|
523
|
+
the monitor overlays transient `output_tokens` on top of `input_tokens` and
|
|
524
|
+
marks the model with `live+<tokens>`; when `task_complete` arrives it drops
|
|
525
|
+
back to verified `input_tokens` only. If a Codex rollout has no token-count
|
|
526
|
+
event, Throughline can show an explicit estimate with `estimated: true` and
|
|
527
|
+
the monitor marks it with `est`; it is not presented as exact usage.
|
|
526
528
|
- **Codex auto-refresh mutates at the verified 90% threshold.** The Codex Stop
|
|
527
529
|
hook captures DB memory, writes monitor state, and when verified usage reaches
|
|
528
530
|
the threshold it attempts rollback + Throughline DB memory injection for the
|
package/package.json
CHANGED
package/src/codex-usage.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export function readLatestCodexUsage(rolloutPath) {
|
|
|
18
18
|
let latest = null;
|
|
19
19
|
let model = 'codex';
|
|
20
20
|
let provider = null;
|
|
21
|
+
let openTaskCount = 0;
|
|
21
22
|
for (const line of raw.split('\n')) {
|
|
22
23
|
if (!line.trim()) continue;
|
|
23
24
|
let row;
|
|
@@ -39,24 +40,43 @@ export function readLatestCodexUsage(rolloutPath) {
|
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
if (row?.type !== 'event_msg'
|
|
43
|
+
if (row?.type !== 'event_msg') continue;
|
|
44
|
+
|
|
45
|
+
if (payload?.type === 'task_started') {
|
|
46
|
+
openTaskCount++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (payload?.type === 'task_complete') {
|
|
51
|
+
openTaskCount = Math.max(0, openTaskCount - 1);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (payload?.type !== 'token_count') continue;
|
|
43
56
|
const info = payload.info ?? {};
|
|
44
57
|
const last = info.last_token_usage ?? {};
|
|
45
|
-
const
|
|
46
|
-
if (!Number.isFinite(
|
|
58
|
+
const inputTokens = Number(last.input_tokens);
|
|
59
|
+
if (!Number.isFinite(inputTokens) || inputTokens < 0) continue;
|
|
60
|
+
const outputTokens = Number.isFinite(Number(last.output_tokens)) ? Number(last.output_tokens) : 0;
|
|
61
|
+
const transientOutputTokens = openTaskCount > 0 ? outputTokens : 0;
|
|
47
62
|
|
|
48
63
|
const windowSize = Number(info.model_context_window);
|
|
49
64
|
latest = {
|
|
50
|
-
tokens,
|
|
65
|
+
tokens: inputTokens + transientOutputTokens,
|
|
66
|
+
inputTokens,
|
|
51
67
|
model: model === 'codex' && provider ? provider : model,
|
|
52
68
|
contextWindowSize:
|
|
53
69
|
Number.isFinite(windowSize) && windowSize > 0
|
|
54
70
|
? windowSize
|
|
55
71
|
: DEFAULT_CODEX_CONTEXT_WINDOW_SIZE,
|
|
56
72
|
contextWindowEstimated: !(Number.isFinite(windowSize) && windowSize > 0),
|
|
57
|
-
outputTokens
|
|
73
|
+
outputTokens,
|
|
74
|
+
transientOutputTokens,
|
|
75
|
+
liveTurn: openTaskCount > 0,
|
|
58
76
|
estimated: false,
|
|
59
|
-
source:
|
|
77
|
+
source: openTaskCount > 0
|
|
78
|
+
? 'codex-rollout-token-count-live-turn'
|
|
79
|
+
: 'codex-rollout-token-count',
|
|
60
80
|
};
|
|
61
81
|
}
|
|
62
82
|
|
package/src/codex-usage.test.mjs
CHANGED
|
@@ -44,10 +44,13 @@ test('readLatestCodexUsage: reads verified Codex token_count event shape', () =>
|
|
|
44
44
|
|
|
45
45
|
assert.deepEqual(readLatestCodexUsage(rollout), {
|
|
46
46
|
tokens: 151914,
|
|
47
|
+
inputTokens: 151914,
|
|
47
48
|
model: 'gpt-5.5',
|
|
48
49
|
contextWindowSize: 258400,
|
|
49
50
|
contextWindowEstimated: false,
|
|
50
51
|
outputTokens: 60,
|
|
52
|
+
transientOutputTokens: 0,
|
|
53
|
+
liveTurn: false,
|
|
51
54
|
estimated: false,
|
|
52
55
|
source: 'codex-rollout-token-count',
|
|
53
56
|
});
|
|
@@ -56,6 +59,86 @@ test('readLatestCodexUsage: reads verified Codex token_count event shape', () =>
|
|
|
56
59
|
}
|
|
57
60
|
});
|
|
58
61
|
|
|
62
|
+
test('readLatestCodexUsage: during an open Codex turn overlays transient output tokens', () => {
|
|
63
|
+
const dir = mkdtempSync(join(tmpdir(), 'tl-codex-usage-'));
|
|
64
|
+
try {
|
|
65
|
+
const rollout = join(dir, 'rollout.jsonl');
|
|
66
|
+
writeFileSync(
|
|
67
|
+
rollout,
|
|
68
|
+
[
|
|
69
|
+
row('session_meta', { id: '019dfaba-thread', cwd: '/repo' }),
|
|
70
|
+
row('turn_context', { turn_id: '019dfaba-turn', model: 'gpt-5.5' }),
|
|
71
|
+
event('task_started'),
|
|
72
|
+
event('token_count', {
|
|
73
|
+
info: {
|
|
74
|
+
last_token_usage: {
|
|
75
|
+
input_tokens: 151914,
|
|
76
|
+
output_tokens: 1200,
|
|
77
|
+
},
|
|
78
|
+
model_context_window: 258400,
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
]
|
|
82
|
+
.map((r) => JSON.stringify(r))
|
|
83
|
+
.join('\n') + '\n',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const usage = readLatestCodexUsage(rollout);
|
|
87
|
+
assert.equal(usage.tokens, 153114);
|
|
88
|
+
assert.equal(usage.inputTokens, 151914);
|
|
89
|
+
assert.equal(usage.outputTokens, 1200);
|
|
90
|
+
assert.equal(usage.transientOutputTokens, 1200);
|
|
91
|
+
assert.equal(usage.liveTurn, true);
|
|
92
|
+
assert.equal(usage.source, 'codex-rollout-token-count-live-turn');
|
|
93
|
+
} finally {
|
|
94
|
+
rmSync(dir, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('readLatestCodexUsage: task_complete drops transient output overlay', () => {
|
|
99
|
+
const dir = mkdtempSync(join(tmpdir(), 'tl-codex-usage-'));
|
|
100
|
+
try {
|
|
101
|
+
const rollout = join(dir, 'rollout.jsonl');
|
|
102
|
+
writeFileSync(
|
|
103
|
+
rollout,
|
|
104
|
+
[
|
|
105
|
+
row('session_meta', { id: '019dfaba-thread', cwd: '/repo' }),
|
|
106
|
+
row('turn_context', { turn_id: '019dfaba-turn', model: 'gpt-5.5' }),
|
|
107
|
+
event('task_started'),
|
|
108
|
+
event('token_count', {
|
|
109
|
+
info: {
|
|
110
|
+
last_token_usage: {
|
|
111
|
+
input_tokens: 151914,
|
|
112
|
+
output_tokens: 1200,
|
|
113
|
+
},
|
|
114
|
+
model_context_window: 258400,
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
event('task_complete'),
|
|
118
|
+
event('token_count', {
|
|
119
|
+
info: {
|
|
120
|
+
last_token_usage: {
|
|
121
|
+
input_tokens: 151914,
|
|
122
|
+
output_tokens: 1200,
|
|
123
|
+
},
|
|
124
|
+
model_context_window: 258400,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
]
|
|
128
|
+
.map((r) => JSON.stringify(r))
|
|
129
|
+
.join('\n') + '\n',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const usage = readLatestCodexUsage(rollout);
|
|
133
|
+
assert.equal(usage.tokens, 151914);
|
|
134
|
+
assert.equal(usage.transientOutputTokens, 0);
|
|
135
|
+
assert.equal(usage.liveTurn, false);
|
|
136
|
+
assert.equal(usage.source, 'codex-rollout-token-count');
|
|
137
|
+
} finally {
|
|
138
|
+
rmSync(dir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
59
142
|
test('readLatestCodexUsage: falls back to model_provider when no turn_context model exists', () => {
|
|
60
143
|
const dir = mkdtempSync(join(tmpdir(), 'tl-codex-usage-'));
|
|
61
144
|
try {
|
package/src/token-monitor.mjs
CHANGED
|
@@ -318,8 +318,11 @@ function formatLine({ state, usage, isActive, now = Date.now() }) {
|
|
|
318
318
|
const tokCol = `${formatNumber(tokens).padStart(6)} / ${formatNumber(max).padStart(6)}`;
|
|
319
319
|
const estimateMark = usage?.estimated ? ' est' : '';
|
|
320
320
|
const windowMark = usage?.contextWindowEstimated ? ' win?' : '';
|
|
321
|
+
const liveMark = usage?.liveTurn && usage?.transientOutputTokens
|
|
322
|
+
? ` live+${formatNumber(usage.transientOutputTokens)}`
|
|
323
|
+
: '';
|
|
321
324
|
const modelCol = usage?.model
|
|
322
|
-
? color(ANSI.dim, `${usage.model}${estimateMark}${windowMark}`)
|
|
325
|
+
? color(ANSI.dim, `${usage.model}${estimateMark}${windowMark}${liveMark}`)
|
|
323
326
|
: color(ANSI.dim, '(未取得)');
|
|
324
327
|
// 最終更新からの経過: 表示が「止まって見える」とき、それが idle なのか障害なのかを
|
|
325
328
|
// 即座に判別できるようにする。updatedAt は state.writeSessionState 時の Date.now()。
|
|
@@ -451,6 +451,27 @@ test('formatLine: Codex estimated usage は host と est marker を表示する'
|
|
|
451
451
|
assert.ok(out.includes('codex est win?'));
|
|
452
452
|
});
|
|
453
453
|
|
|
454
|
+
test('formatLine: Codex live turn usage は transient output marker を表示する', () => {
|
|
455
|
+
const args = makeLineArgs(0.5);
|
|
456
|
+
args.state.host = 'codex';
|
|
457
|
+
args.usage = {
|
|
458
|
+
tokens: 101_200,
|
|
459
|
+
inputTokens: 100_000,
|
|
460
|
+
model: 'gpt-5.5',
|
|
461
|
+
contextWindowSize: 200_000,
|
|
462
|
+
contextWindowEstimated: false,
|
|
463
|
+
outputTokens: 1200,
|
|
464
|
+
transientOutputTokens: 1200,
|
|
465
|
+
liveTurn: true,
|
|
466
|
+
estimated: false,
|
|
467
|
+
source: 'codex-rollout-token-count-live-turn',
|
|
468
|
+
};
|
|
469
|
+
const out = stripColors(formatLine(args));
|
|
470
|
+
assert.ok(out.includes('Codex'));
|
|
471
|
+
assert.ok(out.includes('101.2k / 200.0k'));
|
|
472
|
+
assert.ok(out.includes('gpt-5.5 live+1.2k'));
|
|
473
|
+
});
|
|
474
|
+
|
|
454
475
|
test('formatLine: 70% 以上で "!" マーカーと弱めの文言', () => {
|
|
455
476
|
const out = stripColors(formatLine(makeLineArgs(0.75)));
|
|
456
477
|
assert.ok(out.includes('!'), 'should include ! marker');
|