throughline 0.16.27 → 0.16.29

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,60 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.16.29] — 2026-10-14
14
+
15
+ ### 修正
16
+
17
+ - **Claude Codeで、作業中に届いた入力が、会話の記録(L2)に残っていなかった**([ADR 0060](docs/adr/0060-claude-mid-turn-input-and-host-meta-rows.md))。Claude Codeは、ターンが動いている間に届いた入力を、user行ではなく`attachment`の`queued_command`で書く。この製品はその行を読んでいなかった。Linuxの実機の4日ぶんの記録で、人の入力のuser行1,402に対して、途中で届いた人の入力は833通あり、記録に入っていたのは3通だった。
18
+ - 直した後は、途中で届いた入力を、届いた先のターンの依頼へ、届いた順に足す。前に見出しの1行(`(作業の途中で届いた入力)`)を置く。人が打った文と、別の会話から届いた発言が当たる。裏の作業の完了通知は足さない。
19
+ - 引き継ぎの記憶と、自動継続で後継へ運ぶ依頼にも入る。
20
+ - **Claude Codeがskillを読んだ時などに書く行を、利用者の発言として記録していた**(同じADR)。`isMeta: true`で`origin`の無いuser行。この行でターンを区切っていたので、その前に本文が無いと、元の依頼が記録から消え、skillの本文が利用者の発言として残った。道具の入出力(L3)も、その行より前の分が入らなかった。自動継続で止めたターンでは、後継へ運ぶ依頼がskillの本文になっていた。
21
+ - 直した後は、この行をターンの始まりにも本文にもしない。別の会話から届いた発言(`origin`を持つ)は、今までどおり。
22
+ - **Claude Codeで、ターンの終わりに待ち入力が2通以上続けて届いた時、最後の1通だけが記録されていた**(同じADR)。同じ番へ続けて届いたuser行(`turnPosition.promptIndex`が同じ)を、1つの依頼にまとめる。
23
+
24
+ ### 変わる動き
25
+
26
+ - Claude Codeの会話で、利用者の発言の行に、作業中に届いた入力が見出しつきで足される。見出しの1行は、この製品が足した文。
27
+ - skillの本文は、利用者の発言として記録されなくなる。道具の入出力には残る。
28
+ - 前の版が記録したターンは書き直さない。直しが効くのは、この版が取り込むターンから。行の番号は変わらないので、前の版の行と重なって増える事は無い。
29
+ - Codex・Grok・Cursorの記録の読み方は変えていない。
30
+
31
+ ## [0.16.28] — 2026-10-14
32
+
33
+ ### 修正
34
+
35
+ - **Codex Desktopで、ゴール(`/goal`)を持つタスクを引き継いだ後、旧タスクが止まらず、「タスク終了」の通知と後継への画面の切り替えが続いていた**([ADR 0059](docs/adr/0059-codex-handoff-carries-thread-goal.md))。Windowsの実機で2026-10-13に起きた。
36
+ - Codexは、タスクが手すきになるたびにゴールを読み、実行中なら自分で次のターンを起こす。旧タスクにゴールが実行中のまま残っていたので、止めるたびに次のターンが始まった。22:04から1〜2秒おきに2,067回。入力は全部Codex自身の続行の指示で、人の入力は無かった。Throughlineはそのたびにターンを止めて、後継を画面に開いていた。
37
+ - 後継にはゴールが運ばれていなかった。ゴールで動かしていた作業は、引き継ぎを境に、自分では続かなくなっていた。
38
+ - 直した後は、引き継ぎの最初に旧タスクのゴールを一時停止にし、後継が作業を続けたのを確かめた後で、ゴールを後継へ運ぶ。目標の文はそのまま、予算は使った分を引いた残りを渡す。動いていたゴールは後継でも動き、利用者が止めていたゴールは止めたまま移る。旧タスクのゴールは、一時停止のまま残す。
39
+ - 完了したゴールと、Codexが上限や行き詰まりで止めたゴールは運ばない。後継にもうゴールがあれば書き換えない。
40
+ - ゴールを読めない・書けない時も、引き継ぎは止めない。
41
+ - 引き継ぎが途中で止まった時は、旧タスクのゴールを実行中へ戻さない(戻すと、続行のターンと引き継ぎの失敗が繰り返される)。失敗の画面に、ゴールを止めてある事を書く。
42
+ - **引き継ぎ済みの旧タスクへ、人の入力ではないターンが来た時にも、後継を画面に開いていた**(同じADR)。ターンの入力がCodex自身の文脈だけの時は、止めて後継を示すが、画面は切り替えない。
43
+ - **0.16.27までの版が引き継いだ旧タスクに、ゴールが実行中のまま残っている時の片付け**(同じADR)。引き継ぎ済みの旧タスクへターンが来た時に、別のprocessで旧タスクのゴールを確かめる。実行中なら一時停止にし、今の後継にゴールが無ければ運ぶ。同じ旧タスクへは30秒に1回まで。片付けた事は`~/.throughline/codex-auto-handoff/goal-settles.jsonl`へ1行残す(時刻・ID・付けた状態。目標の文は書かない)。
44
+
45
+ ### 変わる動き
46
+
47
+ - ゴールを持つタスクの引き継ぎで、旧タスクのゴールが「一時停止」になり、後継に同じゴールが付く。後継のゴールの使用量は0から数え直す。
48
+ - 後から片付けたゴール(上の3つめ)は、後継が手すきの時は、その場では動き出さない。後継の次のターンが終わった時からCodexが続ける。
49
+ - 旧タスクの画面のゴールの表示は、Codex Desktopがそのタスクを読み直すまで、古いまま見える事がある。
50
+
51
+ ### 対応の範囲
52
+
53
+ | | macOS | Linux | Windows |
54
+ |---|---|---|---|
55
+ | Codex | 直した。単体試験と、実機で、直した版の関数(止める・運ぶ・片付ける)をCodex Desktop同梱のCodex(0.162.0-alpha.17.2)へ通した(25項目)。止めなければCodexが次のターンを起こし続ける事、止めた後は起きない事、後継がゴールを続けて完了する事まで | 直した。同じ確認を、codex-cli 0.162.1(コンテナ)で。本物のCodex Desktop(同梱の0.162.0-alpha.2)で、ゴールを持つタスクを自動圧縮の手前まで進める通しも流した(入っている0.16.27へ、直した3ファイルを一時だけ重ねた)。8回続けて引き継ぎ、毎回、旧タスクのゴールは一時停止・後継のゴールは実行中になり、最後の後継が作業を終えてゴールを完了した。引き継いだ後の旧タスクへ来たターンは0回(引き継ぎの最中に、1秒以内に4〜5回) | 直した。起きた端末。旧タスクのゴールを一時停止にして、連続が止まるのを見た。同じ確認を、Codex Desktop同梱のCodex(0.162.0-alpha.17.2)で(25項目)。起きた旧タスクの本物の記録へ、直した版のhookの入口を読むだけで当てた(止める・後継を開かない・片付けを起こす、の答え) |
56
+ | Claude Code | 変更なし(タスクごとのゴールが無い) | 同じ | 同じ |
57
+ | Grok | 変更なし(自動継続が無い) | 同じ | 同じ |
58
+ | Cursor | 変更なし(自動継続が無い) | 同じ | 同じ |
59
+
60
+ ### 確かめていない事
61
+
62
+ - macOSとWindowsの本物のCodex Desktopの画面で、ゴールを持つタスクが自動圧縮の手前まで進んで引き継がれる通し。通したのはLinuxのCodex Desktopだけ。macOSとWindowsは、Desktop同梱のCodexへ関数を通す所までで、画面のタスクは使っていない。
63
+ - 0.16.27までの版が引き継いだ旧タスクの片付け(別のprocessを起こす所)を、本物のhookから通す事。関数と、起こす引数と、起きた旧タスクの記録に対するhookの答えは確かめた。Windowsの起きた旧タスクは、手で一時停止にしてあるので、もうこの道を通らない。
64
+ - Codex Desktopは、完了したゴールを自分で消す(Linuxの実機で、完了の直後に画面の側が`thread/goal/clear`を送った)。完了の前に画面の側がゴールを書き換える場面があるかは見ていない。
65
+ - ゴールの続行の指示(Codex自身の文脈)は、記憶では今までどおり利用者の発言として残る。この版では変えていない。
66
+
13
67
  ## [0.16.27] — 2026-10-13
