throughline 0.4.5 → 0.4.7

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,31 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.4.7] — 2026-05-09
14
+
15
+ ### Changed
16
+
17
+ - Codex Stop hook auto-refresh now uses an 80% verified-usage threshold instead
18
+ of 90%, so Throughline can refresh before Codex native auto-compact while
19
+ still staying above the monitor's 70% warning band. Estimate-only usage and
20
+ estimated context windows still do not mutate the thread.
21
+ - Token monitor now discovers active Codex rollout files directly from
22
+ `~/.codex/sessions/**/rollout-*.jsonl`, so current Codex sessions appear even
23
+ when the Codex Stop hook has not written a Throughline state file.
24
+ - Token monitor now displays Codex session ids as the raw first 8 thread-id
25
+ characters (`019e085c`) instead of the confusing prefixed slice (`codex:01`).
26
+ Codex in-flight turns still overlay transient `output_tokens` in the token
27
+ count, but the model column no longer adds a separate `live+<tokens>` marker.
28
+
29
+ ## [0.4.6] — 2026-05-09
30
+
31
+ ### Changed
32
+
33
+ - Codex monitor usage now overlays transient `output_tokens` while a Codex turn
34
+ is open. During an in-flight turn the row displays `input_tokens +
35
+ output_tokens` and marks the model with `live+<tokens>`; after `task_complete`
36
+ the row drops back to verified `input_tokens` only.
37
+
13
38
  ## [0.4.5] — 2026-05-09
14
39
 
15
40
  ### Fixed
package/README.ja.md CHANGED
@@ -18,13 +18,13 @@
18
18
 
19
19
  ```bash
20
20
  npm install -g throughline
21
- throughline install # ~/.claude/settings.json hook を登録
21
+ throughline install # hook / Codex skill / VS Code monitor task を登録
22
22
  ```
23
23
 
24
24
  これだけ。Claude Code のセッションを開けば、以後すべてのターンが
25
25
  `~/.throughline/throughline.db` に自動で流れていく。50 ターン作業した後、
26
- 次のセッションへ記憶を引き継ぎたければ `/clear` の前に `/tl` を打つ。新セッションは
27
- ゼロからのスタートではなく、**思考の途中から再開** される。
26
+ `/clear` を打てば新セッションはゼロからではなく、**思考の途中から再開** される。
27
+ `/clear` を経由しない新規 chat / VS Code 再起動では `/tl` で前任を指名できる。
28
28
 
29
29
  ## 他の手段との比較
30
30
 
@@ -32,10 +32,10 @@ throughline install # ~/.claude/settings.json に hook を登録
32
32
  |---|---|---|---|
33
33
  | **圧縮の軸** | コンテンツの **種類** (テキスト vs ツール I/O) | **新旧** (古い → 要約) | 無し |
34
34
  | **コーディング用途への適合** | 高 — ツール I/O こそ重い 80% | 中 — 残したい部分まで圧縮される | — |
35
- | **`/clear` 後の生存** | ✅ SQLite + `/tl` バトン | ホスト依存 | ❌ |
36
- | **誤継承リスク** | ゼロ (明示的な `/tl`) | 高 | — |
35
+ | **`/clear` 後の生存** | ✅ SQLite + typed `/clear` / `/tl` バトン | ホスト依存 | ❌ |
36
+ | **誤継承リスク** | (typed `/clear` / `/tl` が前任を指名) | 高 | — |
37
37
  | **ランタイム依存** | **ゼロ** (Node 22.5+ 同梱の `node:sqlite`) | 多数 | — |
38
- | **マルチセッション トークン監視** | ✅ 実測 `message.usage`、`len/4` 推定なし | — | — |
38
+ | **マルチセッション トークン監視** | ✅ Claude 実測 `message.usage`、Codex rollout `token_count` | — | — |
39
39
 
40
40
  <details>
41
41
  <summary><b>なぜこれが効くのか — 80% ツール I/O 問題</b></summary>
