throughline 0.7.0 → 0.8.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,52 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ### Fixed
14
+
15
+ - **Native factory diagnostics now report the database compatibility label from
16
+ the canonical schema version.** Throughline schema v9 previously emitted the
17
+ stale `throughline.database.v8` label alongside numeric versions `9`/`9`, so
18
+ exact factory reporters correctly classified the installation as
19
+ incompatible. The label is now derived from the DB migration version and a
20
+ regression test requires the label, actual version, and supported version to
21
+ stay aligned on future schema bumps.
22
+
23
+ ## [0.8.0] — 2026-07-18
24
+
25
+ ### Changed (breaking behavior)
26
+
27
+ - **Injection is now push/pull, and L1 is no longer injected (ADR 0016).** The
28
+ budgeted resume context (9,500 chars) is rebuilt as: header +
29
+ current-position anchor + an always-shown retrieval-guide section as the
30
+ fixed part, then the **entire remaining budget is filled with L2 turns in
31
+ full**, newest-first, turn-atomically (a user+assistant pair goes in whole
32
+ or not at all — no fixed N, no fragment packing). L1 summaries are no longer
33
+ injected; older memory is pulled on demand instead. The guide section bakes
34
+ in the exact session id, ISO-millisecond boundary (strict less-than) and
35
+ turn counts at injection time, so the pull side never recomputes the window.
36
+
37
+ ### Added
38
+
39
+ - **`throughline recall --l2|--l1` (ADR 0016).** Read-only pull commands the
40
+ injected guide section points at. `recall --l2 --session <id> --before
41
+ <ISO ms> --last <N>` returns the N turns of full L2 bodies older than the
42
+ boundary, in the same line grammar as the injection (including L3 inline
43
+ suffixes). `recall --l1 ... --skip <N>` lists every turn older than the
44
+ `--l2` range with its L1 summary, honestly marking unsummarized turns
45
+ ("全 M ターン / 要約済み K") and always pointing at `throughline detail
46
+ <time>` for full text. The DB is opened read-only; a missing DB is an
47
+ explicit error and is never created or migrated.
48
+
49
+ ### Fixed
50
+
51
+ - **Windows ACL scripts get a 15s timeout (was 3s).** On windows-latest CI
52
+ runners a cold PowerShell start was measured at 3.0–3.2s, so the 3s
53
+ `spawnSync` cap killed the ACL apply/verify scripts of the completed-turn
54
+ receipt store and the runtime error store and surfaced as a flaky
55
+ "Windows owner-only ACL verification failed" (2 consecutive runs, including
56
+ a docs-only commit). The explicit hard-failure contract is unchanged; only
57
+ the cap was raised.
58
+
13
59
  ## [0.7.0] — 2026-07-17
14
60
 
15
61
  ### Changed (breaking behavior)
@@ -1063,7 +1109,10 @@ two attempts, instrument first instead of patching again.
1063
1109
 
1064
1110
  ---
1065
1111
 