14
68
 
15
69
  ### 修正
@@ -2522,7 +2576,9 @@ two attempts, instrument first instead of patching again.
2522
2576
 
2523
2577
  ---
2524
2578
 
2525
- [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.27...HEAD
2579
+ [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.29...HEAD
2580
+ [0.16.29]: https://github.com/kitepon/Throughline/compare/v0.16.28...v0.16.29
2581
+ [0.16.28]: https://github.com/kitepon/Throughline/compare/v0.16.27...v0.16.28
2526
2582
  [0.16.27]: https://github.com/kitepon/Throughline/compare/v0.16.26...v0.16.27
2527
2583
  [0.16.26]: https://github.com/kitepon/Throughline/compare/v0.16.25...v0.16.26
2528
2584
  [0.16.25]: https://github.com/kitepon/Throughline/compare/v0.16.24...v0.16.25
package/README.ja.md CHANGED
@@ -29,7 +29,7 @@ throughline auto-handoff disable
29
29
 
30
30
  既定では無効です。`enable`は必要な`PreCompact(auto)`フックを登録し、そのフックの承認と有効状態を公式APIで確認します。`--project`を省略すると全projectを対象にします。macOS・Windows・LinuxのCodex Desktopで実機確認済みです(Windowsは0.16.6から、Linuxは0.16.7から)。Codex Desktopに同梱のCodexが0.162以上の時は、0.16.7以上が要ります。VS Code・CLIの自動継続は未検証です。
31
31
 
32
- 自動圧縮の開始前に旧ターンを停止し、同じprojectの新しいタスクへ記憶を注入して表示します。新しいタスクには、前任のタスクの題から`<project名>|<作業の概要>(自動引き継ぎ)`の名前を付けます。後継が作業を続けたのを確かめた後、旧タスクの名前の頭へ`【引き継ぎ済み】`を付けます(0.16.10から。それより前の版が引き継いだ分は`throughline auto-handoff mark-sources`で付け直せます。名前の無い後継にも名前が付きます。`--dry-run`で、付ける名前だけを先に見られます)。一覧で、印の無い1本が今の続きです。元のモデル・推論強度・権限・モードなどが表示後も一致することを確認してから、継続指示を一度だけ送ります。後継でその入力と作業の進捗を観測して完了とします。手動`/compact`は発火対象に含めません。
32
+ 自動圧縮の開始前に旧ターンを停止し、同じprojectの新しいタスクへ記憶を注入して表示します。新しいタスクには、前任のタスクの題から`<project名>|<作業の概要>(自動引き継ぎ)`の名前を付けます。後継が作業を続けたのを確かめた後、旧タスクの名前の頭へ`【引き継ぎ済み】`を付けます(0.16.10から。それより前の版が引き継いだ分は`throughline auto-handoff mark-sources`で付け直せます。名前の無い後継にも名前が付きます。`--dry-run`で、付ける名前だけを先に見られます)。一覧で、印の無い1本が今の続きです。元のモデル・推論強度・権限・モードなどが表示後も一致することを確認してから、継続指示を一度だけ送ります。後継でその入力と作業の進捗を観測して完了とします。手動`/compact`は発火対象に含めません。旧タスクにCodexのゴール(`/goal`)があれば、引き継ぎの最初に一時停止にし、後継が作業を続けた後で後継へ運びます(0.16.28から。動いていたゴールは後継でも動き、止めていたゴールは止めたまま移ります)。
33
33
 
34
34
  記憶は引き継ぎ全体の直近20ターンをL2全文、それより古いターンをL1で渡します。元のユーザー依頼と中断地点を保持し、tool結果とThinkingは注入しません。本文内の`auto-handoff detail`コマンドで、凍結したL2/L3を引き継ぎID・origin・turnから取得できます。A→B→Cの連続引き継ぎでも祖先の詳細を取得できます。
35
35
 
package/README.md CHANGED
@@ -28,7 +28,7 @@ throughline auto-handoff disable
28
28
 
29
29
  既定では無効です。`enable`は必要な`PreCompact(auto)`フックを登録し、そのフックの承認と有効状態を公式APIで確認します。`--project`を省略すると全projectを対象にします。macOS・Windows・LinuxのCodex Desktopで実機確認済みです(Windowsは0.16.6から、Linuxは0.16.7から)。Codex Desktopに同梱のCodexが0.162以上の時は、0.16.7以上が要ります。VS Code・CLIの自動継続は未検証です。
30
30
 
31
- 自動圧縮の開始前に旧ターンを停止し、同じprojectの新しいタスクへ記憶を注入して表示します。新しいタスクには、前任のタスクの題から`<project名>|<作業の概要>(自動引き継ぎ)`の名前を付けます。後継が作業を続けたのを確かめた後、旧タスクの名前の頭へ`【引き継ぎ済み】`を付けます(0.16.10から。それより前の版が引き継いだ分は`throughline auto-handoff mark-sources`で付け直せます。名前の無い後継にも名前が付きます。`--dry-run`で、付ける名前だけを先に見られます)。一覧で、印の無い1本が今の続きです。元のモデル・推論強度・権限・モードなどが表示後も一致することを確認してから、継続指示を一度だけ送ります。後継でその入力と作業の進捗を観測して完了とします。手動`/compact`は発火対象に含めません。
31
+ 自動圧縮の開始前に旧ターンを停止し、同じprojectの新しいタスクへ記憶を注入して表示します。新しいタスクには、前任のタスクの題から`<project名>|<作業の概要>(自動引き継ぎ)`の名前を付けます。後継が作業を続けたのを確かめた後、旧タスクの名前の頭へ`【引き継ぎ済み】`を付けます(0.16.10から。それより前の版が引き継いだ分は`throughline auto-handoff mark-sources`で付け直せます。名前の無い後継にも名前が付きます。`--dry-run`で、付ける名前だけを先に見られます)。一覧で、印の無い1本が今の続きです。元のモデル・推論強度・権限・モードなどが表示後も一致することを確認してから、継続指示を一度だけ送ります。後継でその入力と作業の進捗を観測して完了とします。手動`/compact`は発火対象に含めません。旧タスクにCodexのゴール(`/goal`)があれば、引き継ぎの最初に一時停止にし、後継が作業を続けた後で後継へ運びます(0.16.28から。動いていたゴールは後継でも動き、止めていたゴールは止めたまま移ります)。
32
32
 
33
33
  記憶は引き継ぎ全体の直近20ターンをL2全文、それより古いターンをL1で渡します。元のユーザー依頼と中断地点を保持し、tool結果とThinkingは注入しません。本文内の`auto-handoff detail`コマンドで、凍結したL2/L3を引き継ぎID・origin・turnから取得できます。A→B→Cの連続引き継ぎでも祖先の詳細を取得できます。
34
34
 
@@ -56,3 +56,9 @@ turn の間に入力が2通たまり、turn が終わった時に2通が続け
56
56
  本物の hook の命令(`process-turn`)へ、合成した transcript を通した(切り離した HOME)。
57
57
  - 確かめていない: 本物の Claude Code が、待っていた入力を2通続けて書く場面での、この版の動き。狙って起こすには、
58
58
  動いている turn の間に入力を2通ためる必要がある。次に起きた時は、失敗が記録されない事で分かる。
59
+
60
+ ## 追記(2026-10-14、ADR 0060)
61
+
62
+ Context の「保存の欠けは無い」は、完了した turn の事だった。続けて届いた入力の1通目は、assistant 断片の無い group として
63
+ 保存されていなかった(コンテナの本物の transcript で 5 回)。ADR 0060 で、同じ `turnPosition.promptIndex` で続けて届いた
64
+ user 行を1つの依頼にまとめる様にした。assistant 断片の無い group を飛ばす作りは、そのまま残している。
@@ -0,0 +1,96 @@
1
+ # ADR 0059: Codex の旧タスクのゴールは、引き継ぎの初めに止めて、後継へ運ぶ
2
+
3
+ 日付: 2026-10-13
4
+
5
+ ## Context
6
+
7
+ Codex には、タスクごとのゴール(`/goal`)がある。目標の文を決めておくと、タスクが手すきになるたびに Codex が自分で
8
+ 次のターンを起こし、目標が終わるまで続ける。
9
+
10
+ 2026-10-13、Windows の Codex Desktop(同梱の Codex 0.162.0-alpha.17.2、Throughline 0.16.27)で、自動継続が
11
+ 引き継いだ後の旧タスクが止まらなかった。
12
+
13
+ - 22:04:09 に旧タスクを止めて引き継ぎを始め、22:05:44 に後継が作業を続けた(`continued`)。引き継ぎ自体は成功している。
14
+ - 旧タスクには、20:10 に利用者が付けたゴールが「実行中」のまま残っていた。後継にはゴールが無かった。
15
+ - 旧タスクでは、22:04:10 から 1〜2 秒おきに新しいターンが始まり続けた。23:43:35 までに 2,067 回。
16
+ どのターンも入力は Codex 自身の文脈(`<codex_internal_context source="goal">` で始まる続行の指示)だけで、人の入力は 0。
17
+ - ターンが始まるたびに、文脈が上限を超えているので `PreCompact(auto)` が来る。Throughline は「引き継ぎ済みの旧タスクへ
18
+ 新しい入力が来た」(ADR 0039)として、ターンを止め、後継を画面に開いた。
19
+ - 利用者には、「タスク終了」の通知が続き、画面が後継へ切り替わり続ける様に見えた。旧タスクの記録(rollout)は
20
+ 約34MB まで増えた。モデルは呼ばれていない(止めるのはモデルを呼ぶ前)。
21
+ - 引き継ぎの最中(22:04:10〜22:05:44)にも同じターンが来ていた。こちらは「引き継ぎの最中の入力」(ADR 0040)として
22
+ 止まっていた。
23
+
24
+ 原因は、Throughline がゴールを扱っていなかった事にある。
25
+
26
+ - Codex は、タスクが手すきになるたびに DB のゴールを読み、`active` なら続行の指示でターンを始める
27
+ (`codex-rs/ext/goal/src/extension.rs` の `on_thread_idle` → `runtime.rs` の `continue_if_idle`)。
28
+ hook が止めたターン(`turn_aborted`、`interrupted`)の後も同じ。利用者が自分で中断した時だけの持ち越しの印は、hook の止めでは付かない。
29
+ - 後継は新しいタスクなので、ゴールを持たない。後継は継続の指示の1ターンを終えると、そこで止まる。
30
+ ゴールで動かしていた作業が、引き継ぎを境に自分では続かなくなっていた。
31
+
32
+ Codex の app-server には、ゴールを読む・変える・消す口がある(`thread/goal/get`・`thread/goal/set`・`thread/goal/clear`。
33
+ experimental)。別の process から使える。確かめた事(Linux の Codex 0.162.1 と、Windows の実機):
34
+
35
+ - 別の process が `status: 'paused'` を書くと、タスクを開いている process は次の手すきで続行を起こさない
36
+ (Windows の実機で、連続が1ターンの内に止まった)。
37
+ - ゴールの無いタスクへ、別の process が目標の文つきで `status: 'active'` を書ける。そのタスクがターンを動かしている間に
38
+ 書くと、タスクを開いている process は、そのターンの後から続行を起こす。
39
+ - 手すきのタスクへ `active` を書いても、その場では続行が起きない。次にそのタスクのターンが終わった時から起きる。
40
+ - `status: 'paused'` と目標の文を一緒に渡して、止まった状態のゴールを新しく作れる。
41
+
42
+ ## Decision
43
+
44
+ 1. worker は、引き継ぎの最初に、旧タスクのゴールを読む。`active` なら `paused` にする(`pauseAutoHandoffSourceGoal`)。
45
+ ゴールは消さない。止めたかどうかを、引き継ぎの記録(`runtime_json.goalCarry.paused`)へ書く。目標の文は記録へ写さない。
46
+ 2. 後継が作業を続けたのを確かめた後(`continued` の直後、順番待ちを運ぶより先)に、旧タスクのゴールを後継へ運ぶ
47
+ (`carryAutoHandoffGoal`)。後継の最初のターン(継続の指示)が動いている間に付けるので、そのターンの後から Codex が続ける。
48
+ - 目標の文はそのまま写す。予算(`tokenBudget`)があれば、旧タスクで使った分を引いた残りを渡す。
49
+ - この製品が止めたゴールは、後継では `active` にする。利用者が止めていたゴール(`paused`)は、`paused` のまま運ぶ。
50
+ - 完了したゴール(`complete`)と、Codex が上限や行き詰まりで止めたゴール(`budgetLimited`・`usageLimited`・`blocked`)は運ばない。
51
+ - 後継にもうゴールがあれば、書き換えない。
52
+ - 運ぶ時に旧タスクのゴールが `active` なら(最初の一時停止が効かなかった)、先に `paused` にしてから運ぶ。
53
+ 3. 旧タスクのゴールは `paused` のまま残す。旧タスクは引き継ぎ済みなので、そのゴールは動かない。
54
+ 4. ゴールを読めない・書けない時も、引き継ぎは止めない(ゴールの口が無い版の Codex を含む)。理由は worker のログに残す。
55
+ 5. 途中で止まった引き継ぎ(`failed`・`unknown`)では、ゴールを後継へ運ばず、旧タスクのゴールも `active` へ戻さない。
56
+ 戻すと、止まったままの旧タスクで、続行のターンと引き継ぎの失敗が繰り返される。失敗の画面に、ゴールを止めてある事と、
57
+ 旧タスクで続ける時の戻し方を書く。やり直しで `continued` になった時に運ぶ。同じ旧タスクから新しい引き継ぎを作った時は、
58
+ 前の引き継ぎが止めていた事を引き継ぐ(後継では `active` にする)。
59
+ 6. 引き継ぎ済みの旧タスクへターンが来た時(ADR 0039 の道)に、次の2つを足す。
60
+ - そのターンの入力が Codex 自身の文脈だけなら(`<codex_internal_context` で始まる物だけ)、後継を画面に開かない。
61
+ 人が入力していないのに画面を切り替えない。ターンを止めて後継を示す事は変えない。
62
+ - 旧タスクにゴールが動いたまま残っていないかを、別の process で確かめる(`auto-handoff settle-goal`。hook は待たない)。
63
+ 動いていれば `paused` にし、今の一番先の後継にゴールが無ければ `active` で運ぶ。同じ旧タスクへは30秒に1回まで。
64
+ 0.16.27 までの版が引き継いだ旧タスクと、引き継いだ後で利用者が旧タスクのゴールを再開した時が当たる。
65
+ 片付けた事は `~/.throughline/codex-auto-handoff/goal-settles.jsonl` へ1行残す(時刻・ID・付けた状態。目標の文は書かない)。
66
+
67
+ ## Consequences
68
+
69
+ - ゴールで動かしていたタスクは、引き継いだ後も後継がゴールで続ける。旧タスクは止まる。
70
+ - 引き継ぎの最中に来る続行のターンは、worker がゴールを止めるまでの数秒の分だけになる。
71
+ - 後継のゴールの使用量(`tokensUsed`・`timeUsedSeconds`)は 0 から数え直しになる。予算は残りを渡すので、合計の上限は変わらない。
72
+ - 6 の片付けで運んだゴールは、後継が手すきの時は、その場では動き出さない。後継の次のターンが終わった時から Codex が続ける。
73
+ - 旧タスクの画面のゴールの表示は、別の process が書いた変更なので、Codex Desktop がそのタスクを読み直すまで古いまま見える事がある。
74
+ - `continued` を記録した直後に worker が落ちると、ゴールは旧タスクで止まったまま、後継へは運ばれない。旧タスクは動かないので
75
+ 連続は起きない。利用者が後継でゴールを付け直す必要がある。
76
+ - `blocked` などで止まっているゴールは運ばない。旧タスクに残る。
77
+ - 引き継ぎ1回につき、Codex の app-server への接続が2回増える(最初の読み書きと、運ぶ時)。
78
+ - Claude Code の自動継続は変えない。Claude Code にはタスクごとのゴールが無い。
79
+
80
+ ## 確かめた範囲
81
+
82
+ | | macOS | Linux | Windows |
83
+ |---|---|---|---|
84
+ | Codex Desktop | 直した版の関数(止める・運ぶ・片付ける)を、Desktop 同梱の Codex(0.162.0-alpha.17.2)へ通した(25項目) | 同じ確認を codex-cli 0.162.1 で。本物の Codex Desktop(同梱の 0.162.0-alpha.2)で、ゴールを持つタスクの引き継ぎを8回続けて通した | 起きた端末。旧タスクのゴールを `paused` にして、連続が止まるのを見た。関数の確認(25項目)を Desktop 同梱の Codex(0.162.0-alpha.17.2)で。起きた旧タスクの本物の記録へ、直した版の hook の入口を読むだけで当てた |
85
+ | Claude Code | 変更なし(ゴールが無い) | 同じ | 同じ |
86
+ | Grok | 変更なし(自動継続が無い) | 同じ | 同じ |
87
+ | Cursor | 変更なし(自動継続が無い) | 同じ | 同じ |
88
+
89
+ Linux の通し(2026-10-14 00:05〜00:14 JST)で見た事:
90
+
91
+ - タスクを Codex Desktop で開くと、実行中のゴールを持つタスクは、入力を待たずに続行のターンを始めた。
92
+ - 引き継ぎのたびに、止めたターンの直後から続行のターンが 0.15 秒ほどの間隔で来た。worker がゴールを止めるまでの1秒以内に 4〜5 回で、
93
+ その後は来ていない。8回とも `goalCarry` は `{"paused":true,"carried":"active"}`。
94
+ - 最後の後継が作業を終えるとゴールは完了になり、Codex Desktop の画面の側が `thread/goal/clear` を送ってゴールを消した。
95
+
96
+ macOS と Windows の本物の Codex Desktop の画面で、ゴールを持つタスクが引き継がれる通しは、公開の時点では確かめていない。
@@ -0,0 +1,123 @@
1
+ # ADR 0060: Claude Code の、ターンの途中で届いた入力は依頼へ足し、host が書く meta の行ではターンを区切らない
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-14
5
+
6
+ ## Context
7
+
8
+ Claude Code の transcript から L2(`bodies`)を作る時、この製品は「本文を持つ user 行1つ + それに続く assistant の本文」を
9
+ 1つのターンとして読んでいた(`getLogicalTurnGroups`)。user 行なら何でもターンの始まりにし、`attachment` の行は L3 にだけ使っていた。
10
+
11
+ 2026-10-14、BellTeam のコンテナ(Linux、Claude Code 2.1.293・2.1.296、この製品は 0.16.27)の本物の transcript で、
12
+ 人の入力が L2 に入っていない形が3つ見つかった(本文は読んでいない。行の種類・印・長さ・時刻だけ)。
13
+
14
+ ### 1. ターンの途中で届いた入力
15
+
16
+ Claude Code は、ターンが動いている間に届いた入力を、user 行ではなく `attachment` の行で書く。
17
+
18
+ ```text
19
+ {"type":"attachment","attachment":{"type":"queued_command","prompt":…,"commandMode":"prompt","origin":{"kind":"human"},"humanTurn":true,…}}
20
+ ```
21
+
22
+ `prompt` は文字列か、text と image の block の配列。裏の作業の完了通知も同じ形で届く(`commandMode: "task-notification"`、
23
+ `origin.kind: "task-notification"`)。前後に `queue-operation` の `enqueue` と `remove` の行が並ぶ。ターンが終わるまで
24
+ 届けられなかった入力は、`dequeue` の後に普通の user 行になる(`promptSource: "queued"`)。
25
+
26
+ 10/10 からの 163 本で、人の入力の `queued_command` は 833 通あった(人の入力の user 行は 1,402)。L2 に入っていたのは 3 通
27
+ (同じ文が別の行にもあった物)。作業中に打った文、ほかの席から差し込まれた文が、記憶に残っていなかった。
28
+
29
+ ### 2. host が書く meta の user 行
30
+
31
+ Claude Code は、skill を読んだ時の本文や、端末で打った命令の注意書きを、`isMeta: true` の user 行で書く。`origin` は無い。
32
+ skill の行は、直後に `attachment` の `command_permissions` が続く。
33
+
34
+ この行をターンの始まりにしていたので、次の事が起きていた。
35
+
36
+ - その行より前に assistant の本文が無いと、元の依頼は「本文の無い群」になって保存されない。meta の行の本文
37
+ (skill の手順)が、利用者の発言として保存される。
38
+ - 本文があると、1つのターンが2つに割れる。
39
+ - L3 は「最後の user 本文の行」から切り出すので、meta の行より前の道具の入出力が入らない。
40
+ - 自動継続で止めたターンの依頼(`snapshotInFlight`)も「最後の user 本文」を読むので、後継へ運ぶ依頼が skill の本文になる。
41
+
42
+ 同じ 163 本で、`origin` の無い meta の行は 61 行(道具の後 27、人の入力の直後 11、meta の行の後 17、端末の命令の注意書き 6)。
43
+ 46 行が利用者の発言として L2 の群に入っていた。
44
+
45
+ 別の会話から届いた発言も `isMeta: true` だが、`origin.kind: "peer"` を持つ。届いた入力なので、今までどおりターンの始まりにする。
46
+
47
+ ### 3. ターンの終わりに続けて届いた待ち入力
48
+
49
+ ターンの間に入力が2通以上たまると、ターンが終わった時に、2通が続けて user 行になる。2行とも `promptSource: "queued"` で、
50
+ 同じ `turnPosition.promptIndex` を持つ(同じ番へ届いた印)。
51
+
52
+ 1通目は「assistant の本文が無い群」になり、保存されなかった。ADR 0055 は、この形で Stop が失敗する事を直した時に
53
+ 「保存の欠けは無い」と書いたが、欠けていたのは完了したターンではなく、1通目の入力だった。同じ 163 本で 5 回。
54
+
55
+ ### ほかの host
56
+
57
+ - Codex: ターンを host の開始と完了の印(`task_started`・`task_complete`)で区切り、その間の利用者の発言を全部持つ
58
+ (`codex-rollout-memory.mjs`)。同じ欠けは起きない作り。
59
+ - Grok: `chat_history.jsonl` に、途中で届いた入力を表す別の行は見つからなかった(80 本)。人の入力の行が本文を挟まずに
60
+ 続く形は 9 回あり、`prompt_index` は違っていた。止められたターンか、途中の入力かは分けていない。この ADR では扱わない。
61
+ - Cursor: 10 本を見て、同じ形の行は無かった。
62
+
63
+ ## Decision
64
+
65
+ 1. **ターンの途中で届いた入力は、届いた先のターンの依頼へ、届いた順に足す。** `attachment` の `queued_command` で、
66
+ `origin.kind` が `human` か `peer` の物。直前に返した user の行の本文の後ろへ、空行・見出しの1行
67
+ (`(作業の途中で届いた入力)`)・本文の順で足す。本文は user 行と同じ正規化を通す(端末の制御の除去、別の会話の注意書きの除去)。
68
+ - `turn_number` は進めない。`attachment` は今までも本文行に数えていない。保存済みの番号とずれない。
69
+ - 完了通知(`origin.kind: "task-notification"`)は足さない。そのターン自身の裏の作業の結果で、L3 に残っている。
70
+ - `origin` の無い物、`isSidechain: true` の物、足す先の user 行がまだ無い物は足さない。
71
+ - ターンの始まり方(`start`)は変えない。完了通知で始まったターンへ人の入力が届いても、`self` のまま。
72
+ 2. **`isMeta: true` で `origin.kind` の無い user 行は、ターンの始まりにも本文にもしない**(`isClaudeHostWrittenMetaEntry`)。
73
+ 圧縮の要約行(ADR 0032)と同じ扱い。`turn_number` は進める(その行を数えていた頃に保存した番号とずれない)。
74
+ L3 の切り出し(`sliceCurrentTurnEntries`・`sliceInFlightTurnEntries`)も、この行で区切らない。
75
+ 3. **同じ番へ続けて届いた user 行は、1つの依頼にまとめる。** 直前の user 行との間に assistant の本文が無く、両方が
76
+ `origin.kind` を持ち、`turnPosition.promptIndex` が同じ整数の時。空行でつなぐ。`turn_number`・時刻・`start`・`prompt_id` は
77
+ 1通目の物。`turn_number` は2通目の分も進める。
78
+ - `promptIndex` が違う行、印の無い行、`origin` の無い行(端末で打った命令)は、まとめない。止められて本文を書かなかった
79
+ ターンの依頼は、今までどおり保存されない。
80
+ 4. 判定は、host が transcript に書いた印(`type`・`isMeta`・`origin.kind`・`turnPosition`)だけで行う。本文は読まない。
81
+ 5. 保存済みのターンは書き直さない。直しが効くのは、この版が取り込むターンから。
82
+
83
+ ## Consequences
84
+
85
+ - 作業中に打った文と、ほかの席から差し込まれた文が、そのターンの利用者の発言として L2 に残る。引き継ぎの記憶と、
86
+ 自動継続で後継へ運ぶ依頼(`in_flight.user`)にも入る。
87
+ - 利用者の発言の行が長くなる。見出しの1行は、この製品が足した文で、利用者の文ではない。
88
+ - skill の本文は L2 に入らなくなる(L3 には `Skill` の道具の入出力として残る)。
89
+ - 前の版が取り込んだ会話では、meta の行で割れていた2つの群が1つにまとまる。`backfillBodies` は「群の断片の番号が1つでも
90
+ 保存済みなら回収しない」ので、行は増えない。前の版が落とした依頼と途中の入力は、さかのぼっては入らない。
91
+ - Stop の flush barrier(ADR 0026・0055)は `readTranscript` の上で動く。続けて届いた待ち入力は1つの群になるので、
92
+ 「本文の無い群を飛ばす」場面が減る。飛ばす作りは残す(`promptIndex` の無い版の Claude Code、止められたターン)。
93
+ - `auditor-context` と `caveat-context` の鮮度の照合は、transcript の最後の群の依頼と、保存済みの行の hash を比べる。
94
+ 前の版が取り込んだ最後のターンに途中の入力や meta の行があると、版を上げた直後の1回だけ合わない(次のターンからは合う)。
95
+ - 途中の入力がターンの最後の本文より後に書かれた時も、そのターンの依頼へ足す。Stop の取り込みがもう済んでいれば、
96
+ その入力は L2 に入らない(保存済みの行は書き直さない)。実物では見ていない。
97
+
98
+ ## 確かめた事・確かめていない事
99
+
100
+ - 確かめた:
101
+ - 単体試験(途中の入力の文字列と配列、完了通知・sidechain・印なしを足さない、別の会話からの文、足す先が無い時、meta の行、
102
+ `origin` を持つ meta の行、続けて届いた待ち入力、まとめない3つの形、L3 の切り出し、前の版が保存した行と重ねない事、
103
+ 止めたターンの取り込み)。直す前の版では、形を表す5本が落ちる。
104
+ - 本物の transcript へ、前の版と直した版の読み手を当てて比べた(DB は開かない。数だけ)。3つの OS とも、assistant の行の番号は
105
+ 全部同じで、直した版の代表の番号に、前の版に無い物は 0。
106
+
107
+ | | 本数 | 群 | 人の入力の user 行が群の依頼にある数 | 途中の人の入力が群の依頼にある数 | 利用者の発言になっていた meta の行 |
108
+ |---|---|---|---|---|---|
109
+ | Linux(コンテナ) | 163 | 1,661 → 1,634 | 1,385 → 1,401(1,402 のうち) | 3 → 832(833 のうち) | 46 → 0 |
110
+ | macOS | 103 | 1,265 → 1,246 | 905 → 910(926 のうち) | 0 → 121(122 のうち) | 36 → 0 |
111
+ | Windows | 108 | 922 → 899 | 324 → 335(369 のうち) | 2 → 111(119 のうち) | 51 → 0 |
112
+
113
+ 入らなかった途中の入力は、3つの OS とも全部「依頼へは足されたが、そのターンに assistant の本文が無い」物だった
114
+ (止められたターン。完了したターンの回収には出ない)。
115
+ - 別の会話からの文(`origin.kind: "peer"`)が、ターンの途中に `queued_command` で届く実物は、Windows に 187 通あった
116
+ (`commandMode: "prompt"`。頭は地の文か `<cross-session-message>`)。同じ扱いで依頼へ足される。
117
+ - 本物の Stop の入口(`process-turn`)へ、本物の transcript の写しを、使い捨ての HOME で通した。空の DB から(Linux 3 本、
118
+ macOS 4 本、Windows 2 本): 途中の人の入力の数だけ見出しが入り、2回目の Stop は 0 行。前の版で取り込んだ後に直した版で
119
+ 通す(Linux 3 本、macOS 4 本、Windows 1 本): 行は増えない。
120
+ - 確かめていない:
121
+ - 直した版を入れた端末で、本物の Claude Code が、途中の入力のあるターンを終えた時の L2。入れた後の追跡で読む。
122
+ - `turnPosition` を書かない版の Claude Code。その時は 3 が効かず、今までと同じ動きになる。`origin` を書かない版の
123
+ `queued_command`(macOS と Windows の古い行にある)は、足さない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.16.27",
3
+ "version": "0.16.29",
4
4
  "type": "module",
5
5
  "description": "Persistent memory hooks for Claude Code, Codex, Grok, and Cursor (/clear-safe context compression)",
6
6
  "keywords": [
@@ -5,7 +5,8 @@ import { getDb, openReadOnlyDb, DB_PATH, CURRENT_VERSION } from '../db.mjs';
5
5
  import { listAutoHandoffs, getAutoHandoff } from '../codex-auto-handoff-store.mjs';
6
6
  import { renderFrozenDetail } from '../codex-auto-handoff-memory.mjs';
7
7
  import { readAutoHandoffConfig, writeAutoHandoffConfig } from '../codex-auto-handoff-config.mjs';
8
- import { autoHandoffDeliveryProfile, runAutoHandoffWorker, markAutoHandoffSources, resolveAutoHandoffSuccessor } from '../codex-auto-handoff.mjs';
8
+ import { autoHandoffDeliveryProfile, runAutoHandoffWorker, markAutoHandoffSources, resolveAutoHandoffSuccessor,
9
+ settleAutoHandoffSourceGoal } from '../codex-auto-handoff.mjs';
9
10
  import { installCodexAutoHandoffHook, installClaudeAutoHandoffHooks, removeClaudeAutoHandoffHooks,
10
11
  claudeAutoHandoffHooksRegistered } from './install.mjs';
11
12
  import { sameProjectPath } from '../project-path.mjs';
@@ -19,9 +20,9 @@ const CLAUDE_ACTIONS = ['enable', 'disable', 'status', 'worker', 'desktop-open',
19
20
 
20
21
  export function parseAutoHandoffArgs(args) {
21
22
  const out = { action: args[0] ?? 'status', host: 'codex', operation: null, project: null, origin: null, turn: null, database: null, thread: null, json: false, dryRun: false };
22
- if (!['enable', 'disable', 'status', 'resume', 'detail', 'worker', 'desktop-open', 'mark-sources', 'successor'].includes(out.action)) throw Error('auto_handoff_action_invalid');
23
+ if (!['enable', 'disable', 'status', 'resume', 'detail', 'worker', 'desktop-open', 'mark-sources', 'successor', 'settle-goal'].includes(out.action)) throw Error('auto_handoff_action_invalid');
23
24
  const allowed = ['host', ...{ enable: ['project'], disable: [], status: ['project', 'operation'],
24
- resume: ['operation'], worker: ['operation'], 'desktop-open': ['operation'], 'mark-sources': [], successor: ['thread'], detail: ['operation', 'origin', 'turn', 'database'] }[out.action]];
25
+ resume: ['operation'], worker: ['operation'], 'desktop-open': ['operation'], 'mark-sources': [], successor: ['thread'], 'settle-goal': ['operation', 'thread'], detail: ['operation', 'origin', 'turn', 'database'] }[out.action]];
25
26
  const seen = new Set();
26
27
  for (let i = 1; i < args.length; i++) {
27
28
  if (seen.has(args[i])) throw Error('auto_handoff_argument_invalid');
@@ -36,8 +37,8 @@ export function parseAutoHandoffArgs(args) {
36
37
  if (out.host === 'claude' ? !CLAUDE_ACTIONS.includes(out.action) : out.action === 'desktop-open') throw Error('auto_handoff_action_unsupported');
37
38
  if (out.project && !isAbsolute(out.project)) out.project = resolve(out.project);
38
39
  if (out.project && out.operation) throw Error('auto_handoff_argument_invalid');
39
- if (['worker', 'resume', 'detail', 'desktop-open'].includes(out.action) && !out.operation) throw Error('auto_handoff_operation_required');
40
- if (out.action === 'successor' && !out.thread) throw Error('auto_handoff_thread_required');
40
+ if (['worker', 'resume', 'detail', 'desktop-open', 'settle-goal'].includes(out.action) && !out.operation) throw Error('auto_handoff_operation_required');
41
+ if (['successor', 'settle-goal'].includes(out.action) && !out.thread) throw Error('auto_handoff_thread_required');
41
42
  if (out.action === 'detail') {
42
43
  out.turn = Number(out.turn);
43
44
  if (!out.origin || !Number.isSafeInteger(out.turn) || out.turn < 1) throw Error('auto_handoff_detail_identity_required');
@@ -146,6 +147,9 @@ export async function run(args = []) {
146
147
  // 0.16.9 までの版が引き継いだ分の名前を付け直す(名前の無い後継へ名前、旧タスクへ引き継ぎ済みの印)。何度流しても同じ結果になる。
147
148
  const db = getDb();
148
149
  result = { status: 'ok', dry_run: parsed.dryRun, ...await markAutoHandoffSources(db, { dryRun: parsed.dryRun }) };
150
+ } else if (parsed.action === 'settle-goal') {
151
+ // 引き継ぎ済みの旧タスクに動いたまま残ったゴールを止めて、今の後継へ運ぶ(hook が起こす。ADR 0059)。
152
+ result = await settleAutoHandoffSourceGoal(getDb(), { operationId: parsed.operation, threadId: parsed.thread });
149
153
  } else if (parsed.action === 'worker') {
150
154
  result = publicOperation(await runAutoHandoffWorker(parsed.operation));
151
155
  } else if (parsed.action === 'resume') {
@@ -1,9 +1,9 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { readFileSync } from 'node:fs';
4
+ import { readFileSync, rmSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
- import { join } from 'node:path';
6
+ import { dirname, join } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import {
@@ -141,7 +141,7 @@ test('missing grok binary does not spawn', () => {
141
141
  assert.equal(spawned.length, 0);
142
142
  });
143
143
 
144
- test('successful continue spawn uses source project cwd and waits', () => {
144
+ test('successful continue spawn uses source project cwd and waits', (t) => {
145
145
  const io = capture();
146
146
  const spawned = [];
147
147
  const uuid = '22222222-2222-4222-8222-222222222222';
@@ -164,6 +164,7 @@ test('successful continue spawn uses source project cwd and waits', () => {
164
164
  assert.equal(spawned.length, 1);
165
165
  assert.equal(io.out, `grok:${uuid}\n`);
166
166
  const { grokArgv, launchFile, cwd } = spawned[0];
167
+ t.after(() => rmSync(dirname(launchFile), { recursive: true, force: true }));
167
168
  assert.equal(cwd, REPO_ROOT);
168
169
  assert.equal(grokArgv.includes('--rules'), false);
169
170
  assert.ok(grokArgv.at(-1).includes(CONTEXT));
@@ -232,7 +233,11 @@ test('bin usage error is exit 2 without spawning a real grok', () => {
232
233
  const result = spawnSync(process.execPath, [BIN_PATH, 'grok-continue'], {
233
234
  cwd: REPO_ROOT,
234
235
  encoding: 'utf8',
235
- env: { ...process.env, HOME: join(tmpdir(), 'tl-grok-continue-missing-home') },
236
+ env: {
237
+ ...process.env,
238
+ HOME: join(tmpdir(), 'tl-grok-continue-missing-home'),
239
+ USERPROFILE: join(tmpdir(), 'tl-grok-continue-missing-home'),
240
+ },
236
241
  });
237
242
  assert.equal(result.status, 2);
238
243
  assert.match(result.stderr, /Usage: throughline grok-continue --session <id>/);
@@ -1,9 +1,8 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { mkdtempSync } from 'node:fs';
5
- import { tmpdir } from 'node:os';
6
4
  import { join } from 'node:path';
5
+ import { testTempDir } from '../test-temp-dir.mjs';
7
6
  import { fileURLToPath } from 'node:url';
8
7
 
9
8
  import { parseArgs, run } from './runtime-errors.mjs';
@@ -43,7 +42,7 @@ test('runtime-errors CLI: internal failure is fixed and does not reflect excepti
43
42
  });
44
43
 
45
44
  test('runtime-errors CLI: snapshot and diagnostics are JSON-only and contain no state path', () => {
46
- const root = mkdtempSync(join(tmpdir(), 'throughline-runtime-cli-'));
45
+ const root = testTempDir('throughline-runtime-cli-');
47
46
  const env = {
48
47
  ...process.env,
49
48
  HOME: root,