throughline 0.8.9 → 0.9.1

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,39 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.9.1] — 2026-08-14
14
+
15
+ ### Fixed
16
+
17
+ - Claude-facing SessionStart, UserPromptSubmit, and Stop entrypoints now ignore
18
+ non-Claude camelCase envelopes immediately after JSON parsing and before any
19
+ database, state, VS Code task, handoff, transcript, or runtime-error side
20
+ effect. The boundary requires non-empty `sessionId` and `hookEventName` and
21
+ the absence of Claude's `session_id`; it does not convert payloads or add a
22
+ Grok transcript reader.
23
+
24
+ ### Changed
25
+
26
+ - CI now uses the shared factory workflow for the maintained native and WSL2
27
+ environments.
28
+
29
+ ### Documentation
30
+
31
+ - Synchronized the current README, Codex skill, contributor entrypoint, docs
32
+ overview, and implementation plan around the v0.9.0 read-only handoff-context
33
+ contract. Historical ADRs, archived plans, and RAG source records remain
34
+ unchanged as point-in-time evidence.
35
+
36
+ ## [0.9.0] — 2026-08-04
37
+
38
+ ### Added
39
+
40
+ - `throughline handoff-context --session <id> --json` returns the exact
41
+ budgeted SessionStart inheritance context through a versioned local CLI
42
+ boundary. It opens only an existing database read-only and never creates or
43
+ migrates it, consumes a baton, merges sessions, changes `sessions.merged_into`,
44
+ or reassigns L1/L2/L3 memory rows.
45
+
13
46
  ## [0.8.9] — 2026-08-02
14
47
 
15
48
  ### Fixed
package/README.ja.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <p align="center">
2
- <img src=".github/og.png" alt="Throughline — Claude Code のコンテキスト消費を約 90% 削減しつつ記憶はほぼ残す" width="100%">
2
+ <img src=".github/og.png" alt="Throughline — 環境や境界が変わっても方向と記憶を保って進むクジラの群れ" width="100%">
3
+ <br>
4
+ <sub><em>この画像は、環境や境界が変わっても、関係・方向・記憶を失わずに進み続ける連続性を表しています。</em></sub>
3
5
  </p>
4
6
 
5
7
  # Throughline
@@ -332,6 +334,7 @@ Throughline state をまだ書いていない現在セッションも表示で
332
334
  | `throughline doctor` | Node バージョン、hook 登録状況、DB、PATH をチェック |
333
335
  | `throughline doctor --trim --host claude` | trim boundary と手動手順を診断 |
334
336
  | `throughline handoff-preview --session <id>` | Codex 向け `throughline_handoff` JSON projection を表示 |
337
+ | `throughline handoff-context --session <id> --json` | SessionStart と同じ引き継ぎ文脈を versioned JSON で取得。記憶行の `session_id` と `sessions.merged_into` は変更せず、同一端末内の別ベンダーランチャーから使える |
335
338
  | `throughline codex-sidecar-diagnostics` | この project の `codex-sidecar` diagnostics status を確認 |
336
339
  | `throughline codex-sidecar-dry-run` | App Server を呼ばずに read-only sidecar request を正規化表示 |
337
340
  | `throughline trim --dry-run --host codex` | Codex same-thread trim の dry-run preview |
@@ -340,6 +343,19 @@ Throughline state をまだ書いていない現在セッションも表示で
340
343
  | `throughline status` | DB 統計表示 (sessions / skeletons / bodies / details) |
341
344
  | `throughline --version` | インストール済みバージョンを表示 |
342
345
 
346
+ ### ローカルlauncher向けread-only handoff context
347
+
348
+ 通常handoffを実行せず、同一端末のlauncherからThroughline記憶だけを使う場合は次を呼ぶ:
349
+
350
+ ```bash
351
+ throughline handoff-context --session codex:<thread-id> --json
352
+ ```
353
+
354
+ 成功時の`throughline.handoff_context.v1`は`schema`、`status`、`sessionId`、`context`だけを返す。
355
+ `context`はSessionStartと同じ予算付き継承文脈で、DB作成・migration・baton消費・session merge・
356
+ latest session推測・`sessions.merged_into`変更・L1/L2/L3 rowの所属変更は行わない。AItermは任意の
357
+ 別vendor portable forkでこの境界を使う。Observer feedはcompleted-turn projectionであり代替ではない。
358
+
343
359
  スラッシュコマンド (Claude Code 内でユーザーが叩く):
344
360
 
345
361
  | コマンド | 役割 |
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <p align="center">
2
- <img src=".github/og.png" alt="Throughline — cut ~90% of Claude Code's context usage while keeping nearly all the memory" width="100%">
2
+ <img src=".github/og.png" alt="Throughline — a whale family carrying direction and memory across changing boundaries" width="100%">
3
+ <br>
4
+ <sub><em>This image represents continuity that preserves relationships, direction, and memory even as environments and boundaries change.</em></sub>
3
5
  </p>
4
6
 
5
7
  # Throughline
@@ -763,15 +765,12 @@ entry to the `tasks` array yourself:
763
765
 
764
766
  ## Commands
765
767
 