@@ -126,33 +126,34 @@ L3 に保存された `kind` 別 (ツール入力 / ツール出力 / hook 出
126
126
 
127
127
  ---
128
128
 
129
- ## 引き継ぎ: `/clear` で自動、env で OFF、`/tl` で明示
129
+ ## 引き継ぎ: typed `/clear` / `/tl` が前任を指名、source-`clear` は補助
130
130
 
131
- Throughline 0.4.0 から引き継ぎは 2 経路:
131
+ Throughline 0.4.1+ の引き継ぎは 2 経路です。主経路は typed `/clear` または
132
+ `/tl` が書く baton で、`source='clear'` の auto path は `/clear` が
133
+ UserPromptSubmit hook に届かない場合の補助です。
132
134
 
133
- ### auto path (デフォルト): `/clear` で自動引き継ぎ
135
+ ### baton path (primary): typed `/clear` または `/tl`
134
136
 
135
- Claude Code 2.1.128 以降は `/clear` 直後の SessionStart hook
136
- `source='clear'` が確実に乗ります。Throughline がこれを検出して、前セッションの
137
- メモリを新セッションに自動 merge します。**ユーザー操作不要** `/clear`
138
- だけで新チャットが「途中から」再開されます。
137
+ ユーザーが prompt `/clear` または `/tl` を打つと、UserPromptSubmit hook
138
+ **そのセッションの** `session_id` `handoff_batons` に書きます。次の
139
+ SessionStart 1 時間以内の baton を消費し、その前任を確定的に merge します。
140
+ 複数ウィンドウで「最新更新セッション」と「今 `/clear` したセッション」が違っても、
141
+ 指名された前任だけを引き継ぎます。
139
142
 
140
- `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` を環境変数に立てると auto path OFF にできます。
143
+ ### auto path (fallback): `source='clear'`
141
144
 
142
- ### baton path (`/tl`): 明示意思マーカー
145
+ baton が無く、SessionStart `source='clear'` が届いた場合だけ、同 project の
146
+ 最新 Claude predecessor を選んで merge します。これは VS Code 拡張メニューなど、
147
+ typed `/clear` が UserPromptSubmit hook に届かない経路のための補助です。
143
148
 
144
- 次のいずれかのユーザー向け:
145
-
146
- - `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` を立てている、**または**
147
- - `/clear` 経由しないで引き継ぎたい (新 chat / VSCode 再起動など)
148
-
149
- 新セッションを開く前に `/tl` を打つと `UserPromptSubmit` hook が baton を書き、
150
- 次の `SessionStart` (1 時間以内) が baton を消費して merge します。
151
- `source` 値関係なく発火します。
149
+ `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` はこの fallback path だけを OFF にします。
150
+ typed `/clear` と `/tl` はユーザーの明示意思なので、この env に関係なく baton を
151
+ 書いて引き継ぎます。
152
152
 
153
153
  ```
154
- auto path: Session A → /clear → Session B (A auto-merge)
155
- baton path: Session A → /tl (新 chat / 再起動) → Session B (baton を消費して A を merge)
154
+ typed /clear: Session A → /clear → Session B (A baton を消費して merge)
155
+ typed /tl: Session A → /tl → 新 chat / 再起動 → Session B (A の baton を消費して merge)
156
+ fallback: baton 無し + source='clear' → latest predecessor を merge
156
157
  ```
157
158
 
158
159
  ### 注入されるもの
@@ -214,8 +215,12 @@ throughline monitor --session <id-prefix>
214
215
  ```
215
216
 
216
217
  監視中は Claude transcript / Codex rollout をライブに読み、Stop hook の state
217
- snapshot はライブ usage が取れない場合の控えとして使います。これにより表示更新は
218
- Stop 完了待ちではなくなります。
218
+ snapshot はライブ usage が取れない場合の控えとして使います。Codex については
219
+ `~/.codex/sessions/**/rollout-*.jsonl` も直接 discovery するため、Stop hook が
220
+ Throughline state をまだ書いていない現在セッションも表示できます。Codex は open turn
221
+ 中だけ `input_tokens + output_tokens` を表示し、`task_complete` 後は verified
222
+ `input_tokens` のみに戻ります。表示 ID は `codex:01` ではなく、raw thread id の
223
+ 先頭 8 桁 (`019e085c` など) です。
219
224
 
220
225
  詳細仕様 (resize 追従、1M context 検出、ステイル隠し、Stop hook の非同期化など) は
221
226
  [英語版 README](README.md#multi-session-token-monitor) を参照してください。
package/README.md CHANGED
@@ -250,7 +250,9 @@ reconnect, but controlled model-visible rollback smokes did not reproduce that
250
250
  path. `throughline trim --execute --host codex` now sends the guarded
251
251
  rollback + Throughline DB memory injection when app-server turn-count guards and
252
252
  injectable DB memory are available. Codex Stop hook auto-refresh also attempts
253
- the same live refresh when verified usage reaches the 90% threshold.
253
+ the same live refresh when verified usage reaches the 80% threshold, which runs
254
+ before Codex native auto-compact while staying above the monitor's 70% warning
255
+ band.
254
256
 
255
257
  `throughline codex-host-primitive-audit` can inspect the installed Codex
256
258
  app-server schema read-only. On the current tested Codex CLI, it finds
@@ -508,7 +510,7 @@ Example output:
508
510
  ```
509
511
  [Throughline] 1 セッション
510
512
  ▶ Throughline Claude 2ed5039c just now ██░░░░░░░░ 205.1k / 1.0M claude-opus-4-6
511
- Throughline Codex codex:01 just now ██████░░░░ 151.9k / 258.4k gpt-5.5
513
+ Throughline Codex 019e085c just now ██████░░░░ 151.9k / 258.4k gpt-5.5
512
514
  ```
513
515
 
514
516
  - **Claude token counts are accurate.** Read straight from the latest
@@ -517,13 +519,17 @@ Example output:
517
519
  (`input_tokens + cache_creation_input_tokens + cache_read_input_tokens`).
518
520
  No `length / 4` approximation.
519
521
  - **Codex token counts use the rollout `token_count` event when present.** The
520
- Codex Stop hook writes `codex:<thread_id>` monitor state with the rollout
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.
526
- - **Codex auto-refresh mutates at the verified 90% threshold.** The Codex Stop
522
+ monitor discovers live Codex rollouts directly from
523
+ `~/.codex/sessions/**/rollout-*.jsonl`, so a current Codex session can appear
524
+ even before the Codex Stop hook writes `codex:<thread_id>` monitor state.
525
+ While the monitor is running it reads the live rollout every tick and prefers
526
+ the latest verified `token_count` sample. During an open Codex turn, the
527
+ monitor overlays transient `output_tokens` on top of `input_tokens`; when
528
+ `task_complete` arrives it drops back to verified `input_tokens` only. If a
529
+ Codex rollout has no token-count event, Throughline can show an explicit
530
+ estimate with `estimated: true` and the monitor marks it with `est`; it is not
531
+ presented as exact usage.
532
+ - **Codex auto-refresh mutates at the verified 80% threshold.** The Codex Stop
527
533
  hook captures DB memory, writes monitor state, and when verified usage reaches
528
534
  the threshold it attempts rollback + Throughline DB memory injection for the
529
535
  current thread.
@@ -531,11 +537,13 @@ Example output:
531
537
  transcript, falls back to string matching on `1M context`, and finally
532
538
  promotes to 1M if observed usage exceeds 200k.
533
539
  - **Multi-session view.** Each Claude Code or Codex session writes its own
534
- state file (`~/.throughline/state/<session_id>.json`). Codex session ids are
535
- stored as `codex:<thread_id>` in the JSON payload; filenames are URL-encoded
536
- so the state directory remains portable. The monitor scans the directory every
537
- second and displays one row per live session, sorted by last activity. The
538
- most recent one is highlighted with `▶`.
540
+ state file (`~/.throughline/state/<session_id>.json`), and active Codex
541
+ rollouts are discovered directly from the Codex session directory. Codex
542
+ session ids are stored as `codex:<thread_id>` in JSON state, while the display
543
+ shows the raw first 8 thread-id characters (for example `019e085c`) to avoid
544
+ the ambiguous `codex:01` prefix slice. The monitor scans every second and
545
+ displays one row per live session, sorted by last activity. The most recent
546
+ one is highlighted with `▶`.
539
547
  - **Stale hiding.** Sessions that haven't been touched in 15 minutes drop out of
540
548
  the default view; files older than 24 hours are deleted entirely. This is the
541
549
  only time threshold in the system and is used solely for display hygiene — no
@@ -119,7 +119,7 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
119
119
  | **npm 公開 (v0.3.24)** | 2026-05-02 v0.3.23 の補完: `.vscode/tasks.json` には現環境の絶対パスが書き込まれるため、**そもそも commit すべきではない**。`shouldRecommendGitignore` を [src/vscode-task.mjs](../src/vscode-task.mjs) に追加し、`ensureMonitorTaskFile` が created / merged / repaired を返すタイミングで「git リポジトリ内かつ `.gitignore` に `.vscode/tasks.json` 系エントリが無い」を判定。該当時に `<system-reminder>` で `.gitignore` 追加推奨を 1 度だけ stdout 通知 (`.throughline-gitignore-noted` marker で抑止)。否定パターン (`!.vscode/tasks.json`) はスキップ判定 = 推奨を出す。README Troubleshooting にも明示。配布物 (npm tarball) には絶対パスは入っていない (`files` フィールドが `.vscode/` を含まない、ソースに hard-coded path 無し) ことを再確認 |
120
120
  | **npm 公開 (v0.3.25): Claude-primary / Codex-sidecar groundwork** | `HandoffRecord` projection、`throughline handoff-preview`、`throughline_handoff` example、`codex-sidecar-diagnostics` / `codex-sidecar-dry-run` を追加。Claude Code hooks / slash command / transcript / baton / resume behavior は正本として維持し、Codex 対応は adapter / projection として足す |
121
121
  | **npm 公開 (v0.3.25): optional Codex-sidecar L1 summarization** | Claude primary の L2→L1 要約は、`codex-sidecar` が `summarize-l1` preset で configured の場合だけ sidecar を使う。disabled / unavailable / run failure では、ユーザー許可済み互換経路として既存 Claude Haiku 要約を維持する。Codex primary の L2→L1 backend は次フェーズ計画で Codex CLI 本線として扱う。Claude CLI smoke / test で Claude を呼ぶ場合は Haiku を使う |
122
- | **npm 公開 (v0.3.25): `/tl-trim` dry-run** | `throughline trim --dry-run`、`--preflight`、guarded `--execute`、`--host`、`--keep-recent`、`--all`、`--memo-stdin`、`--preview-max-chars`、`--codex-thread-id`、`THROUGHLINE_CODEX_THREAD_ID` / `CODEX_THREAD_ID`、`throughline codex-threads`、`throughline doctor --trim`、Claude slash command `/tl-trim` を追加。Codex app-server の rollback / inject primitive は live app-server 上で実測済み。2026-05-06 incident 後は一時 blocked としたが、2026-05-08 の controlled rollback model-visible smoke が app-server restart 境界と VS Code reload/reconnect 境界の両方で `not-reproduced` だったため、過剰 blocker は解除済み。`trim --execute --host codex` は明示実行で current-thread rollback + Throughline DB memory inject を送り、Codex Stop hook auto-refresh は verified usage 90% 以上で同じ guarded path を試行する。DB memory 不在、Codex thread identity 不在、rollout/app-server turn-count 不一致は mutation 前に拒否する。restore-safety / planned restore-safety / host primitive audit は diagnostics として表示する |
122
+ | **npm 公開 (v0.3.25): `/tl-trim` dry-run** | `throughline trim --dry-run`、`--preflight`、guarded `--execute`、`--host`、`--keep-recent`、`--all`、`--memo-stdin`、`--preview-max-chars`、`--codex-thread-id`、`THROUGHLINE_CODEX_THREAD_ID` / `CODEX_THREAD_ID`、`throughline codex-threads`、`throughline doctor --trim`、Claude slash command `/tl-trim` を追加。Codex app-server の rollback / inject primitive は live app-server 上で実測済み。2026-05-06 incident 後は一時 blocked としたが、2026-05-08 の controlled rollback model-visible smoke が app-server restart 境界と VS Code reload/reconnect 境界の両方で `not-reproduced` だったため、過剰 blocker は解除済み。`trim --execute --host codex` は明示実行で current-thread rollback + Throughline DB memory inject を送り、Codex Stop hook auto-refresh は verified usage 80% 以上で同じ guarded path を試行する。DB memory 不在、Codex thread identity 不在、rollout/app-server turn-count 不一致は mutation 前に拒否する。restore-safety / planned restore-safety / host primitive audit は diagnostics として表示する |
123
123
  | **npm 公開 (v0.3.25): Codex primary capture** | `throughline codex-capture --codex-thread-id <id>` を追加。Codex rollout JSONL の active turns を `codex:<thread_id>` session として DB `bodies` に保存し、`function_call` / `function_call_output` を DB `details` の L3 tool input / output として保存する。`thread_rolled_back` 適用後の active thread だけを再構成し、rollback 済み tail は current L2/L3 に残さない。Codex thread id は明示指定または env 指定のみで、自動推測しない |
124
124
  | **npm 公開 (v0.3.25): Codex-primary L1 backend** | `summarizeToL1` に `hostMode` 分岐を追加。Claude primary は既存の `codex-sidecar` / Claude Haiku 互換経路を維持し、Codex primary は Codex CLI backend を使う。Codex CLI failure は `source = codex-cli` / `reason` 付き explicit error とし、Claude Haiku / `raw_l2` に silent fallback しない |
125
125
  | **npm 公開 (v0.3.25): Codex-primary L1 CLI** | `throughline codex-summarize --session codex:<thread_id>` を追加。captured Codex L2 が L2 window を超えた場合、最古の未要約 turn を Codex CLI backend で L1 skeleton に書く。Codex CLI failure は explicit error |
@@ -131,11 +131,12 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
131
131
  | **npm 公開 (v0.3.25): Codex VS Code restore smoke protocol** | `throughline codex-vscode-restore-smoke --prepare/--verify --codex-thread-id <id>` を追加。`--prepare` は hidden active-work marker memory を app-server へ注入し、VS Code reload / reconnect 後に marker を含まない prompt を送る二段階手順を出す。`--verify` は rollout を読み、prepare 後の marker-free smoke prompt、assistant の marker-only answer、user prompt への marker leak 不在を確認する。prepare は `THROUGHLINE_EXPERIMENTAL_CODEX_VSCODE_RESTORE_SMOKE=1` 必須。実 VS Code reload / reconnect marker proof は `TL_CODEX_VSCODE_RESTORE_46888202` で成功済み。ただしこれは hidden developer memory visibility 証明であり、rollback 済み user turn の非復活証明ではない |
132
132
  | **npm 公開 (v0.3.25): Codex VS Code rollback smoke verifier** | `throughline codex-vscode-rollback-smoke --verify --codex-thread-id <id>` を追加。rollout を read-only で読み、rollback event、rollback 済み user text、rollback 後 user turn、`restoreSafety.status = ok` を必須条件にする。`--after-vscode-restart` がある場合だけ `restartSafe: true` を返す。実 incident-shaped live rollback run では `thread_rolled_back` と injected memory は記録されたが、rollback 対象 user text が `compacted.replacement_history` に残り、後続 verifier では rollback 済み user text の再出現も観測した。後続分類で app-server response 上の retained text は `aggregatedOutput` に限定され、controlled rollback model-visible smoke は再現しなかったため、これは現在は diagnostic evidence として扱う |
133
133
  | **npm 公開 (v0.3.25): Codex primary doctor** | `throughline doctor --codex` を追加。現在 project の Codex thread env identity、rollout candidates、captured `codex:<thread_id>` DB sessions、context-refresh memory contract、new-thread handoff readiness、safe continuation status、host primitive audit status、次に使う capture / handoff / resume / audit command を表示する。doctor 自体は read-only で、Codex thread / DB / Claude settings を変更しない。`doctor --trim --host codex` も host primitive audit status を表示する |
134
- | **npm 公開 (v0.3.25): Codex global Stop hook / skill install** | `throughline install` が Claude hooks / slash commands に加えて `~/.codex/hooks.json` に絶対 node + installed `bin/throughline.mjs codex-hook stop` を `async: false` で登録し、`~/.codex/config.toml` の `[features].codex_hooks = true` を有効化し、`~/.codex/skills/throughline` に `$throughline` skill を配置する。Codex App Server / VSCode host の PATH 差分で bare `throughline` が見えない可能性があるため、hook は Caveat と同じ絶対パス型に寄せる。既存 Caveat / Spotter などの Codex hooks は保持し、`throughline uninstall` は Throughline 管理の Codex hook / skill だけを削除する。既に bare command または `async: true` で登録済みの Throughline Codex Stop hook は次回 install で更新する。実環境では `codex exec --json` child thread `019dfd4f-93ff-7522-8f89-bd1e1996c8d7` が Stop hook で自然 capture され、`doctor --codex` の latest DB session が `codex:019dfd4f-93ff-7522-8f89-bd1e1996c8d7` に進むことを確認した。さらに絶対パス型へ更新後、child thread `019dfd5e-1248-7c11-8ddc-97e1b0701e10` でも latest DB session が `codex:019dfd5e-1248-7c11-8ddc-97e1b0701e10` に進むことを確認した。hook shape 変更後に新規開始した VSCode-origin thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` でも `doctor --codex` の current thread と latest DB session が一致し、自然 Stop hook capture を確認済み。hook shape 変更前から開いていた VSCode-origin parent thread は、変更後の自然 Stop smoke としては扱わない。Caveat 側にも `async: false` Stop hook が動く実測があるため、Codex 側は Caveat と同じ同期 hook 方針に寄せる。`codex-capture` / `codex-summarize` / `codex-resume --memo-stdin` は診断・明示操作 surface として維持し、model-visible smoke は明示 opt-in。2026-05-08 以降、Stop hook auto-refresh は verified usage 90% 以上で guarded rollback / inject を試行し、estimate usage では mutation しない |
134
+ | **npm 公開 (v0.3.25): Codex global Stop hook / skill install** | `throughline install` が Claude hooks / slash commands に加えて `~/.codex/hooks.json` に絶対 node + installed `bin/throughline.mjs codex-hook stop` を `async: false` で登録し、`~/.codex/config.toml` の `[features].codex_hooks = true` を有効化し、`~/.codex/skills/throughline` に `$throughline` skill を配置する。Codex App Server / VSCode host の PATH 差分で bare `throughline` が見えない可能性があるため、hook は Caveat と同じ絶対パス型に寄せる。既存 Caveat / Spotter などの Codex hooks は保持し、`throughline uninstall` は Throughline 管理の Codex hook / skill だけを削除する。既に bare command または `async: true` で登録済みの Throughline Codex Stop hook は次回 install で更新する。実環境では `codex exec --json` child thread `019dfd4f-93ff-7522-8f89-bd1e1996c8d7` が Stop hook で自然 capture され、`doctor --codex` の latest DB session が `codex:019dfd4f-93ff-7522-8f89-bd1e1996c8d7` に進むことを確認した。さらに絶対パス型へ更新後、child thread `019dfd5e-1248-7c11-8ddc-97e1b0701e10` でも latest DB session が `codex:019dfd5e-1248-7c11-8ddc-97e1b0701e10` に進むことを確認した。hook shape 変更後に新規開始した VSCode-origin thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` でも `doctor --codex` の current thread と latest DB session が一致し、自然 Stop hook capture を確認済み。hook shape 変更前から開いていた VSCode-origin parent thread は、変更後の自然 Stop smoke としては扱わない。Caveat 側にも `async: false` Stop hook が動く実測があるため、Codex 側は Caveat と同じ同期 hook 方針に寄せる。`codex-capture` / `codex-summarize` / `codex-resume --memo-stdin` は診断・明示操作 surface として維持し、model-visible smoke は明示 opt-in。2026-05-08 以降、Stop hook auto-refresh は verified usage 80% 以上で guarded rollback / inject を試行し、estimate usage では mutation しない。2026-05-09 以降は Codex native auto-compact より先に Throughline refresh を走らせつつ、70% warning よりは mutation を遅らせる |
135
135
  | **npm 公開 (v0.3.25): Codex-first roadmap** | [THROUGHLINE_CODEX_FIRST_ROADMAP.md](THROUGHLINE_CODEX_FIRST_ROADMAP.md) を追加。次フェーズは Codex primary 実用化、Codex Rewind 互換、Claude 側 finalization の順で進める。Codex primary の L2→L1 backend は Codex CLI を本線とし、`codex-sidecar` は Claude primary からの review / risk-check / second opinion / 互換 L2→L1 経路として整理する |
136
136
  | **npm 公開 (v0.3.25): npm docs packaging** | README から参照する `docs/` と `CHANGELOG.md` を npm `files` に追加。`docs/throughline-handoff-context.example.json` を含め、README の sidecar dry-run 例が tarball 内でも成立するようにする |
137
137
  | **npm 公開 (v0.4.0): /clear auto-handoff + memo / save-inflight / /tl-trim retire** | 2026-05-08 Claude Code 2.1.128 で `source='clear'` が reliable になったため、`/clear` で自動引継ぎがデフォルト ON になる auto path を追加。`THROUGHLINE_DISABLE_AUTO_HANDOFF=1` で OFF にできる。`/tl` slash command は明示意思マーカーへ簡素化 (memo 4 項目入力廃止、`save-inflight` CLI 削除、`/tl-trim` slash command 廃止、`updateBatonMemo` 関数削除、`handoff_batons.memo_text` を schema v8 で drop)。注入は L1 + L2 + L3 references のみに簡素化し、memo / 中断直前 thinking セクションを削除。Codex 側 trim path は維持。詳細は [CHANGELOG.md](../CHANGELOG.md) と [THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md](THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md) |
138
138
  | **npm 公開 (v0.4.1): typed `/clear` も baton を書く + 2 経路の優先順位入れ替え** | 2026-05-09 `/clear` を UserPromptSubmit hook で検出した時点で当該セッションの `session_id` を `handoff_batons` に書き込み、次 SessionStart が確定的にそのセッションを引き継ぐ。これで multi-window で「最新更新セッション ≠ /clear したセッション」になるシナリオで `findLatestClaudePredecessor` heuristic が誤った前任を選ぶ問題を解消。2 経路の優先順位を **baton path = primary、auto path = fallback** に変更 (auto path は VSCode 拡張メニュー由来など UserPromptSubmit に届かない経路のフォールバック)。`THROUGHLINE_DISABLE_AUTO_HANDOFF=1` は fallback path のみに作用するようになった (typed `/clear` / `/tl` は env と無関係に発火する)。あわせて `.vscode/tasks.json` を git 追跡から外し (gitignore)、`ensureMonitorTaskFile` が hook 発火ごとに絶対パスを書き換える挙動による別環境での dirty diff を解消。`src/prompt-submit.test.mjs` を新設し、`isClearCommand` / `isBatonCommand` 判定 14 件と subprocess+DB 実体テスト 3 件を追加。詳細は [CHANGELOG.md](../CHANGELOG.md) |
139
+ | **npm 公開 (v0.4.7): Codex monitor direct discovery + 80% auto-refresh** | 2026-05-09 Codex Stop hook auto-refresh の verified usage threshold を 90% から 80% に変更し、Codex native auto-compact より先に Throughline DB memory refresh を試行する。estimate usage / estimated context window では mutation しない。`throughline monitor` は `~/.throughline/state` に加えて `~/.codex/sessions/**/rollout-*.jsonl` を直接 discovery し、Throughline state が未生成の現在 Codex thread も表示する。既存 state がある場合は state の usage snapshot を保持しつつ discovered rollout path / mtime を合流する。Codex 表示 ID は `codex:01` ではなく raw thread id 先頭 8 桁 (`019e085c`) にした。Codex open turn の transient `output_tokens` は token count に overlay するが、モデル欄の `live+<tokens>` marker は表示しない |
139
140
  | **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
140
141
 
141
142
  ### ❌ 未完タスク
@@ -147,7 +148,7 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
147
148
  | **GitHub Actions 自動 publish** | `release` タグ push をトリガー(Phase 3+、Trusted Publishing 使用) |
148
149
  | **Claude Code プラグインマーケットプレース登録** | npm 公開の後継ステップ(Phase 3+) |
149
150
  | **turn-processor.test.mjs の 10 秒タイムアウト解消** | `main()` が stdin を待ち続けるためテストファイルがハングする既存の問題。実装動作は無影響、テスト個別 9/9 は pass |
150
- | **automatic context rollback / inject** | Codex Stop hook auto-refresh は verified usage 90% 以上で guarded rollback + Throughline DB memory inject を試行する。controlled rollback model-visible smoke で復活が未再現となったため、2026-05-06 incident 後の overbroad blocker は解除済み。estimate usage では実行しない |
151
+ | **automatic context rollback / inject** | Codex Stop hook auto-refresh は verified usage 80% 以上で guarded rollback + Throughline DB memory inject を試行する。controlled rollback model-visible smoke で復活が未再現となったため、2026-05-06 incident 後の overbroad blocker は解除済み。estimate usage では実行しない |
151
152
 
152
153
  ---
153
154
 
@@ -278,7 +278,7 @@ Codex 側 v0.3.25 の以下は本計画で **完全に無変更**:
278
278
  - `codex-capture` / `codex-summarize` / `codex-resume` (Codex primary L1/L2/L3 path)
279
279
  - `codex-resume --format handoff` (新規 Codex thread 用 prompt)
280
280
  - `trim --execute --host codex` / `--preflight --host codex` (app-server `thread/rollback` + `thread/inject_items`)
281
- - Codex Stop hook 90% auto-refresh
281
+ - Codex Stop hook 80% auto-refresh
282
282
  - restore-safety / host primitive audit diagnostics
283
283
  - Codex skill ([codex/skills/throughline](../codex/skills/throughline)) の trim 機能 (= 機能自体は無変更、SKILL.md 内の `/tl-trim` 言及があれば 4 TODO で update)
284
284
  - [src/codex-app-server.mjs](../src/codex-app-server.mjs), [src/codex-rollout-memory.mjs](../src/codex-rollout-memory.mjs)
@@ -28,7 +28,7 @@
28
28
  - Claude-primary の現行 L2 -> L1 要約は `codex-sidecar` が configured の場合に sidecar を優先し、使えない場合は Claude Haiku 経路に戻る。Codex-primary は Codex CLI backend 失敗を明示 error にし、Claude Haiku / raw L2 へ fallback しない。
29
29
  - Codex guarded trim は、Codex app-server の `thread/read` / `thread/resume` / `thread/rollback` / `thread/inject_items` を使う。明示 thread identity、rollout/app-server turn count guard、injectable memory がない場合は mutation 前に拒否する。
30
30
  - Claude `/rewind` 自動化はまだ有効化しない。
31
- - Codex automatic refresh mutation は再有効化済み。`90%` の verified usage threshold で rollback / Throughline DB memory inject を試行し、estimate usage では実行しない。
31
+ - Codex automatic refresh mutation は再有効化済み。`80%` の verified usage threshold で rollback / Throughline DB memory inject を試行し、estimate usage では実行しない。Codex native auto-compact より先に Throughline refresh を走らせつつ、70% warning よりは mutation を遅らせる。
32
32
 
33
33
  ## 新セッション引き継ぎ
34
34
 
@@ -466,9 +466,9 @@ Phase 5 implementation status (2026-05-06):
466
466
  - 削減量の記録: `trim --dry-run --host codex` は rollout text がある場合に `contextReductionEstimate` を返す。2026-05-06 の現在 thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` では通常 keep-recent preview だと `capturedTurns = 14` / `keepRecent = 20` のため `rollbackTurns = 0`、推定削減量も 0 だった。旧 bare `$throughline` context refresh は `--all` を使っていたため、この keep-recent preview の 0 は「Codex で削減できない」という意味ではない。
467
467
  - historical live-only `$throughline` context refresh smoke: 同じ VSCode-origin thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` で `trim --dry-run --host codex --all --json` -> `trim --preflight --host codex --all --json` -> `trim --execute --host codex --all` を実行した。preflight は rollout/app-server turn count `20 / 20` match、execute は `rollbackSent = true`、`injectSent = true`、`injectedItems = 1`、`rollback candidate turns = 20`。直前見積もりは `rollbackEstimatedTokens = 179780`、`injectedMemoryEstimatedTokens = 2009`、`netEstimatedTokens = 177771`、`reductionPct = 99` (`chars / 4` heuristic)。これは live app-server smoke であり、restart-safe durability の証明ではない。
468
468
  - post-refresh confirmation: execute 後の次 user turn で `doctor --codex` は current thread と latest DB session の一致を維持し、latest DB session は `2026-05-06 22:41:10` へ更新された。`trim --dry-run --host codex --all --json` は `activeTurns = 2`、`rollbackEvents = 1`、`rolledBackTurns = 20` を返し、`codex-resume` は rollback 後の短い active work context を L2 として描画した。
469
- - monitor adapter 修正: `throughline monitor` は Claude / Codex host-aware state を読む。Codex Stop hook は `codex:<thread_id>` monitor state を書き、Codex rollout path は Claude transcript 用 `transcriptPath` ではなく `rolloutPath` に保存する。rollout に `event_msg` / `token_count` がある場合は verified usage として表示し、無い場合だけ `estimated: true` / `source = codex-rollout-chars-div-4` の明示 estimate を使う。state filename は URL encode し、`codex:` session id を Windows でも保存可能にした。
469
+ - monitor adapter 修正: `throughline monitor` は Claude / Codex host-aware state を読む。Codex Stop hook は `codex:<thread_id>` monitor state を書き、Codex rollout path は Claude transcript 用 `transcriptPath` ではなく `rolloutPath` に保存する。2026-05-09 以降、monitor は `~/.throughline/state` だけでなく `~/.codex/sessions/**/rollout-*.jsonl` も直接 discovery するため、state 未生成の現在 Codex thread も表示できる。既存 state がある場合は usage snapshot を保持しつつ discovered rollout path / mtime を合流する。rollout に `event_msg` / `token_count` がある場合は verified usage として表示し、無い場合だけ `estimated: true` / `source = codex-rollout-chars-div-4` の明示 estimate を使う。state filename は URL encode し、`codex:` session id を Windows でも保存可能にした。表示 ID は `codex:` prefix を外した raw thread id 先頭 8 桁。
470
470
  - monitor doctor 修正: Codex Stop hook stdout は VSCode chat に必ず見えるとは限らないため、`doctor --codex` に `.vscode/tasks.json` の Throughline Monitor task 診断を追加した。登録済み / 未登録 / JSONC / parse error / broken absolute path と `runOn`、および初回作成後は `Developer: Reload Window` が必要という note を read-only で表示する。
471
- - historical automatic refresh 実装: Codex Stop hook は capture / L1 summarize / monitor state 書き込み後、verified usage が verified context window の `90%` 以上なら automatic refresh を試行する経路を持っていた。2026-05-06 incident 後、この経路は blocker として扱い、monitor state / warning / diagnostics は残しても rollback + inject は送らない。
471
+ - historical automatic refresh 実装: Codex Stop hook は capture / L1 summarize / monitor state 書き込み後、verified usage が verified context window の `90%` 以上なら automatic refresh を試行する経路を持っていた。2026-05-06 incident 後、この経路は blocker として扱い、monitor state / warning / diagnostics は残しても rollback + inject は送らない状態にした。2026-05-08 unblock 後は guarded rollback / inject を再有効化し、2026-05-09 以降は Codex native auto-compact より先に Throughline refresh を走らせるため verified usage `80%` 以上を既定閾値にする。
472
472
 
473
473
  ## Phase 6: Claude Side Finalization
474
474
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  Status: implemented
4
4
  Date: 2026-05-06
5
+ Amended: 2026-05-09 — monitor now also discovers Codex rollouts directly
6
+ from `~/.codex/sessions/**/rollout-*.jsonl`, so Codex visibility no longer
7
+ depends on the Stop hook writing a Throughline state file.
5
8
 
6
9
  この計画は、既存の Claude-primary monitor 経路を置き換えずに、
7
10
  `throughline monitor` へ Codex 対応を追加するためのもの。
@@ -24,6 +27,10 @@ Codex 対応では、既存 monitor contract の周辺に Codex adapter / projec
24
27
  Claude hooks、transcript parsing、slash command、baton、resume behavior は rename / 劣化
25
28
  させない。
26
29
 
30
+ 2026-05-09 以降は、Codex Stop hook が state を書く経路に加えて、
31
+ monitor reader 自体が Codex rollout index を直接 discovery する。これにより、
32
+ host が Stop hook をまだ dispatch していない現在 thread でも monitor に表示される。
33
+
27
34
  ## 非目的
28
35
 
29
36
  - Claude transcript usage parsing を置き換えない。
@@ -39,6 +46,8 @@ Monitor reader:
39
46
 
40
47
  - `throughline monitor` は `src/token-monitor.mjs` に dispatch される。
41
48
  - `~/.throughline/state/` の session state files を読む。
49
+ - Codex については `~/.codex/sessions/**/rollout-*.jsonl` から現在 project の
50
+ rollout candidates も読む。`--all` または `--session` 時は全 project 候補を読む。
42
51
  - 現在の state file は主に次を持つ。
43
52
  `sessionId`, `projectPath`, `transcriptPath`, `pid`, `updatedAt`, optional `usage`
44
53
  - `state.usage` があれば monitor はそれをそのまま表示する。
@@ -50,6 +59,9 @@ Monitor reader:
50
59
  - `pid` は既存 state schema に残っているが、現行 stale 判定は `updatedAt` ベース。
51
60
  Codex Stop hook は短命 process なので、Codex の active 判定に hook process PID を
52
61
  意味づけない。
62
+ - state file が無い Codex rollout candidate は、monitor 内で synthetic state として扱う。
63
+ 既存 state がある場合は、state の `usage` snapshot を保持しつつ discovered
64
+ `rolloutPath` / `updatedAt` を合流する。
53
65
 
54
66
  Claude writer:
55
67
 
@@ -148,6 +160,8 @@ adapter 側に置く。
148
160
  - Claude state + real usage: 既存どおり表示。
149
161
  - Codex state + estimated usage: token bar を表示し、`est` marker を付ける。
150
162
  - Codex state + usage 無し: active session として表示し、token usage は未取得扱い。
163
+ - Codex discovered rollout + state 無し: synthetic Codex state として表示し、live rollout
164
+ usage を読む。表示 ID は `codex:` prefix を外した raw thread id 先頭 8 桁にする。
151
165
 
152
166
  ### 5. VSCode monitor task provision を Codex にも接続する
153
167
 
@@ -181,6 +195,9 @@ Codex Stop hook でも、Claude hooks と同じく
181
195
  - [x] Codex context window が未検証の場合は `contextWindowEstimated: true` を保持する。
182
196
  - [x] `src/cli/codex-hook.mjs` で Codex Stop hook state writing を追加する。
183
197
  - [x] monitor session id は `codex:<thread_id>` にする。
198
+ - [x] monitor が Codex rollout を直接 discovery し、state 未生成の current thread も表示する。
199
+ - [x] discovered rollout と既存 Codex state を merge し、state の usage snapshot を保持する。
200
+ - [x] Codex 表示 ID は `codex:` prefix を外した raw thread id 先頭 8 桁にする。
184
201
  - [x] Codex state の `pid` を active 判定に使わないことを test / docs で固定する。
185
202
  - [x] Codex state では `transcriptPath: null` とし、rollout path は `rolloutPath` に保存する。
186
203
  - [x] Codex Stop hook から `ensureMonitorTaskFile` を呼ぶ。
@@ -203,6 +220,8 @@ Codex Stop hook でも、Claude hooks と同じく
203
220
  ## 受け入れ条件
204
221
 
205
222
  - Codex Stop hook capture 後、`throughline monitor` に現在の Codex session が出る。
223
+ - Codex Stop hook state が無くても、現在 project の Codex rollout があれば
224
+ `throughline monitor` に現在の Codex session が出る。
206
225
  - Claude monitor behavior は既存どおり。
207
226
  - Codex session は Codex と分かる。
208
227
  - Codex token usage が estimate の場合、estimate と分かる。
@@ -372,13 +372,13 @@ Phase 6 result (2026-05-06):
372
372
  - `thread/inject_items` に raw Responses API item `{ type: "message", role: "developer", content: [{ type: "input_text", text: "..." }] }` を渡し、次の `turn/start` で injected memory が model-visible になることを確認した。marker `TL_PHASE6_INJECT_OK` を正しく返した。
373
373
  - その後の Codex primary 実装で、`codex-resume` が描画する active-work developer message も実 Codex host で model-visible になることを確認した。marker `TL_CODEX_VISIBLE_REAL_20260506_C` が `item/agentMessage/delta` に出た。詳細な実測値は [THROUGHLINE_CODEX_FIRST_ROADMAP.md](THROUGHLINE_CODEX_FIRST_ROADMAP.md) の Phase 3 result を正とする。
374
374
  - さらに `thread/inject_items` 後にもう一度 `thread/resume` してから `turn/start` を呼ぶ smoke も追加し、実 Codex host で marker `TL_CODEX_RESUME_AFTER_INJECT_REAL_20260506` が `item/agentMessage/delta` に出ることを確認した。
375
- - Codex host primitive は live app-server 上では実測済み。ただし restart-safe durability は未証明。現在は明示 `--codex-thread-id` または env thread identity と rollout/app-server turn count guard が live mutation の最低条件であり、durable success は別分類で扱う。2026-05-06 incident 後はいったん Codex Stop hook 後の automatic refresh mutation を blocked としたが、2026-05-08 unblock 後は verified usage 90% 以上で guarded rollback / inject を試行する。
375
+ - Codex host primitive は live app-server 上では実測済み。ただし restart-safe durability は未証明。現在は明示 `--codex-thread-id` または env thread identity と rollout/app-server turn count guard が live mutation の最低条件であり、durable success は別分類で扱う。2026-05-06 incident 後はいったん Codex Stop hook 後の automatic refresh mutation を blocked としたが、2026-05-08 unblock 後は guarded rollback / inject を試行する。2026-05-09 以降は Codex native auto-compact より先に Throughline refresh を走らせるため、verified usage 80% 以上を既定閾値にする。
376
376
  - Claude `/rewind conversation only` は手動 UX として扱う。外部ツールからの自動化 surface は未確認。Claude host の automatic rollback / inject は `manual-only`。
377
377
  - Phase 8 では dry-run / preflight を本線にしつつ、Codex だけ guarded execute を追加した。Claude は manual-only のまま扱う。
378
378
 
379
379
  完了条件:
380
380
 
381
- - [x] rollback trim を自動実装してよい host と、手動案内に留める host が判断できる。Codex は 2026-05-08 unblock 後、verified usage 90% 以上の Stop hook auto-refresh と明示 execute を許可する。Claude は manual-only。
381
+ - [x] rollback trim を自動実装してよい host と、手動案内に留める host が判断できる。Codex は 2026-05-08 unblock 後、Stop hook auto-refresh と明示 execute を許可する。2026-05-09 以降の auto-refresh 既定閾値は verified usage 80%。Claude は manual-only。
382
382
 
383
383
  ## Phase 7: Trim Handoff Model
384
384
 
@@ -19,7 +19,7 @@ model-visible input に入ることは controlled smoke で再現していませ
19
19
  run の retained text は risk evidence として残す一方、単独では mutation 前 blocker にしません。
20
20
  Codex current-thread trim は、明示 `--execute`、Throughline DB injectable memory、
21
21
  Codex thread identity、rollout/app-server turn-count guard を条件に実行します。Codex Stop hook
22
- auto-refresh は verified usage 90% 以上で同じ guarded path を試行し、estimate usage では実行しません。
22
+ auto-refresh は verified usage 80% 以上で同じ guarded path を試行し、estimate usage では実行しません。
23
23
 
24
24
  最初のインシデント仮説に対する重要な訂正:
25
25
 
@@ -644,7 +644,7 @@ TODO:
644
644
  という記述を訂正する。
645
645
  - [x] hypothesis を compacted replacement-history restore に言及する形へ更新する。
646
646
  - [x] Codex Rewind-equivalent trim が complete だという roadmap claim を格下げする。
647
- - [x] Codex auto-refresh は verified usage 90% 以上で guarded rollback / inject を
647
+ - [x] Codex auto-refresh は verified usage 80% 以上で guarded rollback / inject を
648
648
  実行する。estimate usage では実行しない。
649
649
  - [x] 新セッションが古い「Codex side complete」claim ではなく、この fix plan から
650
650
  再開できるように `CLAUDE.md` を更新する。
@@ -263,7 +263,7 @@ Codex で考えられる流れ:
263
263
  9. ユーザーは同じ Codex thread で続行する。
264
264
  ```
265
265
 
266
- 現行実装では、Codex Stop hook 後の 90% automatic refresh は guarded rollback / inject
266
+ 現行実装では、Codex Stop hook 後の 80% automatic refresh は guarded rollback / inject
267
267
  mutation を試行する。明示 CLI の `throughline trim --execute --host codex --codex-thread-id <id>`
268
268
  も env gate なしで実行する。明示 Codex thread identity、injectable Throughline DB memory、
269
269
  rollout/app-server turn count guard は live mutation の最低条件であり、durable success は
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -121,7 +121,7 @@ test('codex-hook stop captures rollout using Codex stdin payload fields', async
121
121
  }
122
122
  });
