throughline 0.4.2 → 0.4.4

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,29 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.4.4] — 2026-05-09
14
+
15
+ ### Changed
16
+
17
+ - Token monitor now treats Claude transcript and Codex rollout files as live
18
+ inputs. State-file `usage` snapshots remain a fallback, but the display and
19
+ stale hiding no longer wait for Stop hook completion when the live files are
20
+ still changing.
21
+ - `throughline install` now provisions or repairs the current project's VS Code
22
+ `Throughline Monitor` task when running under VS Code / Cursor / VSCodium, so
23
+ monitor auto-start setup no longer depends solely on the first hook event.
24
+
25
+ ## [0.4.3] — 2026-05-09
26
+
27
+ ### Changed
28
+
29
+ - Changed the installed Codex `$throughline` skill so bare `$throughline` runs
30
+ the scripted current-thread refresh directly:
31
+ `throughline trim --execute --host codex --all --json`. Doctor, dry-run,
32
+ preflight, restore-safety analysis, host primitive audit, and fresh-thread
33
+ handoff remain available only when explicitly requested instead of being the
34
+ normal skill path.
35
+
13
36
  ## [0.4.2] — 2026-05-09
14
37
 
15
38
  ### Fixed
package/README.ja.md CHANGED
@@ -188,7 +188,8 @@ adapter / projection として追加されます。
188
188
  Codex sidecar を使えます。使えない場合は、従来どおり Claude Haiku 経路を使います。
189
189
 
190
190
  Codex 側 trim (= same-thread context trim) は `throughline trim --execute --host codex`
191
- で発火します。Claude 側は `/clear` での auto path 引継ぎが本線になったため、
191
+ で発火します。Codex bare `$throughline` skill もこの scripted rollback + DB
192
+ memory inject を直接実行します。Claude 側は `/clear` での auto path 引継ぎが本線になったため、
192
193
  `/tl-trim` slash command は v0.4.0 で廃止されました。current-work framing は
193
194
  SessionStart 注入の Reading Contract / Continuation Instruction で同じ意図を
194
195
  継承しています。
@@ -212,6 +213,10 @@ throughline monitor --session <id-prefix>
212
213
  ▶ Throughline 2ed5039c ████░░░░░░░░░░░░░░░░ 205.1k / 21% 残 794.9k claude-opus-4-6
213
214
  ```
214
215
 
216
+ 監視中は Claude transcript / Codex rollout をライブに読み、Stop hook の state
217
+ snapshot はライブ usage が取れない場合の控えとして使います。これにより表示更新は
218
+ Stop 完了待ちではなくなります。
219
+
215
220
  詳細仕様 (resize 追従、1M context 検出、ステイル隠し、Stop hook の非同期化など) は
216
221
  [英語版 README](README.md#multi-session-token-monitor) を参照してください。
217
222
 
@@ -221,7 +226,7 @@ throughline monitor --session <id-prefix>
221
226
 
222
227
  | コマンド | 役割 |
223
228
  | --- | --- |
224
- | `throughline install` | `~/.claude/settings.json` (ユーザー全体) hook を登録 |
229
+ | `throughline install` | hook / Codex skill を登録し、VS Code 配下なら現プロジェクトの monitor task も配置 |
225
230
  | `throughline install --project` | 現リポジトリの `.claude/settings.json` だけに hook を登録 |
226
231
  | `throughline uninstall` | hook を削除 |
227
232
  | `throughline monitor` | マルチセッション監視を起動 |
@@ -233,7 +238,7 @@ throughline monitor --session <id-prefix>
233
238
  | `throughline codex-sidecar-diagnostics` | この project の `codex-sidecar` diagnostics status を確認 |
234
239
  | `throughline codex-sidecar-dry-run` | App Server を呼ばずに read-only sidecar request を正規化表示 |
235
240
  | `throughline trim --dry-run --host codex` | Codex same-thread trim の dry-run preview |
236
- | `throughline trim --execute --host codex` | Codex 同 thread の guarded rollback + DB memory inject |
241
+ | `throughline trim --execute --host codex` | Codex 同 thread の scripted rollback + DB memory inject |
237
242
  | `throughline doctor --session <id-prefix>` | 特定セッションの state/transcript ズレを診断 |
238
243
  | `throughline status` | DB 統計表示 (sessions / skeletons / bodies / details) |
239
244
  | `throughline --version` | インストール済みバージョンを表示 |
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ```bash
20
20
  npm install -g throughline
