throughline 0.12.9 → 0.13.0
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 +21 -1
- package/README.ja.md +20 -0
- package/README.md +20 -0
- package/bin/throughline.mjs +10 -1
- package/docs/02_clear_auto_handoff_plan.md +2 -1
- package/docs/05_codex_first_roadmap.md +2 -0
- package/docs/adr/0032-claude-compact-continuation.md +88 -0
- package/package.json +1 -1
- package/rag/01-hooks/raw/claude-compact-hooks-extract.md +56 -0
- package/rag/INDEX.md +4 -0
- package/src/claude-auto-handoff-config.mjs +38 -0
- package/src/claude-auto-handoff.mjs +207 -0
- package/src/claude-auto-handoff.test.mjs +308 -0
- package/src/claude-pre-compact.mjs +55 -0
- package/src/cli/auto-handoff.mjs +43 -5
- package/src/cli/install.mjs +28 -0
- package/src/cli/install.test.mjs +6 -0
- package/src/resume-context.mjs +101 -5
- package/src/resume-context.test.mjs +72 -0
- package/src/runtime-error-store.mjs +6 -0
- package/src/session-start.mjs +33 -1
- package/src/transcript-reader.mjs +58 -2
- package/src/transcript-reader.test.mjs +90 -0
- package/src/turn-backfill.mjs +7 -2
- package/src/turn-backfill.test.mjs +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,25 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.13.0] — 2026-10-04
|
|
14
|
+
|
|
15
|
+
### 追加
|
|
16
|
+
|
|
17
|
+
- Claude Codeの自動継続([ADR 0032](docs/adr/0032-claude-compact-continuation.md))。`throughline auto-handoff enable --host claude [--project <path>]`で有効にすると、自動圧縮の直後に、圧縮前の記録を同じ会話へ注入する。注入するのは、作業途中のユーザー依頼と圧縮直前の発言、直近の会話の原文(上限9,500字)。モデルは追加の入力なしで作業を続ける。既定は無効。手動`/compact`は対象外。
|
|
18
|
+
- Claude Codeは、hookから作業を止めることも、新しい会話へ指示を送ることもできない。Codexのように新しいタスクへは切り替えず、hostが続ける同じ会話へ記憶を渡す。
|
|
19
|
+
- `throughline install`は、Claudeの`PreCompact`フック(`throughline pre-compact`)を登録する。自動継続が無効の時は、判定を記録して抜ける。このフックは圧縮を止めない。
|
|
20
|
+
- `auto-handoff`に`--host claude`を足す。使えるのは`enable`・`disable`・`status`。`--host`を省いた時は今までどおりCodex。
|
|
21
|
+
|
|
22
|
+
### 修正
|
|
23
|
+
|
|
24
|
+
- 圧縮をまたいだClaudeのターンが、複数のターンに割れて保存されていた不具合を直す。Claude Codeは圧縮の直後に、要約を本文に持つ`user`行(`isCompactSummary: true`)をtranscriptへ書く。0.12.9以前はこの行をユーザーの発言として数え、要約をユーザー発言としてL2へ保存していた。3回圧縮した実機のターンは4ターンとして保存された。圧縮より前のtool入出力はL3に入らなかった。
|
|
25
|
+
- 修正後は、元の依頼と最終回答の1ターンとして保存し、圧縮より前のtool入出力も同じターンのL3へ入れる。Observer feedの`turn_start`は元の依頼のものになる(今までは要約行のターンとして`unknown`)。修正より前に保存した行は変えない。
|
|
26
|
+
|
|
27
|
+
### 確認した範囲
|
|
28
|
+
|
|
29
|
+
- LinuxのClaude Code 2.1.289(Haiku 4.5)。`claude -p`と対話画面のそれぞれで、自動圧縮2回をまたいで作業が完了し、圧縮のたびに注入が届いた。9,099字の注入はfile化されずに届いた。
|
|
30
|
+
- macOS・Windows、VS Code拡張・Desktop、subagentの中の圧縮は確かめていない。
|
|
31
|
+
|
|
13
32
|
## [0.12.9] — 2026-10-04
|
|
14
33
|
|
|
15
34
|
### 修正
|
|
@@ -1634,7 +1653,8 @@ two attempts, instrument first instead of patching again.
|
|
|
1634
1653
|
|
|
1635
1654
|
---
|
|
1636
1655
|
|
|
1637
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.
|
|
1656
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.13.0...HEAD
|
|
1657
|
+
[0.13.0]: https://github.com/kitepon/Throughline/compare/v0.12.9...v0.13.0
|
|
1638
1658
|
[0.12.9]: https://github.com/kitepon/Throughline/compare/v0.12.8...v0.12.9
|
|
1639
1659
|
[0.12.8]: https://github.com/kitepon/Throughline/compare/v0.12.7...v0.12.8
|
|
1640
1660
|
[0.12.7]: https://github.com/kitepon/Throughline/compare/v0.12.6...v0.12.7
|
package/README.ja.md
CHANGED
|
@@ -44,6 +44,26 @@ throughline auto-handoff resume --operation <handoff-id> --json
|
|
|
44
44
|
|
|
45
45
|
[実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-03-codex-auto-continuation.md)を参照してください。
|
|
46
46
|
|
|
47
|
+
## Claude Codeの自動継続
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
throughline auto-handoff enable --host claude --project /absolute/project/path
|
|
51
|
+
throughline auto-handoff status --host claude --json
|
|
52
|
+
throughline auto-handoff disable --host claude
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
既定では無効です。`--project`を省略すると全projectを対象にします。Codexの設定とは別で、互いに影響しません。
|
|
56
|
+
|
|
57
|
+
Claude Codeは、自動圧縮の前にhookから作業を止めることも、新しい会話へ指示を送ることもできません。自動圧縮の後は、同じ会話で作業をそのまま続けます。Throughlineは会話を切り替えず、圧縮の直後に、圧縮前の記録を同じ会話へ注入します([ADR 0032](docs/adr/0032-claude-compact-continuation.md))。
|
|
58
|
+
|
|
59
|
+
- `PreCompact`フックが、自動圧縮の時だけ印を残します。圧縮は止めません。
|
|
60
|
+
- 圧縮直後の`SessionStart`(`source: "compact"`)フックが、作業途中のユーザー依頼と圧縮直前の発言、直近の会話の原文を注入します。上限は9,500字で、入り切らない古いターンは`throughline recall`の案内になります。
|
|
61
|
+
- モデルは追加の入力なしで作業を続けます。圧縮の要約と記録が食い違う時は、記録を正とするよう指示します。
|
|
62
|
+
|
|
63
|
+
手動`/compact`は対象に含めません。圧縮をまたいだターンは、元の依頼と最終回答の1ターンとして保存します(0.12.9以前は、圧縮の要約をユーザー発言として保存していました)。
|
|
64
|
+
|
|
65
|
+
LinuxのClaude Code 2.1.289(`claude -p`と対話画面)で実機確認済みです。macOS・Windows、VS Code拡張・Desktop、subagentの中の圧縮は未検証です。[実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-04-claude-compact-continuation.md)を参照してください。
|
|
66
|
+
|
|
47
67
|
## 所有境界
|
|
48
68
|
|
|
49
69
|
本repositoryは導入、設定、状態、schemaとmigration、診断、復旧、更新、release判断を
|
package/README.md
CHANGED
|
@@ -43,6 +43,26 @@ throughline auto-handoff resume --operation <handoff-id> --json
|
|
|
43
43
|
|
|
44
44
|
[実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-03-codex-auto-continuation.md)を参照してください。
|
|
45
45
|
|
|
46
|
+
## Claude Codeの自動継続
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
throughline auto-handoff enable --host claude --project /absolute/project/path
|
|
50
|
+
throughline auto-handoff status --host claude --json
|
|
51
|
+
throughline auto-handoff disable --host claude
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
既定では無効です。`--project`を省略すると全projectを対象にします。Codexの設定とは別で、互いに影響しません。
|
|
55
|
+
|
|
56
|
+
Claude Codeは、自動圧縮の前にhookから作業を止めることも、新しい会話へ指示を送ることもできません。自動圧縮の後は、同じ会話で作業をそのまま続けます。Throughlineは会話を切り替えず、圧縮の直後に、圧縮前の記録を同じ会話へ注入します([ADR 0032](docs/adr/0032-claude-compact-continuation.md))。
|
|
57
|
+
|
|
58
|
+
- `PreCompact`フックが、自動圧縮の時だけ印を残します。圧縮は止めません。
|
|
59
|
+
- 圧縮直後の`SessionStart`(`source: "compact"`)フックが、作業途中のユーザー依頼と圧縮直前の発言、直近の会話の原文を注入します。上限は9,500字で、入り切らない古いターンは`throughline recall`の案内になります。
|
|
60
|
+
- モデルは追加の入力なしで作業を続けます。圧縮の要約と記録が食い違う時は、記録を正とするよう指示します。
|
|
61
|
+
|
|
62
|
+
手動`/compact`は対象に含めません。圧縮をまたいだターンは、元の依頼と最終回答の1ターンとして保存します(0.12.9以前は、圧縮の要約をユーザー発言として保存していました)。
|
|
63
|
+
|
|
64
|
+
LinuxのClaude Code 2.1.289(`claude -p`と対話画面)で実機確認済みです。macOS・Windows、VS Code拡張・Desktop、subagentの中の圧縮は未検証です。[実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-04-claude-compact-continuation.md)を参照してください。
|
|
65
|
+
|
|
46
66
|
## Ownership boundary
|
|
47
67
|
|
|
48
68
|
This repository owns installation, configuration, state, schema and migrations,
|
package/bin/throughline.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* throughline uninstall # hook を削除
|
|
9
9
|
* throughline process-turn # Stop hook (Claude Code から呼ばれる)
|
|
10
10
|
* throughline session-start # SessionStart hook (Claude Code から呼ばれる)
|
|
11
|
+
* throughline pre-compact # PreCompact hook (Claude Code から呼ばれる。自動継続の印)
|
|
11
12
|
* throughline detail <時刻> # L2+L3 詳細取得 (Claude が Bash 経由で呼ぶ想定)
|
|
12
13
|
* throughline recall --l2|--l1 # 注入案内から辿る pull 用 read-only 記憶取得
|
|
13
14
|
* throughline handoff-preview # Codex-facing throughline_handoff JSON preview
|
|
@@ -52,7 +53,7 @@ const [, , cmd, ...rest] = process.argv;
|
|
|
52
53
|
|
|
53
54
|
// hook入口では、送信を明示して有効にした端末だけ、未受領のruntime errorを裏で送るprocessを起こす。
|
|
54
55
|
// 既定(送信設定なし)では小さなファイル1つを見るだけで、通信もprocess起動もしない。
|
|
55
|
-
if (['process-turn', 'session-start', 'prompt-submit', 'codex-hook'].includes(cmd)) {
|
|
56
|
+
if (['process-turn', 'session-start', 'prompt-submit', 'pre-compact', 'codex-hook'].includes(cmd)) {
|
|
56
57
|
try {
|
|
57
58
|
(await import('../src/runtime-error-report-trigger.mjs')).triggerRuntimeErrorReportBestEffort();
|
|
58
59
|
} catch {
|
|
@@ -80,6 +81,9 @@ switch (cmd) {
|
|
|
80
81
|
case 'prompt-submit':
|
|
81
82
|
await (await import('../src/prompt-submit.mjs')).run();
|
|
82
83
|
break;
|
|
84
|
+
case 'pre-compact':
|
|
85
|
+
await (await import('../src/claude-pre-compact.mjs')).run();
|
|
86
|
+
break;
|
|
83
87
|
case 'monitor':
|
|
84
88
|
(await import('../src/token-monitor.mjs')).main();
|
|
85
89
|
break;
|
|
@@ -233,6 +237,7 @@ switch (cmd) {
|
|
|
233
237
|
'session-start': 'HOOK_SESSION_START_FAILED',
|
|
234
238
|
'prompt-submit': 'HOOK_PROMPT_SUBMIT_FAILED',
|
|
235
239
|
'process-turn': 'HOOK_PROCESS_TURN_FAILED',
|
|
240
|
+
'pre-compact': 'HOOK_PRE_COMPACT_FAILED',
|
|
236
241
|
}[cmd];
|
|
237
242
|
if (code) {
|
|
238
243
|
const { recordRuntimeErrorBestEffort } = await import('../src/runtime-error-store.mjs');
|
|
@@ -401,6 +406,9 @@ Usage:
|
|
|
401
406
|
同じ引き継ぎを再開(結果不明は再送しない)
|
|
402
407
|
throughline auto-handoff detail --operation <id> --origin <session> --turn <n>
|
|
403
408
|
凍結したL2/L3を取得
|
|
409
|
+
throughline auto-handoff enable|disable|status --host claude [--project <path>] [--json]
|
|
410
|
+
Claude Codeの自動継続(自動圧縮の直後に、同じ会話へ
|
|
411
|
+
圧縮前の記録を注入)を有効化・無効化・確認
|
|
404
412
|
throughline doctor Check environment
|
|
405
413
|
throughline doctor --trim Show trim host boundary diagnostics
|
|
406
414
|
throughline doctor --codex Show Codex primary diagnostics
|
|
@@ -411,6 +419,7 @@ Hook subcommands (called by Claude Code / Codex):
|
|
|
411
419
|
throughline session-start SessionStart hook
|
|
412
420
|
throughline process-turn Stop hook
|
|
413
421
|
throughline prompt-submit UserPromptSubmit hook (/tl & /clear baton writer)
|
|
422
|
+
throughline pre-compact PreCompact hook (Claude自動圧縮の印。圧縮後に記憶を注入)
|
|
414
423
|
throughline codex-hook user-prompt-submit Codex current-session refresh prompt hook
|
|
415
424
|
throughline codex-hook post-tool-use Codex current-session refresh tool-loop hook
|
|
416
425
|
throughline codex-hook stop Codex Stop hook
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| `/tl` | `UserPromptSubmit` が現在の session id を baton として保存する。次の実セッションがその前任を確定的に引き継ぐ |
|
|
12
12
|
| VS Code の `/clear` | 組み込みコマンドは `UserPromptSubmit` に届かない。`SessionStart source='clear'` を使う auto path が、同じ project の直近前任を凍結して引き継ぐ |
|
|
13
13
|
| Claude Desktop の `/clear` | `UserPromptSubmit` に届かず、`SessionStart source='clear'` も来ないため自動継承しない。続ける場合は `/clear` の前に `/tl` を実行する |
|
|
14
|
+
| Claude Code の自動圧縮 | 会話は切り替わらない。`auto-handoff enable --host claude` で有効にした時だけ、`PreCompact`(`trigger: auto`)が印を残し、圧縮直後の `SessionStart source='compact'` が作業途中のターンと直近の会話を同じ会話へ注入する。既定は無効。手動 `/compact` は対象外。正本は [ADR 0032](adr/0032-claude-compact-continuation.md) |
|
|
14
15
|
|
|
15
16
|
`/clear` が `UserPromptSubmit` に届かないことは、VS Code と Desktop の対照実測で
|
|
16
17
|
確定している。`prompt-submit` に残る `/clear` 分岐は host が将来その文字列を渡した場合の
|
|
@@ -43,7 +44,7 @@ baton path と auto path は同じ `buildBudgetedResumeContext` を使う。注
|
|
|
43
44
|
|
|
44
45
|
## host 別境界
|
|
45
46
|
|
|
46
|
-
- Claude Code: VS Code `/clear` は auto path、Desktop `/clear` は `/tl`
|
|
47
|
+
- Claude Code: VS Code `/clear` は auto path、Desktop `/clear` は `/tl` 併用。自動圧縮は、有効にした時だけ同じ会話へ注入する(ADR 0032)。
|
|
47
48
|
- Grok: `/tl` の baton 成功後に `throughline grok-continue --session <id>` で後継席を
|
|
48
49
|
起動する。`/clear` では起動しない。正本は [ADR 0021](adr/0021-grok-host-capture.md)。
|
|
49
50
|
- Cursor: `/tl` は baton を残すが後継会話を自動起動しない。次の会話が baton を消費する。
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
最初の成立検証はCodex Desktopで行う。OSごとの仕組みと受入条件は共通にし、
|
|
29
29
|
差分は既存の `src/os/` と配送ライブラリの環境適合へ閉じ込める。
|
|
30
30
|
VS Code・CLIとClaude/Grok/Cursorの対応はhostごとに同じ条件で検証する。
|
|
31
|
+
Claude Codeは圧縮前の停止と新しい会話への配送がhookからできないため、自動圧縮の直後に同じ会話へ記憶を注入する
|
|
32
|
+
([ADR 0032](adr/0032-claude-compact-continuation.md))。この節の処理順と進行状態はCodexだけに適用する。
|
|
31
33
|
一つのhostの成功で他のhostも対応済みとはしない。
|
|
32
34
|
|
|
33
35
|
### 根拠と検証範囲
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# ADR 0032: Claude Code の自動継続は、自動圧縮の直後に同じ会話へ記憶を注入する
|
|
2
|
+
|
|
3
|
+
日付: 2026-10-04
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Codex Desktop の自動継続(0.11.0)は、自動圧縮の前に旧タスクを止め、記憶を注入した新しいタスクで
|
|
8
|
+
作業を続ける。オーナーの指示(2026-10-04)で、同じことを Claude Code でも行う。
|
|
9
|
+
|
|
10
|
+
Claude Code 2.1.289 で、Codex と同じ方式が取れるかを公式の hook 仕様と実機で確かめた。
|
|
11
|
+
|
|
12
|
+
| 必要な操作 | Codex Desktop | Claude Code 2.1.289 |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| 圧縮の前に作業を止める | `PreCompact` が `continue: false` を返す | `PreCompact` の `continue` は捨てられる。できるのは圧縮を止めること(exit code 2、`decision: "block"`)だけで、止めると会話は圧縮されないまま続き、文脈上限に達した後は元の error で失敗する |
|
|
15
|
+
| 新しい会話を作って記憶を入れる | app-server の `thread/start` と `thread/inject_items` | 公開された入口が無い |
|
|
16
|
+
| 新しい会話へ指示を送って始めさせる | 公式 queue への配送 | `claude-cli://` は入力欄へ下書きを入れるだけで送信しない。`claude --bg` は別の background session を作り、編集の前に git worktree へ移る。元の画面の会話は入れ替わらない |
|
|
17
|
+
|
|
18
|
+
一方、Claude Code は自動圧縮の後、同じ会話で作業をそのまま続ける。圧縮の直後に
|
|
19
|
+
`SessionStart`(`source: "compact"`)の hook を走らせ、その出力を圧縮後の文脈へ足すと公式文書に書かれている。
|
|
20
|
+
|
|
21
|
+
実機の結果(Linux、`claude -p`、Haiku 4.5、`CLAUDE_CODE_AUTO_COMPACT_WINDOW=100000`):
|
|
22
|
+
|
|
23
|
+
- `PreCompact` は `trigger: "auto"` で届く。payload は `session_id`・`transcript_path`・`cwd`・`trigger`・`custom_instructions`。
|
|
24
|
+
- 圧縮の後、同じ `session_id` の `SessionStart(source=compact)` が届く。stdout は `<system-reminder>` として
|
|
25
|
+
モデルへ届き、モデルは追加の入力なしで作業を続けた。9,099 字の出力は file 化されずにそのまま届いた。
|
|
26
|
+
- transcript は圧縮より前の行を残したまま、`system`(`subtype: "compact_boundary"`、`compactMetadata.trigger`)と、
|
|
27
|
+
要約を本文に持つ `user` 行(`isCompactSummary: true`)を足す。圧縮境界の行は `SessionStart` hook が
|
|
28
|
+
走った時刻より後の時刻を持つ。
|
|
29
|
+
- 1回目の圧縮の前に `PreCompact` が5秒の間隔で2回届いたことがある。
|
|
30
|
+
- 依頼を送った直後(最初の応答の前)に自動圧縮が起きることがある。その回では、`SessionStart(source=compact)` の
|
|
31
|
+
時点で、送った依頼の行は transcript に書かれていた。行の `promptId` は hook payload の `prompt_id` と同じ値。
|
|
32
|
+
|
|
33
|
+
あわせて、既存の不具合が見つかった。`readTranscript` は要約の `user` 行を人の発言として数えていた。
|
|
34
|
+
圧縮をまたいだ1ターンは「元の依頼 + 途中の発言」と「要約 + 圧縮後の発言」に割れ、要約が user 発言として
|
|
35
|
+
bodies に入り、圧縮より前の tool 入出力は L3 に入らなかった。3回圧縮した実機のターンは4ターンとして保存された。
|
|
36
|
+
|
|
37
|
+
## Decision
|
|
38
|
+
|
|
39
|
+
Claude では会話を切り替えず、host が続ける同じ会話へ記憶を渡す。
|
|
40
|
+
|
|
41
|
+
1. **印**: `PreCompact` hook(`throughline pre-compact`、matcher 無し)が、`trigger: "auto"` で自動継続が有効な
|
|
42
|
+
project の時だけ、`~/.throughline/claude-auto-handoff/<session_id>.json` に印を残す。手動の `/compact` と
|
|
43
|
+
無効の時は、同じ会話の古い印を消す。subagent の中の圧縮(payload に `agent_id`)は印に触らない。
|
|
44
|
+
hook は圧縮を止めない。stdout へ何も書かず、失敗しても exit code 2 を返さない。
|
|
45
|
+
2. **注入**: `SessionStart(source=compact)` が印を取り出して消し、次を stdout へ書く(上限 9,500 字、ADR 0014)。
|
|
46
|
+
- ヘッダ: 自動圧縮が起きたこと、下記が圧縮前の原文であること、作業の途中なので入力を待たずに続けること、
|
|
47
|
+
完了済みの操作を繰り返さないこと。宣言文は求めない。
|
|
48
|
+
- 現在地: 作業途中のターンの user 依頼(4,000 字まで)と、圧縮直前の assistant 発言(1,500 字まで)。
|
|
49
|
+
transcript から読む。上限を超える時は先頭 3/4 と末尾 1/4 を残し、間を抜いた字数を書く。
|
|
50
|
+
- 案内と L2: `/clear` の引き継ぎ(ADR 0016)と同じ。完了済みのターンを新しい順に、丸ごと入る分だけ。
|
|
51
|
+
3. **作業途中のターンは保存しない**: 注入の前に完了済みのターンを bodies へ回収するが、最後の user 発言の群は除く。
|
|
52
|
+
途中の発言を代表として保存すると、群レベルの重複除去で最終回答が入らなくなる。保存は今までどおり Stop で行う。
|
|
53
|
+
transcript は hook より遅れて書かれることがある。hook payload の `prompt_id` と、最後の user 行の `promptId` が
|
|
54
|
+
両方あって食い違う時は、今の依頼がまだ書かれていない。その時は最後の群を現在地に載せず、回収もせず、
|
|
55
|
+
現在地には「記録からまだ読めない」とだけ書く。
|
|
56
|
+
4. **trigger は `PreCompact` の payload だけから決める**: `SessionStart` の時点の transcript から推測しない
|
|
57
|
+
(圧縮境界の行は hook より後に書かれる)。手動の `/compact` は対象にしない(Codex と同じ)。
|
|
58
|
+
5. **要約行**: `isCompactSummary: true` の行は、ターンの始まりにも本文にもしない。`turn_number`(transcript 内の
|
|
59
|
+
本文行の通し番号)は詰めず、要約行の分も進める。詰めると、要約行を数えていた頃に保存した行と番号が食い違う。
|
|
60
|
+
L3 の範囲も要約行で切らず、元の依頼から取る。
|
|
61
|
+
6. **有効化**: 既定は無効。`throughline auto-handoff enable --host claude [--project <path>]` が
|
|
62
|
+
`~/.throughline/claude-auto-handoff.json` を書く。`--host` を省いた時は今までどおり Codex。
|
|
63
|
+
Codex の設定とは別のファイルで、互いに影響しない。有効判定は会話を起動した project(`CLAUDE_PROJECT_DIR`)で行う。
|
|
64
|
+
`resume`・`detail`・`worker` と `--operation` は Claude では使えない(引き継ぎ操作の記録を持たない)。
|
|
65
|
+
7. **hook の登録**: `throughline install` が Claude の `PreCompact` hook を登録する。`enable` は登録が無ければ足す。
|
|
66
|
+
無効の時、hook は判定を記録して抜ける。
|
|
67
|
+
8. **失敗**: `PreCompact` の失敗は `HOOK_PRE_COMPACT_FAILED`(severity `warn`。圧縮と作業は続く)、
|
|
68
|
+
注入の失敗は既存の `HOOK_SESSION_START_FAILED`。どちらも理由を `hook-failures.log` に残す。
|
|
69
|
+
判定は `inheritance-decision.log` に `phase: "pre-compact"` と `phase: "compact-continuation"` で残す。
|
|
70
|
+
|
|
71
|
+
## Consequences
|
|
72
|
+
|
|
73
|
+
- Claude Code の圧縮は走る。Codex のように圧縮の本文生成を省けない。圧縮後の文脈は、host の要約と
|
|
74
|
+
Throughline の記録の両方を持つ。食い違う時は記録を正とするよう、ヘッダで指示する。
|
|
75
|
+
- schema は変えない(v12 のまま)。新しい会話も、配送も、常駐 process も作らない。`aiterm-steer-delivery` は使わない。
|
|
76
|
+
- Claude の `/clear` と `/tl` の引き継ぎ(ADR 0014・0016)は変えない。`SessionStart(source=compact)` の
|
|
77
|
+
pending intent の登録も今までどおり行う。
|
|
78
|
+
- 作業途中のターンの tool 入出力は、Stop まで `throughline detail` で取れない。
|
|
79
|
+
- 圧縮をまたいだターンは、元の依頼と最終回答の1ターンとして保存される。Observer feed にも1ターンで出て、
|
|
80
|
+
`turn_start` は元の依頼のものになる。今までは要約行のターンとして `unknown` で出ていた。
|
|
81
|
+
- 修正より前に保存した行はそのまま残る。同じ断片を持つ群は入れ直さない。
|
|
82
|
+
- `throughline install`(`self-update` を含む)は `~/.claude/settings.json` に `PreCompact` を1件足す。
|
|
83
|
+
|
|
84
|
+
## 確認した範囲
|
|
85
|
+
|
|
86
|
+
- Linux の Claude Code 2.1.289(`claude -p` と対話画面)、Haiku 4.5。自動圧縮2回をまたいで作業が完了し、
|
|
87
|
+
圧縮のたびに注入が届き、圧縮されたターンが1ターンとして保存された。
|
|
88
|
+
- macOS・Windows、VS Code 拡張・Desktop、subagent の中の圧縮は確かめていない。
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Claude Code: 圧縮まわりの hook(一次ソース抜粋)
|
|
2
|
+
|
|
3
|
+
取得: 2026-10-04、`https://code.claude.com/docs/en/hooks.md`・`context-window.md`・`deep-links.md`・`agent-view.md`・`env-vars.md`。
|
|
4
|
+
実機は Claude Code 2.1.289。設計は [ADR 0032](../../../docs/adr/0032-claude-compact-continuation.md)。
|
|
5
|
+
|
|
6
|
+
## PreCompact
|
|
7
|
+
|
|
8
|
+
> Exit with code 2 to block compaction. For a manual `/compact`, the stderr message is shown to the user. You can also block by returning JSON with `"decision": "block"`.
|
|
9
|
+
>
|
|
10
|
+
> Blocking automatic compaction has different effects depending on when it fires. If compaction was triggered proactively before the context limit, Claude Code skips it and the conversation continues uncompacted. If compaction was triggered to recover from a context-limit error already returned by the API, the underlying error surfaces and the current request fails.
|
|
11
|
+
>
|
|
12
|
+
> Claude Code discards a PreCompact hook's `systemMessage` and `continue` fields.
|
|
13
|
+
|
|
14
|
+
- matcher は `manual` / `auto`。入力は共通項目に加えて `trigger` と `custom_instructions`(auto では `null`)。
|
|
15
|
+
- → 圧縮の前に作業を止める口は無い。止められるのは圧縮だけ。
|
|
16
|
+
|
|
17
|
+
## SessionStart(source=compact)
|
|
18
|
+
|
|
19
|
+
> | `compact` | Auto or manual compaction |
|
|
20
|
+
|
|
21
|
+
> | [SessionStart hooks](/docs/en/hooks-guide#re-inject-context-after-compaction) that match the `compact` source | Claude Code runs them and adds their output to the compacted context |
|
|
22
|
+
|
|
23
|
+
- 入力の `source` は `compact`。auto と manual の区別は入力に無い。
|
|
24
|
+
- → 区別は `PreCompact` の `trigger` で取る。
|
|
25
|
+
|
|
26
|
+
## PostCompact
|
|
27
|
+
|
|
28
|
+
> PostCompact hooks have no decision control. They can't affect the compaction result but can perform follow-up tasks.
|
|
29
|
+
|
|
30
|
+
- 入力に `compact_summary`(生成された要約)。Throughline は使わない。
|
|
31
|
+
|
|
32
|
+
## subagent
|
|
33
|
+
|
|
34
|
+
> | `agent_id` | Unique identifier for the subagent. Present only when the hook fires inside a subagent call. Use this to distinguish subagent hook calls from main-thread calls. |
|
|
35
|
+
|
|
36
|
+
## 新しい会話を始める口
|
|
37
|
+
|
|
38
|
+
> A deep link never executes anything on its own. The link only chooses a directory and fills the prompt box.
|
|
39
|
+
|
|
40
|
+
> When you dispatch a background session from agent view or start one with `claude --bg`, the session starts in your working directory. Before editing files, Claude moves the session into an isolated git worktree under `.claude/worktrees/`
|
|
41
|
+
|
|
42
|
+
- `claude-cli://open?q=…` は入力欄へ下書きを入れるだけ。`claude --bg "<prompt>"` は別の background session。
|
|
43
|
+
- → 対話中の会話を、記憶を入れた新しい会話へ入れ替えて続けさせる公開の口は無い。
|
|
44
|
+
|
|
45
|
+
## 自動圧縮の窓
|
|
46
|
+
|
|
47
|
+
> | `CLAUDE_CODE_AUTO_COMPACT_WINDOW` | Set the auto-compact window in tokens, from `100000` to `1000000`. … |
|
|
48
|
+
|
|
49
|
+
- 実機試験だけで使う。製品は依存しない。
|
|
50
|
+
|
|
51
|
+
## 実機で見た transcript(2.1.289)
|
|
52
|
+
|
|
53
|
+
- 圧縮の後に `{"type":"system","subtype":"compact_boundary","compactMetadata":{"trigger":"auto","preTokens":…,"postTokens":…}}`。
|
|
54
|
+
- 続けて `{"type":"user","isCompactSummary":true,"isVisibleInTranscriptOnly":true,"message":{"role":"user","content":"This session is being continued from a previous conversation that ran out of context. …"}}`。
|
|
55
|
+
- `SessionStart:compact` の hook 出力は `attachment`(`type: "hook_success"`)として残り、モデルには `<system-reminder>` で届く。
|
|
56
|
+
- 圧縮より前の行は消えない。
|
package/rag/INDEX.md
CHANGED
|
@@ -27,6 +27,10 @@ rag/
|
|
|
27
27
|
- [fork rolloutのmetadata実測](01-hooks/codex-fork-rollout-metadata.md) — 子の先頭metadataの後に親のmetadataと履歴がコピーされる並びを実機で確認。自動継続の誤ったthread不一致判定を最小fixtureと実rolloutで再現・修正した。
|
|
28
28
|
- [PreCompact・記憶注入・配送の一次ソース抜粋](01-hooks/raw/codex-auto-handoff-extract.md) — 公式仕様と配送ライブラリの固定source。[Desktop実測](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-03-codex-auto-continuation.md)でauto停止・新規idle taskの自動開始・配送process終了後の継続が成立した。cold readの途中状態と表示後の権限更新を観測し、[設計](../docs/05_codex_first_roadmap.md#自動新規タスク継続の設計案)へ反映した。
|
|
29
29
|
|
|
30
|
+
## Claude自動継続の根拠
|
|
31
|
+
|
|
32
|
+
- [圧縮まわりのhookの一次ソース抜粋](01-hooks/raw/claude-compact-hooks-extract.md) — `PreCompact`は圧縮を止められるが作業は止められない。`SessionStart(source=compact)`の出力は圧縮後の文脈へ足される。新しい会話へ指示を送る公開の口は無い。[実測](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-04-claude-compact-continuation.md)で、自動圧縮の直後に注入が届き、入力なしで作業が続いた。Finding 6・第三仮説(PreCompactでの再投影)に対する現行の答えは[ADR 0032](../docs/adr/0032-claude-compact-continuation.md)。
|
|
33
|
+
|
|
30
34
|
## Question this RAG was built to answer
|
|
31
35
|
|
|
32
36
|
> Throughline は「コンテキスト削減しつつ過去の記憶を一切失わない」と定義されている。記憶を引き継いでいてもモデルがそれを自分の作業履歴として体感していないなら、その記憶は無意味なコンテキストである。
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, dirname, isAbsolute } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { normalizeProjectPathForCompare, sameProjectPath } from './project-path.mjs';
|
|
6
|
+
|
|
7
|
+
// Claude の自動継続の設定。Codex の設定(codex-auto-handoff.json)とは別のファイルで、互いに影響しない。
|
|
8
|
+
export const CLAUDE_AUTO_HANDOFF_CONFIG_SCHEMA = 'throughline.claude-auto-handoff.v1';
|
|
9
|
+
export const claudeAutoHandoffConfigPath = () => join(homedir(), '.throughline', 'claude-auto-handoff.json');
|
|
10
|
+
|
|
11
|
+
export function readClaudeAutoHandoffConfig(path = claudeAutoHandoffConfigPath()) {
|
|
12
|
+
let text;
|
|
13
|
+
try { text = readFileSync(path, 'utf8'); }
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') return { schema: CLAUDE_AUTO_HANDOFF_CONFIG_SCHEMA, enabled: false, projects: [] };
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const config = JSON.parse(text);
|
|
19
|
+
if (config.schema !== CLAUDE_AUTO_HANDOFF_CONFIG_SCHEMA || typeof config.enabled !== 'boolean' ||
|
|
20
|
+
!Array.isArray(config.projects) ||
|
|
21
|
+
config.projects.some(p => typeof p !== 'string' || !isAbsolute(p))) throw new Error('auto_handoff_config_invalid');
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeClaudeAutoHandoffConfig({ enabled, projects = [] }, path = claudeAutoHandoffConfigPath()) {
|
|
26
|
+
if (typeof enabled !== 'boolean') throw new TypeError('auto_handoff_config_invalid');
|
|
27
|
+
const config = { schema: CLAUDE_AUTO_HANDOFF_CONFIG_SCHEMA, enabled,
|
|
28
|
+
projects: projects.map(normalizeProjectPathForCompare) };
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
30
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
31
|
+
writeFileSync(temporary, `${JSON.stringify(config)}\n`, { mode: 0o600, flag: 'wx' });
|
|
32
|
+
renameSync(temporary, path);
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function claudeAutoHandoffEnabledFor(config, projectPath) {
|
|
37
|
+
return config.enabled && (config.projects.length === 0 || config.projects.some(p => sameProjectPath(p, projectPath)));
|
|
38
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-auto-handoff.mjs — Claude Code の自動継続 (ADR 0032)
|
|
3
|
+
*
|
|
4
|
+
* Codex の自動継続は、自動圧縮の前に旧タスクを止めて新しいタスクへ記憶を渡す。Claude Code は
|
|
5
|
+
* hook から作業を止められず、新しい会話へ指示を送る公開 API も無い。自動圧縮の後は host が
|
|
6
|
+
* 同じ会話で作業を続け、SessionStart(source=compact) の hook 出力を圧縮後の文脈へ足す。
|
|
7
|
+
* そこで Claude では、会話を切り替えずに次の 2 段で記憶を渡す。
|
|
8
|
+
*
|
|
9
|
+
* 1. PreCompact : trigger=auto の時だけ、その会話に「自動圧縮が始まった」印を残す。
|
|
10
|
+
* 圧縮は止めない(止めると文脈上限の error になる)。
|
|
11
|
+
* 2. SessionStart : source=compact で印があれば、完了済みのターンを回収し、
|
|
12
|
+
* (source=compact) 作業途中のターンと直近の会話の原文を stdout へ書く。
|
|
13
|
+
*
|
|
14
|
+
* 手動の /compact は対象にしない(Codex と同じ)。印は PreCompact の payload だけから作り、
|
|
15
|
+
* SessionStart の時点の transcript から trigger を推測しない(圧縮境界の行は hook より後に書かれる)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
import { readClaudeAutoHandoffConfig, claudeAutoHandoffEnabledFor } from './claude-auto-handoff-config.mjs';
|
|
23
|
+
import { CLAUDE_HOST, hostOfSessionId } from './hosts/identity.mjs';
|
|
24
|
+
import { claudeHostAdapter } from './hosts/claude.mjs';
|
|
25
|
+
import { readLatestUserGroup } from './transcript-reader.mjs';
|
|
26
|
+
import { backfillBodies, logBackfill } from './turn-backfill.mjs';
|
|
27
|
+
import { resolveMergeTarget } from './session-merger.mjs';
|
|
28
|
+
import { buildCompactContinuationContext } from './resume-context.mjs';
|
|
29
|
+
|
|
30
|
+
export const CLAUDE_COMPACT_REQUEST_SCHEMA = 'throughline.claude-auto-handoff.request.v1';
|
|
31
|
+
// 印は圧縮の完了で消える。圧縮が失敗して残った印は、次の PreCompact の時にこの期限で掃除する。
|
|
32
|
+
const STALE_REQUEST_MS = 24 * 60 * 60 * 1000;
|
|
33
|
+
// session id を印のファイル名に使う。Claude の id は UUID で、path の区切りを含む値は受け付けない。
|
|
34
|
+
const CLAUDE_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
35
|
+
|
|
36
|
+
export const claudeCompactRequestDir = () => join(homedir(), '.throughline', 'claude-auto-handoff');
|
|
37
|
+
|
|
38
|
+
function requestPath(sessionId, dir) {
|
|
39
|
+
if (hostOfSessionId(sessionId) !== CLAUDE_HOST || !CLAUDE_SESSION_ID_PATTERN.test(sessionId)) {
|
|
40
|
+
throw new Error('auto_handoff_session_id_invalid');
|
|
41
|
+
}
|
|
42
|
+
return join(dir, `${sessionId}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function removeStaleRequests(dir, now) {
|
|
46
|
+
let names;
|
|
47
|
+
try { names = readdirSync(dir); }
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') return;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
if (!name.endsWith('.json')) continue;
|
|
54
|
+
const path = join(dir, name);
|
|
55
|
+
let modifiedAt;
|
|
56
|
+
try { modifiedAt = statSync(path).mtimeMs; }
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === 'ENOENT') continue; // 別の hook が同時に消した
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
if (now - modifiedAt > STALE_REQUEST_MS) rmSync(path, { force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* PreCompact hook の本体。自動圧縮で、その project が有効な時だけ印を残す。
|
|
67
|
+
* 手動と無効の時は、同じ会話の古い印を消して何もしない。subagent の中の圧縮は、
|
|
68
|
+
* 親の会話の印に触らない(payload の session_id は親の会話を指す)。
|
|
69
|
+
*
|
|
70
|
+
* @param {{
|
|
71
|
+
* payload: {session_id?: string, trigger?: string, cwd?: string, transcript_path?: string},
|
|
72
|
+
* env?: NodeJS.ProcessEnv,
|
|
73
|
+
* config?: {enabled: boolean, projects: string[]},
|
|
74
|
+
* now?: number,
|
|
75
|
+
* dir?: string,
|
|
76
|
+
* }} params
|
|
77
|
+
* @returns {{status: 'requested'|'skipped', reason?: string, sessionId: string, projectPath?: string}}
|
|
78
|
+
*/
|
|
79
|
+
export function recordClaudeCompactRequest({
|
|
80
|
+
payload,
|
|
81
|
+
env = process.env,
|
|
82
|
+
config = readClaudeAutoHandoffConfig(),
|
|
83
|
+
now = Date.now(),
|
|
84
|
+
dir = claudeCompactRequestDir(),
|
|
85
|
+
}) {
|
|
86
|
+
const sessionId = payload?.session_id;
|
|
87
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new Error('Missing session_id in PreCompact payload');
|
|
88
|
+
if (payload.agent_id) return { status: 'skipped', reason: 'subagent_compact', sessionId };
|
|
89
|
+
const path = requestPath(sessionId, dir);
|
|
90
|
+
// 有効判定は会話を起動した project で行う。hook の cwd は Bash の cd に追従する。
|
|
91
|
+
const projectPath = claudeHostAdapter.completionProjectPath({ cwd: payload.cwd ?? process.cwd(), env });
|
|
92
|
+
|
|
93
|
+
const reason = payload.trigger !== 'auto'
|
|
94
|
+
? 'manual_compact'
|
|
95
|
+
: claudeAutoHandoffEnabledFor(config, projectPath) ? null : 'auto_handoff_disabled';
|
|
96
|
+
if (reason) {
|
|
97
|
+
rmSync(path, { force: true });
|
|
98
|
+
return { status: 'skipped', reason, sessionId, projectPath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
102
|
+
removeStaleRequests(dir, now);
|
|
103
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
104
|
+
writeFileSync(temporary, `${JSON.stringify({
|
|
105
|
+
schema: CLAUDE_COMPACT_REQUEST_SCHEMA,
|
|
106
|
+
session_id: sessionId,
|
|
107
|
+
trigger: 'auto',
|
|
108
|
+
project_path: projectPath,
|
|
109
|
+
transcript_path: payload.transcript_path ?? null,
|
|
110
|
+
requested_at: now,
|
|
111
|
+
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
112
|
+
renameSync(temporary, path);
|
|
113
|
+
return { status: 'requested', sessionId, projectPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* SessionStart(source=compact) から呼ぶ。その会話の印を取り出して消す。無ければ null。
|
|
118
|
+
* @returns {{session_id: string, trigger: 'auto', project_path: string, transcript_path: string|null, requested_at: number}|null}
|
|
119
|
+
*/
|
|
120
|
+
export function consumeClaudeCompactRequest({ sessionId, dir = claudeCompactRequestDir() }) {
|
|
121
|
+
if (hostOfSessionId(sessionId) !== CLAUDE_HOST || !CLAUDE_SESSION_ID_PATTERN.test(sessionId)) return null;
|
|
122
|
+
const path = requestPath(sessionId, dir);
|
|
123
|
+
let text;
|
|
124
|
+
try { text = readFileSync(path, 'utf8'); }
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code === 'ENOENT') return null;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
rmSync(path, { force: true });
|
|
130
|
+
const request = JSON.parse(text);
|
|
131
|
+
if (request.schema !== CLAUDE_COMPACT_REQUEST_SCHEMA || request.session_id !== sessionId ||
|
|
132
|
+
request.trigger !== 'auto') throw new Error('auto_handoff_request_invalid');
|
|
133
|
+
return request;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function listClaudeCompactRequests({ dir = claudeCompactRequestDir() } = {}) {
|
|
137
|
+
let names;
|
|
138
|
+
try { names = readdirSync(dir); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error.code === 'ENOENT') return [];
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return names.filter(name => name.endsWith('.json')).map(name => {
|
|
144
|
+
const { session_id, project_path, requested_at } = JSON.parse(readFileSync(join(dir, name), 'utf8'));
|
|
145
|
+
return { session_id, project_path, requested_at };
|
|
146
|
+
}).sort((a, b) => a.requested_at - b.requested_at);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 自動圧縮の直後に注入するテキストを作る。
|
|
151
|
+
*
|
|
152
|
+
* 作業途中のターン(最後の user 発言の群)は Stop の前なので、回収の対象から外して transcript から読む。
|
|
153
|
+
* それより前の完了済みターンは、Stop が空振りした分もここで bodies へ回収してから並べる。
|
|
154
|
+
*
|
|
155
|
+
* transcript は hook より遅れて書かれることがある。hook payload の `prompt_id` と、最後の user 行の
|
|
156
|
+
* `promptId` が両方あって食い違う時は、今の依頼がまだ書かれていない。その時は最後の群を
|
|
157
|
+
* 作業途中として見せず、回収もしない(どちらのターンか確定できない群を、完了として保存しない)。
|
|
158
|
+
*
|
|
159
|
+
* @param {import('node:sqlite').DatabaseSync} db
|
|
160
|
+
* @param {{ sessionId: string, transcriptPath: string|null|undefined, promptId?: string|null, now?: number }} params
|
|
161
|
+
* @returns {{
|
|
162
|
+
* text: string,
|
|
163
|
+
* stats: {total_chars: number, injected_l2_turns: number, remaining_l2_turns: number, older_turns: number,
|
|
164
|
+
* in_flight_user: boolean, in_flight_unreadable: boolean, in_flight_fragments: number, backfilled_turns: number},
|
|
165
|
+
* } | null}
|
|
166
|
+
*/
|
|
167
|
+
export function buildClaudeCompactContinuation(db, { sessionId, transcriptPath, promptId = null, now = Date.now() }) {
|
|
168
|
+
const { target, origin } = resolveMergeTarget(db, sessionId);
|
|
169
|
+
const latest = readLatestUserGroup(transcriptPath);
|
|
170
|
+
const inFlightUnreadable = Boolean(promptId && latest?.user.prompt_id && latest.user.prompt_id !== promptId);
|
|
171
|
+
const inFlight = inFlightUnreadable ? null : latest;
|
|
172
|
+
|
|
173
|
+
const backfill = backfillBodies(db, {
|
|
174
|
+
targetSessionId: target,
|
|
175
|
+
originSessionId: origin,
|
|
176
|
+
transcriptPath,
|
|
177
|
+
now,
|
|
178
|
+
beforeUserTurnNumber: latest ? latest.user.turn_number : null,
|
|
179
|
+
});
|
|
180
|
+
logBackfill({
|
|
181
|
+
ts: new Date(now).toISOString(),
|
|
182
|
+
hook: 'session-start-compact',
|
|
183
|
+
session_id: sessionId,
|
|
184
|
+
target,
|
|
185
|
+
origin,
|
|
186
|
+
transcript_path: transcriptPath ?? null,
|
|
187
|
+
groups: backfill.groups,
|
|
188
|
+
inserted_turns: backfill.insertedTurns,
|
|
189
|
+
skipped_existing: backfill.skippedExisting,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const context = buildCompactContinuationContext(db, { sessionId: target, inFlight, inFlightUnreadable });
|
|
193
|
+
if (!context) return null;
|
|
194
|
+
return {
|
|
195
|
+
text: context.text,
|
|
196
|
+
stats: {
|
|
197
|
+
total_chars: context.totalChars,
|
|
198
|
+
injected_l2_turns: context.injectedL2Turns,
|
|
199
|
+
remaining_l2_turns: context.remainingL2Turns,
|
|
200
|
+
older_turns: context.olderTurns,
|
|
201
|
+
in_flight_user: Boolean(inFlight),
|
|
202
|
+
in_flight_unreadable: inFlightUnreadable,
|
|
203
|
+
in_flight_fragments: inFlight?.fragments.length ?? 0,
|
|
204
|
+
backfilled_turns: backfill.insertedTurns,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|