throughline 0.16.21 → 0.16.22
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,31 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.16.22] — 2026-10-11
|
|
14
|
+
|
|
15
|
+
### 変更
|
|
16
|
+
|
|
17
|
+
- **Claude Codeの自動継続で、後継をClaude Desktopへ移す処理を、既定で無効にした**([ADR 0051](docs/adr/0051-claude-successor-desktop-move-is-opt-in.md))。後継は裏の会話のまま残り、リモートコントロール(claude.ai/codeとClaudeのアプリ)、`claude agents`、`claude attach <id>`で見られる。Claude Desktopの一覧には出ない。
|
|
18
|
+
- 今までは、Claude Desktopから始まった会話の後継がターンを終えるたびに、`claude stop`で止めて`claude --desktop --resume`で開き直していた。Windowsの実機で2026-10-11に2回(10:31、10:39)、オーナーがスマホから話している最中の会話が消えた。止めると、起動時に付けたリモートコントロールの接続が切れ、移った先のDesktopの会話ではリモートコントロールが有効になっていなかった。1回目は、会話が裏で走らせていた命令(`run_in_background`)も一緒に止まった。作業ツリーで作業していた会話は、開き直した後のcwdが元のrepositoryになった。
|
|
19
|
+
- 0.16.20(保留の文を持つ後継も移す)で、それまで移らなかった会話が移るようになり、この2回が起きた。保留の文が無い会話では、0.16.8(macOS)と0.16.9(Windows)から同じ事が起きていた。
|
|
20
|
+
- 環境変数`THROUGHLINE_AUTO_HANDOFF_OPEN`を`origin`にすると、今までの既定(Desktopから始まった会話の後継だけ移す)で動く。`desktop`は出どころに関係なく移す。未設定とそれ以外の値は移さない。
|
|
21
|
+
- 0.16.21までの版が「移す」と記録した引き継ぎの後継も、有効にしていなければ移さない。
|
|
22
|
+
- 旧い会話を止めた理由の文を直した。移さない時は「後継の会話は裏で作業を続けます」と書き、リモートコントロール付きの時はその事も書く。
|
|
23
|
+
|
|
24
|
+
### 対応の範囲
|
|
25
|
+
|
|
26
|
+
| | macOS | Linux | Windows |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| Claude Code | 止めた。単体試験と、実機(Claude Code 2.1.289)。試験用の場所の記録で、入っていた0.16.20は移すprocessを起こし、この版は未設定では起こさず(記録も変えない)、`origin`では起こす事 | 元から移す処理が動かない(Claude Codeの`--desktop`が無い)。単体試験と、実機(2.1.296)で、記録を探す処理が同じ結果になる事 | 止めた。単体試験と、実機(Claude Code 2.1.296)で同じ確認。今日消えた会話を移した本物の記録の写しでも、未設定では起こさない事 |
|
|
29
|
+
| Codex | 変更なし(Desktopへ移す処理が無い。後継は最初からCodex Desktopのタスク) | 同じ | 同じ |
|
|
30
|
+
| Grok | 変更なし(自動継続が無い) | 同じ | 同じ |
|
|
31
|
+
| Cursor | 変更なし(自動継続が無い) | 同じ | 同じ |
|
|
32
|
+
|
|
33
|
+
### 確かめていない事
|
|
34
|
+
|
|
35
|
+
- この版での本物の引き継ぎ。後継がターンを終えても裏の会話のまま残り、スマホから見え続ける所。
|
|
36
|
+
- 0.16.21までに移した会話(Desktopの会話になっている)へ、リモートコントロールを付け直す方法。
|
|
37
|
+
|
|
13
38
|
## [0.16.21] — 2026-10-11
|
|
14
39
|
|
|
15
40
|
### 修正
|
|
@@ -2353,7 +2378,8 @@ two attempts, instrument first instead of patching again.
|
|
|
2353
2378
|
|
|
2354
2379
|
---
|
|
2355
2380
|
|
|
2356
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.
|
|
2381
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.22...HEAD
|
|
2382
|
+
[0.16.22]: https://github.com/kitepon/Throughline/compare/v0.16.21...v0.16.22
|
|
2357
2383
|
[0.16.21]: https://github.com/kitepon/Throughline/compare/v0.16.20...v0.16.21
|
|
2358
2384
|
[0.16.20]: https://github.com/kitepon/Throughline/compare/v0.16.19...v0.16.20
|
|
2359
2385
|
[0.16.19]: https://github.com/kitepon/Throughline/compare/v0.16.18...v0.16.19
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ throughline auto-handoff disable --host claude
|
|
|
68
68
|
3. 同じprojectに、継続の指示を最初の指示として付けた新しいClaudeの会話を裏で立てます(`claude --bg`)。会話の名前は、前任の会話の題から`<project名>|<作業の概要>(自動引き継ぎ)`にします。モデル・推論強度・権限は旧い会話から引き継ぎます。
|
|
69
69
|
4. 最初の指示を受け取った時に、止めた時点の依頼、そのターンでここまでにしたこと(発言と道具の呼び出し)、実行されなかった道具、直近の会話の原文を注入します(上限9,500字)。止めたターンの道具の入出力は、後継が`throughline detail`で取り出せます。
|
|
70
70
|
|
|
71
|
-
続きは裏の会話で動きます。`claude agents`の一覧と`claude attach <id
|
|
71
|
+
続きは裏の会話で動きます。`claude agents`の一覧と`claude attach <id>`で見られます。後継をClaude Desktopへ移す処理は、0.16.22から既定で無効です([ADR 0051](docs/adr/0051-claude-successor-desktop-move-is-opt-in.md))。移す時は裏の会話を`claude stop`で止めて開き直すので、リモートコントロールの接続が切れ、裏で走らせていた命令も止まり、話している最中の会話が消えます。環境変数`THROUGHLINE_AUTO_HANDOFF_OPEN`を`origin`にすると、Claude Desktopの画面から始まった会話の後継だけ、そのターンの作業を終えた時にClaude Desktopへ移して開きます(macOSとWindows。LinuxのClaude Codeには`--desktop`が無いので開きません。[ADR 0043](docs/adr/0043-claude-successor-opens-in-desktop.md))。`desktop`にすると出どころに関係なく開きます。移す処理が有効な時は、ほかの会話から`SendMessage`で届いた文をClaude Codeが承認待ちで保留している後継も、ターンが終わっていれば開き、保留されている文は会話へ届かずに消えます([ADR 0050](docs/adr/0050-claude-successor-with-held-peer-message-moves-to-desktop.md))。手で開く時は、`claude stop <id>`で裏の実行を停止し、`claude --desktop --resume <session-id>`を実行します。`session-id`は`claude agents --json --all`の`sessionId`です。手動`/compact`、subagentの中の圧縮、`claude -p`などscriptから起動した会話は対象に含めません。 Claude Desktopから始まった会話の後継は、リモートコントロール付きで立てるので、作業の最中もclaude.ai/codeとClaudeのアプリから見られます(0.16.16から。[ADR 0048](docs/adr/0048-claude-desktop-origin-successor-has-remote-control.md))。
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
throughline auto-handoff status --host claude --operation <handoff-id> --json
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ADR 0051: Claude Code の後継を Claude Desktop へ移す処理は、既定で無効にする
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-11
|
|
5
|
+
- Supersedes: [ADR 0043](0043-claude-successor-opens-in-desktop.md) の既定(Desktop から始まった会話の後継を、ターンの後に移す)
|
|
6
|
+
- Related: [ADR 0048](0048-claude-desktop-origin-successor-has-remote-control.md)、[ADR 0049](0049-claude-handoff-uses-registered-project.md)、[ADR 0050](0050-claude-successor-with-held-peer-message-moves-to-desktop.md)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Claude Code の自動継続は、後継を裏の会話(`claude --bg`)で立てる。Claude Desktop から始まった会話の後継は、
|
|
11
|
+
リモートコントロール付きで立て(ADR 0048)、ターンを終えた時に `claude stop` で止めて `claude --desktop --resume` で
|
|
12
|
+
Desktop へ開き直していた(ADR 0043)。
|
|
13
|
+
|
|
14
|
+
2026-10-11、Windows の実機で、オーナーがスマホ(リモートコントロール)から後継と話している最中に、会話が続けて消えた。
|
|
15
|
+
|
|
16
|
+
- 10:31:43 と 10:39:50 ごろの2回。どちらも、後継がターンを終えた(返事を返した)直後に、Throughline が止めて Desktop へ移した。
|
|
17
|
+
- 止めると、Throughline が起動時に付けたリモートコントロールの接続が切れる。移った先の Desktop の会話は、
|
|
18
|
+
リモートコントロールが有効になっていなかった。スマホと claude.ai/code からは、会話が消えたままになる。
|
|
19
|
+
- 1回目の会話は、裏で命令を走らせ(`run_in_background`)、その結果を待つ形でターンを終えていた。止めた時に、その命令も
|
|
20
|
+
止まった(`task-notification` の `status` は `stopped`)。移った後の会話は、その知らせを受けたまま手すきで止まった。
|
|
21
|
+
- 1回目の会話は作業ツリーで作業していたが、Desktop で開き直した後の cwd は、会話を立てた元の repository だった。
|
|
22
|
+
- ADR 0050(0.16.20、同じ日の 09:16 公開)で、保留の文を持つ会話も移すようにした。それまで移らずに済んでいた会話が
|
|
23
|
+
移るようになり、この2回が起きた。保留の文が無い会話では、0.16.8(macOS)と 0.16.9(Windows)から同じ事が起きていた。
|
|
24
|
+
|
|
25
|
+
「ターンが終わった」は「作業が終わった」ではなかった。利用者が話している最中の会話も、裏の命令を待っている会話も、
|
|
26
|
+
ターンは終わる。その会話を止めて別の場所へ開き直すと、利用者は会話を見失う。
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
1. 後継を Claude Desktop へ移す処理は、既定で無効にする。後継は、裏の会話のまま残す。リモートコントロール(ADR 0048)、
|
|
31
|
+
`claude agents`、`claude attach <id>` で見られる。
|
|
32
|
+
2. 環境変数 `THROUGHLINE_AUTO_HANDOFF_OPEN` が `origin` の時だけ、今までの既定(Desktop から始まった会話の後継を、
|
|
33
|
+
ターンの後に移す)で動く。`desktop` は、出どころに関係なく移す(今までと同じ)。それ以外の値と未設定は、移さない。
|
|
34
|
+
3. 0.16.21 までの版が「移す」と記録した引き継ぎ(`desktop.wanted: true`、`desktop.state` が空)の後継も、
|
|
35
|
+
有効にしていなければ移さない。記録は書き換えない。
|
|
36
|
+
4. リモートコントロール付きで立てる処理(ADR 0048)は変えない。
|
|
37
|
+
|
|
38
|
+
## Consequences
|
|
39
|
+
|
|
40
|
+
- 利用者が話している会話を、Throughline が止める事は無くなる。裏の命令も、保留の文も、リモートコントロールの接続も残る。
|
|
41
|
+
- Claude Desktop の一覧には、後継が出ない。Desktop の画面だけを見ている利用者は、引き継ぎの後の会話を Desktop では
|
|
42
|
+
見つけられない。旧い会話の止めた理由に、後継の短い ID(`claude attach <id>`)とリモートコントロールで見られる事を書く。
|
|
43
|
+
- Desktop へ移したい時は、今までどおり手で `claude stop <id>` と `claude --desktop --resume <session-id>` を使える。
|
|
44
|
+
- 移す処理をもう一度既定にするかは、オーナーの裁定を待つ。その時は、少なくとも次を満たす。話している最中の会話を
|
|
45
|
+
止めない。裏の命令が走っている会話を止めない。移した先でリモートコントロールが続く。作業ツリーの会話は作業ツリーで開く。
|
|
46
|
+
- ADR 0050 の「保留の文を持つ後継も移す」と、ADR 0049 の「受領を確かめられなかった後継も移す」は、移す処理を
|
|
47
|
+
有効にした時だけ働く。
|
package/package.json
CHANGED
|
@@ -60,7 +60,7 @@ const PER_SESSION_ENV = ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT', 'CLAUDE_CODE_ME
|
|
|
60
60
|
|
|
61
61
|
// Claude Desktop の画面から始まった会話が transcript に持つ印。
|
|
62
62
|
const DESKTOP_ENTRYPOINT = 'claude-desktop';
|
|
63
|
-
// 後継を Desktop
|
|
63
|
+
// 後継を Desktop へ移すか。既定は移さない(ADR 0051)。`origin` は Desktop から始まった会話の後継だけ、`desktop` は出どころに関係なく移す。
|
|
64
64
|
const OPEN_OVERRIDE_ENV = 'THROUGHLINE_AUTO_HANDOFF_OPEN';
|
|
65
65
|
// 後継をリモートコントロール付きで立てるかの上書き。`on` は出どころに関係なく付け、`off` は付けない。
|
|
66
66
|
const REMOTE_CONTROL_OVERRIDE_ENV = 'THROUGHLINE_AUTO_HANDOFF_REMOTE_CONTROL';
|
|
@@ -214,14 +214,26 @@ function readLatestEntrypoint(transcriptPath) {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
|
-
*
|
|
218
|
-
* Desktop
|
|
217
|
+
* 後継を Claude Desktop へ移す処理を、利用者が有効にしているか(ADR 0051)。既定は無効。
|
|
218
|
+
* 移す時は、裏の会話を `claude stop` で止めて Desktop で開き直す。止めると、その会話のリモートコントロールの接続が切れ
|
|
219
|
+
* (スマホと claude.ai/code から会話が消える)、裏で走らせていた命令も止まる。利用者が話している最中の会話でも起きる。
|
|
220
|
+
* `THROUGHLINE_AUTO_HANDOFF_OPEN` が `origin`(Desktop から始まった会話の後継だけ)か `desktop`(出どころに関係なく)の時だけ移す。
|
|
221
|
+
*/
|
|
222
|
+
function desktopOpenMode(env) {
|
|
223
|
+
const mode = env[OPEN_OVERRIDE_ENV];
|
|
224
|
+
return mode === 'origin' || mode === 'desktop' ? mode : 'off';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* この会話の後継を、Claude Desktop で開くか。既定では開かない(ADR 0051)。
|
|
229
|
+
* `origin` の時は、Desktop の画面から始まった会話と、その後継がさらに引き継ぐ時だけ開く。端末から始めた会話の後継は
|
|
219
230
|
* `claude agents` の一覧に出るので、Desktop へは移さない。Claude Code の `--desktop` があるのは macOS と Windows だけ
|
|
220
231
|
* (Linux は `--desktop isn't available on this platform` で断る)。
|
|
221
232
|
*/
|
|
222
233
|
function openInDesktopWanted({ sessionId, transcriptPath, env, dir, platform }) {
|
|
223
|
-
|
|
224
|
-
if (
|
|
234
|
+
const mode = desktopOpenMode(env);
|
|
235
|
+
if (!['darwin', 'win32'].includes(platform) || mode === 'off') return false;
|
|
236
|
+
if (mode === 'desktop') return true;
|
|
225
237
|
if (env.CLAUDE_CODE_ENTRYPOINT === DESKTOP_ENTRYPOINT || readLatestEntrypoint(transcriptPath) === DESKTOP_ENTRYPOINT) return true;
|
|
226
238
|
return listClaudeAutoHandoffs({ dir }).some(record => record.successor?.session_id === sessionId && record.desktop?.wanted);
|
|
227
239
|
}
|
|
@@ -531,9 +543,9 @@ function stopOutput(record) {
|
|
|
531
543
|
? `この会話はThroughlineが新しい会話へ引き継ぎ済みです(引き継ぎID: ${record.handoff_id})。続きは後継の会話で行ってください: claude attach ${record.successor.short_id}`
|
|
532
544
|
: `Throughlineが、自動圧縮の代わりに新しい会話へ引き継ぎます(引き継ぎID: ${record.handoff_id})。` +
|
|
533
545
|
(record.desktop?.wanted
|
|
534
|
-
? '後継の会話は裏で作業を続け、そのターンを終えた時にClaude Desktopへ開きます。途中の様子は `claude agents` の一覧で見られます。'
|
|
535
|
-
|
|
536
|
-
|
|
546
|
+
? '後継の会話は裏で作業を続け、そのターンを終えた時にClaude Desktopへ開きます。途中の様子は `claude agents` の一覧で見られます。'
|
|
547
|
+
: '後継の会話は裏で作業を続けます。`claude agents` の一覧に出ます。') +
|
|
548
|
+
(record.remote_control?.wanted ? '後継はリモートコントロール付きで立てるので、claude.ai/code とClaudeのアプリからも見られます。' : '');
|
|
537
549
|
return {
|
|
538
550
|
continue: false,
|
|
539
551
|
stopReason: reason,
|
|
@@ -821,13 +833,15 @@ export async function launchClaudeDesktopOpen(sessionId, { dir = claudeAutoHando
|
|
|
821
833
|
|
|
822
834
|
/**
|
|
823
835
|
* 後継の Stop hook から呼ぶ。Claude Desktop で開く引き継ぎの後継が、ターンを終えた。
|
|
824
|
-
* その会話を Desktop へ移す process
|
|
836
|
+
* その会話を Desktop へ移す process を起動する。対象でない会話と、移す処理が無効(既定)の時は何もしない(null)。
|
|
825
837
|
* `transcriptPath` は、その後継の transcript(Stop の payload の値)。移す process が、ターンが終わっているかを読む(ADR 0050)。
|
|
826
838
|
*
|
|
827
839
|
* @returns {Promise<string|null>} 起動した時は、その引き継ぎ ID
|
|
828
840
|
*/
|
|
829
|
-
export async function requestClaudeDesktopOpen({ sessionId, transcriptPath = null, dir = claudeAutoHandoffDir(), launch = launchClaudeDesktopOpen }) {
|
|
841
|
+
export async function requestClaudeDesktopOpen({ sessionId, transcriptPath = null, dir = claudeAutoHandoffDir(), launch = launchClaudeDesktopOpen, env = process.env }) {
|
|
830
842
|
if (!validSessionId(sessionId)) return null;
|
|
843
|
+
// 既定では移さない。0.16.21 までの版が「移す」と記録した引き継ぎの後継も、利用者が有効にしていなければ移さない(ADR 0051)。
|
|
844
|
+
if (desktopOpenMode(env) === 'off') return null;
|
|
831
845
|
// この会話自身が引き継ぎの途中なら、作業は次の後継が続ける。移すのは、最後に作業を終えた後継だけ。
|
|
832
846
|
if (readJson(recordPath(sessionId, dir))) return null;
|
|
833
847
|
const waiting = listClaudeAutoHandoffs({ dir }).filter(item => item.desktop?.wanted && !item.desktop.state);
|
|
@@ -26,6 +26,8 @@ const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
26
26
|
const CLI = join(REPO_ROOT, 'bin', 'throughline.mjs');
|
|
27
27
|
const SESSION = '0f3a6c1e-7b2d-4e5f-9a81-3c4d5e6f7a8b';
|
|
28
28
|
const SUCCESSOR = 'a41a97ae-6b2a-421b-b2d5-1a0b07988d06';
|
|
29
|
+
// Desktop へ移す処理は、利用者が有効にした時だけ動く(ADR 0051)。移す処理の試験は、Desktop から始まった会話の後継だけ移す設定で流す。
|
|
30
|
+
const OPEN = { THROUGHLINE_AUTO_HANDOFF_OPEN: 'origin' };
|
|
29
31
|
const ENABLED = { enabled: true, projects: [] };
|
|
30
32
|
|
|
31
33
|
function withDir(fn) {
|
|
@@ -638,6 +640,11 @@ function writeRecord(dir, sourceId, fields) {
|
|
|
638
640
|
|
|
639
641
|
test('PreCompact: Claude Desktop から始まった会話と、その後継の引き継ぎだけ、後継を Desktop で開く印を付ける(macOS)', () => withDir(dir => {
|
|
640
642
|
const wanted = overrides => {
|
|
643
|
+
rmSync(join(dir, `${SESSION}.json`), { force: true });
|
|
644
|
+
request(dir, { ...overrides, env: { ...OPEN, ...overrides.env } });
|
|
645
|
+
return readClaudeAutoHandoff(SESSION, dir).desktop;
|
|
646
|
+
};
|
|
647
|
+
const byDefault = overrides => {
|
|
641
648
|
rmSync(join(dir, `${SESSION}.json`), { force: true });
|
|
642
649
|
request(dir, overrides);
|
|
643
650
|
return readClaudeAutoHandoff(SESSION, dir).desktop;
|
|
@@ -648,6 +655,17 @@ test('PreCompact: Claude Desktop から始まった会話と、その後継の
|
|
|
648
655
|
const cliTranscript = join(dir, 'cli.jsonl');
|
|
649
656
|
writeFileSync(cliTranscript, jsonl([{ ...user('読んで', '2026-10-08T12:00:00.000Z'), entrypoint: 'cli' }]));
|
|
650
657
|
|
|
658
|
+
// 既定では、Desktop から始まった会話の後継も移さない(移すと、話している最中の会話がスマホから消える)
|
|
659
|
+
for (const platform of ['darwin', 'win32']) {
|
|
660
|
+
const payload = platform === 'win32' ? { cwd: 'C:\\work\\app' } : undefined;
|
|
661
|
+
assert.deepEqual(byDefault({ platform, payload, transcriptPath: desktopTranscript }), { wanted: false, state: null, error_code: null, opened_at: null }, platform);
|
|
662
|
+
assert.equal(byDefault({ platform, payload, env: { CLAUDE_CODE_ENTRYPOINT: 'claude-desktop' } }).wanted, false, platform);
|
|
663
|
+
assert.equal(byDefault({ platform, payload, transcriptPath: desktopTranscript, env: { THROUGHLINE_AUTO_HANDOFF_OPEN: 'on' } }).wanted, false, '知らない値では移さない');
|
|
664
|
+
}
|
|
665
|
+
writeRecord(dir, 'origin-desktop', { successor: { short_id: SESSION.slice(0, 8), session_id: SESSION } });
|
|
666
|
+
assert.equal(byDefault({ platform: 'darwin', transcriptPath: cliTranscript }).wanted, false, '0.16.21 までの版が「移す」と記録した引き継ぎの後継が、さらに引き継ぐ時も移さない');
|
|
667
|
+
rmSync(join(dir, 'origin-desktop.json'), { force: true });
|
|
668
|
+
|
|
651
669
|
assert.deepEqual(wanted({ platform: 'darwin', transcriptPath: desktopTranscript }), { wanted: true, state: null, error_code: null, opened_at: null });
|
|
652
670
|
assert.equal(wanted({ platform: 'darwin', env: { CLAUDE_CODE_ENTRYPOINT: 'claude-desktop' } }).wanted, true);
|
|
653
671
|
assert.equal(wanted({ platform: 'darwin', transcriptPath: cliTranscript }).wanted, false, '端末から始めた会話の後継は、claude agents の一覧に出る');
|
|
@@ -669,7 +687,13 @@ test('PreCompact: Claude Desktop から始まった会話と、その後継の
|
|
|
669
687
|
test('PreToolUse: Desktop で開く引き継ぎは、止めた理由に、後継がターンを終えた時に Desktop へ開く事を書く', () => withDir(async dir => {
|
|
670
688
|
const desktopTranscript = join(dir, 'desktop.jsonl');
|
|
671
689
|
writeFileSync(desktopTranscript, jsonl([{ ...user('読んで', '2026-10-08T12:00:00.000Z'), entrypoint: 'claude-desktop' }]));
|
|
690
|
+
// 既定では移さない。止めた理由には、裏で続ける事と、リモートコントロールで見られる事だけを書く
|
|
672
691
|
request(dir, { platform: 'darwin', transcriptPath: desktopTranscript });
|
|
692
|
+
const kept = await stopClaudeTurnForHandoff({ payload: { session_id: SESSION, cwd: '/work/app', tool_use_id: 't1' }, dir, launchWorker: async () => 1 });
|
|
693
|
+
assert.match(kept.stopReason, /後継の会話は裏で作業を続けます。`claude agents` の一覧に出ます。後継はリモートコントロール付きで立てるので、claude\.ai\/code とClaudeのアプリからも見られます。/);
|
|
694
|
+
assert.doesNotMatch(kept.stopReason, /Claude Desktopへ開きます/);
|
|
695
|
+
rmSync(join(dir, `${SESSION}.json`), { force: true });
|
|
696
|
+
request(dir, { platform: 'darwin', transcriptPath: desktopTranscript, env: OPEN });
|
|
673
697
|
const output = await stopClaudeTurnForHandoff({ payload: { session_id: SESSION, cwd: '/work/app', tool_use_id: 't1' }, dir, launchWorker: async () => 1 });
|
|
674
698
|
assert.match(output.stopReason, /そのターンを終えた時にClaude Desktopへ開きます。途中の様子は `claude agents` の一覧で見られます。後継はリモートコントロール付きで立てるので、claude\.ai\/code とClaudeのアプリからも見られます。$/);
|
|
675
699
|
}));
|
|
@@ -734,7 +758,7 @@ test('worker: リモートコントロールを付けると立てられない時
|
|
|
734
758
|
writeFileSync(transcriptPath, jsonl([{ ...user('読んで', '2026-10-10T05:00:10Z'), entrypoint: 'claude-desktop' }, assistant('読みました。', '2026-10-10T05:00:12Z')]));
|
|
735
759
|
const db = makeBatonDb();
|
|
736
760
|
requestClaudeAutoHandoff({ payload: { session_id: sessionId, trigger: 'auto', cwd: '/work/app', transcript_path: transcriptPath },
|
|
737
|
-
env:
|
|
761
|
+
env: OPEN, config: ENABLED, openDb: () => db, dir, platform: 'darwin' });
|
|
738
762
|
await stopClaudeTurnForHandoff({ payload: { session_id: sessionId, cwd: '/work/app', transcript_path: transcriptPath }, dir, launchWorker: async () => {} });
|
|
739
763
|
const launches = [];
|
|
740
764
|
const record = await runClaudeAutoHandoffWorker(sessionId, { dir, env: {}, pollMs: 10, acceptTimeoutMs: 300, transcriptTimeoutMs: 50,
|
|
@@ -754,29 +778,36 @@ test('Stop: Desktop で開く引き継ぎの後継がターンを終えたら、
|
|
|
754
778
|
const launched = [];
|
|
755
779
|
const launch = async (id) => { launched.push(id); };
|
|
756
780
|
// 対象でない会話
|
|
757
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), null);
|
|
781
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), null);
|
|
782
|
+
// 移す処理は既定で無効。0.16.21 までの版が「移す」と記録した引き継ぎの後継がターンを終えても、止めない・移さない(ADR 0051)
|
|
783
|
+
writeRecord(dir, SESSION, {});
|
|
784
|
+
for (const env of [{}, { THROUGHLINE_AUTO_HANDOFF_OPEN: 'off' }, { THROUGHLINE_AUTO_HANDOFF_OPEN: 'on' }]) {
|
|
785
|
+
assert.equal(await requestClaudeDesktopOpen({ env, sessionId: SUCCESSOR, dir, launch }), null, JSON.stringify(env));
|
|
786
|
+
}
|
|
787
|
+
assert.deepEqual(launched, []);
|
|
788
|
+
assert.equal(readClaudeAutoHandoff(SESSION, dir).desktop.state, null, '記録も変えない');
|
|
758
789
|
writeRecord(dir, SESSION, { desktop: { wanted: false, state: null } });
|
|
759
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), null, '端末から始めた会話の後継は移さない');
|
|
790
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), null, '端末から始めた会話の後継は移さない');
|
|
760
791
|
writeRecord(dir, SESSION, { state: 'failed', error_code: 'handoff_successor_launch_failed', successor: null });
|
|
761
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), null, '後継が立たなかった引き継ぎでは動かない');
|
|
792
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), null, '後継が立たなかった引き継ぎでは動かない');
|
|
762
793
|
// 対象の後継
|
|
763
794
|
writeRecord(dir, SESSION, {});
|
|
764
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SESSION, dir, launch }), null, '旧い会話自身の Stop では動かない');
|
|
765
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), `h-${SESSION}`);
|
|
795
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SESSION, dir, launch }), null, '旧い会話自身の Stop では動かない');
|
|
796
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), `h-${SESSION}`);
|
|
766
797
|
assert.deepEqual(launched, [SESSION]);
|
|
767
798
|
assert.equal(readClaudeAutoHandoff(SESSION, dir).desktop.state, 'requested');
|
|
768
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), null, '2回目の Stop では起動しない');
|
|
799
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), null, '2回目の Stop では起動しない');
|
|
769
800
|
assert.deepEqual(launched, [SESSION]);
|
|
770
801
|
|
|
771
802
|
// 後継自身が引き継ぎの途中(作業は次の後継が続ける)なら、移さない
|
|
772
803
|
writeRecord(dir, SESSION, {});
|
|
773
804
|
writeRecord(dir, SUCCESSOR, { state: 'stopped', successor: null });
|
|
774
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), null);
|
|
805
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), null);
|
|
775
806
|
assert.deepEqual(launched, [SESSION]);
|
|
776
807
|
|
|
777
808
|
// 起動に失敗した時は理由を残す
|
|
778
809
|
rmSync(join(dir, `${SUCCESSOR}.json`));
|
|
779
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch: async () => { throw new Error('spawn'); } }), null);
|
|
810
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch: async () => { throw new Error('spawn'); } }), null);
|
|
780
811
|
assert.deepEqual([readClaudeAutoHandoff(SESSION, dir).desktop.state, readClaudeAutoHandoff(SESSION, dir).desktop.error_code], ['failed', 'desktop_open_start_failed']);
|
|
781
812
|
}));
|
|
782
813
|
|
|
@@ -787,22 +818,22 @@ test('Stop: 受領を確かめられなかった引き継ぎ(unknown)でも
|
|
|
787
818
|
const unconfirmed = { state: 'unknown', error_code: 'handoff_delivery_unconfirmed', accepted_at: null,
|
|
788
819
|
successor: { short_id: SUCCESSOR.slice(0, 8), session_id: null } };
|
|
789
820
|
writeRecord(dir, SESSION, unconfirmed);
|
|
790
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: 'bbbbbbbb-0000-4000-8000-000000000002', dir, launch }), null, '別の会話の Stop では動かない');
|
|
791
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), `h-${SESSION}`);
|
|
821
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: 'bbbbbbbb-0000-4000-8000-000000000002', dir, launch }), null, '別の会話の Stop では動かない');
|
|
822
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), `h-${SESSION}`);
|
|
792
823
|
assert.deepEqual(launched, [SESSION]);
|
|
793
824
|
const record = readClaudeAutoHandoff(SESSION, dir);
|
|
794
825
|
assert.deepEqual(record.successor, { short_id: SUCCESSOR.slice(0, 8), session_id: SUCCESSOR }, '移す process が使う後継の ID を記録へ入れる');
|
|
795
826
|
assert.deepEqual([record.state, record.error_code, record.desktop.state], ['unknown', 'handoff_delivery_unconfirmed', 'requested'],
|
|
796
827
|
'受領を確かめられなかった事は、記録に残したままにする');
|
|
797
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), null, '2回目の Stop では起動しない');
|
|
828
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), null, '2回目の Stop では起動しない');
|
|
798
829
|
|
|
799
830
|
// 移す process が、手すきを待ち切れずに戻った(fox の実物、2026-10-11 02:51)。記録には後継の session id が入っている。
|
|
800
831
|
// 次の Stop で、同じ記録からやり直す
|
|
801
832
|
const busy = await runClaudeDesktopOpen(SESSION, { dir, env: {}, pollMs: 10, idleTimeoutMs: 40, pty: () => { throw new Error('must not open'); },
|
|
802
833
|
spawn: (command, args) => { assert.equal(args[0], 'agents'); return { status: 0, stdout: JSON.stringify([{ id: SUCCESSOR.slice(0, 8), kind: 'background', status: 'busy' }]) }; } });
|
|
803
834
|
assert.deepEqual([busy.state, busy.desktop.state, busy.successor.session_id], ['unknown', null, SUCCESSOR]);
|
|
804
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: `${SUCCESSOR.slice(0, 8)}-0000-4000-8000-000000000009`, dir, launch }), null, '短い ID が同じでも、別の会話の Stop では動かない');
|
|
805
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, dir, launch }), `h-${SESSION}`, '戻った後の Stop でやり直す');
|
|
835
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: `${SUCCESSOR.slice(0, 8)}-0000-4000-8000-000000000009`, dir, launch }), null, '短い ID が同じでも、別の会話の Stop では動かない');
|
|
836
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, dir, launch }), `h-${SESSION}`, '戻った後の Stop でやり直す');
|
|
806
837
|
assert.deepEqual(launched, [SESSION, SESSION]);
|
|
807
838
|
assert.equal(readClaudeAutoHandoff(SESSION, dir).desktop.state, 'requested');
|
|
808
839
|
|
|
@@ -916,7 +947,7 @@ test('transcript: 終わったターンで止まっている時だけ、印(tr
|
|
|
916
947
|
test('Stop: 後継の transcript の場所を、移す process のために記録へ残す', () => withDir(async dir => {
|
|
917
948
|
writeRecord(dir, SESSION, {});
|
|
918
949
|
const launch = async () => {};
|
|
919
|
-
assert.equal(await requestClaudeDesktopOpen({ sessionId: SUCCESSOR, transcriptPath: '/home/u/.claude/projects/-work-app/successor.jsonl', dir, launch }), `h-${SESSION}`);
|
|
950
|
+
assert.equal(await requestClaudeDesktopOpen({ env: OPEN, sessionId: SUCCESSOR, transcriptPath: '/home/u/.claude/projects/-work-app/successor.jsonl', dir, launch }), `h-${SESSION}`);
|
|
920
951
|
assert.deepEqual(readClaudeAutoHandoff(SESSION, dir).successor,
|
|
921
952
|
{ short_id: SUCCESSOR.slice(0, 8), session_id: SUCCESSOR, transcript_path: '/home/u/.claude/projects/-work-app/successor.jsonl' });
|
|
922
953
|
assert.equal(JSON.stringify(publicClaudeAutoHandoff(readClaudeAutoHandoff(SESSION, dir))).includes('successor.jsonl'), false, '公開の表示には場所を出さない');
|