throughline 0.16.31 → 0.16.32
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 +27 -1
- package/README.ja.md +4 -2
- package/README.md +4 -2
- package/bin/throughline.mjs +5 -0
- package/docs/adr/0063-claude-handed-off-input-is-forwarded-to-successor.md +156 -0
- package/package.json +1 -1
- package/src/claude-auto-handoff.mjs +44 -5
- package/src/claude-auto-handoff.test.mjs +35 -13
- package/src/claude-forward-wait.mjs +145 -0
- package/src/claude-forward.mjs +325 -0
- package/src/claude-forward.test.mjs +353 -0
- package/src/claude-pre-tool-use.mjs +33 -15
- package/src/cli/auto-handoff.mjs +2 -2
- package/src/cli/install.mjs +34 -16
- package/src/prompt-submit.mjs +16 -0
- package/src/resume-context.mjs +8 -2
- package/src/resume-context.test.mjs +3 -1
- package/src/runtime-error-store.mjs +6 -0
- package/src/transcript-reader.mjs +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,31 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.16.32] — 2026-10-14
|
|
14
|
+
|
|
15
|
+
### 追加
|
|
16
|
+
|
|
17
|
+
- **Claude Codeの自動継続で、引き継ぎ済みの会話へ届いた入力を、後継の会話へ届ける**([ADR 0063](docs/adr/0063-claude-handed-off-input-is-forwarded-to-successor.md))。0.16.31は、旧い会話へ届いた入力を止めて後継の場所を返すだけで、人が打った文は後継で打ち直し、旧い会話が裏で起こした作業の完了通知は後継へ届かなかった。
|
|
18
|
+
- 旧い会話は、止めた入力を今の続きの会話の置き場(`~/.throughline/claude-auto-handoff/inbox/<後継の会話ID>/`)へ置く。置けた時は「届いた入力は、後継の会話へ渡しました」と、後継が動いているか(動いていなければ、後継を開いて入力した時に届く事)と、後継を開く命令を返す。置けなかった時は、0.16.31と同じ文を返す。
|
|
19
|
+
- slash command(頭が`/`の入力)は運ばない。0.16.31と同じ文を返す。ほかの会話から届いた文は、利用者の入力と分けて「利用者の入力ではない」と書いて運ぶ。
|
|
20
|
+
- 手すきの後継は、番の終わりに走る新しいhook(`throughline claude-forward-wait`。`asyncRewake`)が待っていて、入力が届くと起きる。
|
|
21
|
+
- 作業中の後継は、次の道具の前に、文脈として受け取る(今ある`PreToolUse`のhook)。番の終わりまで待たせない。
|
|
22
|
+
- 止まっている後継(`claude stop`・Claude Codeの片付け)は起こさない。入力は置き場で待ち、後継を開いて何か入力した時に届く(今ある`UserPromptSubmit`のhook)。
|
|
23
|
+
- 運んだ利用者の入力は、後継の会話の記録(L2)の中で、利用者の発言として残る。
|
|
24
|
+
- 後継が最初に受け取る引き継ぎの文脈(読み方)に、運び方と「運ばれた利用者の入力は、利用者の入力として扱う」事を足した。前の会話が裏で動かしていた作業の一覧の文も「終わった通知は、Throughlineがこの会話へ運ぶ」に変えた。
|
|
25
|
+
- `auto-handoff status --host claude --json`の引き継ぎごとの項目に、届けるのを待つ数(`forward_pending`)と、届いたか分からない数(`forward_unknown`)を足した。止めた記録(`redirects.jsonl`)に、渡せたか(`forwarded`)を足した。運んだ記録は`~/.throughline/claude-auto-handoff/forward.jsonl`(入力の本文は書かない)。
|
|
26
|
+
|
|
27
|
+
### 変わる動き
|
|
28
|
+
|
|
29
|
+
- 自動継続が有効な端末では、版を上げた時の`install`(`self-update`)が、Claudeの設定ファイル(`~/.claude/settings.json`)のStopにhookを1つ足す。前からある`PreCompact`・`PreToolUse`のhookの並びは動かさない。Codex・Grok・Cursorの設定は変えない。
|
|
30
|
+
- 自動継続が有効な端末では、Claudeの番の終わりのたびにhookが1つ多く走る。後継でない会話では、置き場のフォルダの有無だけを見て終わる。手すきの後継ごとに、待つprocessが1つ動く(後継が止まれば一緒に止まる。同じ会話で新しい待つhookが立てば、前のhookは退く)。
|
|
31
|
+
- 後継の画面では、運ばれた入力は利用者の発言の形では出ない(番の終わりの道はhookからの通知の形、道具の前と入力の時は、hookが足した文脈の形)。
|
|
32
|
+
- 運ばれた入力に従うかは、後継のモデルが決める。実機(Haiku)では、後継が完了通知を1通受けた後に人の入力を運んだ場面で、27回のうち3回、後継が「自動の通知として届いたので、利用者の指示としては扱わない」と答えた(ADR 0063)。旧い会話へ返す文に「後継のモデルが、運ばれた入力を利用者の依頼として扱わない事があります。答えは後継で確かめてください」と、後継を開く命令を添えてある。
|
|
33
|
+
- 旧い会話が裏で起こした作業の完了通知が後継へ届くので、手すきの後継は通知のたびに1回動く。
|
|
34
|
+
- 置き場のファイルは、届くまで入力の本文を持つ(本人だけが読める権限。届けたら消す。届かないまま7日を過ぎた分は消す)。
|
|
35
|
+
- 届いたか分からない入力(受け取った後で、出力する前にprocessが落ちた分)は、入れ直さない。
|
|
36
|
+
- `auto-handoff enable --host claude`を、登録済みの端末でもう一度流しても、今の形で登録済みのhookの並びは動かさない(前の版は、足りないhookがあると自分のhookを全部後ろへ置き直した)。
|
|
37
|
+
|
|
13
38
|
## [0.16.31] — 2026-10-14
|
|
14
39
|
|
|
15
40
|
### 修正
|
|
@@ -2609,7 +2634,8 @@ two attempts, instrument first instead of patching again.
|
|
|
2609
2634
|
|
|
2610
2635
|
---
|
|
2611
2636
|
|
|
2612
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.
|
|
2637
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.32...HEAD
|
|
2638
|
+
[0.16.32]: https://github.com/kitepon/Throughline/compare/v0.16.31...v0.16.32
|
|
2613
2639
|
[0.16.31]: https://github.com/kitepon/Throughline/compare/v0.16.30...v0.16.31
|
|
2614
2640
|
[0.16.30]: https://github.com/kitepon/Throughline/compare/v0.16.29...v0.16.30
|
|
2615
2641
|
[0.16.29]: https://github.com/kitepon/Throughline/compare/v0.16.28...v0.16.29
|
package/README.ja.md
CHANGED
|
@@ -60,7 +60,7 @@ throughline auto-handoff status --host claude --json
|
|
|
60
60
|
throughline auto-handoff disable --host claude
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
既定では無効です。`enable`は必要な`PreCompact
|
|
63
|
+
既定では無効です。`enable`は必要な`PreCompact`・`PreToolUse`・`Stop`のフックを登録し、`disable`が外します。`--project`を省略すると全projectを対象にします。Codexの設定とは別で、互いに影響しません。
|
|
64
64
|
|
|
65
65
|
自動圧縮を置き換えます。圧縮が走る前に旧い会話を止め、記憶を持った新しい会話で作業を続けます([ADR 0033](docs/adr/0033-claude-auto-handoff-new-session.md))。
|
|
66
66
|
|
|
@@ -77,7 +77,9 @@ throughline auto-handoff status --host claude --operation <handoff-id> --json
|
|
|
77
77
|
|
|
78
78
|
後継の立ち上げや配送に失敗した時は、固定の理由を記録して止まります。結果が不明な指示は再送しません。圧縮を止めた後にモデルが道具を呼ばずにターンを終えた時は、後継を立てません。印は残るので、1時間以内に開いた新しい会話が記憶を引き継ぎます。
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
引き継いだ後の旧い会話へ入力が届いた時は、モデルを動かさずに止めて、後継の会話へ届けます([ADR 0062](docs/adr/0062-claude-handed-off-conversation-blocks-input.md)、[ADR 0063](docs/adr/0063-claude-handed-off-input-is-forwarded-to-successor.md))。旧い会話へ打った文と、旧い会話が裏で起こした作業の完了通知が対象です。打った人には「届いた入力は、後継の会話へ渡しました」と後継を開く命令が返ります。手すきの後継はすぐ動き、作業中の後継は次の道具の前に読みます。後継が止まっている時は起こさず、後継を開いて何か入力した時に届きます。後継の画面では、運ばれた入力は利用者の発言の形ではなく、フックからの通知か文脈の形で出ます。届くまでの間、入力の本文は`~/.throughline/claude-auto-handoff/inbox/`に置かれます(本人だけが読める権限。届けたら消します)。
|
|
81
|
+
|
|
82
|
+
有効にした端末では、Claudeの道具の呼び出しのたびに`PreToolUse`フックが1回、番の終わりのたびに`Stop`フックが1回多く走ります(後継でない会話では、すぐ終わります)。Linux・macOS・Windowsの端末から始めた会話と、macOSのClaude Desktopの画面から始めた会話を実機確認済みです。Desktopでは、移動済みの会話の最初の後継への記憶注入と、最初からフォルダを選んだ会話の連続継承・作業完了を確認しました。[実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-05-claude-auto-handoff.md)を参照してください。
|
|
81
83
|
|
|
82
84
|
## 所有境界
|
|
83
85
|
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ throughline auto-handoff status --host claude --json
|
|
|
59
59
|
throughline auto-handoff disable --host claude
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
既定では無効です。`enable`は必要な`PreCompact
|
|
62
|
+
既定では無効です。`enable`は必要な`PreCompact`・`PreToolUse`・`Stop`のフックを登録し、`disable`が外します。`--project`を省略すると全projectを対象にします。Codexの設定とは別で、互いに影響しません。
|
|
63
63
|
|
|
64
64
|
自動圧縮を置き換えます。圧縮が走る前に旧い会話を止め、記憶を持った新しい会話で作業を続けます([ADR 0033](docs/adr/0033-claude-auto-handoff-new-session.md))。
|
|
65
65
|
|
|
@@ -84,7 +84,9 @@ throughline auto-handoff successor --host claude --thread <claude-session-id> --
|
|
|
84
84
|
|
|
85
85
|
後継の立ち上げや配送に失敗した時は、固定の理由を記録して止まります。結果が不明な指示は再送しません。圧縮を止めた後にモデルが道具を呼ばずにターンを終えた時は、後継を立てません。印は残るので、1時間以内に開いた新しい会話が記憶を引き継ぎます。
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
引き継いだ後の旧い会話へ入力が届いた時は、モデルを動かさずに止めて、後継の会話へ届けます([ADR 0062](docs/adr/0062-claude-handed-off-conversation-blocks-input.md)、[ADR 0063](docs/adr/0063-claude-handed-off-input-is-forwarded-to-successor.md))。旧い会話へ打った文と、旧い会話が裏で起こした作業の完了通知が対象です。打った人には「届いた入力は、後継の会話へ渡しました」と後継を開く命令が返ります。手すきの後継はすぐ動き、作業中の後継は次の道具の前に読みます。後継が止まっている時は起こさず、後継を開いて何か入力した時に届きます。後継の画面では、運ばれた入力は利用者の発言の形ではなく、フックからの通知か文脈の形で出ます。届くまでの間、入力の本文は`~/.throughline/claude-auto-handoff/inbox/`に置かれます(本人だけが読める権限。届けたら消します)。
|
|
88
|
+
|
|
89
|
+
有効にした端末では、Claudeの道具の呼び出しのたびに`PreToolUse`フックが1回、番の終わりのたびに`Stop`フックが1回多く走ります(後継でない会話では、すぐ終わります)。Linux・macOS・Windowsの端末から始めた会話と、macOSのClaude Desktopの画面から始めた会話を実機確認済みです。Desktopでは、移動済みの会話の最初の後継への記憶注入と、最初からフォルダを選んだ会話の連続継承・作業完了を確認しました。[実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-05-claude-auto-handoff.md)を参照してください。
|
|
88
90
|
|
|
89
91
|
## Ownership boundary
|
|
90
92
|
|
package/bin/throughline.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* throughline session-start # SessionStart hook (Claude Code から呼ばれる)
|
|
11
11
|
* throughline pre-compact # PreCompact hook (Claude Code から呼ばれる。自動継続: 自動圧縮を止める)
|
|
12
12
|
* throughline pre-tool-use # PreToolUse hook (Claude Code から呼ばれる。自動継続: 旧い会話を止める)
|
|
13
|
+
* throughline claude-forward-wait # Stop hook (Claude Code から呼ばれる。自動継続: 引き継ぎ前の会話へ届いた入力を後継へ届ける)
|
|
13
14
|
* throughline detail [<日付>T]<時刻> # L2+L3 詳細取得 (Claude が Bash 経由で呼ぶ想定)
|
|
14
15
|
* throughline recall --l2|--l1 # 注入案内から辿る pull 用 read-only 記憶取得
|
|
15
16
|
* throughline handoff-preview # Codex-facing throughline_handoff JSON preview
|
|
@@ -88,6 +89,9 @@ switch (cmd) {
|
|
|
88
89
|
case 'pre-tool-use':
|
|
89
90
|
await (await import('../src/claude-pre-tool-use.mjs')).run();
|
|
90
91
|
break;
|
|
92
|
+
case 'claude-forward-wait':
|
|
93
|
+
await (await import('../src/claude-forward-wait.mjs')).run();
|
|
94
|
+
break;
|
|
91
95
|
case 'monitor':
|
|
92
96
|
(await import('../src/token-monitor.mjs')).main();
|
|
93
97
|
break;
|
|
@@ -243,6 +247,7 @@ switch (cmd) {
|
|
|
243
247
|
'process-turn': 'HOOK_PROCESS_TURN_FAILED',
|
|
244
248
|
'pre-compact': 'HOOK_PRE_COMPACT_FAILED',
|
|
245
249
|
'pre-tool-use': 'HOOK_PRE_TOOL_USE_FAILED',
|
|
250
|
+
'claude-forward-wait': 'HOOK_CLAUDE_FORWARD_WAIT_FAILED',
|
|
246
251
|
}[cmd];
|
|
247
252
|
if (code) {
|
|
248
253
|
const { recordRuntimeErrorBestEffort } = await import('../src/runtime-error-store.mjs');
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# ADR 0063: 引き継ぎ済みの Claude の会話へ届いた入力は、後継の会話へ届ける
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-14
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
ADR 0062 は、引き継ぎ済みの Claude の会話へ届いた入力を、モデルを動かさずに止めて、後継の場所を返す形にした。
|
|
9
|
+
止めた入力は後継へ渡らない。人が打った文は後継で打ち直しになり、旧い会話が裏で起こした作業の完了通知は、後継へ
|
|
10
|
+
届かないままになる。利用者の決め(2026-10-14)は「先に止める形を入れ、続けて、後継へ届ける形を作る」。この ADR は
|
|
11
|
+
後の分を決める。
|
|
12
|
+
|
|
13
|
+
Claude Code に、動いている別の会話へ外から文を入れる命令は無い(`claude --help` の一覧。2.1.296)。会話の受け口
|
|
14
|
+
(`messagingSocketPath`)は、権限のバイパス中の会話では、送り手を確かめられない文を利用者の承認まで止める。この止めは
|
|
15
|
+
回避しない(ADR 0033 の 4)。残る道は、後継自身の hook が文を受け取る形になる。
|
|
16
|
+
|
|
17
|
+
Claude Code 2.1.296 で、次を実機で確かめた(Linux・macOS・Windows の3つとも同じ結果。自分で立てた裏の会話、
|
|
18
|
+
Haiku。利用者の設定の hook を外し、試しの hook だけを付けた。試しの文は自分で決めた物)。
|
|
19
|
+
|
|
20
|
+
| 見た事 | 結果 |
|
|
21
|
+
|---|---|
|
|
22
|
+
| 手すきの会話。Stop の `asyncRewake: true` の hook が待っていて、後から stderr へ文を出して exit code 2 で終わる | 1 秒以内に会話が起きて、文に従った。届く行は `origin.kind: task-notification` の user 行で、`<summary>Stop hook feedback</summary>` と `<system-reminder>` に包まれる |
|
|
23
|
+
| 作業中の会話。番の途中に文を置き、番の終わりの Stop の hook がすぐ exit code 2 で終わる | 番が終わった直後に、次の番として届いた |
|
|
24
|
+
| 道具の前後。`PreToolUse`・`PostToolUse` の hook が `additionalContext` を返す | 番の途中で文脈に入り、モデルが従った(`PreToolUse` は、道具を順に出した会話で3つとも確認) |
|
|
25
|
+
| `additionalContext` の長さ | 10,000 字(UTF-16 の長さ)を超えると、頭の約 2,000 字と「全文は file にある」の案内に置き換わる。9,900 字までは、そのまま届く。日本語でも字数で決まる |
|
|
26
|
+
| Stop の hook の stderr の長さ | 23,000 字が、そのまま届いた |
|
|
27
|
+
| hook の `timeout`(秒)が切れた時 | hook は止められる(POSIX は SIGTERM)。会話は起きない。その後に文を置いても届かない |
|
|
28
|
+
| `timeout: 7200` の hook | 11 分待った後でも届けた(既定の 600 秒より長く待てる) |
|
|
29
|
+
| `claude stop` | 待っている hook も止まる |
|
|
30
|
+
| 待つ hook を持つ手すきの裏の会話を、Claude Code が片付ける時(`bg retire`) | hook が待っていても、61 分で片付けた(Linux)。hook の process は止められず、残った。片付けの後に hook が文を出して exit code 2 で終わっても、会話には何も届かなかった。hook の環境の `CLAUDE_PID` は、片付けられた会話の process の ID だった |
|
|
31
|
+
| hook が待っている間に、会話が別のきっかけ(自分の裏の作業の完了通知)で次の番へ進む | 前の hook は止められず、待ち続ける。次の番の終わりに、もう1つ hook が立つ |
|
|
32
|
+
| 前の番から待っている hook が、次の番の作業中に exit code 2 で終わる | Claude Code が文を順番待ちへ入れ(`attachment` の `queued_command`)、次の道具の合間に届けた。モデルは従った(Linux で確認) |
|
|
33
|
+
| 待つ hook を持つ会話へ `claude -p --resume` で入力する | hook が終わるまで命令が返らず、出力も出ない(hook が 20 秒で自分から終わる時は、22〜23 秒で返った)。この時の hook の環境は `CLAUDE_CODE_ENTRYPOINT=sdk-cli`(裏の会話は `cli`) |
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
1. **置き場。** 後継ごとに、届けるのを待つ入力を1通1ファイルで置く
|
|
38
|
+
(`~/.throughline/claude-auto-handoff/inbox/<後継の会話 ID>/<時刻>-<連番>.json`)。中身は、届いた時刻・旧い会話の ID・
|
|
39
|
+
引き継ぎ ID・種類(`prompt` か `task-notification`)・本文。ファイルは本人だけが読める(0600)。届けたら消す。
|
|
40
|
+
2. **旧い会話の側。** ADR 0062 の止め方は変えない。止める時に、入力を今の続きの会話(後継の先まで引いた会話)の置き場へ
|
|
41
|
+
置く。置けた時は、返す文を「届いた入力は後継の会話へ渡しました」に替え、届く時機と後継を開く命令を添える。
|
|
42
|
+
後継で待つ hook の process が生きていれば「後継は動いています」、生きていなければ「後継は今、入力を待ち受けて
|
|
43
|
+
いません(止まっているか、この版より前に立った会話です)。後継を開いて何か入力した時に届きます」と書き分ける
|
|
44
|
+
(待つ hook は、置き場の印に自分の process の ID を書いている)。
|
|
45
|
+
後継の会話 ID を引けない時と、置き場へ書けなかった時は、0.16.31 と同じ文(後継で打ち直す)を返す。
|
|
46
|
+
止めた記録(`redirects.jsonl`)に、渡せたか(`forwarded`)を足す。本文は書かない。
|
|
47
|
+
- **slash command(頭が `/` の入力。`/tl`・`/clear` など)は運ばない。** その会話への命令で、後継へ文として運んでも
|
|
48
|
+
命令にならない。0.16.31 と同じ文を返す。頭が `/` で始まる普通の文(path など)も、見分けられないので運ばない。
|
|
49
|
+
- **ほかの会話から届いた文(`Another Claude session sent a message:` で始まる)は、利用者の入力と分けて運ぶ。**
|
|
50
|
+
届ける文に「ほかの Claude の会話が送った文で、利用者の入力ではない」と書く。旧い会話の受け口を通った後の文なので、
|
|
51
|
+
Claude Code が付けた注意書きは、そのまま付いて届く。
|
|
52
|
+
3. **後継の側。** 置き場の入力を、次の3か所で受け取る。先に受け取った所が届ける。
|
|
53
|
+
- **Stop の hook(新しく登録する。`throughline claude-forward-wait`、`asyncRewake: true`)。** 番の終わりに走り、置き場に
|
|
54
|
+
入力が在ればすぐ、無ければ届くまで待って、stderr へ出して exit code 2 で終わる。手すきの後継が、これで起きる。
|
|
55
|
+
- **`UserPromptSubmit`(今ある hook)。** 後継へ入力が来た時に、置き場の入力を文脈として足す。待つ hook が居ない後継
|
|
56
|
+
(期限で止められた・`claude stop` の後に開き直した・この版より前に立った)は、ここで受け取る。
|
|
57
|
+
- **`PreToolUse`(自動継続が今も登録している hook)。** 作業中の後継が、次の道具の前に、文脈として受け取る。
|
|
58
|
+
番の終わりまで待たせない。
|
|
59
|
+
4. **文脈として足す道(`UserPromptSubmit`・`PreToolUse`)は、9,000 字までの分だけを運ぶ。** 10,000 字を超えると Claude Code が
|
|
60
|
+
頭だけに切り詰めるので、収まらない入力は置き場に残し、Stop の道で届ける。Stop の道は、1通が 60,000 字を超える時だけ、
|
|
61
|
+
頭の 4,000 字と全文の file の場所を届ける。
|
|
62
|
+
5. **待つ hook が走る会話。** 自分の置き場のフォルダが在る会話だけが待つ。フォルダは、後継が継続の指示を受領した時と、
|
|
63
|
+
旧い会話が最初の入力を置いた時に出来る。フォルダが無い会話(後継でない会話。ほとんどの会話)では、hook はフォルダの
|
|
64
|
+
有無だけを見て終わる。
|
|
65
|
+
- script から起動した Claude(`CLAUDE_CODE_ENTRYPOINT` が `sdk-` で始まる。`claude -p`、Agent SDK)では待たない。
|
|
66
|
+
待つと、呼び出し元の命令が返らない。その時の入力は、`UserPromptSubmit` の道で届く。
|
|
67
|
+
- subagent の中では待たない。
|
|
68
|
+
- 待っている間に、自分の会話がさらに引き継がれて後継が立ったら、置き場に残った入力を今の続きの会話の置き場へ移して
|
|
69
|
+
終わる。旧い会話を起こさない。
|
|
70
|
+
- hook の `timeout` は 86,400 秒にする。期限が切れると、次に後継が動くまで(入力・番の終わり)届かない。
|
|
71
|
+
- 会話の process(hook の環境の `CLAUDE_PID`)が居なくなったら、受け取らずに終わる。Claude Code は、手すきの裏の会話を
|
|
72
|
+
片付ける時に、待っている hook を止めない。居ない会話へ出力すると、入力が届かないまま消える。
|
|
73
|
+
- **1つの会話で待つのは1つだけにする。** Claude Code は、会話が次の番へ進んでも前の hook を止めないので、そのままだと
|
|
74
|
+
番の終わりのたびに待つ process が1つ増える。新しい hook は置き場に自分の印(`waiter`)を書き、前の hook は印が
|
|
75
|
+
替わったのを見て退く。前の hook は、次の番が終わるまでは残る。その間に届いた入力は、前の hook が出して
|
|
76
|
+
Claude Code が道具の合間に届けるか、`PreToolUse` の道が先に受け取る。
|
|
77
|
+
6. **届いたか分からない入力は、入れ直さない。** 受け取る所は、入力のファイルを別のフォルダ(`claimed`)へ改名してから
|
|
78
|
+
出力し、出力した後で消す。改名の後で process が落ちると、その入力は届いたか分からないまま `claimed` に残る。
|
|
79
|
+
これを置き場へ戻さない(二重に届くより、1通落ちて跡が残る方を選ぶ。ADR 0039 の Codex と同じ考え)。
|
|
80
|
+
`auto-handoff status --host claude` に、届けるのを待つ数と、届いたか分からない数を出す。
|
|
81
|
+
7. **届ける文。** 頭に、Throughline が運んだ事・旧い会話の頭8字・種類ごとの数を書いた行を置く。人の入力は
|
|
82
|
+
「利用者がこの会話へ打った入力として扱う」、ほかの会話からの文は「利用者の入力ではない」、完了通知は
|
|
83
|
+
「引き継ぎ前の会話が裏で起こした作業の通知」と書く。
|
|
84
|
+
複数ある時は、届いた順に1つの文にまとめる。完了通知が続く時は、後継が動いている間に溜まった分が、次の1回で届く。
|
|
85
|
+
8. **記憶(L2)。** 運んだ人の入力は、後継の記録の中で、利用者の発言として読む。Stop の道で届いた行は、包みを外して
|
|
86
|
+
届けた文だけを残す。文脈として足した分と、前の hook が作業中に出して順番待ちから届いた分は、作業の途中で届いた入力
|
|
87
|
+
(ADR 0060)と同じに、そのターンの依頼へ足す。完了通知だけを運んだ分は、ターンの途中では足さない(ADR 0060 と同じ)。
|
|
88
|
+
入力の hook が stdout へ出した文脈は、記録では `hook_success`(`hookEvent: UserPromptSubmit`)の `content` に入る。
|
|
89
|
+
道具の前の hook が返した文脈は `hook_additional_context` に入る(実測)。
|
|
90
|
+
9. **片付け。** どの引き継ぎの記録からも後継として指されなくなった会話の置き場は、空なら消す。7日を過ぎた入力は、
|
|
91
|
+
届けずに消して、消した数を記録へ残す。
|
|
92
|
+
10. **登録。** `auto-handoff enable --host claude` が、今までの2つ(`PreCompact`・`PreToolUse`)に加えて Stop の hook を
|
|
93
|
+
登録し、`disable` が外す。有効な端末では、版を上げた時の `install` が足す(Claude の設定ファイルが1か所変わる)。
|
|
94
|
+
Codex・Grok・Cursor の設定は変えない。
|
|
95
|
+
|
|
96
|
+
## Consequences
|
|
97
|
+
|
|
98
|
+
- 旧い会話へ打った文は、打ち直さなくても後継へ届く。手すきの後継はすぐ動き、作業中の後継は次の道具の前に読む。
|
|
99
|
+
- 旧い会話が裏で起こした作業の完了通知が、後継へ届く。手すきの後継は、通知のたびに1回動く(今は同じ回数が、
|
|
100
|
+
文脈がほぼ一杯の旧い会話で止められている)。
|
|
101
|
+
- 後継の画面では、運ばれた入力は利用者の発言の形では出ない。Stop の道では hook からの通知の形、文脈の道では hook が
|
|
102
|
+
足した文脈の形になる。
|
|
103
|
+
- 運ばれた人の入力に従うかは、後継のモデルが決める。hook 経由の文は利用者の発言の形で届かないので、モデルが利用者の
|
|
104
|
+
指示として扱わない事がある(実測は下)。後継が最初に受け取る引き継ぎの文脈で、運び方を先に知らせる。旧い会話へ返す文に
|
|
105
|
+
「打ち直しは要らない」とは書かず、後継を開く命令を添える。
|
|
106
|
+
- 手すきの後継ごとに、待つ process が1つ動く(node。1 秒ごとにフォルダを見る)。後継が `claude stop` や片付けで止まれば、
|
|
107
|
+
一緒に止まる。
|
|
108
|
+
- 自動継続が有効な端末では、Claude の番の終わりのたびに hook が1つ多く走る。置き場のフォルダが無い会話では、
|
|
109
|
+
すぐ終わる。
|
|
110
|
+
- 後継が止まっている時(`claude stop`・Claude Code の片付け)は、起こさない。入力は置き場で待ち、後継を開いて
|
|
111
|
+
何か入力した時に届く。止まっている後継を自動で起こす形は、この ADR では決めない(動いている会話へ
|
|
112
|
+
`claude --bg --resume` を流すと写しが立つので、止まっている事を確かめる道を先に決める必要がある)。
|
|
113
|
+
- 置き場のファイルは、届くまで入力の本文を持つ。DB と会話の記録のほかに、本文が一時的に置かれる場所が増える。
|
|
114
|
+
- `claude -p --resume <後継>` の入力では、置き場の分は文脈として届き、待つ hook は走らない。
|
|
115
|
+
|
|
116
|
+
## 確かめた事・確かめていない事
|
|
117
|
+
|
|
118
|
+
- 確かめた事は、Context の表と、この版の試験(CHANGELOG の 0.16.32)。
|
|
119
|
+
- 直した版を、本物の Claude Code 2.1.296 の裏の会話(Haiku)へ通した。Linux・macOS・Windows の3つ。利用者の設定の hook を
|
|
120
|
+
外し、直した版を切り離した HOME で呼ぶ hook だけを付けた。旧い会話の役と後継の役を立て、引き継ぎの記録を切り離した
|
|
121
|
+
置き場へ置いた。後継の役の置き場は、最初の入力の hook で作った(本物では、継続の指示の受領が作る)。3つとも同じ結果だった。
|
|
122
|
+
|
|
123
|
+
| 見た事 | 結果 |
|
|
124
|
+
|---|---|
|
|
125
|
+
| 旧い会話が裏で起こした作業の完了通知 | 旧い会話では止まり、手すきの後継が起きて、通知の中身に答えた |
|
|
126
|
+
| 人の入力(`claude -p --resume <旧い会話>`) | 「届いた入力は、後継の会話へ渡しました」が返り、手すきの後継が起きて、入力に従った |
|
|
127
|
+
| 後継が止まっている時の人の入力 | 置き場で待った。後継を `claude -p --resume` で開いて入力すると、3〜5 秒で命令が返り、運んだ入力が文脈として届いていた |
|
|
128
|
+
| 作業中の後継への人の入力 | 次の道具の前に文脈として届き、後継はその番の答えで従った |
|
|
129
|
+
| 後継が自分の裏の作業の完了通知で次の番へ進んだ時 | 前の待つ hook は退き、待つ hook は1つだけ残った |
|
|
130
|
+
| 直した版の読み手で読んだ後継の記録 | 運んだ文が利用者の行に入り、Claude Code の通知の包みは入らない。文脈で届いた分は、作業の途中の入力として入る |
|
|
131
|
+
| 切り離した置き場の失敗の記録・届いたか分からない入力 | 無し・0 |
|
|
132
|
+
| 本物の置き場と判定の記録にある、試しの会話 | 0 |
|
|
133
|
+
|
|
134
|
+
- 運んだ人の入力に、後継のモデル(Haiku)が従ったかを、手すきの後継で数えた(3つの OS の合計)。hook 経由の文に従うかは
|
|
135
|
+
モデルが決めるので、従わない事は無くせない。従わなかった後継は「自動の通知として届いたので、利用者の指示としては
|
|
136
|
+
扱わない」と答えた。
|
|
137
|
+
|
|
138
|
+
| 場面と、運ぶ文の形 | 従った回数 |
|
|
139
|
+
|---|---|
|
|
140
|
+
| 後継の最初の番の後に、人の入力だけを運ぶ。見出しに「利用者がこの会話へ打った入力として扱ってください」と書く形 | 26 回のうち 26 回(知らせあり 13、なし 13) |
|
|
141
|
+
| 後継が完了通知を1通受けた後に、人の入力を運ぶ。同じ形。後継は、引き継ぎの文脈の知らせを受け取っていない | 21 回のうち 19 回 |
|
|
142
|
+
| 同じ場面・同じ形。後継は、引き継ぎの文脈(読み方)で運び方の知らせを受け取っている | 6 回のうち 5 回 |
|
|
143
|
+
| 同じ場面。見出しを「利用者が前の会話へ打った文です」と、出どころを述べるだけにした形。知らせなし | 18 回のうち 10 回 |
|
|
144
|
+
| 同じ場面。出どころを述べるだけの形。知らせあり | 18 回のうち 16 回 |
|
|
145
|
+
|
|
146
|
+
出どころを述べるだけの形は、知らせの無い後継で従わない回が増えたので、採らない。見出しに「利用者がこの会話へ打った
|
|
147
|
+
入力として扱ってください」と書く形にし、後継が最初に受け取る引き継ぎの文脈(読み方)でも運び方を知らせる。
|
|
148
|
+
試しの入力は「PROBE: …と答えてください」の形で、本物の作業の依頼より疑われやすい文だった。
|
|
149
|
+
- 確かめていない:
|
|
150
|
+
- 端末の画面・Claude Desktop・リモートコントロール(スマホ)で、運ばれた入力がどう見えるか。
|
|
151
|
+
- 86,400 秒の `timeout` が、そのとおりに待つか。7,200 秒の指定で 11 分より長く待てる所までを見た。
|
|
152
|
+
待つ hook が終わった理由と待った秒数を記録へ残すので、入れた後の追跡で読む。
|
|
153
|
+
- 片付けられた裏の会話の、直した版の待つ hook が、`CLAUDE_PID` を見て退く所(単体試験まで。実機は 61 分かかるので、
|
|
154
|
+
公開の後に読む)。
|
|
155
|
+
- macOS と Windows で、裏の会話が片付けられる時の hook の残り方(Linux だけで見た)。
|
|
156
|
+
- ほかの会話から届いた文(`SendMessage`)を運んだ時の、送り手の側の見え方。
|
package/package.json
CHANGED
|
@@ -43,6 +43,7 @@ import { sameProjectPath } from './project-path.mjs';
|
|
|
43
43
|
import { runWithPty } from './os/pty-run.mjs';
|
|
44
44
|
import { spawnPortable, spawnPortableSync } from './os/portable-spawn-sync.mjs';
|
|
45
45
|
import { composeAutoHandoffTitle } from './auto-handoff-title.mjs';
|
|
46
|
+
import { claudeForwardKind, claudeForwardWaiterAlive, enqueueClaudeForward, ensureClaudeForwardInbox, readClaudeForwardStatus, removeStaleClaudeForwards } from './claude-forward.mjs';
|
|
46
47
|
|
|
47
48
|
export const CLAUDE_AUTO_HANDOFF_SCHEMA = 'throughline.claude-auto-handoff.v2';
|
|
48
49
|
const CLI_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '../bin/throughline.mjs');
|
|
@@ -140,10 +141,13 @@ export function listClaudeAutoHandoffs({ dir = claudeAutoHandoffDir() } = {}) {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
/** 外へ見せる項目だけ。依頼の本文・発言・道具の対象と、受け口は含めない。 */
|
|
143
|
-
export function publicClaudeAutoHandoff(record) {
|
|
144
|
+
export function publicClaudeAutoHandoff(record, { dir = claudeAutoHandoffDir() } = {}) {
|
|
144
145
|
const { handoff_id, source_session_id, project_path, state, error_code, requested_at, updated_at, successor } = record;
|
|
146
|
+
// 後継の置き場の数(ADR 0063)。pending は届けるのを待つ入力、unknown は届いたか分からなくなった入力。
|
|
147
|
+
const forward = validSessionId(successor?.session_id) ? readClaudeForwardStatus(successor.session_id, { dir }) : null;
|
|
145
148
|
return { handoff_id, source_session_id, project_path, state, error_code: error_code ?? null,
|
|
146
149
|
successor_session_id: successor?.session_id ?? null, successor_short_id: successor?.short_id ?? null,
|
|
150
|
+
forward_pending: forward ? forward.pending : null, forward_unknown: forward ? forward.unknown : null,
|
|
147
151
|
// 止めたターンを DB へ取り込めたか。false の時、後継は `throughline detail` でそのターンの入出力を取れない。
|
|
148
152
|
in_flight_captured: Boolean(record.in_flight?.turn),
|
|
149
153
|
// 後継を Claude Desktop で開く引き継ぎか、開けたか(null は対象外)。
|
|
@@ -219,6 +223,12 @@ function removeStale(dir, now) {
|
|
|
219
223
|
rmSync(path, { force: true });
|
|
220
224
|
}
|
|
221
225
|
}
|
|
226
|
+
// 後継の置き場(ADR 0063)。後継として指されている会話の置き場は、空でも残す(待つ hook が、フォルダの有無で待つかを決める)。
|
|
227
|
+
try {
|
|
228
|
+
const keepSessionIds = new Set(listClaudeAutoHandoffs({ dir })
|
|
229
|
+
.map(record => record.successor?.session_id).filter(validSessionId));
|
|
230
|
+
removeStaleClaudeForwards({ dir, now, keepSessionIds });
|
|
231
|
+
} catch { /* 置き場の片付けの失敗で、引き継ぎを止めない */ }
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
/**
|
|
@@ -727,6 +737,22 @@ function handedOffReason(record, dir) {
|
|
|
727
737
|
(sessionId ? `(一覧に無い時は claude --resume ${sessionId})` : '');
|
|
728
738
|
}
|
|
729
739
|
|
|
740
|
+
/**
|
|
741
|
+
* 引き継ぎ済みの会話へ届いた入力を、後継の置き場へ置けた時に見せる文 (ADR 0063)。
|
|
742
|
+
* 後継で待つ hook が生きていれば、後継は動いている。生きていなければ、後継は止まっているか、待つ hook が登録される前に
|
|
743
|
+
* 立った会話で、入力は後継が次に動く時まで置き場で待つ。どちらかを、打った人へ返す。
|
|
744
|
+
*/
|
|
745
|
+
function forwardedReason(record, dir, successorSessionId) {
|
|
746
|
+
const { shortId, sessionId } = currentSuccessor(record, dir);
|
|
747
|
+
const open = `claude attach ${shortId}` + (sessionId ? `(一覧に無い時は claude --resume ${sessionId})` : '');
|
|
748
|
+
return `この会話はThroughlineが新しい会話へ引き継ぎ済みです(引き継ぎID: ${record.handoff_id})。` +
|
|
749
|
+
'届いた入力は、後継の会話へ渡しました。' +
|
|
750
|
+
(claudeForwardWaiterAlive(successorSessionId, dir)
|
|
751
|
+
? '後継は動いています。手すきならすぐ、作業中なら次の道具の前か番の終わりに届きます。' +
|
|
752
|
+
`後継のモデルが、運ばれた入力を利用者の依頼として扱わない事があります。答えは後継で確かめてください: ${open}`
|
|
753
|
+
: `後継は今、入力を待ち受けていません(止まっているか、この版より前に立った会話です)。後継を開いて何か入力した時に届きます: ${open}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
730
756
|
function stopOutput(record, dir) {
|
|
731
757
|
const reason = record.successor?.short_id
|
|
732
758
|
? handedOffReason(record, dir)
|
|
@@ -761,18 +787,29 @@ function recordClaudeRedirect(entry, dir) {
|
|
|
761
787
|
* 何もしない(null)。Aiterm が起こした席の会話(ADR 0057)も止めない。席を外す前の版が引き継いだ席が残っていても、
|
|
762
788
|
* 呼び手は席の番の終わりを待っている。止めると番が始まらず、呼び手は答えを受け取れない。
|
|
763
789
|
*
|
|
764
|
-
*
|
|
790
|
+
* 止めた入力は、今の続きの会話の置き場へ置く (ADR 0063)。後継の hook が受け取って、モデルへ渡す。後継の会話 ID を
|
|
791
|
+
* 引けない時、置き場へ書けなかった時、入力が slash command の時は、置かずに「後継で打ち直す」文を返す。
|
|
792
|
+
*
|
|
793
|
+
* @returns {{output: {decision: 'block', reason: string}, kind: 'prompt'|'task-notification', handoffId: string, successorShortId: string|null, forwarded: boolean}|null}
|
|
765
794
|
*/
|
|
766
795
|
export function redirectClaudeHandedOffPrompt({ payload, now = Date.now(), dir = claudeAutoHandoffDir(), env = process.env }) {
|
|
767
796
|
const sessionId = payload?.session_id;
|
|
768
797
|
if (!validSessionId(sessionId) || payload.agent_id || isAitermSeatConversation(env, sessionId)) return null;
|
|
769
798
|
const record = readClaudeAutoHandoff(sessionId, dir);
|
|
770
799
|
if (!record || !successorRemains(record)) return null;
|
|
771
|
-
const
|
|
800
|
+
const successor = currentSuccessor(record, dir);
|
|
801
|
+
const successorShortId = successor.shortId;
|
|
772
802
|
const kind = typeof payload.prompt === 'string' && /^\s*<task-notification>/.test(payload.prompt) ? 'task-notification' : 'prompt';
|
|
803
|
+
const successorSessionId = successor.sessionId ?? findSuccessorSessionId(record);
|
|
804
|
+
// slash command は、その会話への命令なので運ばない。ほかの会話から届いた文は、利用者の入力と分けて運ぶ。
|
|
805
|
+
const forwardKind = claudeForwardKind(payload.prompt);
|
|
806
|
+
const forwarded = forwardKind !== null && validSessionId(successorSessionId) && successorSessionId !== sessionId &&
|
|
807
|
+
enqueueClaudeForward({ successorSessionId, sourceSessionId: sessionId, handoffId: record.handoff_id, kind: forwardKind,
|
|
808
|
+
text: payload.prompt, now, dir });
|
|
773
809
|
recordClaudeRedirect({ at: new Date(now).toISOString(), source_session_id: sessionId, handoff_id: record.handoff_id,
|
|
774
|
-
successor_short_id: successorShortId, kind }, dir);
|
|
775
|
-
return { output: { decision: 'block', reason:
|
|
810
|
+
successor_short_id: successorShortId, kind, forwarded }, dir);
|
|
811
|
+
return { output: { decision: 'block', reason: forwarded ? forwardedReason(record, dir, successorSessionId) : handedOffReason(record, dir) },
|
|
812
|
+
kind, handoffId: record.handoff_id, successorShortId, forwarded };
|
|
776
813
|
}
|
|
777
814
|
|
|
778
815
|
/**
|
|
@@ -893,6 +930,8 @@ export function acceptClaudeAutoContinuation({ predecessorId, successorSessionId
|
|
|
893
930
|
if (carriesInstruction && !record.accepted_at && !existsSync(acceptancePath(predecessorId, dir))) {
|
|
894
931
|
writeJsonPrivate(acceptancePath(predecessorId, dir),
|
|
895
932
|
{ handoff_id: record.handoff_id, successor_session_id: successorSessionId, accepted_at: now });
|
|
933
|
+
// 後継の置き場を作る(ADR 0063)。番の終わりの待つ hook は、このフォルダが在る会話でだけ待つ。
|
|
934
|
+
if (validSessionId(successorSessionId)) ensureClaudeForwardInbox(successorSessionId, dir);
|
|
896
935
|
}
|
|
897
936
|
return { handoffId: record.handoff_id, projectPath: record.project_path, inFlight: record.in_flight ?? null,
|
|
898
937
|
transcriptPath: record.transcript_path ?? null };
|
|
@@ -1230,15 +1230,16 @@ function fakeClaudePath(home) {
|
|
|
1230
1230
|
return { [pathKey]: `${bin}${delimiter}${process.env[pathKey] ?? ''}` };
|
|
1231
1231
|
}
|
|
1232
1232
|
|
|
1233
|
-
test('公開CLI: enable は
|
|
1233
|
+
test('公開CLI: enable は3つの hook と設定を書き、disable は自分の hook だけを外す。Codex の設定には触らない', () => withDir(home => {
|
|
1234
1234
|
const settingsPath = join(home, '.claude', 'settings.json');
|
|
1235
1235
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
1236
1236
|
// 他製品の hook と、0.13.0 が置いた PreCompact がある状態から始める
|
|
1237
1237
|
const others = { PreCompact: [{ hooks: [{ type: 'command', command: 'other-product pre-compact' }] }],
|
|
1238
|
-
PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'rtk hook claude' }] }]
|
|
1238
|
+
PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'rtk hook claude' }] }],
|
|
1239
|
+
Stop: [{ hooks: [{ type: 'command', command: 'throughline process-turn', async: true }] }] };
|
|
1239
1240
|
writeFileSync(settingsPath, JSON.stringify({ model: 'sonnet', hooks: {
|
|
1240
1241
|
PreCompact: [{ hooks: [{ type: 'command', command: 'throughline pre-compact' }] }, ...others.PreCompact],
|
|
1241
|
-
PreToolUse: others.PreToolUse } }));
|
|
1242
|
+
PreToolUse: others.PreToolUse, Stop: others.Stop } }));
|
|
1242
1243
|
|
|
1243
1244
|
const before = JSON.parse(cli(home, ['auto-handoff', 'status', '--host', 'claude', '--json']).stdout);
|
|
1244
1245
|
assert.deepEqual(before, { host: 'claude', config: { schema: 'throughline.claude-auto-handoff.v1', enabled: false, projects: [] },
|
|
@@ -1250,22 +1251,35 @@ test('公開CLI: enable は2つの hook と設定を書き、disable は自分
|
|
|
1250
1251
|
assert.equal(JSON.parse(enabled.stdout).status, 'enabled');
|
|
1251
1252
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1252
1253
|
assert.equal(settings.model, 'sonnet');
|
|
1254
|
+
// 今の形で登録済みの PreCompact は、並びを動かさない。足りない hook だけを後ろへ足す(ADR 0063)
|
|
1255
|
+
const forwardWait = { hooks: [{ type: 'command', command: 'throughline claude-forward-wait', asyncRewake: true, timeout: 86400 }] };
|
|
1253
1256
|
assert.deepEqual(settings.hooks, {
|
|
1254
|
-
PreCompact: [
|
|
1257
|
+
PreCompact: [{ hooks: [{ type: 'command', command: 'throughline pre-compact' }] }, ...others.PreCompact],
|
|
1255
1258
|
PreToolUse: [...others.PreToolUse, { hooks: [{ type: 'command', command: 'throughline pre-tool-use', timeout: 15 }] }],
|
|
1259
|
+
Stop: [...others.Stop, forwardWait],
|
|
1256
1260
|
});
|
|
1257
1261
|
const again = cli(home, ['auto-handoff', 'enable', '--host', 'claude', '--json']);
|
|
1258
1262
|
assert.equal(again.status, 0, again.stderr);
|
|
1259
1263
|
assert.equal(readFileSync(settingsPath, 'utf8'), JSON.stringify(settings, null, 2) + '\n', '2回目は書き換えない');
|
|
1260
1264
|
|
|
1261
1265
|
assert.equal(cli(home, ['auto-handoff', 'status', '--host', 'claude']).stdout,
|
|
1262
|
-
'自動継続: 有効(Claude Code)\nhook(PreCompact・PreToolUse): 登録済み\n');
|
|
1266
|
+
'自動継続: 有効(Claude Code)\nhook(PreCompact・PreToolUse・Stop): 登録済み\n');
|
|
1263
1267
|
assert.equal(existsSync(join(home, '.throughline', 'codex-auto-handoff.json')), false);
|
|
1264
1268
|
assert.equal(JSON.parse(cli(home, ['auto-handoff', 'status', '--json']).stdout).config.enabled, false, 'Codex は無効のまま');
|
|
1265
1269
|
|
|
1270
|
+
// 前の版が登録した2つだけの端末(自分の hook の後ろに他製品の hook がある)で、版を上げた時の install の形:
|
|
1271
|
+
// 前からある2つの並びは動かさず、Stop の待つ hook だけを足す
|
|
1272
|
+
const later = { hooks: [{ type: 'command', command: 'later-product pre-tool-use' }] };
|
|
1273
|
+
writeFileSync(settingsPath, JSON.stringify({ ...settings, hooks: { ...settings.hooks,
|
|
1274
|
+
PreToolUse: [...settings.hooks.PreToolUse, later], Stop: others.Stop } }, null, 2) + '\n');
|
|
1275
|
+
assert.equal(JSON.parse(cli(home, ['auto-handoff', 'status', '--host', 'claude', '--json']).stdout).hooks.registered, false);
|
|
1276
|
+
assert.equal(cli(home, ['auto-handoff', 'enable', '--host', 'claude', '--json']).status, 0);
|
|
1277
|
+
assert.deepEqual(JSON.parse(readFileSync(settingsPath, 'utf8')).hooks, { ...settings.hooks,
|
|
1278
|
+
PreToolUse: [...settings.hooks.PreToolUse, later], Stop: [...others.Stop, forwardWait] });
|
|
1279
|
+
|
|
1266
1280
|
const disabled = JSON.parse(cli(home, ['auto-handoff', 'disable', '--host', 'claude', '--json']).stdout);
|
|
1267
1281
|
assert.equal(disabled.config.enabled, false);
|
|
1268
|
-
assert.deepEqual(JSON.parse(readFileSync(settingsPath, 'utf8')).hooks, others);
|
|
1282
|
+
assert.deepEqual(JSON.parse(readFileSync(settingsPath, 'utf8')).hooks, { ...others, PreToolUse: [...others.PreToolUse, later] });
|
|
1269
1283
|
const unsupported = cli(home, ['auto-handoff', 'resume', '--host', 'claude', '--operation', 'x', '--json']);
|
|
1270
1284
|
assert.equal(unsupported.status, 1);
|
|
1271
1285
|
assert.deepEqual(JSON.parse(unsupported.stdout), { status: 'failed', code: 'auto_handoff_action_unsupported' });
|
|
@@ -1613,17 +1627,19 @@ test('引き継ぎ済みの会話へ届いた入力: 後継が残っている時
|
|
|
1613
1627
|
writeFileSync(join(projects, `${b}.jsonl`), '');
|
|
1614
1628
|
const human = submit(a, '旧い会話へ打った文');
|
|
1615
1629
|
assert.deepEqual(human, { output: { decision: 'block',
|
|
1616
|
-
reason: 'この会話はThroughlineが新しい会話へ引き継ぎ済みです(引き継ぎID: h-aaaaaaaa
|
|
1617
|
-
|
|
1630
|
+
reason: 'この会話はThroughlineが新しい会話へ引き継ぎ済みです(引き継ぎID: h-aaaaaaaa)。届いた入力は、後継の会話へ渡しました。' +
|
|
1631
|
+
'後継は今、入力を待ち受けていません(止まっているか、この版より前に立った会話です)。後継を開いて何か入力した時に届きます: ' +
|
|
1632
|
+
'claude attach bbbbbbbb(一覧に無い時は claude --resume ' + b + ')' },
|
|
1633
|
+
kind: 'prompt', handoffId: 'h-aaaaaaaa', successorShortId: 'bbbbbbbb', forwarded: true });
|
|
1618
1634
|
assert.equal(submit(a, '<task-notification>\n<task-id>b1</task-id>\n<status>completed</status>\n</task-notification>').kind, 'task-notification');
|
|
1619
1635
|
assert.equal(submit(a, '依頼', { agent_id: 'agent-1' }), null, 'subagent の中の入力');
|
|
1620
1636
|
assert.equal(submit('../escape', '依頼'), null);
|
|
1621
1637
|
assert.equal(submit(a, '依頼', {}, { AITERM_AGENT_LAUNCH_ID: 'abc', AITERM_AGENT_VENDOR_SESSION_ID: a }), null, 'Aiterm の席の会話は止めない');
|
|
1622
1638
|
assert.equal(submit(a, '依頼', {}, { AITERM_AGENT_LAUNCH_ID: 'abc', AITERM_AGENT_VENDOR_SESSION_ID: b }).kind, 'prompt', '席の印だけを継いだ会話は止める');
|
|
1623
1639
|
assert.deepEqual(redirects(), [
|
|
1624
|
-
{ at: '1970-01-01T00:00:05.000Z', source_session_id: a, handoff_id: 'h-aaaaaaaa', successor_short_id: 'bbbbbbbb', kind: 'prompt' },
|
|
1625
|
-
{ at: '1970-01-01T00:00:05.000Z', source_session_id: a, handoff_id: 'h-aaaaaaaa', successor_short_id: 'bbbbbbbb', kind: 'task-notification' },
|
|
1626
|
-
{ at: '1970-01-01T00:00:05.000Z', source_session_id: a, handoff_id: 'h-aaaaaaaa', successor_short_id: 'bbbbbbbb', kind: 'prompt' },
|
|
1640
|
+
{ at: '1970-01-01T00:00:05.000Z', source_session_id: a, handoff_id: 'h-aaaaaaaa', successor_short_id: 'bbbbbbbb', kind: 'prompt', forwarded: true },
|
|
1641
|
+
{ at: '1970-01-01T00:00:05.000Z', source_session_id: a, handoff_id: 'h-aaaaaaaa', successor_short_id: 'bbbbbbbb', kind: 'task-notification', forwarded: true },
|
|
1642
|
+
{ at: '1970-01-01T00:00:05.000Z', source_session_id: a, handoff_id: 'h-aaaaaaaa', successor_short_id: 'bbbbbbbb', kind: 'prompt', forwarded: true },
|
|
1627
1643
|
], '止めた記録に入力の本文は書かない');
|
|
1628
1644
|
|
|
1629
1645
|
// 後継がさらに引き継がれている時は、その先を示す。道具を断る時の文も同じ
|
|
@@ -1652,16 +1668,22 @@ test('公開CLI: prompt-submit は、引き継ぎ済みの会話の入力を止
|
|
|
1652
1668
|
const blocked = submit(SESSION, '旧い会話へ打った文');
|
|
1653
1669
|
assert.equal(blocked.status, 0, blocked.stderr);
|
|
1654
1670
|
assert.deepEqual(JSON.parse(blocked.stdout), { decision: 'block',
|
|
1655
|
-
reason:
|
|
1671
|
+
reason: 'この会話はThroughlineが新しい会話へ引き継ぎ済みです(引き継ぎID: h-1)。届いた入力は、後継の会話へ渡しました。' +
|
|
1672
|
+
'後継は今、入力を待ち受けていません(止まっているか、この版より前に立った会話です)。後継を開いて何か入力した時に届きます: ' +
|
|
1673
|
+
`claude attach ${SUCCESSOR.slice(0, 8)}(一覧に無い時は claude --resume ${SUCCESSOR})` });
|
|
1656
1674
|
assert.equal(existsSync(join(home, '.throughline', 'throughline.db')), false, '止める時は DB を開かない');
|
|
1657
1675
|
const logged = readFileSync(join(home, '.throughline', 'logs', 'inheritance-decision.log'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
1658
1676
|
assert.deepEqual(logged.map(row => [row.phase, row.session_id, row.handoff_id, row.successor_short_id, row.input_kind]),
|
|
1659
1677
|
[['prompt-redirect', SESSION, 'h-1', SUCCESSOR.slice(0, 8), 'prompt']]);
|
|
1660
1678
|
assert.equal(JSON.stringify(logged).includes('旧い会話へ打った文'), false, '判定の記録に本文は書かない');
|
|
1661
1679
|
|
|
1680
|
+
// 止めた入力は後継の置き場で待ち、後継の次の入力で文脈として届く(ADR 0063)。届けた後は残らない
|
|
1662
1681
|
const passed = submit(SUCCESSOR, '後継へ打った文');
|
|
1663
1682
|
assert.equal(passed.status, 0, passed.stderr);
|
|
1664
|
-
assert.
|
|
1683
|
+
assert.match(passed.stdout, /^【Throughline 転送】引き継ぎ前の会話(0f3a6c1e)へ届いた入力を、Throughlineがこの会話へ運びました(利用者の入力 1 通)。/);
|
|
1684
|
+
assert.match(passed.stdout, /\n--- 利用者の入力([^)]*利用者がこの会話へ打った入力として扱ってください)---\n旧い会話へ打った文\n$/);
|
|
1685
|
+
assert.equal(submit(SUCCESSOR, '後継へ打った次の文').stdout, '', '同じ入力を2回は届けない');
|
|
1686
|
+
assert.equal(submit('eeeeeeee-0000-4000-8000-000000000099', '関係の無い会話').stdout, '', '置き場が無い会話の入力は、今までどおり通す');
|
|
1665
1687
|
}));
|
|
1666
1688
|
|
|
1667
1689
|
test('裏で動いている作業: 起こした道具から、終わった通知が届いた物を除く(user 行でも、途中の入力でも)', () => withDir(dir => {
|