1066
- [Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.6.2...HEAD
1112
+ [Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.0...HEAD
1113
+ [0.8.0]: https://github.com/kitepon-rgb/Throughline/compare/v0.7.0...v0.8.0
1114
+ [0.7.0]: https://github.com/kitepon-rgb/Throughline/compare/v0.6.3...v0.7.0
1115
+ [0.6.3]: https://github.com/kitepon-rgb/Throughline/compare/v0.6.2...v0.6.3
1067
1116
  [0.6.2]: https://github.com/kitepon-rgb/Throughline/compare/v0.6.1...v0.6.2
1068
1117
  [0.3.22]: https://github.com/kitepon-rgb/Throughline/releases/tag/v0.3.22
1069
1118
  [0.3.21]: https://github.com/kitepon-rgb/Throughline/compare/v0.3.19...v0.3.21
package/README.ja.md CHANGED
@@ -29,10 +29,14 @@ throughline install # hook / Codex skill / VS Code monitor task を登録
29
29
  <details>
30
30
  <summary><b>Codex も併用する場合</b> Codex hooks も登録される — クリックで詳細</summary>
31
31
 
32
- Codex では `UserPromptSubmit` / `PostToolUse` / `Stop` hook と `$throughline`
33
- skill も登録する。75% 自動発火は token-monitor 依存ではなく、当該 Codex
34
- セッションの rollout `token_count` hook が読み、prompt 送信時または tool loop
35
- 途中の閾値到達時に同じセッションへ `$throughline` 実行指示を注入する。
32
+ global install は Codex `UserPromptSubmit` / `PostToolUse` / `Stop` hook
33
+ (絶対 node パス登録)と `$throughline` skill も登録する。これらの hook は
34
+ rollout capture monitor state 書き込みだけを行い、**使用量閾値での
35
+ `$throughline` 自動注入はしない**(token-monitor は表示専用)。bare
36
+ `$throughline` は app-server 経由で新規 Codex thread を開始し、Throughline DB
37
+ の handoff memory を developer item として注入する。current-thread rollback
38
+ 診断が要る時だけ明示的に `trim --execute --host codex` を使う。既存の
39
+ 非 Throughline Codex hook は保持される。
36
40
 
37
41
  </details>
38
42
 
@@ -42,7 +46,7 @@ skill も登録する。75% 自動発火は token-monitor 依存ではなく、
42
46
  |---|---|---|---|---|
43
47
  | **何をする** | ツール I/O を SQLite に退避、本文は残す | ウィンドウを全消去 | ウィンドウ全体を LLM 要約 | 新旧で要約 |
44
48
  | **圧縮の軸** | コンテンツの **種類** (テキスト vs ツール I/O) | 無し — 全消去 | **新旧** (一律) | **新旧** (一律) |
45
- | **境界後に残る記憶** | ✅ 直近 20 ターン本文 + それ以前 L1 + L3 オンデマンド | ❌ ゼロ | △ 一個の要約 (情報欠落) | △ 要約 (情報欠落) |
49
+ | **境界後に残る記憶** | ✅ 直近ターン本文そのまま (予算内ターン原子詰め) + それ以前は `recall` で pull + L3 オンデマンド | ❌ ゼロ | △ 一個の要約 (情報欠落) | △ 要約 (情報欠落) |
46
50
  | **ツール I/O の扱い** | L3 に退避、`/sc-detail HH:MM:SS` で取り戻せる | 消える | 要約に溶けて読めない | 要約に溶ける |
47
51
  | **コーディング用途への適合** | 高 — ツール I/O こそ重い 80% | 低 — 文脈が切れる | 中 — ただし不可逆 | 中 |
48
52
  | **誤継承リスク** | 低 (typed `/clear` / `/tl` が前任を指名) | n/a | n/a | 高 |
@@ -73,10 +77,10 @@ Throughline 無し (50 ターン、/clear なし):
73
77
  ≈ 125,000 tok 合計
74
78
 
75
79
  Throughline 有り (50 ターン → /clear → 再開):
76
- 直近 20 ターン L2 ~10,000 tok ██
77
- それ以前 30 ターン L1 ~3,000 tok ▌
78
- ツール I/O 0 tok (SQLite 退避、オンデマンド取得)
79
- ≈ 13,000 tok — 90% 軽量
80
+ 再開注入 ~3,000 tok ▌ (直近 L2 ターン丸ごと、≤9,500 字)
81
+ それ以前の記憶 0〜10,000 tok (必要な時だけ recall --l2 / --l1 で pull)
82
+ ツール I/O 0 tok (SQLite 退避、detail でオンデマンド取得)
83
+ 最大 13,000 tok — 90% 軽量
80
84
  ```
81
85
 
82
86
  MemGPT や LangChain の SummaryBufferMemory が **新旧** で圧縮するのに対し、
@@ -94,7 +98,7 @@ Throughline は加えて、トランスクリプト JSONL から実測 API 使
94
98
 
95
99
  ---
96
100
 
97
- ## 3 層メモリーモデル (schema v7)
101
+ ## 3 層メモリーモデル (schema v9)
98
102
 
99
103
  ```mermaid
100
104
  flowchart LR
@@ -102,10 +106,11 @@ flowchart LR
102
106
  T --> H["Stop hook"]
103
107
  H --> L2[("L2 · bodies<br/>本文そのまま")]
104
108
  H --> L3[("L3 · details<br/>ツール I/O · 思考")]
105
- H -. "非同期<br/>Haiku" .-> L1[("L1 · skeletons<br/>一行要約")]
109
+ H -. "非同期<br/>要約器" .-> L1[("L1 · skeletons<br/>一行要約")]
106
110
 
107
- L2 -- "直近 20 ターン" --> S["次セッション<br/>SessionStart 注入"]
108
- L1 -- "それ以前" --> S
111
+ L2 -- "直近ターン丸ごと<br/>(9,500 字予算)" --> S["次セッション<br/>初回プロンプト注入"]
112
+ L2 -. "窓の残り<br/>throughline recall --l2" .-> S
113
+ L1 -. "それ以前すべて<br/>throughline recall --l1" .-> S
109
114
  L3 -. "オンデマンド · throughline detail" .-> S
110
115
 
111
116
  classDef l1 fill:#3aa0ff,stroke:#1a1f2e,color:#fff
@@ -118,28 +123,35 @@ flowchart LR
118
123
 
119
124
  | 層 | 名称 | 保存先 | 内容 | ターンあたりコスト |
120
125
  | --- | --- | --- | --- | --- |
121
- | **L1** | スケルトン | 古いターンとして注入 | ターンの一行要約(既定は Claude Haiku、設定時は Codex sidecar も可) | 約 10 トークン |
122
- | **L2** | ボディ | 直近ターンとして注入 | ユーザー本文 + アシスタント返答そのまま | 自然なフルサイズ |
126
+ | **L1** | スケルトン | `recall --l1` でオンデマンド pull | ターンの一行要約(既定 backend Codex CLI `gpt-5.6-luna`、fallback は ADR 0015 参照) | 約 10 トークン |
127
+ | **L2** | ボディ | 予算内は注入、残りは `recall --l2` | ユーザー本文 + アシスタント返答そのまま | 自然なフルサイズ |
123
128
  | **L3** | ディテール | SQLite のみ | ツール I/O、システムメッセージ、画像、**拡張思考** (オンデマンド) | 重い、退避済 |
124
129
 
125
130
  3 層は **互いに補完的かつ排他的** で、重複保存はありません。
126
131
  拡張思考ブロックは L3 (`kind='thinking'`) に格納されるので、次セッションは
127
132
  **前セッションの Claude が中断時に何を考えていたか** を、発話だけでなく
128
- 内省レベルで参照できます。`SessionStart` では **最終ターンの思考** が L2 履歴の
129
- 直上にインライン注入され、それ以前の思考は `throughline detail <時刻>` で取得できます。
130
-
131
- `SessionStart` 時、Throughline SQLite からコンテキストを再構築し、
132
- プレーンテキストとして注入します:
133
-
134
- - **直近 20 ターン** は L2 (`bodies`) のフル本文として注入
135
- - **それ以前** L1 (`skeletons`) の一行要約として注入
133
+ 内省レベルで参照できます。思考は注入されず、`throughline detail <時刻>`
134
+ いつでも取得できます。
135
+
136
+ 次セッションの **初回ユーザープロンプト** 時(二相ハンドオフ、ADR 0014)、
137
+ Throughline は SQLite からコンテキストを再構築し、プレーンテキストとして
138
+ 注入します(push/pull 設計、ADR 0016):
139
+
140
+ - **直近ターン** L2 (`bodies`) のフル本文として注入 — 新しい順に
141
+ ターン丸ごと(user + assistant 原子)、約 9,500 字の予算に入るだけ
142
+ - **L1 (`skeletons`) の一行要約は注入しません** — 代わりに、そのまま実行
143
+ できる `throughline recall --l2|--l1` コマンド入りの案内セクションを注入し、
144
+ 古い記憶は必要になった時だけ pull します
136
145
  - L3 は SQLite に残り、`/sc-detail <時刻>` でオンデマンド取得
137
146
 
138
147
  L1 要約は遅延実行で、20 ターン未満で終わるセッションでは外部要約器を呼ばず、
139
- 短いタスクの要約コストはゼロです。既定では
140
- `claude -p --model claude-haiku-4-5-*` サブプロセスで **Claude Haiku 4.5** が生成します。
141
- Claude Max のログイン認証を流用するため API キー不要です。
142
- `codex-sidecar` `summarize-l1` preset で明示設定されている場合は、そちらを使えます。
148
+ 短いタスクの要約コストはゼロです。要約は **削減割合**(既定 1/5、
149
+ `THROUGHLINE_L1_RATIO` で変更可。不正値は明示エラー)を目標にします。
150
+ Claude-primary 経路の backend 順は codex-sidecar(`summarize-l1` preset 明示
151
+ 設定時)→ **Codex CLI**(既定 `gpt-5.6-luna`@`low`、実測評価で選定 ADR 0015。
152
+ `THROUGHLINE_L1_MODEL` / `THROUGHLINE_L1_EFFORT` で変更可)→ **Claude Haiku 4.5**
153
+ (`claude -p` サブプロセス、Claude Max のログイン認証流用)で、API キーは
154
+ 不要です。各段の失敗理由は記録されます。
143
155
 
144
156
  3 層 (L1/L2/L3) の書き込みパスは schema v5 から動作しています。
145
157
  `/sc-detail HH:MM:SS` はユーザー / アシスタント本文 (L2) と、そのターンで
@@ -158,13 +170,14 @@ flowchart LR
158
170
  U["ユーザーが入力<br/>/clear または /tl"] -->|UserPromptSubmit| W["writeBaton<br/>(session_id + TTL 1h)"]
159
171
  W --> B[("handoff_batons<br/>SQLite")]
160
172
  M["VS Code メニュー<br/>clear"] -->|UserPromptSubmit に届かない| X["baton 無し"]
161
- NS["次の SessionStart"] --> C{"baton<br/>あり?"}
173
+ NS["次の SessionStart<br/>(intent 登録のみ)"] --> FP["初回ユーザープロンプト<br/>(実セッションの証明)"]
174
+ FP --> C{"baton<br/>あり?"}
162
175
  B -.-> C
163
176
  X -.-> C
164
177
  C -->|あり| P1["baton path<br/>(主経路)<br/>指名された前任を merge"]
165
- C -->|無し / source='clear'| P2["auto path<br/>(補助)<br/>findLatestClaudePredecessor"]
178
+ C -->|無し / source='clear'| P2["auto path<br/>(補助)<br/>前任は SessionStart 時点で凍結"]
166
179
  C -->|無し / source!='clear'| P3["新規セッション<br/>merge 無し"]
167
- P1 --> INJ["L1 + L2 + L3 references を注入"]
180
+ P1 --> INJ["L2 ターン + recall 案内を注入<br/>(予算 9,500 字)"]
168
181
  P2 --> INJ
169
182
 
170
183
  classDef primary fill:#7c5cff,stroke:#1a1f2e,color:#fff
@@ -178,16 +191,26 @@ flowchart LR
178
191
  ### baton path (primary): typed `/clear` または `/tl`
179
192
 
180
193
  ユーザーが prompt に `/clear` または `/tl` を打つと、UserPromptSubmit hook が
181
- **そのセッションの** `session_id` を `handoff_batons` に書きます。次の
182
- SessionStart 1 時間以内の baton を消費し、その前任を確定的に merge します。
194
+ **そのセッションの** `session_id` を `handoff_batons` に書きます。次の新セッション
195
+ **初回ユーザープロンプト時** baton を消費し(適格性: セッション誕生が baton
196
+ 書き込みから TTL 1 時間以内)、その前任を確定的に merge します。
183
197
  複数ウィンドウで「最新更新セッション」と「今 `/clear` したセッション」が違っても、
184
198
  指名された前任だけを引き継ぎます。
185
199
 
200
+ なぜ SessionStart でなく初回プロンプトか: Claude Code は同一 project に数百 ms の
201
+ 間隔で複数の SessionStart を発火させることがあり、その一部は transcript を一切
202
+ 作らない**幽霊セッション**になります。SessionStart 時点では実体と幽霊を判別
203
+ できないため、baton を SessionStart で消費すると幽霊が記憶を飲み込み、実セッション
204
+ が空で始まる事故が起きます。幽霊はプロンプトを発火しないので、消費を初回
205
+ プロンプトへ遅延させればこの事故は構造的に起きません(二相ハンドオフ、ADR 0014)。
206
+
186
207
  ### auto path (fallback): `source='clear'`
187
208
 
188
209
  baton が無く、SessionStart の `source='clear'` が届いた場合だけ、同 project の
189
- 最新 Claude predecessor を選んで merge します。これは VS Code 拡張メニューなど、
190
- typed `/clear` が UserPromptSubmit hook に届かない経路のための補助です。
210
+ 最新 Claude predecessor **SessionStart 時点で** 解決・凍結し(transcript
211
+ 無い幽霊は候補から除外)、merge + 注入は初回プロンプト時に行います。これは
212
+ VS Code 拡張メニューなど、typed `/clear` が UserPromptSubmit hook に届かない
213
+ 経路のための補助です。
191
214
 
192
215
  `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` はこの fallback path だけを OFF にします。
193
216
  typed `/clear` と `/tl` はユーザーの明示意思なので、この env に関係なく baton を
@@ -201,11 +224,18 @@ fallback: baton 無し + source='clear' → latest predecessor を merge
201
224
 
202
225
  ### 注入されるもの
203
226
 
204
- 両経路で同じ curated memory が注入されます:
227
+ 両経路で同じ curated memory が注入されます(push/pull 設計、ADR 0016):
205
228
 
206
- - L1 サマリー (古い turn の一行要約)
207
- - L2 verbatim (直近 20 turn の本文)
208
- - L3 references (`throughline detail <時刻>` で引き出すコマンド一覧、本文は SQLite に残置)
229
+ - **「現在地」アンカー** (v0.4.12〜) — 最新のユーザー指示と最新の assistant turn
230
+ ヘッダ直下に再掲(各 600 字で切り詰め)
231
+ - **pull 案内セクション**(無条件表示)— そのまま実行できる `throughline recall`
232
+ コマンド。session id・ISO ms 境界・件数は注入時に焼き込み済み
233
+ - L2 verbatim — 約 9,500 字の注入予算に入るだけ、新しい turn からターン丸ごと
234
+ 詰める(典型 7〜8 turn、軽い会話ならもっと多い)。**L1 サマリーは注入しません**
235
+ — 20 turn 窓の残りは `recall --l2` で全文、それより古い全 turn は `recall --l1`
236
+ で取得(要約済みは L1 行、未要約は `throughline detail` への誘導付きで明示)
237
+ - L3 references (`throughline detail <時刻>` で引き出すコマンド、各 L2 行末尾に
238
+ inline 付記。本文は SQLite に残置)
209
239
 
210
240
  注入は **「中断されたタスクの再開」** として再フレーミングされます。L2 verbatim に
211
241
  最終 assistant turn (= 次に何をしようとしていたか) が含まれるため、別途 memo /
@@ -231,14 +261,14 @@ slash command / transcript / baton / resume behavior を置き換えるもので
231
261
  adapter / projection として追加されます。
232
262
 
233
263
  現時点で core Throughline が外部モデルを呼ぶのは L2→L1 要約だけです。
234
- `codex-sidecar` が `summarize-l1` preset で設定されている場合はその要約に
235
- Codex sidecar を使えます。使えない場合は、従来どおり Claude Haiku 経路を使います。
264
+ backend 順は codex-sidecar(`summarize-l1` preset 明示設定時)→ Codex CLI
265
+ (既定 `gpt-5.6-luna`)→ Claude Haiku です(ADR 0015)。
236
266
 
237
267
  Codex 側 trim (= same-thread context trim) は `throughline trim --execute --host codex`
238
268
  で発火します。Codex の bare `$throughline` skill もこの scripted rollback + DB
239
269
  memory inject を直接実行します。Claude 側は `/clear` での auto path 引継ぎが本線になったため、
240
270
  `/tl-trim` slash command は v0.4.0 で廃止されました。current-work framing は
241
- SessionStart 注入の Reading Contract / Continuation Instruction で同じ意図を
271
+ 再開注入の Reading Contract / Continuation Instruction で同じ意図を
242
272
  継承しています。
243
273
 
244
274
  </details>
@@ -285,6 +315,7 @@ Throughline state をまだ書いていない現在セッションも表示で
285
315
  | `throughline monitor` | マルチセッション監視を起動 |
286
316
  | `throughline monitor --diag` | TTY/columns/env 診断ダンプ (描画バグ切り分け用) |
287
317
  | `throughline detail <時刻>` | あるターンの L2 本文と L3 ツール I/O を取得 (Claude が使う) |
318
+ | `throughline recall --l2\|--l1 --session <id> --before <ISO> ...` | 注入の案内セクションが指す古い記憶を pull (read-only、正確なコマンドは注入に焼き込み済み) |
288
319
  | `throughline doctor` | Node バージョン、hook 登録状況、DB、PATH をチェック |
289
320
  | `throughline doctor --trim --host claude` | trim boundary と手動手順を診断 |
290
321
  | `throughline handoff-preview --session <id>` | Codex 向け `throughline_handoff` JSON projection を表示 |
@@ -313,7 +344,8 @@ Throughline state をまだ書いていない現在セッションも表示で
313
344
 
314
345
  - **Node.js 22.5 以上** (組み込み `node:sqlite` モジュール使用、ネイティブビルド不要)
315
346
  - **Claude Code** (`SessionStart`, `Stop`, `UserPromptSubmit` hooks 対応版)
316
- - **Claude Max サブスクリプション** (Haiku ベース L1 要約のため `claude -p` 経由)
347
+ - **Codex CLI ログイン**(既定の L1 要約 backend、`gpt-5.6-luna`)または
348
+ **Claude Max サブスクリプション**(`claude -p` 経由の Haiku fallback)— どちらも API キー不要
317
349
  - 対応 OS: **Windows / macOS / Linux**
318
350
 
319
351
  ランタイム依存 **ゼロ**。npm パッケージは純 `.mjs` ファイルのみで構成されています。
package/README.md CHANGED
@@ -54,7 +54,7 @@ guarded `trim --execute --host codex` surface.
54
54
  |---|---|---|---|---|
55
55
  | **What it does** | retire tool I/O to SQLite, keep text in-context | wipe the whole window | LLM-summarize the whole window | recency-based summarize |
56
56
  | **Compression axis** | content **type** (text vs tool I/O) | none — full wipe | **recency** (uniform) | **recency** (uniform) |
57
- | **Memory after the boundary** | ✅ recent 20 turns verbatim + older as L1 + L3 on demand | ❌ zero | △ lossy single summary | △ lossy summary |
57
+ | **Memory after the boundary** | ✅ recent turns verbatim (whole, budget-packed) + everything older via `recall` pull + L3 on demand | ❌ zero | △ lossy single summary | △ lossy summary |
58
58
  | **Tool I/O handling** | retired to L3, retrievable by `/sc-detail HH:MM:SS` | gone | folded into summary, unreadable | folded into summary |
59
59
  | **Coding-assistant fit** | high — tool I/O is the heavy 80% | low — you lose the thread | medium — but irreversible | medium |
60
60
  | **Auto-inheritance risk** | low (typed `/clear` / `/tl` names the predecessor) | n/a | n/a | high |
@@ -86,10 +86,10 @@ Without Throughline (50 turns, no /clear):
86
86
  ≈ 125,000 tok total
87
87
 
88
88
  With Throughline (50 turns → /clear → resume):
89
- recent 20 turns L2 ~10,000 tok ██
90
- older 30 turns L1 ~3,000 tok
91
- tool I/O 0 tok (retired to SQLite, on-demand)
92
- ≈ 13,000 tok total — 90% lighter
89
+ resume injection ~3,000 tok ▌ (recent L2 turns, whole, ≤ 9,500 chars)
90
+ older memory 0–10,000 tok (pulled only when needed: recall --l2 / --l1)
91
+ tool I/O 0 tok (retired to SQLite, on-demand via detail)
92
+ ≈ 13,000 tok worst case — 90% lighter
93
93
  ```
94
94
 
95
95
  Throughline separates conversation content by **type, not time**: human-readable
@@ -117,10 +117,11 @@ flowchart LR
117
117
  T --> H["Stop hook"]
118
118
  H --> L2[("L2 · bodies<br/>verbatim text")]
119
119
  H --> L3[("L3 · details<br/>tool I/O · thinking")]
120
- H -. "async<br/>Haiku" .-> L1[("L1 · skeletons<br/>one-liners")]
120
+ H -. "async<br/>summarizer" .-> L1[("L1 · skeletons<br/>one-liners")]
121
121
 
122
- L2 -- "recent 20 turns" --> S["Next session's first prompt<br/>injection"]
123
- L1 -- "older turns" --> S
122
+ L2 -- "recent turns, whole<br/>(9,500-char budget)" --> S["Next session's first prompt<br/>injection"]
123
+ L2 -. "rest of the window<br/>throughline recall --l2" .-> S
124
+ L1 -. "everything older<br/>throughline recall --l1" .-> S
124
125
  L3 -. "on demand · throughline detail" .-> S
125
126
 
126
127
  classDef l1 fill:#3aa0ff,stroke:#1a1f2e,color:#fff
@@ -133,22 +134,25 @@ flowchart LR
133
134
 
134
135
  | Layer | Name | Where it lives | Content | Cost per turn |
135
136
  | ----- | ---------- | --------------------- | --------------------------------------------------------------------- | ------------- |
136
- | **L1** | Skeleton | injected when old | one-line summary of the turn (current Claude-primary path: Claude Haiku, optional Codex sidecar) | ~10 tok |
137
- | **L2** | Body | injected when recent | user text + assistant reply, verbatim | full natural |
137
+ | **L1** | Skeleton | pulled on demand (`recall --l1`) | one-line summary of the turn (default backend: Codex CLI `gpt-5.6-luna`, fallbacks per ADR 0015) | ~10 tok |
138
+ | **L2** | Body | injected while the budget lasts; rest via `recall --l2` | user text + assistant reply, verbatim | full natural |
138
139
  | **L3** | Detail | SQLite only | tool I/O, system messages, images, **extended thinking** (on-demand) | heavy, retired |
139
140
 
140
141
  The layers are **complementary and disjoint** — nothing is duplicated across
141
142
  them. Extended thinking blocks are stored at L3 (`kind='thinking'`) so the
142
143
  next session can see *what the previous Claude was thinking* at the moment it
143
- was interrupted, not just what it said aloud. On `SessionStart` the thinking
144
- of the **final turn** is injected inline above the L2 history; older thinking
145
- remains retrievable via `throughline detail <time>`.
146
-
147
- On `SessionStart`, Throughline rebuilds the context from SQLite and
148
- injects it as plain text:
149
-
150
- - The **most recent 20 turns** are injected as full L2 (`bodies`) text
151
- - **Older turns** are injected as L1 (`skeletons`) one-liners
144
+ was interrupted, not just what it said aloud. Thinking is never injected —
145
+ it stays retrievable via `throughline detail <time>`.
146
+
147
+ At the **first user prompt** of the next session (two-phase handoff, ADR
148
+ 0014), Throughline rebuilds the context from SQLite and injects it as plain
149
+ text (push/pull design, ADR 0016):
150
+
151
+ - The **most recent turns** are injected as full L2 (`bodies`) text — packed
152
+ whole-turn, newest-first, as many as fit the ~9,500-char budget
153
+ - **L1 (`skeletons`) one-liners are not injected** — a guidance section with
154
+ ready-to-run `throughline recall --l2|--l1` commands is injected instead,
155
+ so older memory is pulled only when needed
152
156
  - L3 stays in SQLite and is retrieved on demand via `/sc-detail <time>`
153
157
 
154
158
  L1 summaries are generated lazily: for sessions that stay under 20 turns, no
@@ -189,7 +193,7 @@ flowchart LR
189
193
  C -->|yes| P1["baton path<br/>(primary)<br/>merge that exact predecessor"]
190
194
  C -->|no, source='clear'| P2["auto path<br/>(fallback)<br/>predecessor frozen at SessionStart"]
191
195
  C -->|no, source!='clear'| P3["fresh session<br/>no merge"]
192
- P1 --> INJ["inject L1 + L2 + L3 refs<br/>(budgeted ≤ 9,500 chars)"]
196
+ P1 --> INJ["inject L2 turns + recall guidance<br/>(budgeted ≤ 9,500 chars)"]
193
197
  P2 --> INJ
194
198
 
195
199
  classDef primary fill:#7c5cff,stroke:#1a1f2e,color:#fff
@@ -245,15 +249,23 @@ this fallback. **The env var only affects the fallback**; typed `/clear` and
245
249
 
246
250
  ### What gets injected
247
251
 
248
- Both paths inject the **same** curated memory:
252
+ Both paths inject the **same** curated memory (push/pull design, ADR 0016):
249
253
 
250
254
  - A **"現在地 (latest exchange)"** anchor (added in v0.4.12) re-surfaces the
251
255
  most recent user directive and the most recent assistant turn directly under
252
256
  the header, each truncated to 600 characters
253
- - L1 summaries (older turns, one-line)
254
- - L2 verbatim (most recent 20 turns, full text)
257
+ - A **pull guidance section** (always present) with ready-to-run
258
+ `throughline recall` commands session id, an ISO-ms boundary, and turn
259
+ counts are baked in at injection time
260
+ - L2 verbatim: as many of the most recent turns as fit the ~9,500-char
261
+ injection budget, packed whole-turn (typically 7–8 turns; more for light
262
+ conversations). **L1 summaries are not injected** — the rest of the
263
+ 20-turn window is retrieved verbatim via `throughline recall --l2`, and
264
+ everything older via `throughline recall --l1` (summarized turns show
265
+ their L1 line; unsummarized ones are listed explicitly with a
266
+ `throughline detail` pointer)
255
267
  - L3 references (`throughline detail <time>` retrieval commands, attached
256
- inline to each L1/L2 row; bodies stay in SQLite)
268
+ inline to each L2 row; bodies stay in SQLite)
257
269
 
258
270
  The injection is reframed as **"resuming an interrupted task"** rather than
259
271
  "reading past logs". The L2 verbatim already contains the last assistant
@@ -754,6 +766,7 @@ aggregate は collection が既定OFFで、canonical dotagents config の
754
766
  | `throughline monitor [--all] [--session <id>]` | Run the multi-session token monitor |
755
767
  | `throughline monitor --diag` | Dump TTY/columns/env diagnostics (for debugging monitor render bugs) |
756
768
  | `throughline detail <time>` | Retrieve L2 body text and L3 tool I/O for a turn (see below) |
769
+ | `throughline recall --l2\|--l1 --session <id> --before <ISO> ...` | Pull older memory referenced by the injection's guidance section (read-only; the exact command is baked into each injection) |
757
770
  | `throughline observer-read --project <absolute-directory> --json` | Read one completed-turn Observer page through the JSON-only public boundary |
758
771
  | `throughline observer-wait --project <absolute-directory> --after-cursor <opaque> [--timeout-seconds 3600] --json` | Wait up to 3600 seconds for a completed-turn Observer cursor change |
759
772
  | `throughline doctor` | Check Node version, hook registration, DB writability, PATH |
@@ -886,7 +899,9 @@ throughline auditor-context --session claude-session-id --project "$PWD" \
886
899
  - **Node.js >= 22.13** (for the stable, flag-free built-in `node:sqlite` module — no native build
887
900
  required, no `npm install` of SQLite bindings)
888
901
  - **Claude Code** with hooks support (`SessionStart`, `Stop`)
889
- - **Claude Max subscription** (for Haiku-based L1 summarization via `claude -p`)
902
+ - **Codex CLI login** (default L1 summarization backend, `gpt-5.6-luna`) or a
903
+ **Claude Max subscription** (Haiku fallback via `claude -p`) — no API key
904
+ either way
890
905
  - Works on **Windows, macOS, Linux**
891
906
 
892
907
  Throughline has **zero runtime dependencies**. The published tarball is just
@@ -9,6 +9,7 @@
9
9
  * throughline process-turn # Stop hook (Claude Code から呼ばれる)
10
10
  * throughline session-start # SessionStart hook (Claude Code から呼ばれる)
11
11
  * throughline detail <時刻> # L2+L3 詳細取得 (Claude が Bash 経由で呼ぶ想定)
12
+ * throughline recall --l2|--l1 # 注入案内から辿る pull 用 read-only 記憶取得
12
13
  * throughline handoff-preview # Codex-facing throughline_handoff JSON preview
13
14
  * throughline auditor-context --json # Read-only bounded auditor context JSON
14
15
  * throughline factory-diagnostics --json # Native factory read-only readiness JSON
@@ -66,6 +67,11 @@ switch (cmd) {
66
67
  case 'detail':
67
68
  (await import('../src/sc-detail.mjs')).run(rest);
68
69
  break;
70
+ case 'recall': {
71
+ const exitCode = (await import('../src/cli/recall.mjs')).run(rest);
72
+ process.exitCode = exitCode;
73
+ break;
74
+ }
69
75
  case 'handoff-preview':
70
76
  await (await import('../src/cli/handoff-preview.mjs')).run(rest);
71
77
  break;
@@ -17,6 +17,14 @@ A 案 (= /clear で自動引継ぎ + /tl は逃げ道として残す + /tl-trim
17
17
  > `THROUGHLINE_DISABLE_AUTO_HANDOFF=1` は **fallback path のみに作用** する
18
18
  > ようになった (typed `/clear` / `/tl` には効かない)。
19
19
 
20
+ > **2026-07-18 (ADR 0016) update**: 注入の中身を push/pull 二段に再設計した。
21
+ > push (9,500 字) はヘッダ + 現在地アンカー + 案内セクション + **L2 をターン原子で
22
+ > 入るだけ全文**(L1 は注入しない)。窓 20 ターンの残りは `throughline recall --l2`、
23
+ > それより古い全ターンは `recall --l1`(要約 or 未要約明示)で pull する。範囲・境界
24
+ > (ISO ms)・件数・session は注入時に案内コマンドへ焼き込み、recall 側は窓を再計算
25
+ > しない。正典は [ADR 0016](adr/0016-push-pull-recall-injection.md)。本書内の
26
+ > 「L1 + L2 を注入する」旧記述はこの update で読み替えること。
27
+
20
28
  ---
21
29
 
22
30
  ## 1. 確定した事実 (実機検証済み)
@@ -0,0 +1,105 @@
1
+ # ADR 0016: 注入の push/pull 再設計 — 現在地 + 入るだけ L2 の push と recall CLI による pull
2
+
3
+ - Status: accepted (2026-07-18 オーナー裁定)
4
+ - 関連: [ADR 0014](0014-two-phase-handoff-ghost-baton.md)(9,500 字注入予算)、
5
+ [ADR 0015](0015-l1-summarizer-model-effort-ratio.md)(L1 要約体制)
6
+
7
+ ## 問題
8
+
9
+ 9,500 字の注入予算(hook stdout ~10k で `<persisted-output>` file 化、ADR 0014)の中で、
10
+ 旧 `buildBudgetedResumeContext` は **L1 を先に詰めてから L2 を詰めて**いた。実測
11
+ (このMacのDB、L2 1 ターン中央値 ~800 字・平均 ~2,750 字)では L2 は 5〜10 ターンしか
12
+ 入らず、しかも L1 生成は `L2_WINDOW = 20` より古いターンにしか走らない(Stop 毎 1 件の
13
+ 遅延生成)ため、「予算落ちした 5〜20 ターン前」が **L2 も注入されず L1 も未生成**の
14
+ 記憶空白帯になっていた。省略告知は時刻列挙のみで、取り出しは 1 ターンずつ
15
+ `throughline detail` を叩くしかなかった。
16
+
17
+ ## 検討して不採用にした案
18
+
19
+ 1. **固定 N ターン + per-role 切り詰めで L2 を保証し、残りに L1 充填**:
20
+ L1 生成閾値を N に連動させる案。pull 設計に移った時点で、注入 L1 が pull の L2×残り
21
+ と同じターンを二重に運ぶ矛盾が出て破棄(会話での敵対的整理)。固定 N 自体も
22
+ 「軽い会話で予算を設計的に遊ばせる」ため破棄。
23
+ 2. **multi-hook による 10k 突破**: 実測で**可能**と確認した(下記「実測」)が、hook の
24
+ 構造的想定(1 本 = 1 context string)に無い使い方であり、attachment 順序も非決定の
25
+ ため不採用。
26
+ 3. **全文ログのファイル書き出し + Read 誘導**: DB が正本なのに派生ファイルの寿命管理が
27
+ 発生する。過去ターンのレコードは不変なので、スナップショット性は DB 直参照でも
28
+ 担保できる。CLI 経由の DB 直参照に統一して不採用。
29
+ 4. **生 SQL をモデルに案内**: schema 講義で注入が太り、origin フィルタ等の誤クエリ事故面
30
+ が開く。`detail` と同じ「DB 直参照だがモデルにはコマンド 1 発」の流儀で不採用。
31
+
32
+ ## 決定
33
+
34
+ push は「現在地」に徹し、過去は pull に出す。
35
+
36
+ - **push(注入、9,500 字内)**: ヘッダ + 現在地アンカー + 案内セクション
37
+ (固定部として最優先予約・無条件表示)+ 残り全予算に **L2 を新しい順で
38
+ 丸ごと入るターンだけ全文**(ターン単位の原子。固定 N なし・断片詰めなし・
39
+ ターン境界の自然な端数は許容)。**L1 は注入しない**。
40
+ 最新ターンが単体で予算超過する場合だけ切り詰めて入れる(従来規則の維持)。
41
+ - **pull(新 CLI `throughline recall` = read-only DB 直参照)**:
42
+ - `recall --l2 --session <id> --before <ISO8601 ms> --last <N>` —
43
+ 境界より古いターンを新しい側から N 件、L2 全文(注入と同じ行文法 + L3 suffix)
44
+ - `recall --l1 --session <id> --before <ISO8601 ms> --skip <N>` —
45
+ --l2 の担当分を飛ばした先の全ターン一覧。**L1 要約があれば要約、無ければ
46
+ 「未要約」と明示**して detail 誘導。冒頭に「全 M ターン / 要約済み K」を正直に表示
47
+ - 一点掘りは従来どおり `throughline detail <時刻>`(L2+L3)
48
+ - **`L2_WINDOW = 20` は据え置き**。20 の意味が「push(入るだけ)+ pull(残り)」の
49
+ 合計窓に再定義されるだけで、L1 要約ペース・Codex 側・schema への変更なし。
50
+
51
+ ### 間抜け防止 — 機械用境界の完全焼き込み(refuter 敵対的検証 2026-07-18)
52
+
53
+ 実装前に refuter による敵対的検証を通し、real 指摘 6 件を全て設計へ反映した:
54
+
55
+ | 指摘 | 対処 |
56
+ |---|---|
57
+ | HH:MM:SS 境界は「当日」解決(sc-detail の `timeToUnixRange`)で深夜跨ぎに壊れる | `--before` は ISO 8601 完全日時(ms 精度)。表示用 HH:MM:SS と機械用境界を分離 |
58
+ | 秒切り捨てで同秒行の境界包含が未定義 | 境界は strict less-than の ms 比較で規定 |
59
+ | 20 ターン窓はクエリ時再計算のため、新セッションのターン追記で窓がスライドし古い側が黙って欠落 | `--last <残り件数>` も注入時に焼き込み、recall 側の窓再計算を全廃 |
60
+ | L1 は遅延生成でバックログがあり「全 N ターンの要約」が虚偽になる。未要約ターンがどの取っ手からも見えない | `--l1` は全ターン一覧(要約 or 未要約明示)。件数は「全 M / 要約済み K」形式 |
61
+ | 既定 session 解決(cwd 系)は Codex 併走・複数ウィンドウで非決定 | `--session` も焼き込み。recall は既定解決を持たない |
62
+ | `getDb()` は mkdir + read-write open + migration を行い read-only 契約に反する | recall は `DatabaseSync(path, { readOnly: true })` + 存在チェック。DB を作成しない |
63
+
64
+ 併せて、予算詰めを行(role)単位からターン単位の原子に変更した(行単位だと同一ターンの
65
+ assistant 行だけ入り user 行が pull 側に半身で現れ、境界の算術が濁るため)。
66
+
67
+ ## 実測
68
+
69
+ ### 新設計のレンダリング(2026-07-18、実 DB 191 ターンセッション)
70
+
71
+ - totalChars 9,158 / 9,500、L2 9 ターン注入、残り 11 ターン、窓外 171 ターン
72
+ (要約済み 124 / 未要約 47)
73
+ - 焼き込まれた案内コマンドをそのまま実行して、`--l2` が境界ぴったりから 11 ターン
74
+ (33k 字)、`--l1` が全 171 ターン一覧(118k 字、`--last` で部分取得可)を返すことを確認
75
+
76
+ ### 実機 E2E(2026-07-18、global 0.8.0 / baton path)
77
+
78
+ - `/tl` → 新セッションの初回プロンプトで新形式注入が発火(47 ターン引き継ぎ)。
79
+ 注入は L1 非注入・現在地アンカー・L2 ターン原子詰め(11 ターン全文)・案内セクション
80
+ 焼き込みで、設計どおりの形を目視確認
81
+ - 焼き込まれた `recall --l2 --session <id> --before <ISO ms> --last 11` を新セッション側から
82
+ そのまま実行し、境界より古い 11 ターン(11:02:27〜11:38:57)が古い順・全文・L3 suffix
83
+ 付きで返り、注入分の最古ターン(11:48:57)と連続・重複欠落なしを確認
84
+ - 焼き込まれた `recall --l1 ... --skip 11` が窓外の全 9 ターンを「全 9 / 要約済み 7」の
85
+ 正直表示で返し、未要約 2 件は `throughline detail <時刻>` への誘導付きで明示された
86
+ - これをもって v0.8.0 の残タスク(新注入形式の実機 E2E)は完了
87
+
88
+ ### multi-hook 10k 突破の実測(2026-07-18、Claude Code 2.1.211 / 不採用)
89
+
90
+ - 同一 UserPromptSubmit に hook を 3 本登録 → それぞれ独立の `hook_success` attachment
91
+ になり 9,000 字 × 3 = 27,000 字が全部モデル可視 inline
92
+ - 1 本だけ 12k にすると**その 1 本だけ**が `<persisted-output>` 化(隣の 9k は無傷)
93
+ = 10k 判定は per context string
94
+ - 5 本 × 9k = 45,000 字でも全部可視。合算上限は 45k まで観測されず
95
+ - **attachment の並び順は登録順と一致しない**(並列実行のため非決定)
96
+ - 詳細は [rag/01-hooks/hook-stdout-10k-persisted-output.md](../../rag/01-hooks/hook-stdout-10k-persisted-output.md)
97
+
98
+ ## 帰結
99
+
100
+ - 「5〜20 ターン前の空白帯」は消滅: 窓内の非注入分は `recall --l2` が verbatim で、
101
+ 窓外は `recall --l1` が全ターン(未要約含む)で必ず到達可能
102
+ - push は会話密度に自動適応(中央値 7〜8 ターン、軽い会話なら 10 ターン超、重くて 2〜3)
103
+ - コンテキスト衛生は維持: pull はモデルが必要と判断した時だけ発生する
104
+ - `handoff-executor` の injection stats は
105
+ `injected_l2_turns / remaining_l2_turns / older_turns / older_summarized` に更新
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -40,6 +40,23 @@ v2.1.195 より前に 10k 超を emit した実績が手元に無いため、「
40
40
  ヘッダ + アンカー常時全文、L1 → L2 を新しい側から詰め、省略は注入文へ明示)。
41
41
  詳細は [ADR 0014](../../docs/adr/0014-two-phase-handoff-ghost-baton.md)。
42
42
 
43
+ ## 追記: 10k 判定は per context string — multi-hook で突破可能(2026-07-18 実測、不採用)
44
+
45
+ 同一イベントに複数 hook を登録した場合の挙動を実測した(Claude Code 2.1.211、
46
+ 一時 project + 3〜5 本の UserPromptSubmit tracer hook + `claude -p` Haiku):
47
+
48
+ - 各 hook stdout は**独立の `hook_success` attachment** になり、10k 判定も per string。
49
+ 3 本 × 9,000 字 = 27,000 字、5 本 × 9,000 字 = 45,000 字が全部モデル可視 inline
50
+ - 1 本だけ 12k にすると**その 1 本だけ**が `<persisted-output>` 化し、隣の 9k は無傷
51
+ - hooks reference の「10,000 characters per context string」「複数 hook の
52
+ additionalContext は all of the values が届く」の文言と整合
53
+ - **罠**: attachment の並び順は登録順と一致しない(hook 並列実行のため非決定)。
54
+ multi-part 注入に使うなら各 part に自己記述ヘッダが必須
55
+
56
+ Throughline では「hook の構造的想定(1 本 = 1 context string)に無い使い方」として
57
+ **不採用**(オーナー裁定 2026-07-18)。注入は 9,500 字 push + `throughline recall` pull の
58
+ 二段構成にした([ADR 0016](../../docs/adr/0016-push-pull-recall-injection.md))。
59
+
43
60
  ## 検証手順(再現用)
44
61
 
45
62
  1. 一時 project に `.claude/settings.json` で UserPromptSubmit hook を登録し、