21
- throughline install # registers Claude hooks, Codex Stop hook, and Codex skill
21
+ throughline install # registers hooks/skills and provisions the VS Code monitor task
22
22
  ```
23
23
 
24
24
  That's it. Open any Claude Code session and your turns flow into
@@ -33,8 +33,10 @@ hook invokes the installed `bin/throughline.mjs` through an absolute Node path,
33
33
  so Codex App Server PATH differences do not hide the command. It is registered
34
34
  synchronously (`async: false`), matching the Codex hook behavior verified in
35
35
  Caveat. Existing non-Throughline Codex hooks are preserved. It also installs a
36
- global `$throughline` Codex skill, so in Codex you can ask for Throughline
37
- status, resume, summarize, or trim without typing the full guarded command.
36
+ global `$throughline` Codex skill. Bare `$throughline` runs the scripted
37
+ current-thread rollback + Throughline DB memory injection directly; ask
38
+ explicitly for status, resume, summarize, diagnostics, or fresh-thread handoff
39
+ when you want those read-only surfaces instead.
38
40
 
39
41
  ## How it compares
40
42
 
@@ -344,6 +346,7 @@ throughline trim --dry-run --host codex --codex-thread-id <id>
344
346
  throughline trim --dry-run --host codex --codex-thread-id <id> --preview-max-chars 4000
345
347
  throughline trim --preflight --host codex --codex-thread-id <id>
346
348
  CODEX_THREAD_ID=<id> throughline trim --preflight --host codex
349
+ throughline trim --execute --host codex --all --json
347
350
  # read-only app-server process restart smoke; not full VS Code restart-safe proof:
348
351
  # THROUGHLINE_EXPERIMENTAL_CODEX_RESTORE_SMOKE=1 throughline codex-restore-smoke --codex-thread-id <id> --json
349
352
  # read-only local restore source inventory; not full VS Code restart-safe proof:
@@ -514,9 +517,10 @@ Example output:
514
517
  (`input_tokens + cache_creation_input_tokens + cache_read_input_tokens`).
515
518
  No `length / 4` approximation.
516
519
  - **Codex token counts use the rollout `token_count` event when present.** The
517
- Codex Stop hook writes `codex:<thread_id>` monitor state and snapshots the
518
- latest verified rollout `token_count` sample. If a Codex rollout has no
519
- token-count event, Throughline can store an explicit estimate with
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
520
524
  `estimated: true` and the monitor marks it with `est`; it is not presented as
521
525
  exact usage.
522
526
  - **Codex auto-refresh mutates at the verified 90% threshold.** The Codex Stop
@@ -551,17 +555,17 @@ Example output:
551
555
  frame so the previous, wrongly-sized frame can't stack beneath it.
552
556
  - **Per-row "last updated" stamp.** Each session row carries an 8-cell
553
557
  `just now` / `24m ago` stamp right after the session id, placed before the
554
- bar so narrow terminals don't truncate it. It resets to `just now` on every
555
- Stop hook, so a growing stamp means the session is truly idle — not the
556
- monitor stuck. When you need more detail,
558
+ bar so narrow terminals don't truncate it. It follows the newest state,
559
+ transcript, or Codex rollout mtime, so active sessions stay visible and the
560
+ stamp can move before the next Stop hook completes. When you need more detail,
557
561
  `throughline doctor --session <id-prefix>` compares the state file against
558
562
  the actual transcript JSONL and flags drift, idle time, and
559
563
  `/clear`-induced transcript path staleness.
560
- - **State-backed usage snapshot.** When the Stop hook finishes a turn it
561
- persists the latest `tokens / model / contextWindowSize` back into the state
562
- file. The monitor prefers this snapshot over re-reading the JSONL, which
563
- removes a source of flicker when the transcript path in state drifts from
564
- the one Claude Code is currently appending to.
564
+ - **Live usage first, state snapshot as fallback.** When the Stop hook finishes
565
+ a turn it persists the latest `tokens / model / contextWindowSize` back into
566
+ the state file. The monitor now prefers live Claude transcript / Codex rollout
567
+ reads and uses the snapshot only when the live file cannot provide usage, so
568
+ the display no longer waits for Stop to update.
565
569
  - **Host-aware state.** Missing `host` means an older Claude state file.
566
570
  Codex states use `host: "codex"`, keep `transcriptPath: null`, and store the
567
571
  Codex rollout path separately as `rolloutPath` so the Claude transcript parser
@@ -578,15 +582,17 @@ Example output:
578
582
 
579
583
  ### VS Code auto-start (automatic)
580
584
 
581
- After `throughline install`, any VS Code / Cursor / VSCodium project you work in
582
- gets `.vscode/tasks.json` provisioned automatically on the first session event.
583
- The file configures `runOn: folderOpen` so the monitor appears in a dedicated
584
- terminal panel the next time you open that folder.
585
+ After `throughline install`, the current VS Code / Cursor / VSCodium project
586
+ gets `.vscode/tasks.json` provisioned immediately when VS Code environment
587
+ variables are present. Any other VS Code project you work in also gets the file
588
+ on the first session event. The file configures `runOn: folderOpen` so the
589
+ monitor appears in a dedicated terminal panel the next time you open that
590
+ folder.
585
591
 
586
- **How it works.** `ensureMonitorTaskFile` is called from **all three hooks
587
- (SessionStart, UserPromptSubmit, Stop)** as of v0.3.18. Whichever one fires
588
- first in your environment creates the file; the rest are idempotent no-ops.
589
- Once per project it inspects `.vscode/tasks.json`:
592
+ **How it works.** `ensureMonitorTaskFile` is called from `throughline install`
593
+ and from **all three hooks (SessionStart, UserPromptSubmit, Stop)**. Whichever
594
+ one fires first in your environment creates the file; the rest are idempotent
595
+ no-ops. Once per project it inspects `.vscode/tasks.json`:
590
596
 
591
597
  - **No file yet** → creates one with a single `Throughline Monitor` task, and
592
598
  emits a one-time `<system-reminder>` to stdout so Claude tells you a
@@ -644,7 +650,7 @@ entry to the `tasks` array yourself:
644
650
 
645
651
  | Command | What it does |
646
652
  | ---------------------------------------------- | ------------------------------------------------------------ |
647
- | `throughline install` | Register Claude user hooks/slash commands, the global Codex Stop hook, and the global `$throughline` Codex skill |
653
+ | `throughline install` | Register Claude user hooks/slash commands, the global Codex Stop hook, the global `$throughline` Codex skill, and the current VS Code monitor task when applicable |
648
654
  | `throughline install --project` | Register Claude hooks/slash commands in this repo only |
649
655
  | `throughline uninstall` | Remove Throughline-managed Claude hooks/slash commands, only the Throughline-managed Codex hook, and the `$throughline` Codex skill |
650
656
  | `throughline monitor [--all] [--session <id>]` | Run the multi-session token monitor |
@@ -673,7 +679,7 @@ entry to the `tasks` array yourself:
673
679
  | `throughline codex-sidecar-dry-run` | Print a normalized read-only sidecar request without running the app server |
674
680
  | `throughline trim --dry-run --host codex` | Preview Codex same-thread context trim memory and host boundary; does not rollback automatically |
675
681
  | `throughline trim --preflight --host codex` | Read/resume the explicit Codex thread and verify turn-count guards without rollback/inject |
676
- | `throughline trim --execute --host codex` | Explicit Codex rollback-inject path; requires Codex thread identity, injectable DB memory, and rollout/app-server turn-count agreement |
682
+ | `throughline trim --execute --host codex` | Scripted Codex current-thread rollback + Throughline DB memory inject; this is what bare `$throughline` runs in Codex |
677
683
  | `throughline status` | Print DB statistics (sessions, skeletons, bodies, details) |
678
684
  | `throughline --version` | Print the installed version |
679
685
 
@@ -8,20 +8,21 @@ description: Use when the user asks to use Throughline from Codex, continue or r
8
8
  Use this skill to operate Throughline from Codex without making the user type long
9
9
  commands.
10
10
 
11
- If the user invokes `$throughline` by itself, treat that as a request to inspect
12
- the current Codex context and prepare a refresh plan. Codex rollback / inject is
13
- enabled again after controlled rollback model-visible smokes failed to reproduce
14
- rollback marker resurrection.
11
+ If the user invokes `$throughline` by itself, treat that as a request to run the
12
+ scripted current-thread refresh now. The normal path is not an AI planning
13
+ exercise: it rolls back the current Codex thread and injects Throughline DB
14
+ memory using the original `/tl` contract.
15
15
 
16
16
  ## Core Rule
17
17
 
18
18
  Do not ask the user for a Codex thread id when the current environment can
19
19
  provide it. Prefer the current `CODEX_THREAD_ID` / `THROUGHLINE_CODEX_THREAD_ID`
20
- identity and verify it with `throughline doctor --codex`.
20
+ identity.
21
21
 
22
- Do not manually capture payloads before checking natural Stop hook capture.
23
- If natural capture looks wrong, inspect `doctor --codex`, the latest rollout,
24
- and hook logs first.
22
+ For bare `$throughline`, do not run doctor / dry-run / handoff / preflight first
23
+ and do not ask for confirmation. Execute the script command directly. If it
24
+ fails, report the error plainly instead of silently falling back to another
25
+ memory source or a fresh-thread handoff.
25
26
 
26
27
  ## Common Requests
27
28
 
@@ -30,27 +31,21 @@ and hook logs first.
30
31
  Run:
31
32
 
32
33
  ```bash
