throughline 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,45 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.4.1] — 2026-05-09
14
+
15
+ ### Changed
16
+
17
+ - **`/clear` も baton を書き込むように変更**。UserPromptSubmit hook で `/clear`
18
+ を検出した時点で当該セッションの `session_id` を `handoff_batons` に書き、
19
+ 次の新規 SessionStart が確定的にそのセッションを引き継ぐ。これにより、複数
20
+ VSCode ウィンドウなどで「最新更新セッション ≠ /clear したセッション」になる
21
+ シナリオで `findLatestClaudePredecessor` heuristic が誤った前任を選ぶ問題を
22
+ 解消。
23
+ - **2 経路の優先順位を入れ替え**: baton path が **primary**、`source='clear'`
24
+ の auto path は **fallback**。auto path は `/clear` が UserPromptSubmit hook
25
+ に届かない経路 (例: VSCode 拡張のメニュー由来) のためのフォールバック扱い。
26
+
27
+ ### Added
28
+
29
+ - `src/prompt-submit.mjs`: `isClearCommand` 判定 (`/clear`, `/clear ...`,
30
+ 前後空白許容、`/cleared` / `/clearcache` 等の prefix 偽陽性は拒否)。
31
+ - `~/.throughline/logs/baton-write.log` の `trigger` フィールドに
32
+ `'tl' | 'clear'` を記録。
33
+ - `src/prompt-submit.test.mjs`: `isBatonCommand` / `isClearCommand` の判定
34
+ テスト 14 件。
35
+ - `src/hook-entrypoints.test.mjs`: `/clear` baton の subprocess+DB 実体テスト
36
+ 3 件 (`/clear` 書き込み / `/tl` → `/clear` 後勝ち上書き / 通常 prompt は no-op)。
37
+
38
+ ### Notes
39
+
40
+ - 既存の `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` env は **fallback path のみに作用**
41
+ するようになった。typed `/clear` は env に関係なく baton 書き込み → 引継ぎ発火
42
+ する (= ユーザーが明示的に `/clear` を打った時点で「続けたい」という意思表示
43
+ と解釈する)。auto path (VSCode メニュー由来) には引き続き env が効く。
44
+
45
+ ### Repository hygiene
46
+
47
+ - `.vscode/tasks.json` を git 追跡から外す (`.gitignore` に追加)。
48
+ `ensureMonitorTaskFile` が hook 発火ごとに絶対パスを書き換えるため、追跡
49
+ 対象に置くと別 OS / 別マシンで毎回 dirty diff が出続けていた。各マシンでは
50
+ 初回 hook 発火時に自動生成される。
51
+
13
52
  ## [0.4.0] — 2026-05-08
14
53
 
15
54
  ### Breaking changes
