throughline 0.8.3 → 0.8.5

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,27 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.8.5] — 2026-07-20
14
+
15
+ ### Added
16
+
17
+ - `throughline migrate --json` now provides the product-owned database
18
+ migration entry point used after package updates. It migrates only an
19
+ existing Throughline database, reports a versioned bounded result, leaves a
20
+ missing database absent, and rejects future schemas or migration failures
21
+ with a non-zero exit status.
22
+
23
+ ## [0.8.4] — 2026-07-20
24
+
25
+ ### Fixed
26
+
27
+ - The installed Codex skill now selects Desktop, VS Code, or CLI from the
28
+ current Codex surface and passes an explicit `--open-host` value. A command
29
+ launched through an older persistent PTY can no longer silently redirect a
30
+ Desktop handoff to the PTY's inherited VS Code or Terminal host.
31
+ - `codex-handoff-start` now reports both requested and resolved open hosts in
32
+ JSON and text output while retaining the existing `openHost` field.
33
+
13
34
  ## [0.8.3] — 2026-07-20
14
35
 
15
36
  ### Fixed
@@ -1133,7 +1154,9 @@ two attempts, instrument first instead of patching again.
1133
1154
 
1134
1155
  ---
1135
1156
 
1136
- [Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.3...HEAD
1157
+ [Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.5...HEAD
1158
+ [0.8.5]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.4...v0.8.5
1159
+ [0.8.4]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.3...v0.8.4
1137
1160
  [0.8.3]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.2...v0.8.3
1138
1161
  [0.8.2]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.1...v0.8.2
1139
1162
  [0.8.1]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.0...v0.8.1
package/README.ja.md CHANGED
@@ -34,7 +34,10 @@ global install は Codex の `UserPromptSubmit` / `PostToolUse` / `Stop` hook
34
34
  rollout capture と monitor state 書き込みだけを行い、**使用量閾値での
35
35
  `$throughline` 自動注入はしない**(token-monitor は表示専用)。bare
36
36
  `$throughline` は app-server 経由で新規 Codex thread を開始し、Throughline DB
37
- の handoff memory を developer item として注入する。current-thread rollback
37
+ の handoff memory を developer item として注入する。このとき現在のCodex UI surfaceから
38
+ Desktop/VS Code/CLIを選び、対応する`--open-host`を明示する。shellや永続PTYから継承した
39
+ 環境変数はsurface判定に使わない。`auto`はsurfaceが本当に不明な直接CLI利用時だけの互換経路。
40
+ current-thread rollback
38
41
  診断が要る時だけ明示的に `trim --execute --host codex` を使う。既存の
39
42
  非 Throughline Codex hook は保持される。
40
43
 
package/README.md CHANGED
@@ -569,7 +569,10 @@ reminds you to pipe the same memo when using them separately. Add `--execute`
569
569
  to create a new Codex app-server thread, inject the handoff memory as a
570
570
  developer item, and open it with `--open-host auto|desktop|vscode|cli|none`. `auto`
571
571
  opens the new task in Codex Desktop when invoked there, while preserving the
572
- existing VS Code and CLI routes. The
572
+ existing VS Code and CLI routes. The result reports both the requested and
573
+ resolved host. The installed `$throughline` skill passes the current Codex
574
+ surface explicitly so a persistent shell or PTY cannot redirect the handoff.
575
+ The
573
576
  individual commands remain available: validate the fresh-thread handoff with
574
577
  `throughline codex-handoff-smoke --session codex:<thread-id>`, optionally audit
575
578
  the model-smoke boundary with
@@ -776,6 +779,7 @@ aggregate は collection が既定OFFで、canonical dotagents config の
776
779
  | `throughline doctor --trim --host claude\|codex` | Diagnose trim host boundaries, manual procedure, and Codex host primitive blockage |
777
780
  | `throughline doctor --codex` | Diagnose Codex primary entry state, captured DB sessions, context-refresh memory contract, new-thread handoff readiness, safe continuation status, and host primitive audit |
778
781
  | `throughline factory-diagnostics --json` | Versioned read-only native factory readiness JSON for database schema/migration, connector hooks, and representative capture/restore/handoff; does not emit bodies, secrets, absolute paths, or raw state |
782
+ | `throughline migrate --json` | Migrate only an existing Throughline database to the current schema and emit a versioned bounded result; a missing database is not created, while future schemas and migration failures exit non-zero |
779
783
  | `throughline runtime-errors snapshot --json` | Read the bounded product-owned runtime error aggregate. Collection occurs only when canonical dotagents config explicitly sets `collection.enabled: true`; this command performs no network I/O |
780
784
  | `throughline runtime-errors diagnostics --json` | Read bounded collection/store status without exposing the state path or raw errors |
781
785
  | `throughline runtime-errors ack <cursor> --json` | Explicitly acknowledge records through a monotonic cursor; unacknowledged records are never compacted |
@@ -787,7 +791,7 @@ aggregate は collection が既定OFFで、canonical dotagents config の
787
791
  | `throughline codex-summarize --session codex:<id>` | Summarize captured Codex L2 into L1 with the Codex CLI backend |
788
792
  | `throughline codex-resume --session codex:<id>` | Render Codex active-work context from a captured Codex session |
789
793
  | `throughline codex-resume --session codex:<id> --format handoff` | Render a concise fresh-thread handoff prompt without mutating the current thread |
790
- | `throughline codex-handoff-start --session codex:<id>` | Guided start plan for moving handoff memory into a new Codex thread; add `--execute` to create the thread through app-server, inject developer memory, and open it with `--open-host auto\|desktop\|vscode\|cli\|none`; `auto` keeps Codex Desktop handoffs in the Desktop app; use `--print-prompt` to include the prompt and `--memo-stdin` to carry a current-work memo |
794
+ | `throughline codex-handoff-start --session codex:<id>` | Guided start plan for moving handoff memory into a new Codex thread; add `--execute` to create the thread through app-server, inject developer memory, and open it with `--open-host auto\|desktop\|vscode\|cli\|none`; output includes requested and resolved host, and the installed `$throughline` skill passes the current Codex surface explicitly; use `--print-prompt` to include the prompt and `--memo-stdin` to carry a current-work memo |
791
795
  | `throughline codex-handoff-smoke --session codex:<id>` | Read-only validation that the fresh-thread handoff prompt is pasteable before starting a new thread |
792
796
  | `throughline codex-handoff-model-smoke --session codex:<id>` | Experimental marker smoke for the handoff prompt. `--dry-run` checks readiness / command boundary without starting Codex exec; `--memo-stdin` carries a current-work memo; live `codex exec --ephemeral --sandbox read-only` requires explicit env opt-in |
793
797
  | `throughline codex-visibility-smoke --session codex:<id>` | Experimental Codex app-server marker smoke; injects memory and starts a model turn |
@@ -13,6 +13,7 @@
13
13
  * throughline handoff-preview # Codex-facing throughline_handoff JSON preview
14
14
  * throughline auditor-context --json # Read-only bounded auditor context JSON
15
15
  * throughline factory-diagnostics --json # Native factory read-only readiness JSON
16
+ * throughline migrate --json # Migrate the existing Throughline database only
16
17
  * throughline runtime-errors snapshot --json # Product-owned runtime error aggregates
17
18
  * throughline codex-capture # Capture active Codex rollout turns into Throughline DB
18
19
  * throughline codex-hook user-prompt-submit # Codex current-session auto-refresh prompt hook
@@ -95,6 +96,11 @@ switch (cmd) {
95
96
  if (exitCode !== 0) process.exitCode = exitCode;
96
97
  break;
97
98
  }
99
+ case 'migrate': {
100
+ const exitCode = (await import('../src/cli/migrate.mjs')).run(rest);
101
+ if (exitCode !== 0) process.exitCode = exitCode;
102
+ break;
103
+ }
98
104
  case 'runtime-errors': {
99
105
  const exitCode = (await import('../src/cli/runtime-errors.mjs')).run(rest);
100
106
  if (exitCode !== 0) process.exitCode = exitCode;
@@ -207,6 +213,8 @@ Usage:
207
213
  throughline factory-diagnostics --json
208
214
  Read-only native factory readiness JSON. Never emits
209
215
  session/prompt bodies, secrets, absolute paths, or raw state
216
+ throughline migrate --json Migrate the existing Throughline database only.
217
+ Does not create a missing database and emits a versioned JSON result
210
218
  throughline runtime-errors snapshot --json
211
219
  Read bounded local runtime error aggregates. Also supports
212
220
  diagnostics, ack <cursor>, resolve <fingerprint>, and compact
@@ -25,6 +25,17 @@ not ask for confirmation. Execute the handoff-start command directly. If it
25
25
  fails, report the error plainly instead of silently falling back to another
26
26
  memory source or current-thread rollback.
27
27
 
28
+ Choose the open host from the current Codex application surface, not from
29
+ environment variables inherited by the shell or a persistent PTY. Pass it
30
+ explicitly whenever the surface is known:
31
+
32
+ - Codex Desktop: `--open-host desktop`
33
+ - Codex in VS Code: `--open-host vscode`
34
+ - Codex CLI: `--open-host cli`
35
+
36
+ Use `--open-host auto` only when the Codex surface is genuinely unknown. In
37
+ that case, report both the requested and resolved hosts from the CLI result.
38
+
28
39
  ## Common Requests
29
40
 
30
41
  ### Bare "$throughline" / "use Throughline"
@@ -32,9 +43,13 @@ memory source or current-thread rollback.
32
43
  Run:
33
44
 
34
45
  ```bash
35
- throughline codex-handoff-start --execute
46
+ throughline codex-handoff-start --execute --open-host desktop
36
47
  ```
37
48
 
49
+ The example above is for Codex Desktop. Replace `desktop` with `vscode` or
50
+ `cli` when that is the current Codex surface. Do not copy the host identity
51
+ from the shell or PTY that happens to execute the command.
52
+
38
53
  This is the Codex new-thread continuation flow. It does not mutate the current
39
54
  Codex thread. Report the new thread id, open status, and any manual resume
40
55
  command if the host could not be opened automatically.
@@ -80,7 +95,7 @@ If the user wants to continue in a fresh Codex thread instead of mutating the
80
95
  current thread, use:
81
96
 
82
97
  ```bash
83
- throughline codex-handoff-start --session codex:<current-thread-id> --execute
98
+ throughline codex-handoff-start --session codex:<current-thread-id> --execute --open-host <current-codex-surface>
84
99
  ```
85
100
 
86
101
  If the user gave a current-work memo, pipe it with `--memo-stdin`. This starts a
@@ -106,7 +121,7 @@ memory, unless they explicitly ask to mutate the current Codex thread.
106
121
  Execute:
107
122
 
108
123
  ```bash
109
- throughline codex-handoff-start --execute
124
+ throughline codex-handoff-start --execute --open-host <current-codex-surface>
110
125
  ```
111
126
 
112
127
  Report only the essential outcome, especially the new thread id and open status.
@@ -120,7 +135,7 @@ throughline trim --dry-run --host codex
120
135
  Safe new-thread continuation:
121
136
 
122
137
  ```bash
123
- throughline codex-handoff-start --session codex:<current-thread-id> --execute --json
138
+ throughline codex-handoff-start --session codex:<current-thread-id> --execute --open-host <current-codex-surface> --json
124
139
  ```
125
140
 
126
141
  Report the context reduction estimate from the dry-run when present:
@@ -140,6 +140,8 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
140
140
  | **npm 公開 (v0.6.1): Spotter auditor context projection** | `throughline auditor-context`を追加。exact session/projectと最新完了L2 pairのorigin/turn/hash freshnessを検査し、fresh時だけbounded user/assistant本文をversioned JSONで返す。DBはread-onlyで開き、作成・migration・WAL書き込みをしない。Spotter側はproject opt-inでのみ利用する。580 tests、CI 6/6、pack scan、registry global install / doctorを通過し、npm / tag / GitHub Releaseを2026-07-13に公開した |
141
141
  | **npm 公開 (v0.6.2): factory diagnostics + local runtime error store** | `throughline factory-diagnostics --json` と `throughline runtime-errors ... --json` を追加。collection は canonical dotagents config の `collection.enabled: true` が明示された時だけ有効で既定OFF、store は network I/O を行わない。公開commit `e6ce6e3`、CI `29238704750`、npm `latest`、tag / GitHub Release、registry由来隔離installと診断snapshotを2026-07-13に確認した |
142
142
  | **npm 公開 (v0.6.3): Codex diagnostics ready 集約 + Windows bounded observer** | 3 managed Codex hooks が canonical `ready` のとき hook summary / Codex connector を `ready` とし、未検査 Claude connector は明示 `unverified` のまま Codex-only overall から除外する。Windows runtime-error observer は、既存 state の使用前検証と新規 file の ACL 付与・read-back を維持しつつ、同一 mutation 内の重複 PowerShell ACL 検証を除いた。公開commit `fc83ddf`、CI `29284655280`(9/9 green)、npm `throughline@0.6.3`、tag / GitHub Release、npm shasum `4f3fcd2598a75f026358dae7f3eb3165242b580b` を2026-07-14に確認した |
143
+ | **npm 公開 (v0.8.4): Codex handoff host境界の明示化** | `$throughline` skillは現在のCodex UI surfaceからDesktop/VS Code/CLIを選び、`--open-host`を明示する。shell/永続PTYの継承環境はsurface判定に使わない。`codex-handoff-start`は互換の`openHost`を残し、requested / resolved hostをJSONとtextで報告する。古いVS Code由来PTY事故形と配布skillをfocused testで固定し、Claude-facing hook/`/tl`/baton/resume契約は変更しない。公開commit `5b840b6`、CI run `29721583754` 9/9 green、npm `latest` 0.8.4、shasum `1f2c39a22e45f3e02e8739ee5fd6ceefc6a71034`、tag / GitHub Release、registry由来global install、配置skill一致、`doctor --codex` exit 0を2026-07-20に確認した。判断証拠は [ADR 0017](adr/0017-codex-handoff-host-boundary.md) |
144
+ | **release準備 (v0.8.5): 製品所有DB migration入口** | `throughline migrate --json` は既存DBだけをproduction migrationで現行schemaへ移行し、versioned bounded JSONを返す。DB不在は作らず`not_applicable`、現行は`already_current`、future schemaと失敗は非0。`factory-diagnostics`のread-only契約は維持する。設計判断は [ADR 0018](adr/0018-product-owned-database-migration.md) |
143
145
  | **未リリース: 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` も有効化する |
144
146
  | **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
145
147
 
@@ -28,7 +28,7 @@
28
28
  - Claude-primary の現行 L2 -> L1 要約は `codex-sidecar` が configured の場合に sidecar を優先し、使えない場合は Claude Haiku 経路に戻る。Codex-primary は Codex CLI backend 失敗を明示 error にし、Claude Haiku / raw L2 へ fallback しない。
29
29
  - Codex guarded trim は、Codex app-server の `thread/read` / `thread/resume` / `thread/rollback` / `thread/inject_items` を使う。明示 thread identity と injectable memory がない場合は mutation 前に拒否する。rollout/app-server turn count mismatch は診断に残し、`thread/read` / `thread/resume` が同じ count を返す場合は app-server 側の差分で rollback `numTurns` を補正する。
30
30
  - Claude `/rewind` 自動化はまだ有効化しない。
31
- - 2026-05-10 update: Codex automatic current-thread refresh mutation は無効化する。live rollback / inject で token_count が一時的に落ちても同一 thread で戻る実測があるため、`UserPromptSubmit` / `PostToolUse` / `Stop` hooks は capture / monitor state write のみ行い、`codex_auto_refresh_disabled` で quiet にする。bare `$throughline` は `codex-handoff-start --execute` による app-server 新スレッド handoff とし、明示 `trim --execute --host codex` だけを診断用 current-thread rollback / inject path として残す。
31
+ - 2026-05-10 update: Codex automatic current-thread refresh mutation は無効化する。live rollback / inject で token_count が一時的に落ちても同一 thread で戻る実測があるため、`UserPromptSubmit` / `PostToolUse` / `Stop` hooks は capture / monitor state write のみ行い、`codex_auto_refresh_disabled` で quiet にする。bare `$throughline` は `codex-handoff-start --execute --open-host <current-codex-surface>` による app-server 新スレッド handoff とし、明示 `trim --execute --host codex` だけを診断用 current-thread rollback / inject path として残す。current surfaceはCodex UI contextから決め、shell/永続PTYの継承環境へ委ねない。
32
32
 
33
33
  ## 新セッション引き継ぎ
34
34
 
@@ -271,7 +271,7 @@ Phase 3 implementation result (2026-05-06):
271
271
  - [x] `throughline codex-resume --format handoff` は current thread を mutate せず、新規 Codex thread に貼る短い handoff prompt を返す。handoff view は L2 件数 / 本文長 / detail refs を cap し、full active-work context は通常 text renderer に残す。
272
272
  - [x] `throughline codex-handoff-smoke --session codex:<thread_id>` は新規 thread handoff prompt を read-only に検査し、prompt size / required sections / mutation boundary / detail command dedupe を固定する。
273
273
  - [x] `throughline codex-handoff-model-smoke --session codex:<thread_id>` は明示 opt-in 時だけ `codex exec --ephemeral --ignore-user-config --ignore-rules --sandbox read-only` で handoff prompt の marker model smoke を行う。`--dry-run` は env なしで readiness / command boundary を監査し、`--print-prompt` で結合 prompt を出せる。`--memo-stdin` で Codex-primary current-work memo も同じ prompt に含める。structural handoff smoke が ready でなければ拒否し、current thread は mutate しない。
274
- - [x] `throughline codex-handoff-start --session codex:<thread_id>` は safe continuation の guided entrypoint として、structural smoke / model smoke dry-run / handoff render / optional live smoke / `--print-prompt` をまとめて表示する。`--execute` では app-server `thread/start` + `thread/inject_items` で新 thread に developer memory を注入し、`--open-host auto|desktop|vscode|cli|none` で表示を開く。`auto` は Codex Desktop 起点(`CODEX_INTERNAL_ORIGINATOR_OVERRIDE="Codex Desktop"` または `__CFBundleIdentifier=com.openai.codex`)を CLI より先に識別し、Desktop の `codex://threads/<thread-id>` deep link で新 thread を同アプリ内に開く。VS Code CLI の既存経路は維持する。`--memo-stdin` 時は replay 用コマンドにも `--memo-stdin` を伝播し、same memo を pipe する注意を出す。
274
+ - [x] `throughline codex-handoff-start --session codex:<thread_id>` は safe continuation の guided entrypoint として、structural smoke / model smoke dry-run / handoff render / optional live smoke / `--print-prompt` をまとめて表示する。`--execute` では app-server `thread/start` + `thread/inject_items` で新 thread に developer memory を注入し、`--open-host auto|desktop|vscode|cli|none` で表示を開く。CLIはrequested / resolved hostを両方報告する。`auto` は Codex Desktop 起点(`CODEX_INTERNAL_ORIGINATOR_OVERRIDE="Codex Desktop"` または `__CFBundleIdentifier=com.openai.codex`)を CLI より先に識別するが、bare `$throughline` skillは現在のCodex UI surfaceを明示指定し、永続PTYの継承環境へhost選択を委ねない。Desktop の `codex://threads/<thread-id>` deep linkVS Code / CLI の既存経路は維持する。`--memo-stdin` 時は replay 用コマンドにも `--memo-stdin` を伝播し、same memo を pipe する注意を出す。
275
275
  - [x] `throughline codex-resume --format item-json` は Codex developer message item JSON を返す。
276
276
  - [x] `codex-resume` は explicit session を受け取れる。省略時は cwd の最新 `codex:%` session だけを対象にし、Claude session を混ぜない。
277
277
  - [x] `src/codex-handoff.test.mjs` / `src/codex-resume.test.mjs` で renderer と CLI shape を固定した。
@@ -440,7 +440,7 @@ Phase 5 implementation status (2026-05-06):
440
440
  - [x] `doctor --codex` は Claude settings を変更しない。Codex primary entrypoint の診断に限定する。
441
441
  - [x] 実セッションで `doctor --codex` -> `codex-capture` -> `codex-resume --format item-json` -> `doctor --codex` の local smoke を実施した。`codex-resume` は developer message item JSON を描画し、再診断で captured DB session が 1 件として表示された。
442
442
  - [x] 実セッションで `codex-visibility-smoke` を実施した。`THROUGHLINE_EXPERIMENTAL_CODEX_MODEL_VISIBLE_SMOKE=1` を必須にし、長い model turn に備えて `--request-timeout-ms 150000` / `--timeout-ms 180000` を使えるようにした。
443
- - [x] Codex primary の setup / install 手順は README に記録した。global install は Codex hooks と `$throughline` skill を自動登録する。2026-05-10 以降、bare `$throughline` は `throughline codex-handoff-start --execute` による app-server 新スレッド handoff とする。hooks は capture / monitor state write だけを行い、current-thread rollback / inject は明示 `trim --execute --host codex` を要求された時だけ使う。
443
+ - [x] Codex primary の setup / install 手順は README に記録した。global install は Codex hooks と `$throughline` skill を自動登録する。2026-05-10 以降、bare `$throughline` は `throughline codex-handoff-start --execute --open-host <current-codex-surface>` による app-server 新スレッド handoffとし、current surfaceはCodex UI contextから明示する。hooks は capture / monitor state write だけを行い、current-thread rollback / inject は明示 `trim --execute --host codex` を要求された時だけ使う。
444
444
  - [x] `codex-summarize` を明示診断・運用 flow に追加した。Codex CLI backend を使い、Claude Haiku へ fallback しない。
445
445
  - [x] Codex primary の summarize / guarded execute まで含む end-to-end smoke を実施した。2026-05-07 correction では guarded execute を live app-server smoke としてのみ扱った。2026-05-08 unblock 後は、controlled rollback model-visible smoke の `not-reproduced` と current-thread live run の `execute-durable-verified` を合わせて、Codex current-thread trim 完了条件に含める。
446
446
  - `codex-capture`: `capturedTurns = 41`, `capturedRows = 75`, `capturedDetails = 2424`
@@ -460,7 +460,7 @@ Phase 5 implementation status (2026-05-06):
460
460
  - 追加確認: current VSCode-origin parent thread `019dfd38-c530-71c3-b7b8-180bdd3054bc` は hook shape 変更前に開始していたため、変更後の自然 Stop smoke としては不適格。assistant final 後に rollout は `task_complete` まで進んだが latest DB session は exec child のままだった。次に VSCode-origin を見る場合は、hook shape 変更後に新しく開始した Codex session で確認する。
461
461
  - 最終確認: hook shape 変更後に新しく開始した VSCode-origin Codex session で 1 turn 完了後、`throughline doctor --codex` を実行した。`current Codex thread` は `019dfd62-9a9d-7211-bf91-89d8e3fc908e`、`latest DB session` は `codex:019dfd62-9a9d-7211-bf91-89d8e3fc908e` で一致し、`Codex hooks feature: enabled`、`Codex Stop hook: registered`、command は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false`、`timeoutSec: 300` だった。VSCode-origin の自然 Stop hook による DB capture も解決済みとして扱う。
462
462
  - historical 2026-05-07 correction: 以下の Codex trim smoke は live app-server primitive の履歴として残す。ただし 2026-05-06 incident 後、restart / reconnect 越しの durable context trim 成功とは一時的に扱わず、`$throughline` / Codex Stop hook の automatic mutation を止めていた。
463
- - 2026-05-10 UX 修正: bare `$throughline` は `doctor --codex` / `trim --dry-run --all` / `trim --preflight --all` を AI に順番実行させる説明 surface でも、`trim --execute --host codex --all` を直接実行する current-thread refresh でもなく、`throughline codex-handoff-start --execute` による app-server 新スレッド handoff とする。current-thread rollback / inject は明示要求時だけ使う。
463
+ - 2026-05-10 UX 修正: bare `$throughline` は `doctor --codex` / `trim --dry-run --all` / `trim --preflight --all` を AI に順番実行させる説明 surface でも、`trim --execute --host codex --all` を直接実行する current-thread refresh でもなく、`throughline codex-handoff-start --execute --open-host <current-codex-surface>` による app-server 新スレッド handoff とする。current surfaceはCodex UI contextから明示し、current-thread rollback / inject は明示要求時だけ使う。
464
464
  - memory contract 修正: Codex guarded trim でも注入 memory は元の `/tl` 思想を正とする。古い turn は L1 summaries、直近 20 turn は L2 full bodies、L3 は reference only で、L3 bodies / tool payloads は注入しない。rollout source は rollback candidate と app-server turn-count 補正の根拠であり、Throughline DB memory がある場合に rollout active work preview を注入 memory として使わない。DB memory が無い execute は rollout preview を注入せず、mutation 前に拒否する。
465
465
  - doctor visibility 修正: `doctor --codex` は旧 context refresh readiness として rollback source、inject memory source、memory contract、L1 summaries / recent L2 bodies / L3 references-only count、heuristic reduction estimate を表示していた。実 thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` では live readiness として `context refresh: ready`、`rollback source: codex-rollout`、`inject memory source: throughline-db`、`memory contract: older L1 + latest 20 L2 full bodies + L3 references only` を確認した。ただし incident 後は restart-safe readiness ではない。
466
466
  - 削減量の記録: `trim --dry-run --host codex` は rollout text がある場合に `contextReductionEstimate` を返す。2026-05-06 の現在 thread `019dfd62-9a9d-7211-bf91-89d8e3fc908e` では通常 keep-recent preview だと `capturedTurns = 14` / `keepRecent = 20` のため `rollbackTurns = 0`、推定削減量も 0 だった。旧 bare `$throughline` context refresh は `--all` を使っていたため、この keep-recent preview の 0 は「Codex で削減できない」という意味ではない。
@@ -25,8 +25,9 @@ count を返す場合は、その差分で rollback `numTurns` を補正しま
25
25
  一時的に下がっても同一 thread で戻る挙動を確認したため、Codex automatic
26
26
  current-thread refresh は無効化します。`UserPromptSubmit` / `PostToolUse` / `Stop`
27
27
  hooks は capture / monitor state write のみ行い、`codex_auto_refresh_disabled` で
28
- quiet にします。bare `$throughline` は `codex-handoff-start --execute` による
29
- app-server 新スレッド handoff とします。
28
+ quiet にします。bare `$throughline` は
29
+ `codex-handoff-start --execute --open-host <current-codex-surface>` による
30
+ app-server 新スレッド handoff とし、surfaceはCodex UI contextから明示します。
30
31
 
31
32
  最初のインシデント仮説に対する重要な訂正:
32
33
 
@@ -25,7 +25,7 @@
25
25
 
26
26
  2026-05-09 skill UX: bare `$throughline` は diagnostics / dry-run / preflight を AI に順番実行させる surface ではなく、`throughline trim --execute --host codex --all` を直接走らせる scripted current-thread refresh としていた。目的は rollback と、Throughline DB の L2 最新 20 full bodies + older L1 summaries + L3 references-only memory injection のみだった。2026-05-10 以降は `--json` の full plan / memory preview が tool output として context を再膨張させるため、通常 path では付けないようにした。
27
27
 
28
- 2026-05-10 rollback UX correction: 追加 live 実験で、current-thread rollback / inject 後に token_count が一時的に下がっても同一 thread で戻る挙動を確認した。これを受け、Codex automatic current-thread refresh は無効化し、`UserPromptSubmit` / `PostToolUse` / `Stop` hooks は capture / monitor state write のみ行う。bare `$throughline` は `throughline codex-handoff-start --execute` による app-server 新スレッド handoff とし、明示 `trim --execute --host codex` は診断用 current-thread path として残す。
28
+ 2026-05-10 rollback UX correction: 追加 live 実験で、current-thread rollback / inject 後に token_count が一時的に下がっても同一 thread で戻る挙動を確認した。これを受け、Codex automatic current-thread refresh は無効化し、`UserPromptSubmit` / `PostToolUse` / `Stop` hooks は capture / monitor state write のみ行う。bare `$throughline` は `throughline codex-handoff-start --execute --open-host <current-codex-surface>` による app-server 新スレッド handoffとし、surfaceはCodex UI contextから明示する。明示 `trim --execute --host codex` は診断用 current-thread path として残す。
29
29
 
30
30
  2026-05-07 host primitive audit: `throughline codex-host-primitive-audit` で installed Codex app-server schema を機械監査した。`thread/rollback` / `thread/inject_items` / `thread/compact/start` / `thread/start` / `thread/fork` / `thread/resume` は存在するが、rollback 済み user text を current-thread の model-visible input へ復活させない deletion / isolation / projection primitive は見つからなかった。`thread/resume(history)` は schema 上 `[UNSTABLE] FOR CODEX CLOUD - DO NOT USE` で、`thread_id` も ignored になるため、Throughline の current-thread repair primitive には採用しない。
31
31
 
@@ -0,0 +1,82 @@
1
+ # Windows CI・リリース待ち時間改修計画
2
+
3
+ ## 目的
4
+
5
+ ThroughlineのWindows CIがLinux/macOSの約1分に対して16〜18分かかり、定型releaseを
6
+ 実用不能な長さへ押し上げている。Windows互換性matrixとowner-only ACL契約を維持したまま、
7
+ unit testを5分以内(目標3分以内)へ短縮し、release前の同一candidateに対するCIを1回へ
8
+ 集約する。
9
+
10
+ ## 現状の実測
11
+
12
+ GitHub Actions run `29722650046`(Windows Node 22.13.0)のTAP計測では、次の3テストだけで
13
+ 約26分相当の直列test時間を占めた。実jobはfile単位の重なりがあるため約18分で完了する。
14
+
15
+ | テスト | `duration_ms` |
16
+ |---|---:|
17
+ | observer feed: Claude history floor, host/thread switch, cross-host tie, and opaque cursor | 568,093 ms |
18
+ | completed turn receipt: bounded store drops only oldest receipts | 559,807 ms |
19
+ | completed turn receipt: noisy project cannot evict another project anchor | 436,267 ms |
20
+
21
+ 3テストはいずれも256件境界を作るため公開APIを257回呼び、その各mutationでWindows
22
+ PowerShellを複数回起動してdirectory/lock/temporary/final storeのACLを検証している。
23
+ 境界計算の反復とACL実機検証が結合されていることが主因で、runner一般の遅さではない。
24
+
25
+ また現行workflowはmainへのpushごとに9 matrixを新規起動し、同じbranchの旧runをcancelしない。
26
+ 短時間の段階commitでWindows 18分runが重複する。
27
+
28
+ ## 決定
29
+
30
+ 1. completed-turn receiptの大量境界テストは、正規schemaの境界直前storeをfixtureとして用意し、
31
+ 最後の1 mutationだけ公開APIで実行する。limit超過、history floor、project分離、cursor判定の
32
+ 受入条件は変更しない。
33
+ 2. Windows owner-only ACLは専用integration testでdirectory、SQLite lock、final storeを
34
+ `windows-acl-test-helper`から外部検証する。大量境界テストからPowerShell反復を除いても、
35
+ production ACL契約の実機coverageを失わない。
36
+ 3. CIのunit test stepへ`timeout-minutes: 5`を設定する。5分超は性能劣化としてfail closedにする。
37
+ 4. workflowへbranch単位の`concurrency`を設定し、新しいcommitが来た時は同一workflow/refの
38
+ 古いrunをcancelする。
39
+ 5. OS 3種 × Node `22.13.0`/`22.x`/`24.x`の9 matrixは維持する。互換範囲を速度対策のために
40
+ 縮小しない。
41
+ 6. `0.8.5`の最終candidateへ実装・version・CHANGELOG・README・正本文書をまとめ、push後CIを
42
+ 1回だけrelease gateとして使う。公開後のSHA/CI番号はGitHub Releaseを正本とし、証拠追記だけの
43
+ 追加commit/追加CIを作らない。
44
+
45
+ ## 非目標
46
+
47
+ - Windows ACLの適用・read-back検証、atomic rename、失敗時の旧store保持を弱めない。
48
+ - Windows testをskipしない。matrixのOS/Node versionを減らさない。
49
+ - completed-turn receiptの256件limit、history floor、cursor、project分離契約を変えない。
50
+ - Claude hooks、Codex adapter、DB migration、handoff契約を変更しない。
51
+ - release自動publishやcredential保管をworkflowへ追加しない。
52
+
53
+ ## 既知の罠
54
+
55
+ - fixtureを直接書くだけでは公開APIの境界mutationを検証できない。必ずlimit直前から最後の1件を
56
+ `writeCompletedTurnReceipt`で追加する。
57
+ - Windowsでfixtureを書き換える時も既存owner-only ACLを保持し、専用ACL testはproduction APIが
58
+ 作った実pathを外部helperで検査する。
59
+ - `concurrency.cancel-in-progress`は別branch/別PRをcancelしないref単位にする。
60
+ - test timeoutはjob全体ではなくunit test stepに置き、setup-node等の外部遅延と製品test性能を混ぜない。
61
+ - 進行中の旧CIを新しいcandidateの成功証拠として流用しない。
62
+
63
+ ## 受入条件
64
+
65
+ - [ ] 3つの大量境界テストが公開契約を維持したままPowerShell反復を行わない。
66
+ - [ ] Windows専用ACL testがdirectory/lock/storeのowner-only ACLを外部検証する。
67
+ - [ ] focused testとfull `npm test`がgreen。
68
+ - [ ] CI定義にunit test 5分上限とref単位concurrencyがある。
69
+ - [ ] 最終GitHub Actionsで9/9 green、Windows各jobのunit testが5分以内(目標3分以内)。
70
+ - [ ] `npm pack --dry-run --json`で公開物を確認する。
71
+ - [ ] npm `throughline@0.8.5`、tag/GitHub Release、registry由来global install、
72
+ `throughline --version = 0.8.5`、配置skill/hooks/doctorを確認する。
73
+
74
+ ## 工程
75
+
76
+ 工程状態と完了証拠の正本はLattice storeとし、この文書は目的、判断、非目標、受入条件を所有する。
77
+
78
+ 1. ベースライン計測と原因同定
79
+ 2. 敵対的検証と設計裁定
80
+ 3. 安全網とfixture分離
81
+ 4. workflow・文書統合
82
+ 5. push後CI、npm公開、global install
@@ -0,0 +1,50 @@
1
+ # ADR 0017: Codex handoff の open host は現在の UI surface から明示する
2
+
3
+ - Status: accepted(2026-07-20実装・v0.8.4公開)
4
+ - 関連: [Codex First Roadmap](../05_codex_first_roadmap.md)、
5
+ [Codex Trim Rollback修正計画](../06_codex_trim_rollback_fix_plan.md)
6
+
7
+ ## 問題
8
+
9
+ Codex Desktopから `$throughline` を使っても、コマンド実行shellが以前のVS CodeやTerminalから
10
+ 継承した永続PTYである場合、`--open-host auto` は現在のUIではなく古い環境変数を見てしまう。
11
+ 新しいthread自体は正しく作られても、表示先だけがVS CodeやCLIへ逸れる事故になっていた。
12
+
13
+ ## 決定
14
+
15
+ - `$throughline` skillは、AIが現在動いているCodex UI surfaceをDesktop/VS Code/CLIから選び、
16
+ `codex-handoff-start --execute --open-host <surface>` として明示する。
17
+ - shell/永続PTYの継承環境は、skillのsurface判定根拠にしない。
18
+ - `auto`は直接CLIからsurfaceが本当に不明な場合の互換経路として残す。
19
+ - `codex-handoff-start`は既存consumer向けの`openHost`を維持し、requested hostとresolved hostを
20
+ JSON/textの両方へ追加して、host解決を観測可能にする。
21
+ - Claude-facing hooks、`/tl`、baton、transcript、resume契約は変更しない。
22
+
23
+ ## 棄却した案
24
+
25
+ 1. **Desktop判定環境変数の優先順位だけを上げる**: 古いPTYに残った値を「現在のUI」と誤認する
26
+ 根本問題が残るため棄却した。
27
+ 2. **`auto`を削除する**: 既存CLI consumerとの互換を不要に壊すため棄却した。
28
+ 3. **既存`openHost` fieldをrenameする**: machine-readable consumerを壊すため、追加fieldで拡張した。
29
+
30
+ ## 受入証拠
31
+
32
+ - 公開commit: `5b840b69688713ba29f4d39f8520953bae846ea7`
33
+ - GitHub Actions: run `29721583754`、Linux/macOS/Windows × Node 22.13.0/22.x/24.xの9/9成功
34
+ - npm: `throughline@0.8.4`、`latest = 0.8.4`
35
+ - npm shasum: `1f2c39a22e45f3e02e8739ee5fd6ceefc6a71034`
36
+ - GitHub Release: `v0.8.4`
37
+ - registry由来global install: `/opt/homebrew/lib/node_modules/throughline`はsymlinkでないcopy、
38
+ `throughline --version`は`0.8.4`
39
+ - `throughline install`後の`~/.codex/skills/throughline/SKILL.md`はrepoと公開packageの双方に
40
+ byte-for-byte一致し、`doctor --codex`はexit 0
41
+
42
+ `doctor --codex`はローカルの`.vscode/tasks.json`に古い絶対パスがあることも警告したが、
43
+ 診断上は「次のVS Code hook eventで修復」とされる既存ローカル状態であり、本releaseの
44
+ host選択契約や公開packageの受入失敗ではない。
45
+
46
+ ## 帰結
47
+
48
+ - Codex UIとcommand実行shellの由来が異なっても、handoffの表示先は現在のUIへ固定される。
49
+ - requested/resolved hostの差を出力から追跡でき、将来のhost追加でもsilent fallbackを避けられる。
50
+ - Claude primaryの既存surfaceとCodexのcurrent-thread診断pathは影響を受けない。
@@ -0,0 +1,43 @@
1
+ # ADR 0018: product-owned database migration
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-20
5
+ - Scope: dotagents `factory-master/fm-0645`
6
+
7
+ ## Context
8
+
9
+ `agents-update` はregistry packageを更新した直後にfactory reporterを実行する。Throughlineの
10
+ `factory-diagnostics` は意図的にread-onlyであり、旧schema DBを検出してもmigrationせず
11
+ `not_ready`を返す。一方、現行のmigration入口は通常のwrite CLIがDBを開く副作用に埋もれている。
12
+ このためschema更新後の初回updateは、利用者が別CLIを手で一度実行しない限り失敗する。
13
+
14
+ ## Decision
15
+
16
+ 1. Throughlineは `throughline migrate --json` を製品所有の正規入口として提供する。
17
+ 2. 対象は既存のdefault DBだけとする。DBが存在しない場合は作成せず`not_applicable`、現行schemaは
18
+ `already_current`、旧schemaは既存のproduction migrationを実行して`migrated`を返す。
19
+ 3. 対応版より新しいschema、migration失敗、migration後のversion不一致は非0で明示する。
20
+ silent fallback、DB削除、空DBへの置換は行わない。
21
+ 4. JSONはversioned schema、before/after/supported schema version、statusを持つ。秘密、DB内容、絶対pathは
22
+ 出力しない。
23
+ 5. dotagentsの`agents-update`はThroughline package更新後、factory reporterより前にこの入口を自動実行する。
24
+ 利用者に別の手動migrationを要求しない。migration失敗はupdate失敗として残し、factory gateで隠さない。
25
+ 6. v8 fixtureからv9への移行、現行schemaの冪等性、DB不在時の非作成、future schema拒否、strict CLI引数、
26
+ updaterの実行順と失敗伝播をfocused testで固定する。
27
+
28
+ ## Compatibility
29
+
30
+ - Claude/Codex hooks、handoff、baton、recall、通常の`getDb()`自動migrationは変更しない。
31
+ - `factory-diagnostics`はread-onlyのまま維持し、診断にmutationを混ぜない。
32
+ - migration commandは任意pathや任意SQLを受け取らず、Throughline所有DBだけを扱う。
33
+
34
+ ## Rollback
35
+
36
+ 公開前は本commitをrevertする。公開後は旧packageへ戻せるが、DB schema downgradeは行わない。
37
+ schema更新前のDBへ戻す必要がある場合は、hostごとに更新前backupを復元して対応版packageを使う。
38
+
39
+ ## Non-goals
40
+
41
+ - Lattice本体・Lattice repoの変更
42
+ - factory reporter側でのThroughline DB mutation
43
+ - schema downgrade、DB repair、破損DBの自動再生成
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -190,6 +190,8 @@ function buildGuidance({ sessionId, parsed, handoffSmoke, handoffPrompt }) {
190
190
  ]),
191
191
  };
192
192
  const ready = handoffSmoke.status === 'ready';
193
+ const requestedOpenHost = parsed.openHost;
194
+ const resolvedOpenHost = resolveOpenHost(requestedOpenHost);
193
195
  return {
194
196
  status: ready ? 'ready' : 'not-ready',
195
197
  reason: ready ? 'fresh_thread_handoff_start_ready' : 'handoff_smoke_not_ready',
@@ -198,6 +200,8 @@ function buildGuidance({ sessionId, parsed, handoffSmoke, handoffPrompt }) {
198
200
  startThreadManually: !parsed.execute,
199
201
  execute: parsed.execute,
200
202
  openHost: parsed.openHost,
203
+ requestedOpenHost,
204
+ resolvedOpenHost,
201
205
  handoffSmoke,
202
206
  modelPromptChars: modelPrompt.length,
203
207
  estimatedModelPromptTokens: estimateTokens(modelPrompt),
@@ -234,7 +238,8 @@ function renderTextResult(result) {
234
238
  lines.push(` session: ${result.sessionId}`);
235
239
  lines.push(` mutates thread: ${result.mutatesCurrentThread ? 'yes' : 'no'}`);
236
240
  lines.push(` execute: ${result.execute ? 'yes' : 'no'}`);
237
- lines.push(` open host: ${result.openHost}`);
241
+ lines.push(` open requested: ${result.requestedOpenHost ?? result.openHost}`);
242
+ lines.push(` open resolved: ${result.resolvedOpenHost ?? result.open?.host ?? result.openHost}`);
238
243
  lines.push(` handoff smoke: ${result.handoffSmoke.status}`);
239
244
  lines.push(` prompt chars: ${result.handoffSmoke.promptChars}/${result.handoffSmoke.maxPromptChars}`);
240
245
  lines.push(` model prompt: ${result.modelPromptChars}`);
@@ -304,6 +309,8 @@ function openStartedCodexThread({ threadId, host, cwd }) {
304
309
  status: 'skipped',
305
310
  reason: 'open_host_none',
306
311
  host: resolvedHost,
312
+ requestedHost: host,
313
+ resolvedHost,
307
314
  desktopUrl,
308
315
  vscodeUrl,
309
316
  resumeCommand,
@@ -323,6 +330,8 @@ function openStartedCodexThread({ threadId, host, cwd }) {
323
330
  status: 'failed',
324
331
  reason: `${resolvedHost}_deep_link_open_failed`,
325
332
  host: resolvedHost,
333
+ requestedHost: host,
334
+ resolvedHost,
326
335
  desktopUrl,
327
336
  vscodeUrl,
328
337
  resumeCommand,
@@ -333,6 +342,8 @@ function openStartedCodexThread({ threadId, host, cwd }) {
333
342
  status: 'opened',
334
343
  reason: `${resolvedHost}_deep_link_opened`,
335
344
  host: resolvedHost,
345
+ requestedHost: host,
346
+ resolvedHost,
336
347
  desktopUrl,
337
348
  vscodeUrl,
338
349
  resumeCommand,
@@ -355,6 +366,8 @@ end tell
355
366
  status: 'failed',
356
367
  reason: 'terminal_open_failed',
357
368
  host: resolvedHost,
369
+ requestedHost: host,
370
+ resolvedHost,
358
371
  desktopUrl,
359
372
  vscodeUrl,
360
373
  resumeCommand,
@@ -365,6 +378,8 @@ end tell
365
378
  status: 'opened',
366
379
  reason: 'terminal_resume_opened',
367
380
  host: resolvedHost,
381
+ requestedHost: host,
382
+ resolvedHost,
368
383
  desktopUrl,
369
384
  vscodeUrl,
370
385
  resumeCommand,
@@ -374,6 +389,8 @@ end tell
374
389
  status: 'manual',
375
390
  reason: 'cli_auto_open_unsupported_on_platform',
376
391
  host: resolvedHost,
392
+ requestedHost: host,
393
+ resolvedHost,
377
394
  desktopUrl,
378
395
  vscodeUrl,
379
396
  resumeCommand,
@@ -384,6 +401,8 @@ end tell
384
401
  status: 'failed',
385
402
  reason: 'unsupported_open_host',
386
403
  host: resolvedHost,
404
+ requestedHost: host,
405
+ resolvedHost,
387
406
  desktopUrl,
388
407
  vscodeUrl,
389
408
  resumeCommand,
@@ -27,6 +27,21 @@ test('codex-handoff-start auto-selects Codex Desktop before inherited VS Code si
27
27
  );
28
28
  });
29
29
 
30
+ test('codex-handoff-start does not mistake a stale VS Code PTY for Desktop without a Desktop signal', () => {
31
+ assert.equal(
32
+ _internal.resolveOpenHost('auto', {
33
+ VSCODE_IPC_HOOK_CLI: '/tmp/stale-vscode.sock',
34
+ }),
35
+ 'vscode',
36
+ );
37
+ assert.equal(
38
+ _internal.resolveOpenHost('desktop', {
39
+ VSCODE_IPC_HOOK_CLI: '/tmp/stale-vscode.sock',
40
+ }),
41
+ 'desktop',
42
+ );
43
+ });
44
+
30
45
  test('codex-handoff-start keeps VS Code and CLI auto-open behavior', () => {
31
46
  assert.equal(
32
47
  _internal.resolveOpenHost('auto', { CODEX_INTERNAL_ORIGINATOR_OVERRIDE: 'codex_vscode' }),
@@ -149,6 +164,9 @@ test('codex-handoff-start prints guided ready JSON for latest Codex session', as
149
164
  assert.equal(payload.sessionId, 'codex:thread-handoff-start');
150
165
  assert.equal(payload.mutatesCurrentThread, false);
151
166
  assert.equal(payload.startThreadManually, true);
167
+ assert.equal(payload.openHost, 'auto');
168
+ assert.equal(payload.requestedOpenHost, 'auto');
169
+ assert.ok(['desktop', 'vscode', 'cli'].includes(payload.resolvedOpenHost));
152
170
  assert.equal(payload.memoStdin, false);
153
171
  assert.equal(payload.memoReplayNote, null);
154
172
  assert.equal(payload.handoffSmoke.status, 'ready');
@@ -178,6 +196,8 @@ test('codex-handoff-start can print the exact fresh-thread prompt', async () =>
178
196
  assert.equal(result.status, 0, result.stderr);
179
197
  assert.match(result.stdout, /throughline codex handoff start/);
180
198
  assert.match(result.stdout, /status:\s+ready/);
199
+ assert.match(result.stdout, /open requested:\s+auto/);
200
+ assert.match(result.stdout, /open resolved:\s+(desktop|vscode|cli)/);
181
201
  assert.match(result.stdout, /commands:/);
182
202
  assert.match(result.stdout, /## Throughline: New Codex Thread Handoff/);
183
203
  assert.match(result.stdout, /latest handoff start body/);
@@ -284,12 +304,18 @@ test('codex-handoff-start execute creates a new app-server thread and can skip o
284
304
  assert.equal(payload.status, 'started');
285
305
  assert.equal(payload.reason, 'new_thread_handoff_started');
286
306
  assert.equal(payload.execute, true);
307
+ assert.equal(payload.openHost, 'none');
308
+ assert.equal(payload.requestedOpenHost, 'none');
309
+ assert.equal(payload.resolvedOpenHost, 'none');
287
310
  assert.equal(payload.startThreadManually, false);
288
311
  assert.equal(payload.newThread.threadId, '019e2000-0000-7000-8000-000000000001');
289
312
  assert.equal(payload.newThread.delivery, 'developer-item');
290
313
  assert.equal(payload.newThread.injectSent, true);
291
314
  assert.equal(payload.newThread.turnStatus, 'not-started');
292
315
  assert.equal(payload.open.status, 'skipped');
316
+ assert.equal(payload.open.host, 'none');
317
+ assert.equal(payload.open.requestedHost, 'none');
318
+ assert.equal(payload.open.resolvedHost, 'none');
293
319
  assert.equal(
294
320
  payload.open.desktopUrl,
295
321
  'codex://threads/019e2000-0000-7000-8000-000000000001',
@@ -181,6 +181,10 @@ test('global install copies Throughline Codex skill to ~/.codex/skills/', async
181
181
  assert.match(skillBody, /name: throughline/);
182
182
  assert.match(skillBody, /Bare "\$throughline"/);
183
183
  assert.match(skillBody, /throughline codex-handoff-start --execute/);
184
+ assert.match(skillBody, /--open-host desktop/);
185
+ assert.match(skillBody, /--open-host vscode/);
186
+ assert.match(skillBody, /--open-host cli/);
187
+ assert.match(skillBody, /not from[\s\S]*persistent PTY/);
184
188
  assert.match(skillBody, /do not run doctor \/ dry-run \/ preflight first/);
185
189
  assert.match(metadataBody, /start a new Codex thread with Throughline handoff memory/);
186
190
  assert.doesNotMatch(metadataBody, /scripted current-thread rollback/);
@@ -0,0 +1,44 @@
1
+ import { CURRENT_VERSION, DatabaseMigrationError, migrateDefaultDb } from '../db.mjs';
2
+
3
+ export const MIGRATION_SCHEMA = 'throughline.database_migration.v1';
4
+
5
+ export function parseArgs(argv = []) {
6
+ if (argv.length !== 1 || argv[0] !== '--json') throw new TypeError('usage error');
7
+ return { json: true };
8
+ }
9
+
10
+ function result({ status, beforeSchemaVersion, afterSchemaVersion }) {
11
+ return {
12
+ schema: MIGRATION_SCHEMA,
13
+ status,
14
+ beforeSchemaVersion,
15
+ afterSchemaVersion,
16
+ supportedSchemaVersion: CURRENT_VERSION,
17
+ };
18
+ }
19
+
20
+ export function run(argv = [], { stdout = process.stdout, migrate = migrateDefaultDb } = {}) {
21
+ try {
22
+ parseArgs(argv);
23
+ } catch {
24
+ stdout.write(`${JSON.stringify(result({
25
+ status: 'invalid_request', beforeSchemaVersion: null, afterSchemaVersion: null,
26
+ }))}\n`);
27
+ return 2;
28
+ }
29
+
30
+ try {
31
+ stdout.write(`${JSON.stringify(result(migrate()))}\n`);
32
+ return 0;
33
+ } catch (error) {
34
+ const failure = error instanceof DatabaseMigrationError
35
+ ? error
36
+ : new DatabaseMigrationError('migration_failed', null, null);
37
+ stdout.write(`${JSON.stringify(result({
38
+ status: failure.code,
39
+ beforeSchemaVersion: failure.beforeSchemaVersion,
40
+ afterSchemaVersion: failure.afterSchemaVersion,
41
+ }))}\n`);
42
+ return 1;
43
+ }
44
+ }
@@ -0,0 +1,140 @@
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 { CURRENT_VERSION } from '../db.mjs';
11
+ import { MIGRATION_SCHEMA, parseArgs, run } from './migrate.mjs';
12
+
13
+ const REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url));
14
+ const BIN_PATH = join(REPO_ROOT, 'bin/throughline.mjs');
15
+
16
+ function runCli(home, args = ['migrate', '--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 createV8Db(home) {
25
+ const dir = join(home, '.throughline');
26
+ mkdirSync(dir, { recursive: true });
27
+ const db = new DatabaseSync(join(dir, 'throughline.db'));
28
+ db.exec(`
29
+ PRAGMA user_version = 8;
30
+ CREATE TABLE sessions (session_id TEXT PRIMARY KEY, project_path TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, merged_into TEXT);
31
+ CREATE TABLE skeletons (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, turn_number INTEGER NOT NULL, role TEXT NOT NULL, summary TEXT NOT NULL, created_at INTEGER NOT NULL, origin_session_id TEXT);
32
+ CREATE TABLE bodies (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, origin_session_id TEXT NOT NULL, turn_number INTEGER NOT NULL, role TEXT NOT NULL, text TEXT NOT NULL, token_count INTEGER, created_at INTEGER NOT NULL, UNIQUE(session_id, origin_session_id, turn_number, role));
33
+ CREATE TABLE details (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, turn_number INTEGER, tool_name TEXT NOT NULL, input_text TEXT, output_text TEXT, token_count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, origin_session_id TEXT, kind TEXT NOT NULL DEFAULT 'tool_input', source_id TEXT);
34
+ CREATE TABLE handoff_batons (project_path TEXT PRIMARY KEY, session_id TEXT NOT NULL, created_at INTEGER NOT NULL);
35
+ CREATE UNIQUE INDEX uq_skeletons_turn_v3 ON skeletons(session_id, origin_session_id, turn_number, role);
36
+ CREATE UNIQUE INDEX uq_details_source ON details(session_id, origin_session_id, source_id) WHERE source_id IS NOT NULL;
37
+ `);
38
+ db.close();
39
+ }
40
+
41
+ test('migrate CLI migrates a v8 fixture to the current schema', () => {
42
+ const home = mkdtempSync(join(tmpdir(), 'tl-migrate-v8-'));
43
+ try {
44
+ createV8Db(home);
45
+ const result = runCli(home);
46
+ assert.equal(result.status, 0, result.stderr);
47
+ assert.deepEqual(JSON.parse(result.stdout), {
48
+ schema: MIGRATION_SCHEMA,
49
+ status: 'migrated',
50
+ beforeSchemaVersion: 8,
51
+ afterSchemaVersion: CURRENT_VERSION,
52
+ supportedSchemaVersion: CURRENT_VERSION,
53
+ });
54
+ const db = new DatabaseSync(join(home, '.throughline', 'throughline.db'), { readOnly: true });
55
+ assert.equal(db.prepare('PRAGMA user_version').get().user_version, CURRENT_VERSION);
56
+ assert.deepEqual(db.prepare('PRAGMA table_info(pending_handoffs)').all().map((row) => row.name), [
57
+ 'session_id', 'project_path', 'source', 'auto_predecessor_id', 'created_at',
58
+ ]);
59
+ db.close();
60
+ } finally {
61
+ rmSync(home, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test('migrate CLI is idempotent for the current schema', () => {
66
+ const home = mkdtempSync(join(tmpdir(), 'tl-migrate-current-'));
67
+ try {
68
+ createV8Db(home);
69
+ assert.equal(runCli(home).status, 0);
70
+ const result = runCli(home);
71
+ assert.equal(result.status, 0, result.stderr);
72
+ assert.equal(JSON.parse(result.stdout).status, 'already_current');
73
+ } finally {
74
+ rmSync(home, { recursive: true, force: true });
75
+ }
76
+ });
77
+
78
+ test('migrate CLI does not create a missing database', () => {
79
+ const home = mkdtempSync(join(tmpdir(), 'tl-migrate-missing-'));
80
+ try {
81
+ const result = runCli(home);
82
+ assert.equal(result.status, 0, result.stderr);
83
+ assert.equal(JSON.parse(result.stdout).status, 'not_applicable');
84
+ assert.equal(existsSync(join(home, '.throughline')), false);
85
+ } finally {
86
+ rmSync(home, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test('migrate CLI rejects a future schema without changing it', () => {
91
+ const home = mkdtempSync(join(tmpdir(), 'tl-migrate-future-'));
92
+ try {
93
+ createV8Db(home);
94
+ const dbPath = join(home, '.throughline', 'throughline.db');
95
+ const db = new DatabaseSync(dbPath);
96
+ db.exec(`PRAGMA user_version = ${CURRENT_VERSION + 1}`);
97
+ db.close();
98
+ const result = runCli(home);
99
+ assert.equal(result.status, 1, result.stderr);
100
+ assert.deepEqual(JSON.parse(result.stdout), {
101
+ schema: MIGRATION_SCHEMA,
102
+ status: 'future_schema',
103
+ beforeSchemaVersion: CURRENT_VERSION + 1,
104
+ afterSchemaVersion: CURRENT_VERSION + 1,
105
+ supportedSchemaVersion: CURRENT_VERSION,
106
+ });
107
+ const verify = new DatabaseSync(dbPath, { readOnly: true });
108
+ assert.equal(verify.prepare('PRAGMA user_version').get().user_version, CURRENT_VERSION + 1);
109
+ verify.close();
110
+ } finally {
111
+ rmSync(home, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ test('migrate CLI accepts only --json and does not reflect arguments', () => {
116
+ for (const argv of [[], ['--json', '--json'], ['--db', '/private/secret.db', '--json']]) {
117
+ const output = [];
118
+ assert.equal(run(argv, { stdout: { write(value) { output.push(value); } } }), 2);
119
+ assert.equal(JSON.parse(output[0]).status, 'invalid_request');
120
+ assert.doesNotMatch(output[0], /private|secret/);
121
+ }
122
+ assert.deepEqual(parseArgs(['--json']), { json: true });
123
+ assert.throws(() => parseArgs([]), /usage error/);
124
+ });
125
+
126
+ test('migrate CLI reports an internal migration failure without reflecting its cause', () => {
127
+ const output = [];
128
+ assert.equal(run(['--json'], {
129
+ stdout: { write(value) { output.push(value); } },
130
+ migrate() { throw new Error('/private/throughline.db contents'); },
131
+ }), 1);
132
+ assert.deepEqual(JSON.parse(output[0]), {
133
+ schema: MIGRATION_SCHEMA,
134
+ status: 'migration_failed',
135
+ beforeSchemaVersion: null,
136
+ afterSchemaVersion: null,
137
+ supportedSchemaVersion: CURRENT_VERSION,
138
+ });
139
+ assert.doesNotMatch(output[0], /private|contents/);
140
+ });
package/src/db.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { DatabaseSync } from 'node:sqlite';
6
- import { mkdirSync } from 'fs';
6
+ import { existsSync, mkdirSync } from 'fs';
7
7
  import { homedir } from 'os';
8
8
  import { join } from 'path';
9
9
 
@@ -240,6 +240,74 @@ function initSchema(db) {
240
240
  }
241
241
  }
242
242
 
243
+ /**
244
+ * 既存の Throughline DB だけを現在 schema へ移行する。
245
+ * 通常の getDb() と異なり、DB や親ディレクトリを作成しない。
246
+ *
247
+ * @returns {{ status: 'not_applicable' | 'already_current' | 'migrated', beforeSchemaVersion: number | null, afterSchemaVersion: number | null, supportedSchemaVersion: number }}
248
+ */
249
+ export function migrateDefaultDb() {
250
+ if (!existsSync(DB_PATH)) {
251
+ return {
252
+ status: 'not_applicable',
253
+ beforeSchemaVersion: null,
254
+ afterSchemaVersion: null,
255
+ supportedSchemaVersion: CURRENT_VERSION,
256
+ };
257
+ }
258
+
259
+ let db;
260
+ try {
261
+ db = new DatabaseSync(DB_PATH);
262
+ db.exec(`PRAGMA busy_timeout = ${DB_BUSY_TIMEOUT_MS}`);
263
+ db.exec('PRAGMA foreign_keys = ON');
264
+
265
+ const beforeSchemaVersion = Number(db.prepare('PRAGMA user_version').get().user_version ?? 0);
266
+ if (beforeSchemaVersion > CURRENT_VERSION) {
267
+ throw new DatabaseMigrationError('future_schema', beforeSchemaVersion, beforeSchemaVersion);
268
+ }
269
+
270
+ if (beforeSchemaVersion === CURRENT_VERSION) {
271
+ return {
272
+ status: 'already_current',
273
+ beforeSchemaVersion,
274
+ afterSchemaVersion: beforeSchemaVersion,
275
+ supportedSchemaVersion: CURRENT_VERSION,
276
+ };
277
+ }
278
+
279
+ const journalMode = db.prepare('PRAGMA journal_mode').get().journal_mode;
280
+ if (String(journalMode).toLowerCase() !== 'wal') {
281
+ db.exec('PRAGMA journal_mode = WAL');
282
+ }
283
+ initSchema(db);
284
+ const afterSchemaVersion = Number(db.prepare('PRAGMA user_version').get().user_version ?? 0);
285
+ if (afterSchemaVersion !== CURRENT_VERSION) {
286
+ throw new DatabaseMigrationError('version_mismatch', beforeSchemaVersion, afterSchemaVersion);
287
+ }
288
+ return {
289
+ status: 'migrated',
290
+ beforeSchemaVersion,
291
+ afterSchemaVersion,
292
+ supportedSchemaVersion: CURRENT_VERSION,
293
+ };
294
+ } catch (error) {
295
+ if (error instanceof DatabaseMigrationError) throw error;
296
+ throw new DatabaseMigrationError('migration_failed', null, null);
297
+ } finally {
298
+ db?.close();
299
+ }
300
+ }
301
+
302
+ export class DatabaseMigrationError extends Error {
303
+ constructor(code, beforeSchemaVersion, afterSchemaVersion) {
304
+ super(code);
305
+ this.code = code;
306
+ this.beforeSchemaVersion = beforeSchemaVersion;
307
+ this.afterSchemaVersion = afterSchemaVersion;
308
+ }
309
+ }
310
+
243
311
  /**
244
312
  * DB インスタンスを返す(シングルトン)
245
313
  * @returns {DatabaseSync}