33
- throughline doctor --codex
34
- throughline trim --dry-run --host codex --all --json
35
- throughline codex-handoff-start --session codex:<current-thread-id> --json
36
- throughline trim --preflight --host codex --all --json
34
+ throughline trim --execute --host codex --all --json
37
35
  ```
38
36
 
39
- This is the safe Codex context-refresh inspection flow. `--all` previews a
40
- rollback-based reset of the model-visible thread. `codex-handoff-start` is an
41
- optional fresh-thread continuation surface: it validates the handoff, shows the
42
- model-smoke dry-run boundary, and can render the prompt with `--print-prompt`
43
- without mutating the current thread. Report the preflight result, handoff-start
44
- status, and context reduction estimate from the dry-run. Do not claim the
45
- refresh happened unless `trim --execute` or auto-refresh actually ran.
37
+ This is the scripted Codex context-refresh flow. It mutates the current Codex
38
+ thread by sending rollback + Throughline DB memory injection. Report only the
39
+ execution status, whether rollback / inject were sent, whether durable evidence
40
+ was observed, and the selected memory session.
46
41
 
47
42
  The injected memory must preserve the original `/tl` memory contract:
48
43
 
49
- - older turns: L1 summaries
50
44
  - recent work: L2 full bodies for the latest 20 turns
45
+ - older turns: L1 summaries
51
46
  - L3: detail references only; L3 bodies / tool payloads are not injected
52
47
 
53
- If the dry-run reports no captured turns or no injectable memory, say that
48
+ If there are no captured turns or no injectable Throughline DB memory, say that
54
49
  clearly.
55
50
 
56
51
  ### "Throughline status" / "doctor"
@@ -104,9 +99,19 @@ back to Claude Haiku.
104
99
 
105
100
  ### "trim" / "rewind" / "rollback" / "context cleanup"
106
101
 
107
- Default to the same inspection flow as bare `$throughline` when the user
102
+ Default to the same scripted execute flow as bare `$throughline` when the user
108
103
  asks to trim, rewind, rollback, clean up context, or use Throughline memory.
109
104
 
105
+ Execute:
106
+
107
+ ```bash
108
+ throughline trim --execute --host codex --all --json
109
+ ```
110
+
111
+ Report only the essential outcome. Do not introduce fresh-thread handoff,
112
+ restore-safety analysis, host primitive audit, or dry-run planning unless the
113
+ user explicitly asks for those diagnostics.
114
+
110
115
  Preview:
111
116
 
112
117
  ```bash
@@ -141,17 +146,17 @@ Execute path:
141
146
  throughline trim --execute --host codex --all