766
- **v0.6.3 2026-07-14に公開済みです。** factory diagnostics Codex の managed
767
- `UserPromptSubmit` / `PostToolUse` / `Stop` hooks がすべて canonical `ready` の場合に
768
- Codex readiness `ready` と返す。Claude connector は未検査のまま明示的に
769
- `unverified` とし、Codex-only overall を妨げない。local runtime error
770
- aggregate collection が既定OFFで、canonical dotagents config
771
- `collection.enabled: true` を明示した場合だけ収集します。いずれも network I/O は
772
- 行いません。`throughline@0.6.3`、tag / GitHub Release、公開 CI run
773
- `29284655280`(9/9 green)を確認済みです。npm registry artifact の shasum は
774
- `4f3fcd2598a75f026358dae7f3eb3165242b580b` です。
768
+ **v0.9.0 was published on 2026-08-04.** It adds the versioned, read-only
769
+ `handoff-context` boundary for local launchers. The command opens only an
770
+ existing database and leaves baton state, session ownership, and memory rows
771
+ unchanged. Existing factory diagnostics, Observer, runtime-error, capture, and
772
+ normal handoff behavior remain available under the same explicit-failure and
773
+ local-only contracts.
775
774
 
776
775
  | Command | What it does |
777
776
  | ---------------------------------------------- | ------------------------------------------------------------ |
@@ -797,6 +796,7 @@ aggregate は collection が既定OFFで、canonical dotagents config の
797
796
  | `throughline runtime-errors reopen <fingerprint> --json` | Explicitly reopen a resolved aggregate without fabricating a new occurrence |
798
797
  | `throughline runtime-errors compact --json` | Remove only acknowledged, resolved aggregates after retention; open or unacknowledged records remain |
799
798
  | `throughline handoff-preview --session <id>` | Print a Codex-facing `throughline_handoff` JSON projection |
799
+ | `throughline handoff-context --session <id> --json` | Print the exact SessionStart inheritance context as versioned JSON without moving memory rows or changing `sessions.merged_into`; intended for local launchers that need portable cross-vendor context |
800
800
  | `throughline codex-capture --codex-thread-id <id>` | Capture active Codex rollout turns into a `codex:<thread_id>` DB session |
801
801
  | `throughline codex-summarize --session codex:<id>` | Summarize captured Codex L2 into L1 with the Codex CLI backend |
802
802
  | `throughline codex-resume --session codex:<id>` | Render Codex active-work context from a captured Codex session |
@@ -819,6 +819,23 @@ aggregate は collection が既定OFFで、canonical dotagents config の
819
819
  | `throughline status` | Print DB statistics (sessions, skeletons, bodies, details) |
820
820
  | `throughline --version` | Print the installed version |
821
821
 
822
+ ### Read-only handoff context for local launchers
823
+
824
+ Use this boundary when a local launcher needs Throughline memory without
825
+ performing a normal handoff:
826
+
827
+ ```bash
828
+ throughline handoff-context --session codex:<thread-id> --json
829
+ ```
830
+
831
+ The successful `throughline.handoff_context.v1` object contains only `schema`,
832
+ `status`, `sessionId`, and `context`. The context is the same budgeted
833
+ inheritance text used by SessionStart. The command does not create or migrate a
834
+ database, consume a baton, merge sessions, infer a latest session, change
835
+ `sessions.merged_into`, or reassign L1/L2/L3 rows. AIterm uses this boundary for
836
+ its optional cross-vendor portable fork; the Observer feed is a separate
837
+ completed-turn projection and is not a substitute.
838
+
822
839
  Slash commands (invoked by the user in Claude Code):
823
840
 
824
841
  | Command | What it does |
@@ -11,6 +11,7 @@
11
11
  * throughline detail <時刻> # L2+L3 詳細取得 (Claude が Bash 経由で呼ぶ想定)
12
12
  * throughline recall --l2|--l1 # 注入案内から辿る pull 用 read-only 記憶取得
13
13
  * throughline handoff-preview # Codex-facing throughline_handoff JSON preview
14
+ * throughline handoff-context --session <id> --json # Read-only inheritance context JSON
14
15
  * throughline auditor-context --json # Read-only bounded auditor context JSON
15
16
  * throughline factory-diagnostics --json # Native factory read-only readiness JSON
16
17
  * throughline migrate --json # Migrate the existing Throughline database only