package/README.md CHANGED
@@ -22,10 +22,10 @@ throughline install # registers Claude hooks, Codex Stop hook, and Codex ski
22
22
  ```
23
23
 
24
24
  That's it. Open any Claude Code session and your turns flow into
25
- `~/.throughline/throughline.db` automatically. After 50 turns of work, type
26
- `/clear` (or just open a new chat), then `/tl` first if you want the next
27
- session to inherit the memory the new session resumes mid-thought instead
28
- of starting from zero.
25
+ `~/.throughline/throughline.db` automatically. After 50 turns of work, just
26
+ type `/clear` the new session resumes mid-thought instead of starting from
27
+ zero. (For non-`/clear` boundaries such as a brand-new chat or a VSCode
28
+ restart, type `/tl` first to mark the predecessor.)
29
29
 
30
30
  Global install also registers a Codex Stop hook in `~/.codex/hooks.json` and
31
31
  enables `[features].codex_hooks = true` in `~/.codex/config.toml`. The Codex
@@ -42,8 +42,8 @@ status, resume, summarize, or trim without typing the full guarded command.
42
42
  |---|---|---|---|
43
43
  | **Compression axis** | content **type** (text vs tool I/O) | **recency** (old → summarized) | none |
44
44
  | **Coding-assistant fit** | high — tool I/O is the heavy 80% | medium — also compresses what you want to keep | — |
45
- | **`/clear` survival** | ✅ via SQLite + `/tl` baton | depends on host | ❌ |
46
- | **Auto-inheritance risk** | zero (explicit `/tl`) | high | — |
45
+ | **`/clear` survival** | ✅ via SQLite + typed `/clear` / `/tl` baton | depends on host | ❌ |
46
+ | **Auto-inheritance risk** | low (typed `/clear` or `/tl` names the predecessor) | high | — |
47
47
  | **Runtime deps** | **zero** (Node 22.5+ built-in `node:sqlite`) | many | — |
48
48
  | **Multi-session token monitor** | ✅ Claude real `message.usage`; Codex rollout `token_count` when available | — | — |
49
49
 
@@ -141,34 +141,42 @@ of tool inputs, tool outputs, and hook output captured at L3 for that turn.
141
141
 
142
142
  ---
143
143
 
144
- ## Inheritance: auto via `/clear`, opt-out via env, opt-in via `/tl`
144
+ ## Inheritance: typed `/clear` and `/tl` write a baton, source-`clear` is the fallback
145
145
 
146
- Throughline 0.4.0+ supports two inheritance paths:
146
+ Throughline 0.4.1+ supports two inheritance paths. The **baton path is the
147
+ primary route**; the source-`clear` auto path is the fallback for cases where
148
+ the user's `/clear` does not reach the `UserPromptSubmit` hook (for example
149
+ the VSCode extension's menu-driven `/clear`).
147
150
 
148
- ### auto path (default): `/clear` → automatic inheritance
151
+ ### baton path (primary): typed `/clear` or `/tl` deterministic inheritance
149
152
 
150
- Since Claude Code 2.1.128, the SessionStart hook receives `source='clear'`
151
- reliably after `/clear`. Throughline detects this and automatically merges the
152
- previous session's memory into the new one. **No user action required** —
153
- just type `/clear` and the new chat resumes mid-thought.
154
-
155
- Set `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` in your environment to opt out.
153
+ When the user types `/clear` or `/tl` in the prompt, the `UserPromptSubmit`
154
+ hook writes a handoff baton with **that session's `session_id`** into the
155
+ `handoff_batons` table. The next `SessionStart` (within the 1-hour TTL)
156
+ consumes the baton and merges that exact predecessor's memory into the new
157
+ session, regardless of the `source` value.
156
158
 
157
- ### baton path (`/tl`): explicit inheritance signal
159
+ This path is deterministic: it names the predecessor by id rather than
160
+ guessing, so multi-window scenarios where "most recently updated session"
161
+ does not equal "the session you just `/clear`-ed" still inherit correctly.
158
162
 
159
- For users who:
163
+ ```
164
+ typed /clear: Session A → /clear → Session B (consumes A's baton, merges A)
165
+ typed /tl: Session A → /tl → (new chat / restart) → Session B (consumes baton, merges A)
166
+ ```
160
167
 
161
- - have `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` set, **or**
162
- - want to inherit across a non-`/clear` boundary (new chat / VSCode restart),
168
+ ### auto path (fallback): `source='clear'` heuristic inheritance
163
169
 
164
- type `/tl` before opening the new session. The `UserPromptSubmit` hook writes
165
- a handoff baton; the next `SessionStart` (within 1 hour) consumes the baton
166
- and merges the previous session's memory, regardless of the `source` value.
170
+ Since Claude Code 2.1.128, the SessionStart hook receives `source='clear'`
171
+ reliably after `/clear`. When no baton is present (for example because the
172
+ `/clear` was triggered by the VSCode extension's menu and never reached
173
+ `UserPromptSubmit`), Throughline falls back to `findLatestClaudePredecessor`
174
+ to pick the most recent unmerged session for the same project and merges it.
167
175
 
168
- ```
169
- auto path: Session A /clear Session B (auto-merges A)
170
- baton path: Session A → /tl (new chat / restart) Session B (consumes baton, merges A)
171
- ```
176
+ Set `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` in your environment to opt out of
177
+ this fallback. **The env var only affects the fallback**; typed `/clear` and
178
+ `/tl` still write a baton and inherit because the user explicitly signalled
179
+ "continue this work".
172
180
 
173
181
  ### What gets injected
174
182
 
@@ -667,17 +675,20 @@ Slash commands (invoked by the user in Claude Code):
667
675
 
668
676
  | Command | What it does |
669
677
  | ------------- | ----------------------------------------------------------------- |
670
- | `/tl` | Write a handoff baton (used as opt-in inheritance signal when `/clear` auto path is OFF or you skip `/clear`) |
678
+ | `/tl` | Write a handoff baton (explicit inheritance signal across non-`/clear` boundaries new chat / VSCode restart) |
679
+ | `/clear` | Built-in Claude Code reset. Throughline's `UserPromptSubmit` hook also writes a baton so the next session inherits the cleared session's memory |
671
680
  | `/sc-detail <time>` | Retrieve L2 body text and L3 tool I/O for a past turn |
672
681
 
673
- > Auto-handoff is ON by default since v0.4.0: just type `/clear` and the new
674
- > chat resumes mid-thought. Set `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` in your
675
- > environment to opt out. `/tl` is for users who opt out, or who want to
676
- > inherit across non-`/clear` boundaries (new chat / VSCode restart).
682
+ > Since v0.4.1, both `/clear` and `/tl` typed in the prompt write a baton
683
+ > identifying the current session, so the next `SessionStart` deterministically
684
+ > inherits that exact predecessor. The `source='clear'` auto path remains as a
685
+ > fallback for `/clear` triggered outside `UserPromptSubmit` (for example via
686
+ > the VSCode extension menu); `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` only opts
687
+ > out of that fallback.
677
688
 
678
689
  Hook subcommands (invoked by Claude Code, not by humans):
679
690
  `session-start` (SessionStart), `process-turn` (Stop),
680
- `prompt-submit` (UserPromptSubmit — detects `/tl` and writes baton).
691
+ `prompt-submit` (UserPromptSubmit — detects `/tl` and `/clear` and writes a baton).
681
692
 
682
693
  ### `throughline detail` — for AI, not humans
683
694
 
@@ -727,7 +738,7 @@ Schema v7:
727
738
  - `skeletons` — L1 one-liners, keyed by `(session_id, origin_session_id, turn, role)`
728
739
  - `bodies` — L2 verbatim text (user + assistant), same key shape
729
740
  - `details` — L3 records with `kind` column (`tool_input` / `tool_output` / `system` / `image` / `thinking`) and `source_id` for idempotent re-processing
730
- - `handoff_batons` — one row per `project_path`, with `session_id` and `created_at`. Consumed and deleted by the next `SessionStart` if within the 1-hour TTL. (v8 dropped the `memo_text` column when memo was retired in v0.4.0.)
741
+ - `handoff_batons` — one row per `project_path`, with `session_id` and `created_at`. Written by the `UserPromptSubmit` hook when the user types `/tl` or `/clear`. Consumed and deleted by the next `SessionStart` if within the 1-hour TTL. (v8 dropped the `memo_text` column when memo was retired in v0.4.0.)
731
742
  - `injection_log` — audit trail of injection events
732
743
 
733
744
  All memory tables carry an `origin_session_id` so rebonded rows keep their
@@ -879,9 +890,15 @@ and `~/.throughline/state/*.json`. A fresh database with schema v7 is created on
879
890
  the next hook fire.
880
891
 
881
892
  **New session didn't inherit memory from the previous one**
882
- This is the designed behavior inheritance requires an explicit `/tl` in the
883
- previous session. If you forgot to type it before `/clear`, the memory is still
884
- in SQLite but won't auto-inject. You can still retrieve specific turns with
893
+ Since v0.4.1, both typed `/clear` and `/tl` write a baton, and the auto path
894
+ falls back on `source='clear'` for menu-driven `/clear`. If inheritance still
895
+ did not happen, the most likely cause is one of: (a) the previous session was
896
+ never recorded (no Stop hook fired — check `throughline status`), (b) the
897
+ 1-hour baton TTL expired before the new session opened, (c) the new session's
898
+ `project_path` (cwd) differs from the previous one, so they live in different
899
+ session chains, or (d) you set `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` and the
900
+ `/clear` came from the VSCode menu which never reaches `UserPromptSubmit`.
901
+ Memory is still in SQLite — you can retrieve specific turns with
885
902
  `/sc-detail <time>`.
886
903
 
887
904
  ---
@@ -902,8 +919,11 @@ Run the monitor directly without a global install:
902
919
  node src/token-monitor.mjs
903
920
  ```
904
921
 
905
- The `.vscode/tasks.json` in this repo auto-launches the monitor when you open
906
- the folder in VS Code.
922
+ When any Throughline hook fires for the first time in a folder, it
923
+ auto-generates `.vscode/tasks.json` with an absolute path to the monitor
924
+ executable for the current machine. The file is **gitignored** (since v0.4.1)
925
+ because the absolute path is per-machine. Reload the VS Code window after
926
+ the first generation to pick up the auto-start task.
907
927
 
908
928
  ---
909
929
 
@@ -242,7 +242,7 @@ Usage:
242
242
  Hook subcommands (called by Claude Code / Codex):
243
243
  throughline session-start SessionStart hook
244
244
  throughline process-turn Stop hook
245
- throughline prompt-submit UserPromptSubmit hook (/tl baton writer)
245
+ throughline prompt-submit UserPromptSubmit hook (/tl & /clear baton writer)
246
246
  throughline codex-hook stop Codex Stop hook
247
247
  `);
248
248
  }
@@ -1,17 +1,26 @@
1
1
  # 引き継ぎ発火条件の絞り込み調査 & 実装計画
2
2
 
3
- > **Status (2026-05-08 update): 本書は履歴扱い**
3
+ > **Status (2026-05-09 update): 本書は履歴扱い**
4
4
  >
5
5
  > 当時 (2026-04-18) は VSCode 拡張 2.1.112 で `/clear` 後も `source='startup'` に
6
6
  > 潰される問題があり、バトン方式 (案 E) を採用した。その後 Claude Code 2.1.105
7
7
  > (VSCode `/clear` not clearing context fix) と 2.1.126 (Windows env fix) で
8
- > 段階的に修正され、**2.1.128 で `source='clear'` が reliable**になっている。
8
+ > 段階的に修正され、**2.1.128 で `source='clear'` が reliable**になった。
9
9
  >
10
- > 現行仕様 (v0.4.0): auto path (`/clear` で自動引継ぎ) + baton path (`/tl` 明示
11
- > マーカー) 2 経路。`THROUGHLINE_DISABLE_AUTO_HANDOFF=1` auto path OFF
12
- > にできる。詳細は [THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md](THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md)。
10
+ > 現行仕様 (v0.4.1): **baton path (primary)** + auto path (fallback) の 2 経路。
11
+ > typed `/clear` / `/tl` はどちらも UserPromptSubmit hook baton を書き、次
12
+ > SessionStart が確定的にそのセッションを引き継ぐ。auto path は VSCode 拡張
13
+ > メニュー由来 `/clear` のように UserPromptSubmit に届かない経路のための
14
+ > fallback で、`THROUGHLINE_DISABLE_AUTO_HANDOFF=1` で OFF にできる (typed
15
+ > `/clear` / `/tl` は env と無関係に引き続き発火する)。詳細は
16
+ > [THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md](THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md)。
13
17
  >
14
- > 本書は当時のバトン採用判断を残す履歴ドキュメント。
18
+ > 本書は当時のバトン採用判断を残す履歴ドキュメント。「結局 baton primary に
19
+ > 戻った」という結末は皮肉だが、当時の判断は VSCode 拡張側 source バグへの
20
+ > 適切な対応だった。今回 baton primary を再採択した理由は (a) typed `/clear`
21
+ > が UserPromptSubmit に確実に届く、(b) multi-window で「最新更新セッション
22
+ > ≠ /clear したセッション」になる場合に heuristic が誤った前任を選ばない、
23
+ > の 2 点。
15
24
 
16
25
  ## Status (2026-04-18 更新)
17
26
 
@@ -134,6 +134,8 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
134
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 しない |
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
+ | **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
+ | **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) |
137
139
  | **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
138
140
 
139
141
  ### ❌ 未完タスク
@@ -7,6 +7,16 @@ A 案 (= /clear で自動引継ぎ + /tl は逃げ道として残す + /tl-trim
7
7
  > 過去の経緯 (なぜ `/tl` バトンを採用したか) は [INHERITANCE_ON_CLEAR_ONLY.md](INHERITANCE_ON_CLEAR_ONLY.md) を参照。
8
8
  > 本書は **2026-05-08 時点の現状検証 + 新理想設計** を扱う。
9
9
 
10
+ > **2026-05-09 (v0.4.1) update**: 2 経路の優先順位を **入れ替えた**。
11
+ > baton path が **primary**、auto path は **fallback**。理由: typed `/clear`
12
+ > は UserPromptSubmit に届くので baton 書き込みで確定的に当該セッションを
13
+ > 指名できる。一方 VSCode 拡張のメニュー由来 `/clear` は UserPromptSubmit
14
+ > に届かない (= `findLatestClaudePredecessor` heuristic が誤った前任を選ぶ
15
+ > リスクあり) ため、auto path を残してフォールバック化した。typed `/clear`
16
+ > も UserPromptSubmit hook で baton を書く ([src/prompt-submit.mjs](../src/prompt-submit.mjs))。
17
+ > `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` は **fallback path のみに作用** する
18
+ > ようになった (typed `/clear` / `/tl` には効かない)。
19
+
10
20
  ---
11
21
 
12
22
  ## 1. 確定した事実 (実機検証済み)
@@ -57,28 +67,33 @@ source: [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)
57
67
 
58
68
  ## 2. 採用する理想設計
59
69
 
60
- ### 2.1 引継ぎ発火条件 (2 経路)
70
+ ### 2.1 引継ぎ発火条件 (2 経路、v0.4.1 で baton primary に変更)
61
71
 
62
72
  | 経路 | 条件 | 起動 |
63
73
  |---|---|---|
64
- | **auto path** | `source='clear'` かつ env `THROUGHLINE_DISABLE_AUTO_HANDOFF` の値が `'1'` でない | 自動引継ぎ |
65
- | **baton path** | `handoff_batons` テーブルに TTL (1 時間) baton あり (= ユーザーが `/tl` を打った) | `source` 値関係なく引継ぎ |
74
+ | **baton path (primary)** | `handoff_batons` テーブルに TTL (1 時間) 内 baton あり (= ユーザーが `/tl` または `/clear` を打った) | `source` 値関係なく確定的に引継ぎ |
75
+ | **auto path (fallback)** | baton 不在 + `source='clear'` + env `THROUGHLINE_DISABLE_AUTO_HANDOFF` `'1'` でない | `findLatestClaudePredecessor` heuristic で引継ぎ |
66
76
 
67
77
  判定ロジック (擬似コード):
68
78
 
69
79
  ```
80
+ on UserPromptSubmit(prompt, session_id, project_path):
81
+ if isBatonCommand(prompt) or isClearCommand(prompt):
82
+ writeBaton(project_path, session_id, now) // typed /clear / /tl が確定的に baton を書く
83
+
70
84
  on SessionStart(source, session_id, project_path):
71
85
  baton = consumeBaton(project_path) // atomic SELECT + DELETE, TTL 超過は sessionId=null で返る
72
86
  if baton.sessionId:
73
- inject(curated_memory) // baton path
87
+ inject(curated_memory_from(baton.sessionId)) // baton path (primary, env 関係なく発火)
74
88
  return
75
89
  if source == 'clear' and env.THROUGHLINE_DISABLE_AUTO_HANDOFF != '1':
76
- inject(curated_memory) // auto path
90
+ predecessor = findLatestClaudePredecessor(project_path, session_id)
91
+ inject(curated_memory_from(predecessor)) // auto path (fallback)
77
92
  return
78
93
  // 何もしない
79
94
  ```
80
95
 
81
- `consumeBaton` が先発なので「両方同時成立」は構造上発生しない (= baton ありなら baton 経路、無ければ source 判定)。
96
+ `consumeBaton` が先発なので「両方同時成立」は構造上発生しない (= baton ありなら baton 経路、無ければ source 判定)。typed `/clear` も UserPromptSubmit hook で baton を書くため、通常はほぼ常に baton path が走る。auto path は VSCode 拡張のメニュー由来 `/clear` のように UserPromptSubmit に届かない経路のためのフォールバック。
82
97
 
83
98
  ### 2.2 注入内容: L1 + L2 + L3 refs のみ (baton/auto どちらの経路でも同一)
84
99
 
@@ -105,10 +120,13 @@ on SessionStart(source, session_id, project_path):
105
120
  - `handoff_batons.memo_text` 列を **drop** (schema v8 migration)
106
121
  - `src/baton.mjs` の `updateBatonMemo` 関数を **削除** (memo_text 列が drop されるため)
107
122
  - `prompt-submit.mjs` の baton 書き込み path は **維持** (`UserPromptSubmit` hook で `/tl` 検出 + writeBaton)
123
+ - v0.4.1 で `/clear` も同じ hook で baton を書くように拡張 ([src/prompt-submit.mjs](../src/prompt-submit.mjs) `isClearCommand`)
108
124
 
109
125
  ユーザーから見た `/tl` の使い方:
110
- - 自動引継ぎ ON (デフォルト): `/tl` を打たなくても `/clear` で自動引継ぎ。打っても挙動は同じ
111
- - 自動引継ぎ OFF (env disable): `/tl` を打ってから新セッションスタートすれば baton 経由で引継ぎ
126
+ - typed `/clear` (デフォルト): `/clear` を打った時点で baton が書かれ、次セッションが確定的に引継ぐ。`/tl` を打つ必要なし
127
+ - VSCode メニュー `/clear` 経由: UserPromptSubmit に届かないので baton は書かれず、auto path (fallback) `findLatestClaudePredecessor` で前任を選ぶ
128
+ - 非 `/clear` 境界 (新規 chat / VSCode 再起動): `/tl` で baton を立ててから新セッションを開く
129
+ - env で auto OFF: typed `/clear` / `/tl` は引き続き動く (env は fallback 専用)
112
130
 
113
131
  ### 2.4 `/tl-trim` 廃止 (Codex 側を壊さない)
114
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -415,7 +415,7 @@ export async function run(args = []) {
415
415
  console.log('有効な hooks:');
416
416
  console.log(' SessionStart → throughline session-start (セッション記録・バトン消費・引き継ぎ注入)');
417
417
  console.log(' Stop → throughline process-turn (L1 要約 + L2 本文保存 + L3 詳細保存)');
418
- console.log(' UserPromptSubmit → throughline prompt-submit (/tl バトン書き込み)');
418
+ console.log(' UserPromptSubmit → throughline prompt-submit (/tl & /clear バトン書き込み)');
419
419
  if (codex) {
420
420
  console.log(` Codex Stop → ${buildCodexStopHookCommand()} (Codex rollout capture + L1 要約)`);
421
421
  }
@@ -98,6 +98,89 @@ test('prompt-submit subprocess writes a /tl baton into an isolated DB', () => {
98
98
  }
99
99
  });
100
100
 
101
+ test('prompt-submit subprocess writes a /clear baton (specific session marker)', () => {
102
+ const home = makeTempHome();
103
+ const project = makeTempProject();
104
+ try {
105
+ const result = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
106
+ home,
107
+ cwd: project,
108
+ input: JSON.stringify({
109
+ session_id: 'cleared-session',
110
+ cwd: project,
111
+ prompt: '/clear',
112
+ }),
113
+ });
114
+
115
+ assert.equal(result.status, 0, result.stderr);
116
+
117
+ const db = openDb(home);
118
+ const row = db.prepare('SELECT project_path, session_id FROM handoff_batons').get();
119
+ assert.equal(row.project_path, project);
120
+ assert.equal(row.session_id, 'cleared-session');
121
+ db.close();
122
+ } finally {
123
+ rmSync(project, { recursive: true, force: true });
124
+ rmSync(home, { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ test('prompt-submit: /clear baton overwrites previous /tl baton in same project', () => {
129
+ const home = makeTempHome();
130
+ const project = makeTempProject();
131
+ try {
132
+ const tl = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
133
+ home,
134
+ cwd: project,
135
+ input: JSON.stringify({ session_id: 'session-A', cwd: project, prompt: '/tl' }),
136
+ });
137
+ assert.equal(tl.status, 0, tl.stderr);
138
+
139
+ const clear = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
140
+ home,
141
+ cwd: project,
142
+ input: JSON.stringify({ session_id: 'session-B', cwd: project, prompt: '/clear' }),
143
+ });
144
+ assert.equal(clear.status, 0, clear.stderr);
145
+
146
+ const db = openDb(home);
147
+ const row = db.prepare('SELECT session_id FROM handoff_batons').get();
148
+ assert.equal(row.session_id, 'session-B', 'most recent baton write wins');
149
+ db.close();
150
+ } finally {
151
+ rmSync(project, { recursive: true, force: true });
152
+ rmSync(home, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ test('prompt-submit: non-baton prompt does not write any baton', () => {
157
+ const home = makeTempHome();
158
+ const project = makeTempProject();
159
+ try {
160
+ const result = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
161
+ home,
162
+ cwd: project,
163
+ input: JSON.stringify({
164
+ session_id: 'session-X',
165
+ cwd: project,
166
+ prompt: 'hello world',
167
+ }),
168
+ });
169
+
170
+ assert.equal(result.status, 0, result.stderr);
171
+
172
+ if (existsSync(join(home, '.throughline', 'throughline.db'))) {
173
+ const db = openDb(home);
174
+ const row = db.prepare('SELECT COUNT(*) AS n FROM handoff_batons').get();
175
+ assert.equal(row.n, 0, 'a normal prompt must not create any baton');
176
+ db.close();
177
+ }
178
+ } finally {
179
+ rmSync(project, { recursive: true, force: true });
180
+ rmSync(home, { recursive: true, force: true });
181
+ }
182
+ });
183
+
101
184
  test('session-start subprocess consumes baton and injects inherited resume context', () => {
102
185
  const home = makeTempHome();
103
186
  const project = makeTempProject();
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * UserPromptSubmit hook — /tl スラッシュコマンド検出 + バトン書き込み
3
+ * UserPromptSubmit hook — /tl & /clear スラッシュコマンド検出 + バトン書き込み
4
4
  *
5
5
  * stdin: { session_id, cwd, prompt, hook_event_name, ... }
6
6
  *
7
7
  * 動作:
8
8
  * - prompt が /tl (単独 or /tl ... 形式) で始まっていればバトンを書き込んで終了
9
+ * - prompt が /clear (単独 or /clear ... 形式) で始まっていれば、現セッションの
10
+ * session_id をバトンに書き込んで終了。
11
+ * (これにより SessionStart 側の findLatestClaudePredecessor heuristic に頼らず、
12
+ * 確定的に「/clear が打たれたセッション」を新セッションに引き継げる。複数
13
+ * VSCode ウィンドウ等で「最新更新セッション = clear されたセッション」が
14
+ * 成立しない multi-window シナリオで誤った前任を選ばないための確定的指名)
9
15
  * - それ以外は何もせず exit 0(プロンプトはそのまま Claude に渡る)
10
16
  * - 本 hook は注入を一切行わない (SessionStart の引き継ぎ注入と二重にならないため)
11
17
  *
@@ -43,6 +49,18 @@ export function isBatonCommand(prompt) {
43
49
  return false;
44
50
  }
45
51
 
52
+ /**
53
+ * プロンプトが /clear バトン発動コマンドか判定する。
54
+ * 許容: "/clear", "/clear\n", "/clear 何か" (前後空白は trim 済み前提)
55
+ */
56
+ export function isClearCommand(prompt) {
57
+ if (typeof prompt !== 'string') return false;
58
+ const trimmed = prompt.trim();
59
+ if (trimmed === '/clear') return true;
60
+ if (trimmed.startsWith('/clear ') || trimmed.startsWith('/clear\n')) return true;
61
+ return false;
62
+ }
63
+
46
64
  export async function run() {
47
65
  let raw = '';
48
66
  await new Promise((resolve) => {
@@ -66,7 +84,10 @@ export async function run() {
66
84
  process.stderr.write(`[vscode-task] ${msg}\n`);
67
85
  }
68
86
 
69
- if (!isBatonCommand(prompt)) {
87
+ const tlMatch = isBatonCommand(prompt);
88
+ const clearMatch = !tlMatch && isClearCommand(prompt);
89
+
90
+ if (!tlMatch && !clearMatch) {
70
91
  process.exit(0);
71
92
  return;
72
93
  }
@@ -87,6 +108,7 @@ export async function run() {
87
108
  ts: new Date(now).toISOString(),
88
109
  session_id,
89
110
  project_path: projectPath,
111
+ trigger: tlMatch ? 'tl' : 'clear',
90
112
  });
91
113
 
92
114
  process.exit(0);
@@ -0,0 +1,66 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { isBatonCommand, isClearCommand } from './prompt-submit.mjs';
4
+
5
+ test('isBatonCommand: bare /tl', () => {
6
+ assert.equal(isBatonCommand('/tl'), true);
7
+ });
8
+
9
+ test('isBatonCommand: /tl with trailing newline', () => {
10
+ assert.equal(isBatonCommand('/tl\n'), true);
11
+ });
12
+
13
+ test('isBatonCommand: /tl with leading/trailing whitespace', () => {
14
+ assert.equal(isBatonCommand(' /tl '), true);
15
+ });
16
+
17
+ test('isBatonCommand: /tl with arguments', () => {
18
+ assert.equal(isBatonCommand('/tl some memo'), true);
19
+ });
20
+
21
+ test('isBatonCommand: rejects /tl-prefixed identifier', () => {
22
+ assert.equal(isBatonCommand('/tldr summary'), false);
23
+ });
24
+
25
+ test('isBatonCommand: rejects /clear', () => {
26
+ assert.equal(isBatonCommand('/clear'), false);
27
+ });
28
+
29
+ test('isBatonCommand: rejects empty / non-string', () => {
30
+ assert.equal(isBatonCommand(''), false);
31
+ assert.equal(isBatonCommand(null), false);
32
+ assert.equal(isBatonCommand(undefined), false);
33
+ assert.equal(isBatonCommand(42), false);
34
+ });
35
+
36
+ test('isClearCommand: bare /clear', () => {
37
+ assert.equal(isClearCommand('/clear'), true);
38
+ });
39
+
40
+ test('isClearCommand: /clear with trailing newline', () => {
41
+ assert.equal(isClearCommand('/clear\n'), true);
42
+ });
43
+
44
+ test('isClearCommand: /clear with leading/trailing whitespace', () => {
45
+ assert.equal(isClearCommand(' /clear '), true);
46
+ });
47
+
48
+ test('isClearCommand: /clear with arguments', () => {
49
+ assert.equal(isClearCommand('/clear something'), true);
50
+ });
51
+
52
+ test('isClearCommand: rejects /clear-prefixed identifier', () => {
53
+ assert.equal(isClearCommand('/cleared'), false);
54
+ assert.equal(isClearCommand('/clearcache'), false);
55
+ });
56
+
57
+ test('isClearCommand: rejects /tl', () => {
58
+ assert.equal(isClearCommand('/tl'), false);
59
+ });
60
+
61
+ test('isClearCommand: rejects empty / non-string', () => {
62
+ assert.equal(isClearCommand(''), false);
63
+ assert.equal(isClearCommand(null), false);
64
+ assert.equal(isClearCommand(undefined), false);
65
+ assert.equal(isClearCommand(42), false);
66
+ });