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 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. If a Codex rollout has no
523
- token-count event, Throughline can show an explicit estimate with
524
- `estimated: true` and the monitor marks it with `est`; it is not presented as
525
- exact usage.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -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' || payload?.type !== 'token_count') continue;
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 tokens = Number(last.input_tokens);
46
- if (!Number.isFinite(tokens) || tokens < 0) continue;
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: Number.isFinite(Number(last.output_tokens)) ? Number(last.output_tokens) : 0,
73
+ outputTokens,
74
+ transientOutputTokens,
75
+ liveTurn: openTaskCount > 0,
58
76
  estimated: false,
59
- source: 'codex-rollout-token-count',
77
+ source: openTaskCount > 0
78
+ ? 'codex-rollout-token-count-live-turn'
79
+ : 'codex-rollout-token-count',
60
80
  };
61
81
  }
62
82
 
@@ -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 {
@@ -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');