142
147
  ```
143
148
 
144
- Run this only when the user explicitly wants the current thread trimmed. It sends
145
- rollback + Throughline DB memory injection after the app-server guard checks.
149
+ This is the same command used by bare `$throughline`.
146
150
 
147
151
  ## User-Facing Explanation
148
152
 
149
153
  Explain the behavior simply:
150
154
 
151
155
  - normal Codex turn end: Stop hook captures DB memory and writes monitor state
152
- - `$throughline` / context refresh: doctor, dry-run, preflight, and optional
153
- execute; execute mutates the current Codex thread
154
- - Stop hook auto-refresh attempts rollback / inject when verified usage reaches
155
- 90%; estimate usage does not trigger mutation
156
- - dry-run reports estimated savings when there are rollback candidate turns, but
157
- exact host-visible token reduction is not yet measured with the host tokenizer
156
+ - `$throughline` / context refresh: one script command mutates the current Codex
157
+ thread by rollback + memory inject
158
+ - injected memory is L2 latest 20 full bodies + older L1 summaries + L3
159
+ references only
160
+ - diagnostics such as doctor, dry-run, preflight, fresh-thread handoff, restore
161
+ safety, and host primitive audit are optional tools, not the normal
162
+ `$throughline` path
@@ -1,7 +1,7 @@
1
1
  interface:
2
2
  display_name: "Throughline"
3
3
  short_description: "Operate Throughline memory, Codex capture, resume, and trim"
4
- default_prompt: "Use $throughline to check capture, resume memory, summarize, inspect guarded Codex trim, or prepare a safe fresh-thread handoff without typing long commands."
4
+ default_prompt: "Use $throughline to run the scripted current-thread rollback + Throughline memory inject, or explicitly ask for status, resume, summarize, diagnostics, or fresh-thread handoff."
5
5
 
6
6
  policy:
7
7
  allow_implicit_invocation: true
@@ -440,7 +440,7 @@ Phase 5 implementation status (2026-05-06):
440
440
  - [x] `doctor --codex` は Claude settings を変更しない。Codex primary entrypoint の診断に限定する。
441
441
  - [x] 実セッションで `doctor --codex` -> `codex-capture` -> `codex-resume --format item-json` -> `doctor --codex` の local smoke を実施した。`codex-resume` は developer message item JSON を描画し、再診断で captured DB session が 1 件として表示された。
442
442
  - [x] 実セッションで `codex-visibility-smoke` を実施した。`THROUGHLINE_EXPERIMENTAL_CODEX_MODEL_VISIBLE_SMOKE=1` を必須にし、長い model turn に備えて `--request-timeout-ms 150000` / `--timeout-ms 180000` を使えるようにした。
443
- - [x] Codex primary の setup / install 手順は README に記録した。global install は Codex Stop hook と `$throughline` skill を自動登録する。2026-05-08 以降、skilldiagnostics / dry-run / preflight を既定の確認 flow とし、ユーザーが current-thread trim を明示した場合は `trim --execute --host codex --all` を案内する。
443
+ - [x] Codex primary の setup / install 手順は README に記録した。global install は Codex Stop hook と `$throughline` skill を自動登録する。2026-05-09 以降、bare `$throughline` AI による診断 flow ではなく `trim --execute --host codex --all --json` を直接走らせる scripted current-thread refresh とする。diagnostics / dry-run / preflight / fresh-thread handoff は明示要求時だけ使う。
444
444
  - [x] `codex-summarize` を明示診断・運用 flow に追加した。Codex CLI backend を使い、Claude Haiku へ fallback しない。
445
445
  - [x] Codex primary の summarize / guarded execute まで含む end-to-end smoke を実施した。2026-05-07 correction では guarded execute を live app-server smoke としてのみ扱った。2026-05-08 unblock 後は、controlled rollback model-visible smoke の `not-reproduced` と current-thread live run の `execute-durable-verified` を合わせて、Codex current-thread trim 完了条件に含める。
446
446
  - `codex-capture`: `capturedTurns = 41`, `capturedRows = 75`, `capturedDetails = 2424`
@@ -459,8 +459,8 @@ Phase 5 implementation status (2026-05-06):
459
459
  - 絶対パス型の再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Stop 先頭は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false`、`timeoutSec: 300` になり、Caveat / Spotter hooks は 2 番目以降に保持された。`doctor --codex` は `Codex hooks feature: enabled` / `Codex Stop hook: registered` を表示した。`codex exec --json -C /home/kite/projects/Throughline "Reply exactly: TL_CODEX_ABSOLUTE_STOP_SMOKE_20260506"` で child thread `019dfd5e-1248-7c11-8ddc-97e1b0701e10` が作られ、直後の `throughline doctor --codex` で latest DB session が `codex:019dfd5e-1248-7c11-8ddc-97e1b0701e10` に進んだ。
460
460
  - 追加確認: current VSCode-origin parent thread `019dfd38-c530-71c3-b7b8-180bdd3054bc` は hook shape 変更前に開始していたため、変更後の自然 Stop smoke としては不適格。assistant final 後に rollout は `task_complete` まで進んだが latest DB session は exec child のままだった。次に VSCode-origin を見る場合は、hook shape 変更後に新しく開始した Codex session で確認する。
