throughline 0.15.3 → 0.15.4
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,24 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.15.4] — 2026-10-06
|
|
14
|
+
|
|
15
|
+
### 修正
|
|
16
|
+
|
|
17
|
+
- Claude Desktopで別のprojectへ移した会話を、移った後に1回もターンを終えないまま自動継続で引き継ぐと、後継へ記憶が入らない不具合を直す(0.15.2・0.15.3)。後継の最初の指示は、前任と後継のprojectが同じ時だけ前任を合流させる。前任の`sessions.project_path`を移った先へ付け替えるのはターン終了(Stop)だけだったので、その前に引き継ぐと移る前の場所のまま残り、合流が`project_mismatch`で見送られていた。後継は継続の指示だけを受け取り、記憶と止めた時点の依頼を持たずに動き出していた。
|
|
18
|
+
- 自動圧縮を止める時(`pre-compact`)にも、移っている会話の`sessions.project_path`を移った先へ付け替える。
|
|
19
|
+
|
|
20
|
+
### 確認した範囲
|
|
21
|
+
|
|
22
|
+
- macOS、Claude Desktop(同梱の2.1.286、権限はバイパス、0.15.3)。画面から送った指示で、圧縮を止める・旧い会話を止める・後継を立てる・継続の指示が届く、の4段が通り、承認待ちは出なかった。1つ目の後継には記憶が入らず(上の不具合)、後継は自分でフォルダを調べて作業を続け、次の引き継ぎ(後継から後継、記憶あり)を経て作業を完了した。
|
|
23
|
+
- macOS、同じ会話の本物のtranscriptの写しを使い、切り離したHOMEでhookを手で呼ぶ再現。0.15.3は`pre-compact`の後も`sessions.project_path`が移る前の場所で、後継の最初の指示は`project_mismatch`・注入なし。この版は移った先へ付け替わり、合流して注入する。
|
|
24
|
+
|
|
25
|
+
### 確認していない範囲
|
|
26
|
+
|
|
27
|
+
- Claude Desktopの画面で、この版の1つ目の後継に記憶が入ること。
|
|
28
|
+
- Linuxで実物のClaude Codeを動かした通し。Windowsのバイパスの会話、Opus・Fableの後継(0.15.3と同じ)。
|
|
29
|
+
|
|
30
|
+
|
|
13
31
|
## [0.15.3] — 2026-10-06
|
|
14
32
|
|
|
15
33
|
### 修正
|
|
@@ -1813,7 +1831,8 @@ two attempts, instrument first instead of patching again.
|
|
|
1813
1831
|
|
|
1814
1832
|
---
|
|
1815
1833
|
|
|
1816
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.15.
|
|
1834
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.15.4...HEAD
|
|
1835
|
+
[0.15.4]: https://github.com/kitepon/Throughline/compare/v0.15.3...v0.15.4
|
|
1817
1836
|
[0.15.3]: https://github.com/kitepon/Throughline/compare/v0.15.2...v0.15.3
|
|
1818
1837
|
[0.15.2]: https://github.com/kitepon/Throughline/compare/v0.15.1...v0.15.2
|
|
1819
1838
|
[0.15.1]: https://github.com/kitepon/Throughline/compare/v0.15.0...v0.15.1
|
|
@@ -33,6 +33,7 @@ Claude Desktop の新しい会話は、画面を開いただけでは process
|
|
|
33
33
|
印の project は、会話を起動した場所(`CLAUDE_PROJECT_DIR`)を、その OS の書き方にそろえたもの(0.14.2)。
|
|
34
34
|
Claude Desktop で会話を別の project へ移している時は、transcript の `relocated` の行(`relocatedCwd`)が示す移った先を使う。
|
|
35
35
|
`CLAUDE_PROJECT_DIR` は移る前の場所のまま届くため(0.15.2)。
|
|
36
|
+
この時、前任の `sessions.project_path` も移った先へ付け替える(0.15.4)。後継は同じ project の前任だけを合流させる。
|
|
36
37
|
hook の cwd は Bash の `cd` に追従するので使わない。後継は起動した場所で立ち、その cwd で印を探す。
|
|
37
38
|
手動の `/compact`、無効な project、subagent の中の圧縮(payload に `agent_id`)、script から起動した会話
|
|
38
39
|
(`CLAUDE_CODE_ENTRYPOINT` が `sdk-` で始まる)では止めない。
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
25
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
26
26
|
import { readClaudeAutoHandoffConfig, claudeAutoHandoffEnabledFor } from './claude-auto-handoff-config.mjs';
|
|
27
27
|
import { CLAUDE_HOST, hostOfSessionId } from './hosts/identity.mjs';
|
|
28
|
-
import { claudeHostAdapter } from './hosts/claude.mjs';
|
|
28
|
+
import { claudeHostAdapter, readClaudeRelocatedCwd } from './hosts/claude.mjs';
|
|
29
29
|
import {
|
|
30
30
|
isJunkAssistantText,
|
|
31
31
|
normalizeTerminalText,
|
|
@@ -222,7 +222,15 @@ export function requestClaudeAutoHandoff({
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// /tl と同じ印。後継の最初の指示が、この会話を前任として合流させる。
|
|
225
|
-
|
|
225
|
+
const db = openDb();
|
|
226
|
+
// 会話が別の project へ移っている時(Claude Desktop)は、sessions の project も移った先にそろえる。
|
|
227
|
+
// 合流は前任と後継の project が同じ時だけ行う。付け替えは Stop でも行うが、移った後に1回も Stop を
|
|
228
|
+
// 通らないまま引き継ぐ会話があり、その時は後継へ記憶が入らなかった(0.15.2・0.15.3、macOS で実測)。
|
|
229
|
+
if (readClaudeRelocatedCwd(payload.transcript_path)) {
|
|
230
|
+
db.prepare('UPDATE sessions SET project_path = ? WHERE session_id = ? AND project_path <> ?')
|
|
231
|
+
.run(projectPath, sessionId, projectPath);
|
|
232
|
+
}
|
|
233
|
+
writeBaton(db, { projectPath, sessionId, now });
|
|
226
234
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
227
235
|
removeStale(dir, now);
|
|
228
236
|
const handoffId = randomUUID();
|
|
@@ -70,13 +70,19 @@ test('PreCompact: 会話が別の project へ移っている時は、移った
|
|
|
70
70
|
writeFileSync(transcriptPath, jsonl([user('読んで', '2026-10-05T22:38:17.050Z'), assistant('読みました', '2026-10-05T22:39:18.926Z'),
|
|
71
71
|
{ type: 'relocated', sessionId: SESSION, relocatedCwd: moved }]));
|
|
72
72
|
const onlyMoved = { enabled: true, projects: [moved] };
|
|
73
|
+
// SessionStart は移る前の場所で sessions の行を作っている
|
|
74
|
+
const relocatedDb = makeBatonDb();
|
|
75
|
+
relocatedDb.exec('CREATE TABLE sessions (session_id TEXT PRIMARY KEY, project_path TEXT NOT NULL)');
|
|
76
|
+
relocatedDb.prepare('INSERT INTO sessions VALUES (?, ?)').run(SESSION, scratch);
|
|
73
77
|
|
|
74
|
-
const { result, db } = request(dir, { config: onlyMoved, transcriptPath, env: { CLAUDE_PROJECT_DIR: scratch }, payload: { cwd: moved } });
|
|
78
|
+
const { result, db } = request(dir, { db: relocatedDb, config: onlyMoved, transcriptPath, env: { CLAUDE_PROJECT_DIR: scratch }, payload: { cwd: moved } });
|
|
75
79
|
assert.equal(result.status, 'requested');
|
|
76
80
|
assert.equal(result.block, true);
|
|
77
81
|
assert.equal(result.projectPath, moved);
|
|
78
82
|
assert.equal(readClaudeAutoHandoff(SESSION, dir).project_path, moved);
|
|
79
83
|
assert.equal(db.prepare('SELECT session_id FROM handoff_batons WHERE project_path = ?').get(moved).session_id, SESSION);
|
|
84
|
+
assert.equal(db.prepare('SELECT project_path FROM sessions WHERE session_id = ?').get(SESSION).project_path, moved,
|
|
85
|
+
'後継は同じ project の前任だけを合流させるので、sessions の project も移った先にそろえる');
|
|
80
86
|
}));
|
|
81
87
|
|
|
82
88
|
test('PreCompact: 移っていない会話は、これまでどおり起動した project で決める', () => withDir(dir => {
|
|
@@ -717,6 +723,59 @@ test('hook: 自動圧縮を止め、次の道具を止めて worker を起動し
|
|
|
717
723
|
assert.deepEqual([merged.triggered_path, merged.injection.auto_handoff_id], ['baton', record.handoff_id]);
|
|
718
724
|
}));
|
|
719
725
|
|
|
726
|
+
test('hook: 別の project へ移した会話(Claude Desktop)を、移った後に1回も Stop を通らないまま引き継いでも、後継へ記憶が入る', () => withDir(async home => {
|
|
727
|
+
// Desktop は「フォルダなし」で始めた会話を project へ移す。SessionStart は移る前の場所で呼ばれ、
|
|
728
|
+
// CLAUDE_PROJECT_DIR は移った後も移る前の場所のまま届く。
|
|
729
|
+
const scratch = join(home, 'scratch-2026-10-05-ac3fa8');
|
|
730
|
+
const project = join(home, 'project');
|
|
731
|
+
mkdirSync(scratch, { recursive: true });
|
|
732
|
+
mkdirSync(project, { recursive: true });
|
|
733
|
+
const transcriptPath = join(home, 'transcript.jsonl');
|
|
734
|
+
const base = Date.parse('2026-10-05T22:38:00Z');
|
|
735
|
+
const at = seconds => new Date(base + seconds * 1000).toISOString();
|
|
736
|
+
const moved = { ...fakeClaudePath(home), CLAUDE_PROJECT_DIR: scratch };
|
|
737
|
+
const hook = (command, payload, cwd = project) =>
|
|
738
|
+
cli(home, [command], JSON.stringify({ session_id: SESSION, cwd, transcript_path: transcriptPath, ...payload }), moved);
|
|
739
|
+
|
|
740
|
+
assert.equal(cli(home, ['auto-handoff', 'enable', '--host', 'claude', '--project', project, '--json']).status, 0);
|
|
741
|
+
assert.equal(hook('session-start', { hook_event_name: 'SessionStart', source: 'startup' }, scratch).status, 0);
|
|
742
|
+
writeFileSync(transcriptPath, jsonl([
|
|
743
|
+
user('14個を順に読んで', at(0)),
|
|
744
|
+
assistant('読み終えました。', at(60)),
|
|
745
|
+
{ type: 'relocated', sessionId: SESSION, relocatedCwd: project },
|
|
746
|
+
user('part06 から part09 をもう一度読んで', at(3000)),
|
|
747
|
+
assistant('part06 から読みます。', at(3002)),
|
|
748
|
+
toolUse('toolu_6', 'Read', { file_path: join(project, 'part06.txt') }),
|
|
749
|
+
]));
|
|
750
|
+
|
|
751
|
+
const pre = hook('pre-compact', { hook_event_name: 'PreCompact', trigger: 'auto' });
|
|
752
|
+
assert.equal(pre.status, 2, pre.stderr);
|
|
753
|
+
const stopped = hook('pre-tool-use', { hook_event_name: 'PreToolUse', tool_name: 'Read', tool_use_id: 'toolu_6', permission_mode: 'bypassPermissions' });
|
|
754
|
+
assert.equal(JSON.parse(stopped.stdout).continue, false);
|
|
755
|
+
const status = () => JSON.parse(cli(home, ['auto-handoff', 'status', '--host', 'claude', '--json']).stdout).handoffs;
|
|
756
|
+
let handoff;
|
|
757
|
+
for (let i = 0; i < 100 && (handoff = status()[0]).state !== 'failed'; i++) await new Promise(r => setTimeout(r, 100));
|
|
758
|
+
assert.equal(handoff.error_code, 'handoff_successor_launch_failed', '偽の claude で止まる。本物は起動しない');
|
|
759
|
+
|
|
760
|
+
// 後継の最初の指示(起動時に渡された継続の指示)
|
|
761
|
+
const recordFile = join(home, '.throughline', 'claude-auto-handoff', `${SESSION}.json`);
|
|
762
|
+
const record = JSON.parse(readFileSync(recordFile, 'utf8'));
|
|
763
|
+
writeFileSync(recordFile, JSON.stringify({ ...record, state: 'launching', error_code: null }));
|
|
764
|
+
const successorPayload = { session_id: SUCCESSOR, cwd: project, hook_event_name: 'SessionStart', source: 'startup' };
|
|
765
|
+
assert.equal(cli(home, ['session-start'], JSON.stringify(successorPayload), fakeClaudePath(home)).status, 0);
|
|
766
|
+
const first = cli(home, ['prompt-submit'], JSON.stringify({ ...successorPayload, hook_event_name: 'UserPromptSubmit',
|
|
767
|
+
prompt: claudeContinuationInput(record) }), fakeClaudePath(home));
|
|
768
|
+
assert.equal(first.status, 0, first.stderr);
|
|
769
|
+
assert.match(first.stdout, /^## Throughline: 自動継続の文脈\n/);
|
|
770
|
+
assert.match(first.stdout, /\*\*作業中のユーザー依頼\*\* \[\d\d:\d\d:\d\d\]: part06 から part09 をもう一度読んで\n/);
|
|
771
|
+
assert.match(first.stdout, /\*\*止める直前に呼ぼうとして、実行されなかった道具\*\*: Read .*part06\.txt\n/);
|
|
772
|
+
|
|
773
|
+
const db = new DatabaseSync(join(home, '.throughline', 'throughline.db'));
|
|
774
|
+
assert.equal(db.prepare('SELECT merged_into FROM sessions WHERE session_id = ?').get(SESSION).merged_into, SUCCESSOR);
|
|
775
|
+
assert.equal(db.prepare('SELECT project_path FROM sessions WHERE session_id = ?').get(SESSION).project_path, project);
|
|
776
|
+
db.close();
|
|
777
|
+
}));
|
|
778
|
+
|
|
720
779
|
test('hook: 自動継続が無効な時、自動圧縮を止めず、何も残さない', () => withDir(home => {
|
|
721
780
|
const project = join(home, 'project');
|
|
722
781
|
mkdirSync(project, { recursive: true });
|