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 +39 -0
- package/README.md +58 -38
- package/bin/throughline.mjs +1 -1
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +15 -6
- package/docs/PUBLIC_RELEASE_PLAN.md +2 -0
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +26 -8
- package/package.json +1 -1
- package/src/cli/install.mjs +1 -1
- package/src/hook-entrypoints.test.mjs +83 -0
- package/src/prompt-submit.mjs +24 -2
- package/src/prompt-submit.test.mjs +66 -0
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,
|
|
26
|
-
`/clear`
|
|
27
|
-
|
|
28
|
-
|
|
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** |
|
|
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:
|
|
144
|
+
## Inheritance: typed `/clear` and `/tl` write a baton, source-`clear` is the fallback
|
|
145
145
|
|
|
146
|
-
Throughline 0.4.
|
|
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
|
-
###
|
|
151
|
+
### baton path (primary): typed `/clear` or `/tl` → deterministic inheritance
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
- want to inherit across a non-`/clear` boundary (new chat / VSCode restart),
|
|
168
|
+
### auto path (fallback): `source='clear'` → heuristic inheritance
|
|
163
169
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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 (
|
|
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
|
-
>
|
|
674
|
-
>
|
|
675
|
-
>
|
|
676
|
-
>
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
906
|
-
|
|
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
|
|
package/bin/throughline.mjs
CHANGED
|
@@ -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-
|
|
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.
|
|
11
|
-
>
|
|
12
|
-
>
|
|
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
|
-
| **
|
|
65
|
-
| **
|
|
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(
|
|
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
|
-
|
|
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
|
-
-
|
|
111
|
-
-
|
|
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
package/src/cli/install.mjs
CHANGED
|
@@ -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();
|
package/src/prompt-submit.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|