461
461
  - 最終確認: hook shape 変更後に新しく開始した VSCode-origin Codex session で 1 turn 完了後、`throughline doctor --codex` を実行した。`current Codex thread` は `019dfd62-9a9d-7211-bf91-89d8e3fc908e`、`latest DB session` は `codex:019dfd62-9a9d-7211-bf91-89d8e3fc908e` で一致し、`Codex hooks feature: enabled`、`Codex Stop hook: registered`、command は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false`、`timeoutSec: 300` だった。VSCode-origin の自然 Stop hook による DB capture も解決済みとして扱う。
462
- - 2026-05-07 correction: 以下の Codex trim smoke は live app-server primitive の履歴として残す。ただし 2026-05-06 incident 後、restart / reconnect 越しの durable context trim 成功とは扱わない。現在の正は [THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md](THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md) であり、`$throughline` / Codex Stop hook mutation を自動実行しない。
463
- - UX 修正: 手動 trim の長い `throughline trim --execute --host codex --codex-thread-id <id>` をユーザーに直接打たせるのではなく、global install `$throughline` Codex skill を配置する。ただし 2026-05-06 incident 後、skill の既定動作は `doctor --codex`、`trim --dry-run --all`、`trim --preflight --all` までに制限する。bare `$throughline` rollback + curated memory inject を実行する context refresh ではなく、安全な現状確認として扱う。
462
+ - historical 2026-05-07 correction: 以下の Codex trim smoke は live app-server primitive の履歴として残す。ただし 2026-05-06 incident 後、restart / reconnect 越しの durable context trim 成功とは一時的に扱わず、`$throughline` / Codex Stop hook automatic mutation を止めていた。
463
+ - 2026-05-09 UX 修正: bare `$throughline` は `doctor --codex` / `trim --dry-run --all` / `trim --preflight --all` を AI に順番実行させる説明 surface ではなく、`throughline trim --execute --host codex --all --json` を直接実行する scripted current-thread refresh に戻す。注入 memory L2 最新 20 full bodies + L1 older summaries + L3 references only。diagnostics / dry-run / preflight / fresh-thread handoff は明示要求時だけ使う。
464
464
  - memory contract 修正: Codex guarded trim でも注入 memory は元の `/tl` 思想を正とする。古い turn は L1 summaries、直近 20 turn は L2 full bodies、L3 は reference only で、L3 bodies / tool payloads は注入しない。rollout source は rollback candidate / app-server turn count guard の根拠であり、Throughline DB memory がある場合に rollout active work preview を注入 memory として使わない。DB memory が無い execute は rollout preview を注入せず、mutation 前に拒否する。
465
465
  - doctor visibility 修正: `doctor --codex` は旧 context refresh readiness として rollback source、inject memory source、memory contract、L1 summaries / recent L2 bodies / L3 references-only count、heuristic reduction estimate を表示していた。実 thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` では live readiness として `context refresh: ready`、`rollback source: codex-rollout`、`inject memory source: throughline-db`、`memory contract: older L1 + latest 20 L2 full bodies + L3 references only` を確認した。ただし incident 後は restart-safe readiness ではない。
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 で削減できない」という意味ではない。
@@ -21,6 +21,8 @@
21
21
 
22
22
  2026-05-08 unblock: その後の切り分けで、incident thread の retained text は app-server response 上では `aggregatedOutput` など quoted/tool-output field に分類され、controlled rollback model-visible smoke は app-server restart 境界と VS Code reload/reconnect 境界の両方で `not-reproduced` だった。これを受け、Codex `trim --execute --host codex` と Stop hook auto-refresh の過剰 blocker は解除する。`compacted.replacement_history` retention、restore-safety risk、host primitive audit は引き続き diagnostics だが、単独では mutation 前 refusal にしない。DB memory 不在と rollout/app-server turn-count 不一致は引き続き mutation 前 blocker。
23
23
 
24
+ 2026-05-09 skill UX: bare `$throughline` は diagnostics / dry-run / preflight を AI に順番実行させる surface ではなく、`throughline trim --execute --host codex --all --json` を直接走らせる scripted current-thread refresh とする。目的は rollback と、Throughline DB の L2 最新 20 full bodies + older L1 summaries + L3 references-only memory injection のみ。doctor / dry-run / preflight / fresh-thread handoff / restore-safety / host primitive audit は明示診断用であり、通常 `$throughline` の前段にはしない。
25
+
24
26
  2026-05-07 host primitive audit: `throughline codex-host-primitive-audit` で installed Codex app-server schema を機械監査した。`thread/rollback` / `thread/inject_items` / `thread/compact/start` / `thread/start` / `thread/fork` / `thread/resume` は存在するが、rollback 済み user text を current-thread の model-visible input へ復活させない deletion / isolation / projection primitive は見つからなかった。`thread/resume(history)` は schema 上 `[UNSTABLE] FOR CODEX CLOUD - DO NOT USE` で、`thread_id` も ignored になるため、Throughline の current-thread repair primitive には採用しない。
25
27
 
26
28
  ## 概要
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -15,6 +15,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, copyFi
15
15
  import { join, dirname, resolve, delimiter } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { homedir } from 'node:os';
18
+ import { ensureMonitorTaskFile, shouldRecommendGitignore } from '../vscode-task.mjs';
18
19
 
19
20
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
20
21
  const SLASH_COMMANDS_SRC = join(PACKAGE_ROOT, '.claude', 'commands');
@@ -400,6 +401,10 @@ export async function run(args = []) {
400
401
  const { installed: installedCommands, skipped } = installSlashCommands(commandsDir);
401
402
  const codex = args.includes('--project') ? null : installCodexHooks();
402
403
  const codexSkills = args.includes('--project') ? { installed: [], skipped: null } : installCodexSkills(codexSkillsDir);
404
+ const monitorTask = ensureMonitorTaskFile({
405
+ cwd: process.cwd(),
406
+ env: { ...process.env, THROUGHLINE_SUPPRESS_VSCODE_NOTICES: '1' },
407
+ });
403
408
 
404
409
  const scope = args.includes('--project') ? 'プロジェクトローカル' : 'グローバル(全プロジェクト)';
405
410
  console.log(`Throughline hooks をインストールしました [${scope}]`);
@@ -436,6 +441,15 @@ export async function run(args = []) {
436
441
  console.log('注意: パッケージ内に Codex skills のソースが見つからないためスキップしました。');
437
442
  console.log('');
438
443
  }
444
+ if (monitorTask.action === 'created' || monitorTask.action === 'merged' || monitorTask.action === 'repaired') {
445
+ console.log(`VSCode monitor task を${monitorTask.action === 'repaired' ? '修復' : '配置'}しました:`);
446
+ console.log(` ${monitorTask.path}`);
447
+ console.log(' 既に VSCode でこのフォルダを開いている場合は Developer: Reload Window を 1 回実行してください。');
448
+ if (shouldRecommendGitignore(process.cwd())) {
449
+ console.log(' 共有リポジトリでは .vscode/tasks.json を .gitignore に追加することを推奨します。');
450
+ }
451
+ console.log('');
452
+ }
439
453
  console.log(' アンインストール: throughline uninstall');
440
454
 
441
455
  if (!resolveThroughlineOnPath()) {
@@ -30,13 +30,16 @@ function silence() {
30
30
  const origLog = console.log;
31
31
  const origErr = console.error;
32
32
  const origStderrWrite = process.stderr.write.bind(process.stderr);
33
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
33
34
  console.log = () => {};
34
35
  console.error = () => {};
35
36
  process.stderr.write = () => true;
37
+ process.stdout.write = () => true;
36
38
  return () => {
37
39
  console.log = origLog;
38
40
  console.error = origErr;
39
41
  process.stderr.write = origStderrWrite;
42
+ process.stdout.write = origStdoutWrite;
40
43
  };
41
44
  }
42
45
 
@@ -133,16 +136,48 @@ test('global install copies Throughline Codex skill to ~/.codex/skills/', async
133
136
  const metadataBody = readFileSync(metadata, 'utf8');
134
137
  assert.match(skillBody, /name: throughline/);
135
138
  assert.match(skillBody, /Bare "\$throughline"/);
136
- assert.match(skillBody, /throughline codex-handoff-start --session codex:<current-thread-id> --json/);
137
139
  assert.match(skillBody, /throughline trim --execute --host codex --all/);
138
- assert.match(metadataBody, /inspect guarded Codex trim/);
140
+ assert.match(skillBody, /do not run doctor \/ dry-run \/ handoff \/ preflight first/);
141
+ assert.match(metadataBody, /scripted current-thread rollback \+ Throughline memory inject/);
139
142
  assert.doesNotMatch(metadataBody, /preview blocked Codex trim/);
143
+ assert.doesNotMatch(metadataBody, /inspect guarded Codex trim/);
140
144
  } finally {
141
145
  unsilence();
142
146
  home.restore();
143
147
  }
144
148
  });