@@ -76,6 +77,11 @@ switch (cmd) {
76
77
  case 'handoff-preview':
77
78
  await (await import('../src/cli/handoff-preview.mjs')).run(rest);
78
79
  break;
80
+ case 'handoff-context': {
81
+ const exitCode = (await import('../src/cli/handoff-context.mjs')).run(rest);
82
+ if (exitCode !== 0) process.exitCode = exitCode;
83
+ break;
84
+ }
79
85
  case 'auditor-context': {
80
86
  const exitCode = (await import('../src/cli/auditor-context.mjs')).run(rest);
81
87
  if (exitCode !== 0) process.exitCode = exitCode;
@@ -202,6 +208,9 @@ Usage:
202
208
  throughline monitor Multi-session token monitor (use --all, --session <id>)
203
209
  throughline detail <time> Retrieve L2+L3 detail for a turn (e.g. 14:23:05 or 14:23-14:30)
204
210
  throughline handoff-preview Print Codex-facing throughline_handoff JSON
211
+ throughline handoff-context --session <id> --json
212
+ Print the exact inheritance context without
213
+ changing database ownership
205
214
  throughline auditor-context --session <id> --project <root>
206
215
  Read only bounded completed user/assistant context
207
216
  for an auditor; requires either --host plus --transcript,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: throughline
3
- description: Use when the user asks to use Throughline from Codex, continue or restore Throughline memory, prepare a new Codex thread handoff, summarize a captured Codex session, or check whether the Throughline Codex Stop hook captured the current session. Hide long Throughline command details behind this workflow.
3
+ description: Use when the user asks to use Throughline from Codex, continue or restore Throughline memory, export read-only handoff context for a local launcher, prepare a new Codex thread handoff, summarize a captured Codex session, or check whether the Throughline Codex Stop hook captured the current session. Hide long Throughline command details behind this workflow.
4
4
  ---
5
5
 
6
6
  # Throughline
@@ -112,6 +112,22 @@ throughline codex-summarize --session codex:<current-thread-id> --json
112
112
  Codex-primary summarization uses the Codex CLI backend. Do not claim it fell
113
113
  back to Claude Haiku.
114
114
 
115
+ ### "export memory" / "portable fork context"
116
+
117
+ Run:
118
+
119
+ ```bash
120
+ throughline handoff-context --session <exact-session-id> --json
121
+ ```
122
+
123
+ This is a local read-only export for another launcher. It requires an exact
124
+ session id and returns `schema`, `status`, `sessionId`, and `context`. It does
125
+ not create or migrate the database, consume a baton, merge sessions, move
126
+ memory rows, or change `sessions.merged_into`. Do not substitute the Observer
127
+ completed-turn feed, infer the latest session, or read the SQLite database
128
+ directly. If the command fails or returns no context, report that failure
129
+ instead of falling back to a different memory source.
130
+
115
131
  ### "trim" / "rewind" / "rollback" / "context cleanup"
116
132
 
117
133
  Default to the same fresh-thread handoff flow as bare `$throughline` when the
@@ -18,8 +18,9 @@
18
18
  | [10_transcript_injection_plan.md](10_transcript_injection_plan.md) | transcript injection 検証計画と v0.5 実機結果 |
19
19
  | [11_codex_monitor_implementation_plan.md](11_codex_monitor_implementation_plan.md) | Codex monitor 対応の実装記録 |
20
20
  | [13_native_factory_diagnostics_plan.md](13_native_factory_diagnostics_plan.md) | native factory read-only readiness 診断の実装記録 |
21
- | [14_observer_completed_turn_feed_plan.md](14_observer_completed_turn_feed_plan.md) | Observer向けcompleted-only read / wait CLIのactive計画。CLI・opaque cursor・pagination・最大3600秒waitは実装済み、公開/full regression gateは継続中 |
21
+ | [14_observer_completed_turn_feed_plan.md](14_observer_completed_turn_feed_plan.md) | Observer向けcompleted-only read / wait CLIの完了済み設計・受入記録。v0.7.0で公開済み |
22
22
  | [15_windows_ci_release_latency_plan.md](15_windows_ci_release_latency_plan.md) | Windows CI 18分の原因、ACL安全網を維持した短縮、release gateの受入条件 |
23
+ | [16_readonly_handoff_context_plan.md](16_readonly_handoff_context_plan.md) | DB所有権を変更せずSessionStartと同じ記憶を返す、ローカルランチャー向けread-only I/F。v0.9.0で公開済み |
23
24
  | [BUGHUB_RUNTIME_ERROR_STORE_PLAN.md](BUGHUB_RUNTIME_ERROR_STORE_PLAN.md) | local runtime error aggregate store の契約と実装 TODO |
24
25
 
25
26
  ## Supporting Records
@@ -43,6 +44,11 @@ ThroughlineはClaude Stop receiptとCodex rolloutの`task_complete`だけからc
43
44
  ObserverがDB、WAL、rolloutを直接監視するfallbackは持たない。waitは最大3600秒で、`changed`、`timeout`、
44
45
  `resync_required`、`ambiguous_parent`を返す。
45
46
 
47
+ portable contextの公開境界は`throughline handoff-context --session <id> --json`である。既存DBを
48
+ read-onlyで開き、SessionStartと同じbudgeted inheritance contextを返すが、baton、merge、
49
+ `sessions.merged_into`、L1/L2/L3 rowの`session_id`は変更しない。Observer境界とは用途もschemaも別で、
50
+ AIterm v0.23.0はこのCLIだけをconsumer境界として使う。
51
+
46
52
  ## Entrypoints
47
53
 
48
54
  - [../CLAUDE.md](../CLAUDE.md): AI 作業者向けの正本。
@@ -144,7 +144,7 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
144
144
  | **npm公開 (v0.8.6): 製品所有DB migration入口** | `throughline migrate --json` は既存DBだけをproduction migrationで現行schemaへ移行し、versioned bounded JSONを返す。DB不在は作らず`not_applicable`、現行は`already_current`、future schemaと失敗は非0。`factory-diagnostics`のread-only契約は維持する。0.8.5はdry-run後に別作業の未コミット文書が混入したためdeprecateし、同一runtimeをclean worktreeから0.8.6として再公開した。設計判断は [ADR 0018](adr/0018-product-owned-database-migration.md) |
145
145
  | **公開済み (v0.8.7): Windows CI release latency** | completed-turn receiptの256件境界fixtureと実ACL検証を分離し、production mutationのPowerShellはdistinct state transitionだけに限定する。temporary ACL失敗時の旧store保持とdirectory/lock/final storeのnative ACL testを維持する。CIは9 matrixのままevent/ref単位concurrency、unit test 5分SLO/8分timeoutとする。設計判断は [ADR 0020](adr/0020-windows-ci-release-latency.md) |
146
146
  | **npm公開 (v0.8.8): Codex hook timeout key修理** | Codex `hooks.json` の3 managed hookは秒単位の正規key `timeout`を30/30/300で書く。無視される旧`timeoutSec` entryは次回global installでcommand identityにより置換し、doctorは旧形式を要再install、factory diagnosticsはnot readyとする。Claude hooksとObserver wait deadline APIは変更しない。npm `latest` として2026-08-02T00:43Zに公開済み。tag / GitHub Releaseは未作成(要作成) |
147
- | **release準備 (v0.8.9): Codex hook診断のPATH非依存化** | 期待hook commandのnode表記は呼び出し元の`PATH`で変わるため、登録済みcommandとの文字列比較では最小PATH(launchd factory reporter等)から正規登録が「legacy command」と誤判定され、`codex_hooks`が恒常的に`not_ready`になっていた。比較を解析済みidentity(node実体realpath+CLI script realpath+event)へ変更する。別install/別event/旧PATH解決型/realpath未解決は従来どおり要再install扱い。hooks.json の書式と Claude-facing hook は変更しない |
147
+ | **npm公開 (v0.8.9): Codex hook診断のPATH非依存化** | 期待hook commandのnode表記は呼び出し元の`PATH`で変わるため、登録済みcommandとの文字列比較では最小PATH(launchd factory reporter等)から正規登録が「legacy command」と誤判定され、`codex_hooks`が恒常的に`not_ready`になっていた。比較を解析済みidentity(node実体realpath+CLI script realpath+event)へ変更した。別install/別event/旧PATH解決型/realpath未解決は従来どおり要再install扱い。hooks.jsonの書式とClaude-facing hookは変更しない。公開commit `5dbd3af`、CI run `30731697079` 9/9 green、npm `latest` 0.8.9、shasum `780f8e72d9bd4b0c4a331db48116a415a4bde34b`、tag / GitHub Release(v0.8.8分も遡って作成)、registry由来global installを2026-08-02に確認した |
148
148
  | **未リリース: Codex current-session 75% trigger** | Codex 自動発火を token-monitor に依存させず、global install が Codex `UserPromptSubmit` / `PostToolUse` hooks も登録する。hook は当該 Codex session の rollout `token_count` を直接読み、verified 75% 以上なら同じ user turn または tool loop 継続前に `$throughline` workflow 実行指示を `additionalContext` で注入する。Stop hook の guarded auto-refresh は残す。`~/.codex/config.toml` は旧 `codex_hooks = true` に加えて現行 `hooks = true` も有効化する |
149
149
  | **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
150
150
 
@@ -0,0 +1,40 @@
1
+ # Read-only handoff context
2
+
3
+ ## 目的
4
+
5
+ 通常 handoff が使う `buildBudgetedResumeContext` の出力を、DB 所有権を変えずにローカルプログラムが
6
+ 取得できる read-only CLI として公開する。最初の consumer は AIterm の portable fork だが、I/F は
7
+ 特定 consumer や vendor に依存させない。
8
+
9
+ 実行 ToDo、依存、状態、完了証拠の正本は Lattice plan `readonly-handoff-context` とする。
10
+
11
+ ## 完了
12
+
13
+ 2026-08-04に`throughline@0.9.0`としてnpm、tag、GitHub Release、global installまで公開した。
14
+ focused契約testと全回帰は729 pass/1 skip/0 fail。AIterm v0.23.0の代表cross-vendor smokeでは
15
+ Codex source memoryをClaudeへ注入し、前後でsource session、`sessions.merged_into`、L1/L2/L3 row所属が
16
+ 完全一致することを確認した。公開後の現行ドキュメント全域監査は、Latticeの終端ToDoを再openして
17
+ README、作業者入口、配布Codex skill、docs索引、計画、CHANGELOGへ同期した。変更Markdownの
18
+ 相対リンク監査とCLI help/handoff-contextのfocused test 5/5を通過した。
19
+
20
+ ## 契約
21
+
22
+ - `throughline handoff-context --session <id> --json` は session を明示必須とする。
23
+ - 既存 DB を `DatabaseSync(..., { readOnly: true })` で開き、作成・migration・書込みをしない。
24
+ - `buildBudgetedResumeContext(db, { sessionId, isInheritance: true })` をそのまま使う。
25
+ - 成功 JSON は schema、status、sessionId、context だけを返す。renderer 統計は公開しない。
26
+ - 引数不正、DB open 失敗、context 不在は非 0 終了とする。
27
+
28
+ ## 非目標
29
+
30
+ - baton、pending handoff、merge、backfill、通常 SessionStart / UserPromptSubmit の変更。
31
+ - Observer projection、Codex 専用 renderer、latest session 推測、project/cwd 照合。
32
+ - DB schema 全面診断、hash、cursor、暗号化、daemon、network I/O、retry、cache。
33
+ - budget や `excludeOriginId` を CLI option として公開すること。
34
+
35
+ ## 受入条件
36
+
37
+ - CLI の context が同じ DB に対する既存 budgeted renderer の出力と完全一致する。
38
+ - 実行前後で `sessions.merged_into` と L1/L2/L3 の `session_id` が変わらない。
39
+ - DB 不在時に DB や親ディレクトリを作らない。
40
+ - focused test と全回帰が成功し、公開 package と global install から新 command を実行できる。
@@ -54,3 +54,26 @@ read-only refuterによる敵対的検証ではP0はなく、次のP1を設計
54
54
  - matrix縮小
55
55
  - npm自動publish、credential保管
56
56
  - release番号だけを根拠にした性能成功扱い
57
+
58
+ ## Terminal audit evidence
59
+
60
+ 2026-08-08 に公開後の終端監査を再確認し、次を受け入れた。
61
+
62
+ - 公開commit `df215fcaeb6d09d13bfbf5389c6f7c98a995b25c` は現在の `origin/main` の祖先で、
63
+ annotated tag `v0.8.7` は同commitへ解決する。
64
+ - GitHub Actions run `29726067549` は公開commitに対して9/9 greenである。Windows jobは
65
+ 2分46秒/2分53秒/2分59秒、unit test stepは2分14秒/2分26秒/2分38秒で、
66
+ 5分SLOと3分目標を満たした。
67
+ - GitHub Release `v0.8.7` はdraft/prereleaseではなく公開済みで、公開commit、CI run、
68
+ Windows実測、npm shasumを保持する。
69
+ - npm registryの `throughline@0.8.7` はshasum
70
+ `35a50f6878095d0881e75ebfb1da097a8da937c8`、integrity
71
+ `sha512-7Z/Mz0FRT2bJaxOLl+M8AYlNp5AhmaouQYeiLQ5BKu94+iyo/p1l1vrOhlFHWMlNSv4a89mGYtEAzolPZGRKtA==`
72
+ を返す。registryから再取得したtarballも同じSHA-1で、212 entries、package version `0.8.7`
73
+ を確認した。
74
+ - 元のrelease sessionではregistry版global install、`throughline --version = 0.8.7`、
75
+ managed hooks/skillの再install、migration、`doctor --codex` exit 0まで受け入れた。
76
+ 現在のglobal installは後続release `0.9.0`へ正当に更新済みのため、監査目的のdowngradeは行わない。
77
+
78
+ 以上により、製品受入は2026-07-20時点で完了しており、遅延していたLattice terminal-auditの
79
+ 記録を証拠付きで閉じてよいと裁定する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.8.9",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -38,7 +38,9 @@
38
38
  "LICENSE"
39
39
  ],
40
40
  "scripts": {
41
- "test": "node --import ./src/test-env.mjs --test src/*.test.mjs src/cli/*.test.mjs"
41
+ "test": "node --import ./src/test-env.mjs --test scripts/verify-release-commit.test.mjs src/*.test.mjs src/cli/*.test.mjs",
42
+ "verify:release-commit": "node scripts/verify-release-commit.mjs",
43
+ "prepublishOnly": "npm run verify:release-commit && npm test"
42
44
  },
43
45
  "engines": {
44
46
  "node": ">=22.13"
@@ -0,0 +1,71 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { buildBudgetedResumeContext } from '../resume-context.mjs';
7
+
8
+ export const HANDOFF_CONTEXT_SCHEMA = 'throughline.handoff_context.v1';
9
+
10
+ export function parseArgs(argv = []) {
11
+ if (
12
+ argv.length !== 3 ||
13
+ argv[0] !== '--session' ||
14
+ typeof argv[1] !== 'string' ||
15
+ argv[1].length === 0 ||
16
+ argv[2] !== '--json'
17
+ ) {
18
+ throw new TypeError('usage error');
19
+ }
20
+ return { sessionId: argv[1] };
21
+ }
22
+
23
+ export function readHandoffContext(sessionId, {
24
+ dbPath = join(homedir(), '.throughline', 'throughline.db'),
25
+ } = {}) {
26
+ if (!existsSync(dbPath)) return null;
27
+
28
+ const db = new DatabaseSync(dbPath, { readOnly: true });
29
+ try {
30
+ return buildBudgetedResumeContext(db, {
31
+ sessionId,
32
+ isInheritance: true,
33
+ })?.text ?? null;
34
+ } finally {
35
+ db.close();
36
+ }
37
+ }
38
+
39
+ export function run(argv = [], {
40
+ stdout = process.stdout,
41
+ stderr = process.stderr,
42
+ readContext = readHandoffContext,
43
+ } = {}) {
44
+ let sessionId;
45
+ try {
46
+ ({ sessionId } = parseArgs(argv));
47
+ } catch {
48
+ stderr.write('Usage: throughline handoff-context --session <id> --json\n');
49
+ return 2;
50
+ }
51
+
52
+ let context;
53
+ try {
54
+ context = readContext(sessionId);
55
+ } catch {
56
+ stderr.write('Throughline handoff context could not be read.\n');
57
+ return 1;
58
+ }
59
+ if (!context) {
60
+ stderr.write('Throughline handoff context is not available for that session.\n');
61
+ return 1;
62
+ }
63
+
64
+ stdout.write(`${JSON.stringify({
65
+ schema: HANDOFF_CONTEXT_SCHEMA,
66
+ status: 'ready',
67
+ sessionId,
68
+ context,
69
+ })}\n`);
70
+ return 0;
71
+ }
@@ -0,0 +1,143 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { DatabaseSync } from 'node:sqlite';
9
+
10
+ import { buildBudgetedResumeContext } from '../resume-context.mjs';
11
+
12
+ const REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url));
13
+ const BIN_PATH = join(REPO_ROOT, 'bin/throughline.mjs');
14
+ const SESSION_ID = 'claude-source-session';
15
+
16
+ function runCli(home, args = ['handoff-context', '--session', SESSION_ID, '--json']) {
17
+ return spawnSync(process.execPath, [BIN_PATH, ...args], {
18
+ cwd: REPO_ROOT,
19
+ env: { ...process.env, HOME: home, USERPROFILE: home },
20
+ encoding: 'utf8',
21
+ });
22
+ }
23
+
24
+ function createFixture(home) {
25
+ const dir = join(home, '.throughline');
26
+ mkdirSync(dir, { recursive: true });
27
+ const dbPath = join(dir, 'throughline.db');
28
+ const db = new DatabaseSync(dbPath);
29
+ db.exec(`
30
+ PRAGMA user_version = 9;
31
+ CREATE TABLE sessions (
32
+ session_id TEXT PRIMARY KEY,
33
+ project_path TEXT NOT NULL,
34
+ status TEXT NOT NULL DEFAULT 'active',
35
+ created_at INTEGER NOT NULL,
36
+ updated_at INTEGER NOT NULL,
37
+ merged_into TEXT
38
+ );
39
+ CREATE TABLE skeletons (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ session_id TEXT NOT NULL,
42
+ turn_number INTEGER NOT NULL,
43
+ role TEXT NOT NULL,
44
+ summary TEXT NOT NULL,
45
+ created_at INTEGER NOT NULL,
46
+ origin_session_id TEXT
47
+ );
48
+ CREATE TABLE bodies (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ session_id TEXT NOT NULL,
51
+ origin_session_id TEXT NOT NULL,
52
+ turn_number INTEGER NOT NULL,
53
+ role TEXT NOT NULL,
54
+ text TEXT NOT NULL,
55
+ token_count INTEGER,
56
+ created_at INTEGER NOT NULL
57
+ );
58
+ CREATE TABLE details (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ session_id TEXT NOT NULL,
61
+ turn_number INTEGER,
62
+ tool_name TEXT NOT NULL,
63
+ input_text TEXT,
64
+ output_text TEXT,
65
+ token_count INTEGER NOT NULL DEFAULT 0,
66
+ created_at INTEGER NOT NULL,
67
+ origin_session_id TEXT,
68
+ kind TEXT NOT NULL DEFAULT 'tool_input',
69
+ source_id TEXT
70
+ );
71
+ `);
72
+ db.prepare(
73
+ `INSERT INTO sessions
74
+ (session_id, project_path, status, created_at, updated_at, merged_into)
75
+ VALUES (?, ?, 'active', ?, ?, NULL)`,
76
+ ).run(SESSION_ID, '/work/project', 1_700_000_000_000, 1_700_000_004_000);
77
+ db.prepare(
78
+ `INSERT INTO skeletons
79
+ (session_id, origin_session_id, turn_number, role, summary, created_at)
80
+ VALUES (?, ?, 1, 'assistant', ?, ?)`,
81
+ ).run(SESSION_ID, 'older-origin', '以前に portable fork の方針を決めた', 1_700_000_001_000);
82
+ const insertBody = db.prepare(
83
+ `INSERT INTO bodies
84
+ (session_id, origin_session_id, turn_number, role, text, token_count, created_at)
85
+ VALUES (?, ?, 2, ?, ?, 8, ?)`,
86
+ );
87
+ insertBody.run(SESSION_ID, SESSION_ID, 'user', '所有権を変えずに記憶を渡して', 1_700_000_002_000);
88
+ insertBody.run(SESSION_ID, SESSION_ID, 'assistant', 'read-only I/F を実装する', 1_700_000_003_000);
89
+ db.prepare(
90
+ `INSERT INTO details
91
+ (session_id, origin_session_id, turn_number, tool_name, output_text, created_at, kind, source_id)
92
+ VALUES (?, ?, 2, 'Read', 'schema inspected', ?, 'tool_output', 'detail-1')`,
93
+ ).run(SESSION_ID, SESSION_ID, 1_700_000_003_500);
94
+ return { db, dbPath };
95
+ }
96
+
97
+ function ownershipSnapshot(db) {
98
+ return {
99
+ sessions: db.prepare('SELECT session_id, merged_into FROM sessions ORDER BY session_id').all(),
100
+ skeletons: db.prepare('SELECT id, session_id FROM skeletons ORDER BY id').all(),
101
+ bodies: db.prepare('SELECT id, session_id FROM bodies ORDER BY id').all(),
102
+ details: db.prepare('SELECT id, session_id FROM details ORDER BY id').all(),
103
+ };
104
+ }
105
+
106
+ test('handoff-context emits the exact inheritance context without changing DB ownership', () => {
107
+ const home = mkdtempSync(join(tmpdir(), 'tl-handoff-context-'));
108
+ try {
109
+ const { db, dbPath } = createFixture(home);
110
+ const before = ownershipSnapshot(db);
111
+ const expected = buildBudgetedResumeContext(db, {
112
+ sessionId: SESSION_ID,
113
+ isInheritance: true,
114
+ })?.text;
115
+ db.close();
116
+
117
+ const result = runCli(home);
118
+ assert.equal(result.status, 0, result.stderr);
119
+ assert.deepEqual(JSON.parse(result.stdout), {
120
+ schema: 'throughline.handoff_context.v1',
121
+ status: 'ready',
122
+ sessionId: SESSION_ID,
123
+ context: expected,
124
+ });
125
+
126
+ const verify = new DatabaseSync(dbPath, { readOnly: true });
127
+ assert.deepEqual(ownershipSnapshot(verify), before);
128
+ verify.close();
129
+ } finally {
130
+ rmSync(home, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ test('handoff-context fails without creating a missing database', () => {
135
+ const home = mkdtempSync(join(tmpdir(), 'tl-handoff-context-missing-'));
136
+ try {
137
+ const result = runCli(home);
138
+ assert.notEqual(result.status, 0);
139
+ assert.equal(existsSync(join(home, '.throughline')), false);
140
+ } finally {
141
+ rmSync(home, { recursive: true, force: true });
142
+ }
143
+ });
@@ -51,6 +51,8 @@ test('CLI help exposes guided Codex handoff and guarded execute semantics', () =
51
51
  assert.match(result.stdout, /Use --execute to/);
52
52
  assert.match(result.stdout, /--open-host auto\|desktop\|vscode\|cli\|none/);
53
53
  assert.match(result.stdout, /throughline trim --execute/);
54
+ assert.match(result.stdout, /throughline handoff-context --session <id> --json/);
55
+ assert.match(result.stdout, /without\s+changing database ownership/);
54
56
  assert.match(result.stdout, /injectable DB memory/);
55
57
  assert.match(result.stdout, /matching/);
56
58
  assert.match(result.stdout, /rollout\/app-server turns/);
@@ -66,3 +68,8 @@ test('CLI help Codex commands are dispatchable', () => {
66
68
  assert.match(bin, new RegExp(`case '${subcommand}':`), `${command} is missing dispatch`);
67
69
  }
68
70
  });
71
+
72
+ test('CLI help exposes the dispatchable read-only handoff context boundary', () => {
73
+ const bin = readFileSync(BIN_PATH, 'utf8');
74
+ assert.match(bin, /case 'handoff-context':/);
75
+ });
@@ -34,10 +34,10 @@ function childEnv(home) {
34
34
  };
35
35
  }
36
36
 
37
- function runNode(args, { home, cwd = REPO_ROOT, input = '' }) {
37
+ function runNode(args, { home, cwd = REPO_ROOT, input = '', env = {} }) {
38
38
  return spawnSync(process.execPath, args, {
39
39
  cwd,
40
- env: childEnv(home),
40
+ env: { ...childEnv(home), ...env },
41
41
  input,
42
42
  encoding: 'utf8',
43
43
  });
@@ -74,6 +74,71 @@ test('hook modules can be imported without executing their hook body', () => {
74
74
  }
75
75
  });
76
76
 
77
+ test('Grok camelCase envelopes are unsupported no-op before Throughline side effects [GF04T]', () => {
78
+ const home = makeTempHome();
79
+ const project = makeTempProject();
80
+ const common = {
81
+ sessionId: 'grok-session',
82
+ cwd: project,
83
+ workspaceRoot: project,
84
+ timestamp: '2026-08-14T00:00:00Z',
85
+ permissionMode: 'default',
86
+ };
87
+ const cases = [
88
+ {
89
+ event: 'SessionStart',
90
+ entrypoint: 'src/session-start.mjs',
91
+ input: { ...common, hookEventName: 'session_start', source: 'startup' },
92
+ },
93
+ {
94
+ event: 'UserPromptSubmit',
95
+ entrypoint: 'src/prompt-submit.mjs',
96
+ input: { ...common, hookEventName: 'user_prompt_submit', prompt: 'hello' },
97
+ },
98
+ {
99
+ event: 'Stop',
100
+ entrypoint: 'src/turn-processor.mjs',
101
+ input: {
102
+ ...common,
103
+ hookEventName: 'stop',
104
+ reason: 'end_turn',
105
+ stopHookActive: false,
106
+ lastAssistantMessage: 'done',
107
+ backgroundTasks: [],
108
+ sessionCrons: [],
109
+ },
110
+ },
111
+ ];
112
+
113
+ try {
114
+ const results = cases.map(({ event, entrypoint, input }) => {
115
+ const result = runNode([join(REPO_ROOT, entrypoint)], {
116
+ home,
117
+ cwd: project,
118
+ input: JSON.stringify(input),
119
+ env: { THROUGHLINE_NO_VSCODE: '0', TERM_PROGRAM: 'vscode' },
120
+ });
121
+ return { event, status: result.status, stdout: result.stdout, stderr: result.stderr };
122
+ });
123
+
124
+ assert.deepEqual(
125
+ {
126
+ results,
127
+ throughlineStateExists: existsSync(join(home, '.throughline')),
128
+ vscodeTaskExists: existsSync(join(project, '.vscode', 'tasks.json')),
129
+ },
130
+ {
131
+ results: cases.map(({ event }) => ({ event, status: 0, stdout: '', stderr: '' })),
132
+ throughlineStateExists: false,
133
+ vscodeTaskExists: false,
134
+ },
135
+ );
136
+ } finally {
137
+ rmSync(project, { recursive: true, force: true });
138
+ rmSync(home, { recursive: true, force: true });
139
+ }
140
+ });
141
+
77
142
  test('prompt-submit subprocess writes a /tl baton into an isolated DB', () => {
78
143
  const home = makeTempHome();
79
144
  const project = makeTempProject();
@@ -0,0 +1,12 @@
1
+ // Grok can invoke Claude-compatible hook commands with its camelCase wire.
2
+ // Throughline does not support Grok as a host, so this envelope is ignored
3
+ // before DB, state, VS Code task, transcript, or runtime-error side effects.
4
+ export function isUnsupportedNonClaudeEnvelope(payload) {
5
+ return payload !== null
6
+ && typeof payload === 'object'
7
+ && typeof payload.sessionId === 'string'
8
+ && payload.sessionId.length > 0
9
+ && typeof payload.hookEventName === 'string'
10
+ && payload.hookEventName.length > 0
11
+ && !Object.hasOwn(payload, 'session_id');
12
+ }
@@ -42,6 +42,7 @@ import { join, dirname } from 'node:path';
42
42
  import { homedir } from 'node:os';
43
43
  import { pathToFileURL } from 'node:url';
44
44
  import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
45
+ import { isUnsupportedNonClaudeEnvelope } from './hook-envelope.mjs';
45
46
 
46
47
  // Phase 0-5 spike marker (SessionStart の spike-inject.flag とは別)
47
48
  const PROMPT_SPIKE_MARKER_PATH = join(homedir(), '.throughline', 'spike-prompt.flag');
@@ -143,6 +144,7 @@ export async function run() {
143
144
  });
144
145
 
145
146
  const payload = JSON.parse(raw);
147
+ if (isUnsupportedNonClaudeEnvelope(payload)) return;
146
148
  const { session_id, cwd, prompt } = payload;
147
149
 
148
150
  // VSCode 新規プロジェクトへの tasks.json 自動プロビジョニング。
@@ -32,6 +32,7 @@ import { logDecision } from './decision-log.mjs';
32
32
  import { existsSync } from 'node:fs';
33
33
  import { pathToFileURL } from 'node:url';
34
34
  import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
35
+ import { isUnsupportedNonClaudeEnvelope } from './hook-envelope.mjs';
35
36
 
36
37
  const ENV_DISABLE_AUTO_HANDOFF = 'THROUGHLINE_DISABLE_AUTO_HANDOFF';
37
38
 
@@ -91,6 +92,7 @@ export async function run() {
91
92
  });
92
93
 
93
94
  const payload = JSON.parse(raw);
95
+ if (isUnsupportedNonClaudeEnvelope(payload)) return;
94
96
  const { session_id, cwd, source, transcript_path } = payload;
95
97
 
96
98
  if (!session_id) throw new Error('Missing session_id in SessionStart payload');
@@ -46,6 +46,7 @@ import { readLatestUsage } from './transcript-usage.mjs';
46
46
  import { pathToFileURL } from 'node:url';
47
47
  import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
48
48
  import { writeCompletedTurnReceipt } from './completed-turn-receipts.mjs';
49
+ import { isUnsupportedNonClaudeEnvelope } from './hook-envelope.mjs';
49
50
 
50
51
  /** 直近 N ターンは bodies を生で残し、それより古いものだけ L1 要約する。 */
51
52
  export const L2_WINDOW = 20;
@@ -191,6 +192,7 @@ export async function run() {
191
192
  });
192
193
 
193
194
  const payload = JSON.parse(raw || '{}');
195
+ if (isUnsupportedNonClaudeEnvelope(payload)) return;
194
196
  const { session_id, transcript_path, cwd, last_assistant_message } = payload;
195
197
  if (!session_id) throw new Error('Missing session_id in Stop payload');
196
198
 
@@ -1,5 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
 
3
+ // GitHub hosted Windows runners can spend more than 15 seconds starting the
4
+ // inbox Windows PowerShell process while the Node matrix is contended. This is
5
+ // a hang guard for a fixture boundary, not a latency assertion.
6
+ const WINDOWS_ACL_FIXTURE_TIMEOUT_MS = 30_000;
7
+
3
8
  export function applyWindowsPrivateAcl(path, directory = false) {
4
9
  if (process.platform !== 'win32') return;
5
10
  const script = String.raw`
@@ -14,10 +19,10 @@ $acl.AddAccessRule($rule)
14
19
  if($isDir){[System.IO.Directory]::SetAccessControl($target,$acl)}else{[System.IO.File]::SetAccessControl($target,$acl)}
15
20
  `;
16
21
  const result = spawnSync('powershell.exe', [
17
- '-NoProfile', '-NonInteractive', '-Command', script,
22
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script,
18
23
  ], {
19
24
  encoding: 'utf8',
20
- timeout: 15_000,
25
+ timeout: WINDOWS_ACL_FIXTURE_TIMEOUT_MS,
21
26
  windowsHide: true,
22
27
  env: {
23
28
  ...process.env,
@@ -43,10 +48,10 @@ $rule=$rules[0]
43
48
  if($rule.IdentityReference.Value -ne $sid -or $rule.AccessControlType -ne 'Allow' -or $rule.IsInherited -or ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl){exit 43}
44
49
  `;
45
50
  const result = spawnSync('powershell.exe', [
46
- '-NoProfile', '-NonInteractive', '-Command', script,
51
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script,
47
52
  ], {
48
53
  encoding: 'utf8',
49
- timeout: 15_000,
54
+ timeout: WINDOWS_ACL_FIXTURE_TIMEOUT_MS,
50
55
  windowsHide: true,
51
56
  env: {
52
57
  ...process.env,