123
123
 
124
- test('codex-hook stop runs auto refresh when verified usage reaches 90%', async () => {
124
+ test('codex-hook stop runs auto refresh when verified usage reaches 80%', async () => {
125
125
  const codexHome = mkdtempSync(join(tmpdir(), 'tl-codex-hook-home-'));
126
126
  const project = mkdtempSync(join(tmpdir(), 'tl-codex-hook-project-'));
127
127
  const threadId = '019dfaba-f87e-7f41-a144-d5ca7c6dd7f9';
@@ -2,7 +2,7 @@ import { runCodexTrimExecution } from './codex-app-server.mjs';
2
2
  import { buildCodexRolloutTrimSource } from './codex-rollout-memory.mjs';
3
3
  import { buildTrimPlan } from './trim-model.mjs';
4
4
 
5
- export const CODEX_AUTO_REFRESH_THRESHOLD = 0.9;
5
+ export const CODEX_AUTO_REFRESH_THRESHOLD = 0.8;
6
6
 
7
7
  export function evaluateCodexAutoRefreshUsage(usage, { threshold = CODEX_AUTO_REFRESH_THRESHOLD } = {}) {
8
8
  if (!usage) {
@@ -7,10 +7,10 @@ import {
7
7
  runCodexAutoRefresh,
8
8
  } from './codex-auto-refresh.mjs';
9
9
 
10
- test('evaluateCodexAutoRefreshUsage: default threshold is 90%', () => {
11
- assert.equal(CODEX_AUTO_REFRESH_THRESHOLD, 0.9);
10
+ test('evaluateCodexAutoRefreshUsage: default threshold is 80%', () => {
11
+ assert.equal(CODEX_AUTO_REFRESH_THRESHOLD, 0.8);
12
12
  const below = evaluateCodexAutoRefreshUsage({
13
- tokens: 232_559,
13
+ tokens: 206_719,
14
14
  contextWindowSize: 258_400,
15
15
  estimated: false,
16
16
  contextWindowEstimated: false,
@@ -19,7 +19,7 @@ test('evaluateCodexAutoRefreshUsage: default threshold is 90%', () => {
19
19
  assert.equal(below.reason, 'below_threshold');
20
20
 
21
21
  const atThreshold = evaluateCodexAutoRefreshUsage({
22
- tokens: 232_560,
22
+ tokens: 206_720,
23
23
  contextWindowSize: 258_400,
24
24
  estimated: false,
25
25
  contextWindowEstimated: false,
@@ -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 {
@@ -25,6 +25,7 @@ import { statSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
25
25
  import { homedir } from 'node:os';
26
26
  import { getStateDir, readAllSessionStates, snapshotStateMtimes, normalizeProjectPath, STALE_HIDE_MS } from './state-file.mjs';
27
27
  import { buildCodexMonitorUsage } from './codex-usage.mjs';
28
+ import { listCodexThreadCandidates } from './codex-thread-index.mjs';
28
29
  import { readLatestUsage } from './transcript-usage.mjs';
29
30
  import { startSizeQuery } from './terminal-size.mjs';
30
31
 
@@ -290,7 +291,7 @@ export function resolveColumns() {
290
291
 
291
292
  function formatLine({ state, usage, isActive, now = Date.now() }) {
292
293
  const project = basename(state.projectPath || '?');
293
- const shortId = state.sessionId.slice(0, 8);
294
+ const shortId = formatShortSessionId(state);
294
295
  const host = state.host === 'codex' ? 'Codex' : state.host === 'unknown' ? 'Unknown' : 'Claude';
295
296
  const tokens = usage?.tokens ?? 0;
296
297
  const max = usage?.contextWindowSize ?? 200_000;
@@ -334,6 +335,14 @@ function formatLine({ state, usage, isActive, now = Date.now() }) {
334
335
  return `${marker} ${projectCol} ${hostCol} ${idCol} ${agoCol} ${barCol} ${tokCol} ${modelCol}${warn}`;
335
336
  }
336
337
 
338
+ function formatShortSessionId(state) {
339
+ const sessionId = String(state?.sessionId ?? '');
340
+ if (state?.host === 'codex' && sessionId.startsWith('codex:')) {
341
+ return sessionId.slice('codex:'.length, 'codex:'.length + 8);
342
+ }
343
+ return sessionId.slice(0, 8);
344
+ }
345
+
337
346
  function statFile(path) {
338
347
  if (!path || !existsSync(path)) return null;
339
348
  try {
@@ -372,6 +381,82 @@ function resolveMonitorUsage(state) {
372
381
  return state.usage ?? null;
373
382
  }
374
383
 
384
+ let lastCodexDiscoveryError = null;
385
+
386
+ function reportCodexDiscoveryError(err) {
387
+ const msg = err instanceof Error ? err.message : 'unknown error';
388
+ if (msg === lastCodexDiscoveryError) return;
389
+ lastCodexDiscoveryError = msg;
390
+ process.stderr.write(`[Throughline] codex discovery error: ${msg}\n`);
391
+ }
392
+
393
+ function codexDiscoveryOptions(args = {}, cwd = process.cwd()) {
394
+ const allProjects = Boolean(args.all || args.session);
395
+ return {
396
+ projectPath: cwd,
397
+ allProjects,
398
+ limit: allProjects ? 100 : 30,
399
+ };
400
+ }
401
+
402
+ function discoverCodexSessionStates(args = {}, cwd = process.cwd()) {
403
+ let candidates;
404
+ try {
405
+ candidates = listCodexThreadCandidates(codexDiscoveryOptions(args, cwd));
406
+ } catch (err) {
407
+ reportCodexDiscoveryError(err);
408
+ return [];
409
+ }
410
+
411
+ lastCodexDiscoveryError = null;
412
+ return candidates.map((candidate) => ({
413
+ sessionId: `codex:${candidate.id}`,
414
+ host: 'codex',
415
+ projectPath: normalizeProjectPath(candidate.cwd ?? cwd),
416
+ transcriptPath: null,
417
+ rolloutPath: candidate.rolloutPath,
418
+ pid: null,
419
+ updatedAt: codexCandidateUpdatedAt(candidate),
420
+ discoveredFrom: 'codex-rollout-discovery',
421
+ }));
422
+ }
423
+
424
+ function codexCandidateUpdatedAt(candidate) {
425
+ const mtime = Number(candidate?.mtimeMs);
426
+ if (Number.isFinite(mtime) && mtime > 0) return mtime;
427
+ const parsed = Date.parse(candidate?.updatedAt ?? '');
428
+ return Number.isFinite(parsed) ? parsed : 0;
429
+ }
430
+
431
+ function mergeCodexDiscoveredStates(states, discovered) {
432
+ const bySession = new Map();
433
+ for (const state of states) bySession.set(state.sessionId, state);
434
+
435
+ for (const state of discovered) {
436
+ const existing = bySession.get(state.sessionId);
437
+ if (!existing) {
438
+ bySession.set(state.sessionId, state);
439
+ continue;
440
+ }
441
+
442
+ bySession.set(state.sessionId, {
443
+ ...existing,
444
+ host: existing.host ?? state.host,
445
+ projectPath: existing.projectPath || state.projectPath,
446
+ rolloutPath: state.rolloutPath ?? existing.rolloutPath,
447
+ updatedAt: Math.max(Number(existing.updatedAt) || 0, Number(state.updatedAt) || 0),
448
+ discoveredFrom: state.discoveredFrom,
449
+ });
450
+ }
451
+
452
+ return Array.from(bySession.values());
453
+ }
454
+
455
+ function readMonitorStates(args = {}, cwd = process.cwd()) {
456
+ const states = readAllSessionStates();
457
+ return mergeCodexDiscoveredStates(states, discoverCodexSessionStates(args, cwd));
458
+ }
459
+
375
460
  // --- フィルタ ---
376
461
  /**
377
462
  * セッション一覧に表示フィルタを適用する。
@@ -403,7 +488,7 @@ let lastRenderKey = '';
403
488
  * 注: state-file の mtime は Stop hook のタイミングで更新されるが、
404
489
  * transcript / rollout は実行中に太る。その live file 変化も render key に含める。
405
490
  */
406
- function computeRenderKey() {
491
+ function computeRenderKey(args = {}, cwd = process.cwd()) {
407
492
  const parts = [];
408
493
  // state mtimes
409
494
  const mtimes = snapshotStateMtimes();
@@ -411,7 +496,7 @@ function computeRenderKey() {
411
496
  for (const name of names) parts.push(`s:${name}:${mtimes.get(name)}`);
412
497
  // live transcript / rollout sizes(state ファイルを読まずに直接 stat、IO 最小化)
413
498
  try {
414
- const states = readAllSessionStates();
499
+ const states = readMonitorStates(args, cwd);
415
500
  for (const st of states) {
416
501
  for (const [kind, path] of [['t', st.transcriptPath], ['r', st.rolloutPath]]) {
417
502
  if (!path || !existsSync(path)) continue;
@@ -432,8 +517,8 @@ function computeRenderKey() {
432
517
  /**
433
518
  * 前回と比べてキーが変化していれば true。副作用として lastRenderKey を更新する。
434
519
  */
435
- function needsRerender() {
436
- const key = computeRenderKey();
520
+ function needsRerender(args = {}, cwd = process.cwd()) {
521
+ const key = computeRenderKey(args, cwd);
437
522
  if (key !== lastRenderKey) {
438
523
  lastRenderKey = key;
439
524
  return true;
@@ -448,7 +533,7 @@ function resetRenderKeyCache() {
448
533
 
449
534
  function renderFrame(args) {
450
535
  const now = Date.now();
451
- const states = readAllSessionStates().map((state) => withLiveActivity(state, now));
536
+ const states = readMonitorStates(args).map((state) => withLiveActivity(state, now));
452
537
  const filtered = filterStates(states, args, process.cwd()).sort(
453
538
  (a, b) => b.updatedAt - a.updatedAt,
454
539
  );
@@ -662,7 +747,7 @@ export function main() {
662
747
  safeRenderFrame(args);
663
748
  return;
664
749
  }
665
- if (needsRerender()) safeRenderFrame(args);
750
+ if (needsRerender(args)) safeRenderFrame(args);
666
751
  if (Date.now() - lastTimeAgoRefresh > TIME_AGO_REFRESH_MS) {
667
752
  lastTimeAgoRefresh = Date.now();
668
753
  safeRenderFrame(args);
@@ -738,6 +823,10 @@ export const _internal = {
738
823
  liveActivityMs,
739
824
  withLiveActivity,
740
825
  resolveMonitorUsage,
826
+ codexDiscoveryOptions,
827
+ discoverCodexSessionStates,
828
+ mergeCodexDiscoveredStates,
829
+ readMonitorStates,
741
830
  };
742
831
 
743
832
  // --- エントリポイント自動起動 ---
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
 
@@ -22,6 +22,8 @@ const {
22
22
  setMeasuredColumns,
23
23
  withLiveActivity,
24
24
  resolveMonitorUsage,
25
+ discoverCodexSessionStates,
26
+ mergeCodexDiscoveredStates,
25
27
  } = _internal;
26
28
 
27
29
  // state-file は projectPath を resolve + lowercase 正規化する。
@@ -244,6 +246,73 @@ test('resolveMonitorUsage: live Codex rollout overrides stale Stop snapshot', ()
244
246
  }
245
247
  });
246
248
 
249
+ test('discoverCodexSessionStates: finds Codex rollout without a monitor state file', () => {
250
+ const home = mkdtempSync(join(tmpdir(), 'tl-monitor-codex-home-'));
251
+ const project = mkdtempSync(join(tmpdir(), 'tl-monitor-codex-project-'));
252
+ const oldHome = process.env.CODEX_HOME;
253
+ try {
254
+ process.env.CODEX_HOME = home;
255
+ const threadId = '019e085c-d3b9-7b43-8445-0efe32416e3d';
256
+ const dir = join(home, 'sessions', '2026', '05', '09');
257
+ mkdirSync(dir, { recursive: true });
258
+ const rollout = join(dir, `rollout-2026-05-09T01-12-41-${threadId}.jsonl`);
259
+ writeFileSync(
260
+ rollout,
261
+ JSON.stringify({
262
+ type: 'session_meta',
263
+ payload: {
264
+ id: threadId,
265
+ cwd: project,
266
+ source: 'vscode',
267
+ cli_version: '0.129.0-alpha.15',
268
+ },
269
+ }) + '\n',
270
+ );
271
+
272
+ const states = discoverCodexSessionStates({ all: false, session: null }, project);
273
+
274
+ assert.equal(states.length, 1);
275
+ assert.equal(states[0].sessionId, `codex:${threadId}`);
276
+ assert.equal(states[0].host, 'codex');
277
+ assert.equal(states[0].projectPath, normalizeProjectPath(project));
278
+ assert.equal(states[0].rolloutPath, rollout);
279
+ assert.equal(states[0].discoveredFrom, 'codex-rollout-discovery');
280
+ } finally {
281
+ if (oldHome === undefined) delete process.env.CODEX_HOME;
282
+ else process.env.CODEX_HOME = oldHome;
283
+ rmSync(home, { recursive: true, force: true });
284
+ rmSync(project, { recursive: true, force: true });
285
+ }
286
+ });
287
+
288
+ test('mergeCodexDiscoveredStates: discovered rollout updates an existing Codex state entry', () => {
289
+ const existing = {
290
+ sessionId: 'codex:thread-a',
291
+ host: 'codex',
292
+ projectPath: CWD_FOO,
293
+ transcriptPath: null,
294
+ rolloutPath: '/old/rollout.jsonl',
295
+ updatedAt: 100,
296
+ usage: { tokens: 1 },
297
+ };
298
+ const discovered = {
299
+ sessionId: 'codex:thread-a',
300
+ host: 'codex',
301
+ projectPath: CWD_FOO,
302
+ transcriptPath: null,
303
+ rolloutPath: '/new/rollout.jsonl',
304
+ updatedAt: 200,
305
+ discoveredFrom: 'codex-rollout-discovery',
306
+ };
307
+
308
+ const states = mergeCodexDiscoveredStates([existing], [discovered]);
309
+
310
+ assert.equal(states.length, 1);
311
+ assert.equal(states[0].rolloutPath, '/new/rollout.jsonl');
312
+ assert.equal(states[0].updatedAt, 200);
313
+ assert.deepEqual(states[0].usage, { tokens: 1 });
314
+ });
315
+
247
316
  // ─── cellWidth ─────────────────────────────────────────────────────
248
317
 
249
318
  test('cellWidth: ASCII は 1 セル', () => {
@@ -451,6 +520,39 @@ test('formatLine: Codex estimated usage は host と est marker を表示する'
451
520
  assert.ok(out.includes('codex est win?'));
452
521
  });
453
522
 
523
+ test('formatLine: Codex session id は host prefix を外して 8 桁表示する', () => {
524
+ const args = makeLineArgs(0.5);
525
+ args.state.host = 'codex';
526
+ args.state.sessionId = 'codex:019e085c-d3b9-7b43-8445-0efe32416e3d';
527
+
528
+ const out = stripColors(formatLine(args));
529
+
530
+ assert.ok(out.includes('019e085c'), `expected raw Codex short id in output: ${out}`);
531
+ assert.ok(!out.includes('codex:01'), `did not expect prefixed short id in output: ${out}`);
532
+ });
533
+
534
+ test('formatLine: Codex live turn usage は transient output marker を表示しない', () => {
535
+ const args = makeLineArgs(0.5);
536
+ args.state.host = 'codex';
537
+ args.usage = {
538
+ tokens: 101_200,
539
+ inputTokens: 100_000,
540
+ model: 'gpt-5.5',
541
+ contextWindowSize: 200_000,
542
+ contextWindowEstimated: false,
543
+ outputTokens: 1200,
544
+ transientOutputTokens: 1200,
545
+ liveTurn: true,
546
+ estimated: false,
547
+ source: 'codex-rollout-token-count-live-turn',
548
+ };
549
+ const out = stripColors(formatLine(args));
550
+ assert.ok(out.includes('Codex'));
551
+ assert.ok(out.includes('101.2k / 200.0k'));
552
+ assert.ok(out.includes('gpt-5.5'));
553
+ assert.ok(!out.includes('live+'));
554
+ });
555
+
454
556
  test('formatLine: 70% 以上で "!" マーカーと弱めの文言', () => {
455
557
  const out = stripColors(formatLine(makeLineArgs(0.75)));
456
558
  assert.ok(out.includes('!'), 'should include ! marker');