145
149
 
150
+ test('global install provisions VSCode monitor task for the current project when running under VSCode', async () => {
151
+ const home = makeTempHome();
152
+ if (home.resolved !== home.dir) {
153
+ home.restore();
154
+ return;
155
+ }
156
+ const projectDir = mkdtempSync(join(tmpdir(), 'tl-install-monitor-'));
157
+ const origCwd = process.cwd();
158
+ const origVscodePid = process.env.VSCODE_PID;
159
+ process.chdir(projectDir);
160
+ process.env.VSCODE_PID = '12345';
161
+ const unsilence = silence();
162
+ try {
163
+ await run([]);
164
+ const tasksPath = join(projectDir, '.vscode', 'tasks.json');
165
+ assert.ok(existsSync(tasksPath), 'install should create current-project VSCode tasks.json');
166
+ const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
167
+ const task = tasks.tasks.find((t) => t.label === 'Throughline Monitor');
168
+ assert.ok(task, 'Throughline Monitor task should be present');
169
+ assert.deepEqual(task.args.slice(1), ['monitor']);
170
+ assert.deepEqual(task.runOptions, { runOn: 'folderOpen' });
171
+ } finally {
172
+ unsilence();
173
+ if (origVscodePid === undefined) delete process.env.VSCODE_PID;
174
+ else process.env.VSCODE_PID = origVscodePid;
175
+ process.chdir(origCwd);
176
+ home.restore();
177
+ rmSync(projectDir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
146
181
  test('global install preserves existing Codex hooks and is idempotent', async () => {
147
182
  const home = makeTempHome();
148
183
  if (home.resolved !== home.dir) {
@@ -47,10 +47,9 @@ export function normalizeProjectPath(p) {
47
47
  * host?: 'claude'|'codex',
48
48
  * }} data
49
49
  *
50
- * usage: monitor が表示する tokens/model/contextWindowSize をここに固定保存する。
51
- * Stop hook readLatestUsage の結果を載せることで、monitor 側が毎フレーム JSONL
52
- * 再スキャンする必要がなくなる。旧バージョン互換のため optional (無ければ monitor が
53
- * transcriptPath を読んでフォールバック)。
50
+ * usage: Stop hook 完了時点の tokens/model/contextWindowSize fallback snapshot。
51
+ * monitor はライブ transcript / rollout を優先して読み、ライブ usage が取れない場合だけ
52
+ * この snapshot を使う。旧バージョン互換のため optional。
54
53
  */
55
54
  export function writeSessionState({
56
55
  sessionId,
@@ -13,16 +13,18 @@
13
13
  * - 状態ファイルはセッション単位 (~/.throughline/state/<session_id>.json)
14
14
  * - setInterval (1s) + mtime 差分検知で更新を捕捉
15
15
  * - updatedAt 降順ソート、先頭行を ▶ でハイライト
16
- * - stale は PID 生存チェックで判定
16
+ * - stale は state 更新時刻 + live transcript / rollout mtime で判定
17
17
  * - Claude は transcript JSONL の最新 assistant usage を直読
18
- * - Codex は Stop hook state.usage に固定した rollout usage / estimate を表示
18
+ * - Codex は rollout JSONL token_count / active-text estimate を直読
19
+ * - state.usage は live ファイルが読めない場合の最後の既知値としてだけ使う
19
20
  */
20
21
 
21
22
  import { basename, dirname, join } from 'node:path';
22
23
  import { stripVTControlCharacters } from 'node:util';
23
24
  import { statSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
24
25
  import { homedir } from 'node:os';
25
- import { getStateDir, readAllSessionStates, snapshotStateMtimes, normalizeProjectPath } from './state-file.mjs';
26
+ import { getStateDir, readAllSessionStates, snapshotStateMtimes, normalizeProjectPath, STALE_HIDE_MS } from './state-file.mjs';
27
+ import { buildCodexMonitorUsage } from './codex-usage.mjs';
26
28
  import { readLatestUsage } from './transcript-usage.mjs';
27
29
  import { startSizeQuery } from './terminal-size.mjs';
28
30
 
@@ -332,6 +334,44 @@ function formatLine({ state, usage, isActive, now = Date.now() }) {
332
334
  return `${marker} ${projectCol} ${hostCol} ${idCol} ${agoCol} ${barCol} ${tokCol} ${modelCol}${warn}`;
333
335
  }
334
336
 
337
+ function statFile(path) {
338
+ if (!path || !existsSync(path)) return null;
339
+ try {
340
+ return statSync(path);
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+
346
+ function liveActivityMs(state) {
347
+ const transcript = statFile(state.transcriptPath);
348
+ const rollout = statFile(state.rolloutPath);
349
+ return Math.max(
350
+ Number(state.updatedAt) || 0,
351
+ transcript?.mtimeMs ?? 0,
352
+ rollout?.mtimeMs ?? 0,
353
+ );
354
+ }
355
+
356
+ function withLiveActivity(state, now = Date.now()) {
357
+ const updatedAt = liveActivityMs(state);
358
+ return {
359
+ ...state,
360
+ updatedAt,
361
+ stale: now - updatedAt > STALE_HIDE_MS,
362
+ };
363
+ }
364
+
365
+ function resolveMonitorUsage(state) {
366
+ if (state.host === 'codex' && state.rolloutPath) {
367
+ return buildCodexMonitorUsage(state.rolloutPath) ?? state.usage ?? null;
368
+ }
369
+ if (state.transcriptPath) {
370
+ return readLatestUsage(state.transcriptPath) ?? state.usage ?? null;
371
+ }
372
+ return state.usage ?? null;
373
+ }
374
+
335
375
  // --- フィルタ ---
336
376
  /**
337
377
  * セッション一覧に表示フィルタを適用する。
@@ -356,12 +396,12 @@ let lastRenderedLines = 0;
356
396
  let lastRenderKey = '';
357
397
 
358
398
  /**
359
- * 再描画要否の判定キー。state ファイル群の mtime と transcript JSONL の size を
399
+ * 再描画要否の判定キー。state ファイル群の mtime と transcript / rollout JSONL の
400
+ * size + mtime を
360
401
  * 1 本の文字列にまとめてハッシュキーとする。キーが前回と同じなら描画スキップ。
361
402
  *
362
- * 注: transcript は JSONL append-only なので size 変化 = 新しい usage エントリ到来と
363
- * 同義。mtime だけでは transcript 更新を検出できない(state-file mtime
364
- * Stop hook のタイミングで更新され、transcript は Claude の stream 中に太る)。
403
+ * 注: state-file mtime Stop hook のタイミングで更新されるが、
404
+ * transcript / rollout は実行中に太る。その live file 変化も render key に含める。
365
405
  */
366
406
  function computeRenderKey() {
367
407
  const parts = [];
@@ -369,16 +409,18 @@ function computeRenderKey() {
369
409
  const mtimes = snapshotStateMtimes();
370
410
  const names = Array.from(mtimes.keys()).sort();
371
411
  for (const name of names) parts.push(`s:${name}:${mtimes.get(name)}`);
372
- // transcript sizes(state ファイルを読まずに直接 stat、IO 最小化)
412
+ // live transcript / rollout sizes(state ファイルを読まずに直接 stat、IO 最小化)
373
413
  try {
374
414
  const states = readAllSessionStates();
375
415
  for (const st of states) {
376
- if (!st.transcriptPath || !existsSync(st.transcriptPath)) continue;
377
- try {
378
- const size = statSync(st.transcriptPath).size;
379
- parts.push(`t:${st.sessionId}:${size}`);
380
- } catch {
381
- // stat 失敗は無視(次フレームで回復)
416
+ for (const [kind, path] of [['t', st.transcriptPath], ['r', st.rolloutPath]]) {
417
+ if (!path || !existsSync(path)) continue;
418
+ try {
419
+ const stat = statSync(path);
420
+ parts.push(`${kind}:${st.sessionId}:${stat.size}:${stat.mtimeMs}`);
421
+ } catch {
422
+ // stat 失敗は無視(次フレームで回復)
423
+ }
382
424
  }
383
425
  }
384
426
  } catch {
@@ -405,7 +447,8 @@ function resetRenderKeyCache() {
405
447
  }
406
448
 
407
449
  function renderFrame(args) {
408
- const states = readAllSessionStates();
450
+ const now = Date.now();
451
+ const states = readAllSessionStates().map((state) => withLiveActivity(state, now));
409
452
  const filtered = filterStates(states, args, process.cwd()).sort(
410
453
  (a, b) => b.updatedAt - a.updatedAt,
411
454
  );
@@ -428,15 +471,9 @@ function renderFrame(args) {
428
471
  `[Throughline] ${filtered.length} セッション${args.all ? ' (--all)' : ''}`,
429
472
  );
430
473
  lines.push(header);
431
- const now = Date.now();
432
474
  for (let i = 0; i < filtered.length; i++) {
433
475
  const state = filtered[i];
434
- // Stop hook state.usage に固定値を入れていればそれを使う(JSONL 再スキャン不要)。
435
- // 旧バージョンが書いた Claude state や usage スナップショットが取れなかったターンでは
436
- // transcriptPath を直読。state 側の情報が 1 本化されると
437
- // 「state が古い JSONL を指している」時の表示ブレが減る。
438
- const usage = state.usage
439
- ?? (state.transcriptPath ? readLatestUsage(state.transcriptPath) : null);
476
+ const usage = resolveMonitorUsage(state);
440
477
  lines.push(formatLine({ state, usage, isActive: i === 0, now }));
441
478
  }
442
479
  }
@@ -698,6 +735,9 @@ export const _internal = {
698
735
  shouldForceFullRedraw,
699
736
  resolveColumns,
700
737
  setMeasuredColumns,
738
+ liveActivityMs,
739
+ withLiveActivity,
740
+ resolveMonitorUsage,
701
741
  };
702
742
 
703
743
  // --- エントリポイント自動起動 ---
@@ -1,5 +1,8 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
3
6
 
4
7
  import { _internal } from './token-monitor.mjs';
5
8
  import { normalizeProjectPath } from './state-file.mjs';
@@ -17,6 +20,8 @@ const {
17
20
  shouldForceFullRedraw,
18
21
  resolveColumns,
19
22
  setMeasuredColumns,
23
+ withLiveActivity,
24
+ resolveMonitorUsage,
20
25
  } = _internal;
21
26
 
22
27
  // state-file は projectPath を resolve + lowercase 正規化する。
@@ -128,6 +133,117 @@ test('filterStates: cwd 不一致は除外(--session も --all もなし)',
128
133
  assert.equal(result[0].sessionId, 'a');
129
134
  });
130
135
 
136
+ test('withLiveActivity: transcript mtime keeps a long-running session visible before Stop', () => {
137
+ const dir = mkdtempSync(join(tmpdir(), 'tl-monitor-live-activity-'));
138
+ try {
139
+ const transcript = join(dir, 'session.jsonl');
140
+ writeFileSync(transcript, '{"type":"user","message":{"content":"working"}}\n');
141
+ const now = Date.now();
142
+ const old = now - (20 * 60 * 1000);
143
+ const state = withLiveActivity({
144
+ sessionId: 'live-session',
145
+ host: 'claude',
146
+ projectPath: CWD_FOO,
147
+ transcriptPath: transcript,
148
+ rolloutPath: null,
149
+ updatedAt: old,
150
+ stale: true,
151
+ }, now);
152
+
153
+ assert.equal(state.stale, false);
154
+ assert.ok(state.updatedAt > old);
155
+ } finally {
156
+ rmSync(dir, { recursive: true, force: true });
157
+ }
158
+ });
159
+
160
+ test('resolveMonitorUsage: live Claude transcript overrides stale Stop snapshot', () => {
161
+ const dir = mkdtempSync(join(tmpdir(), 'tl-monitor-live-usage-'));
162
+ try {
163
+ const transcript = join(dir, 'session.jsonl');
164
+ writeFileSync(transcript, [
165
+ JSON.stringify({
166
+ type: 'assistant',
167
+ message: {
168
+ model: 'claude-opus-4-6',
169
+ usage: {
170
+ input_tokens: 1234,
171
+ cache_creation_input_tokens: 200,
172
+ cache_read_input_tokens: 300,
173
+ output_tokens: 10,
174
+ },
175
+ },
176
+ }),
177
+ '',
178
+ ].join('\n'));
179
+
180
+ const usage = resolveMonitorUsage({
181
+ sessionId: 'claude-session',
182
+ host: 'claude',
183
+ projectPath: CWD_FOO,
184
+ transcriptPath: transcript,
185
+ rolloutPath: null,
186
+ updatedAt: Date.now(),
187
+ usage: {
188
+ tokens: 1,
189
+ model: 'old-snapshot',
190
+ contextWindowSize: 200_000,
191
+ outputTokens: 0,
192
+ },
193
+ });
194
+
195
+ assert.equal(usage.tokens, 1734);
196
+ assert.equal(usage.model, 'claude-opus-4-6');
197
+ } finally {
198
+ rmSync(dir, { recursive: true, force: true });
199
+ }
200
+ });
201
+
202
+ test('resolveMonitorUsage: live Codex rollout overrides stale Stop snapshot', () => {
203
+ const dir = mkdtempSync(join(tmpdir(), 'tl-monitor-live-codex-'));
204
+ try {
205
+ const rollout = join(dir, 'rollout.jsonl');
206
+ writeFileSync(rollout, [
207
+ JSON.stringify({
208
+ type: 'turn_context',
209
+ payload: { model: 'gpt-5.5' },
210
+ }),
211
+ JSON.stringify({
212
+ type: 'event_msg',
213
+ payload: {
214
+ type: 'token_count',
215
+ info: {
216
+ last_token_usage: { input_tokens: 4567, output_tokens: 89 },
217
+ model_context_window: 258400,
218
+ },
219
+ },
220
+ }),
221
+ '',
222
+ ].join('\n'));
223
+
224
+ const usage = resolveMonitorUsage({
225
+ sessionId: 'codex:thread',
226
+ host: 'codex',
227
+ projectPath: CWD_FOO,
228
+ transcriptPath: null,
229
+ rolloutPath: rollout,
230
+ updatedAt: Date.now(),
231
+ usage: {
232
+ tokens: 1,
233
+ model: 'old-snapshot',
234
+ contextWindowSize: 200_000,
235
+ outputTokens: 0,
236
+ },
237
+ });
238
+
239
+ assert.equal(usage.tokens, 4567);
240
+ assert.equal(usage.model, 'gpt-5.5');
241
+ assert.equal(usage.estimated, false);
242
+ } finally {
243
+ rmSync(dir, { recursive: true, force: true });
244
+ }
245
+ });
246
+
131
247
  // ─── cellWidth ─────────────────────────────────────────────────────
132
248
 
133
249
  test('cellWidth: ASCII は 1 セル', () => {
@@ -279,9 +279,8 @@ export async function run() {
279
279
  }
280
280
  }
281
281
 
282
- // monitor JSONL を毎フレーム再スキャンせずに済むよう、現在確定している usage を
283
- // state ファイルに固定する。Stop 完了時点で assistant エントリは transcript に
284
- // 書き出し済みなので readLatestUsage が最新値を返す。
282
+ // monitor fallback 用に、Stop 完了時点で確定している usage を state ファイルにも
283
+ // 保存する。通常表示はライブ transcript を優先し、読めない時だけ snapshot を使う。
285
284
  // 取得失敗は致命ではないので try/catch で握る(stderr には出す)。
286
285
  try {
287
286
  const usage = transcript_path ? readLatestUsage(transcript_path) : null;