throughline 0.8.6 → 0.8.8
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 +31 -1
- package/README.ja.md +11 -0
- package/README.md +12 -0
- package/docs/00_overview.md +2 -0
- package/docs/04_public_release_plan.md +2 -0
- package/docs/05_codex_first_roadmap.md +2 -2
- package/docs/15_windows_ci_release_latency_plan.md +89 -0
- package/docs/adr/0019-product-owned-database-migration-acceptance.md +54 -0
- package/docs/adr/0020-windows-ci-release-latency.md +56 -0
- package/package.json +9 -2
- package/src/cli/doctor.mjs +45 -36
- package/src/cli/doctor.test.mjs +37 -3
- package/src/cli/factory-diagnostics.mjs +5 -5
- package/src/cli/factory-diagnostics.test.mjs +30 -6
- package/src/cli/install.mjs +3 -3
- package/src/cli/install.test.mjs +14 -9
- package/src/completed-turn-receipts-test-fixture.mjs +54 -0
- package/src/completed-turn-receipts.mjs +9 -3
- package/src/completed-turn-receipts.test.mjs +75 -7
- package/src/observer-turn-feed.test.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,34 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.8.8] — 2026-08-02
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Codex hook installation now writes the supported `timeout` field in seconds
|
|
18
|
+
for UserPromptSubmit, PostToolUse, and Stop. The previously emitted
|
|
19
|
+
`timeoutSec` field was ignored by Codex, leaving all three hooks at the
|
|
20
|
+
600-second default.
|
|
21
|
+
- Reinstall canonicalizes existing Throughline-managed hooks by command
|
|
22
|
+
identity, replacing legacy `timeoutSec` entries while preserving unrelated
|
|
23
|
+
Codex hooks. Doctor reports the effective `timeout` field and explicitly
|
|
24
|
+
flags the legacy key for reinstall; factory diagnostics no longer classify
|
|
25
|
+
the ignored key as ready.
|
|
26
|
+
|
|
27
|
+
## [0.8.7] — 2026-07-20
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Windows CI no longer calls the completed-turn receipt ACL PowerShell path
|
|
32
|
+
hundreds of times just to construct 256-record boundary fixtures. Boundary
|
|
33
|
+
behavior still crosses the limit through the public API, while dedicated
|
|
34
|
+
tests preserve native owner-only ACL verification and atomic failure safety.
|
|
35
|
+
- Completed-turn receipt mutations now spend ACL subprocesses only on distinct
|
|
36
|
+
state transitions: three for a new Windows store and four for a replacement.
|
|
37
|
+
- CI keeps the full 3-OS by 3-Node matrix, cancels only older runs for the same
|
|
38
|
+
event and ref, and gives the unit-test step an eight-minute regression guard
|
|
39
|
+
around a five-minute Windows SLO.
|
|
40
|
+
|
|
13
41
|
## [0.8.6] — 2026-07-20
|
|
14
42
|
|
|
15
43
|
### Fixed
|
|
@@ -1163,7 +1191,9 @@ two attempts, instrument first instead of patching again.
|
|
|
1163
1191
|
|
|
1164
1192
|
---
|
|
1165
1193
|
|
|
1166
|
-
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.
|
|
1194
|
+
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.8...HEAD
|
|
1195
|
+
[0.8.8]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.7...v0.8.8
|
|
1196
|
+
[0.8.7]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.6...v0.8.7
|
|
1167
1197
|
[0.8.6]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.5...v0.8.6
|
|
1168
1198
|
[0.8.5]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.4...v0.8.5
|
|
1169
1199
|
[0.8.4]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.3...v0.8.4
|
package/README.ja.md
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
> **Claude Code のコンテキスト消費を約 90% 削減しつつ、記憶はほぼそのまま残す。**
|
|
15
15
|
> 「時間の新旧」ではなく **「コンテンツの種類」** で会話を分離する。人間が読みたいテキストは残し、機械が出力したツール I/O は SQLite に退避する。同じ判断、同じ文脈、9 割軽量。
|
|
16
16
|
|
|
17
|
+
[kitepon.dev](https://kitepon.dev/)を運営する[クオ(@QLyun35332)](https://x.com/QLyun35332)が
|
|
18
|
+
開発・メンテナンスしています。
|
|
19
|
+
|
|
20
|
+
## 所有境界
|
|
21
|
+
|
|
22
|
+
本repositoryはdatabase、migration、capture契約、release、diagnosticsを所有します。
|
|
23
|
+
製品横断の導入とhost統合は、kitepon.devの製品開発を支える内部基盤
|
|
24
|
+
[dotagents](https://github.com/kitepon-rgb/dotagents)が担当します。
|
|
25
|
+
MarkItDownは別区分の第三者CLIです。
|
|
26
|
+
|
|
17
27
|
## 30 秒で始める
|
|
18
28
|
|
|
19
29
|
```bash
|
|
@@ -363,6 +373,7 @@ Throughline state をまだ書いていない現在セッションも表示で
|
|
|
363
373
|
- [`docs/09_rollback_context_trim_insight.md`](docs/09_rollback_context_trim_insight.md) — rollback / trim 設計 insight。復元 memory を current work として読ませる制約も記録
|
|
364
374
|
- [`docs/07_codex_trim_implementation_plan.md`](docs/07_codex_trim_implementation_plan.md) — Claude/Codex 両対応と rollback trim の統合 TODO 計画
|
|
365
375
|
- [`docs/04_public_release_plan.md`](docs/04_public_release_plan.md) — 公開配布化プラン、§ 0 フォールバック禁止ルール、バージョン別実装ステータス
|
|
376
|
+
- [`docs/15_windows_ci_release_latency_plan.md`](docs/15_windows_ci_release_latency_plan.md) — Windows CI性能gateとACL契約を維持するrelease工程
|
|
366
377
|
- [`CHANGELOG.md`](CHANGELOG.md) — リリース履歴
|
|
367
378
|
- [`docs/archive/`](docs/archive/) — 破棄済み旧設計 (CONCEPT 初期案、session-linking 実験記録など)
|
|
368
379
|
|
package/README.md
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
> **Cut ~90% of Claude Code's context usage while keeping nearly all the memory.**
|
|
15
15
|
> Throughline separates conversation by **type, not time** — humans-readable text stays, machine-generated tool output retires to SQLite. Same decisions, same context, 90% lighter.
|
|
16
16
|
|
|
17
|
+
Built and maintained by [Quo](https://x.com/QLyun35332) at [kitepon.dev](https://kitepon.dev/en).
|
|
18
|
+
|
|
19
|
+
## Ownership boundary
|
|
20
|
+
|
|
21
|
+
This repository owns the database, migrations, capture contracts, release, and
|
|
22
|
+
diagnostics. Cross-product installation and host integration are handled by
|
|
23
|
+
[dotagents](https://github.com/kitepon-rgb/dotagents), the internal development
|
|
24
|
+
toolchain behind kitepon.dev's products.
|
|
25
|
+
MarkItDown is a separately managed third-party CLI.
|
|
26
|
+
|
|
17
27
|
## In 30 seconds
|
|
18
28
|
|
|
19
29
|
```bash
|
|
@@ -1148,6 +1158,8 @@ the first generation to pick up the auto-start task.
|
|
|
1148
1158
|
- [`docs/04_public_release_plan.md`](docs/04_public_release_plan.md) — public
|
|
1149
1159
|
release plan, implementation status by version, § 0 fallback rule, and
|
|
1150
1160
|
remaining tasks.
|
|
1161
|
+
- [`docs/15_windows_ci_release_latency_plan.md`](docs/15_windows_ci_release_latency_plan.md) —
|
|
1162
|
+
Windows CI performance gate and the ACL-preserving release workflow.
|
|
1151
1163
|
- [`docs/archive/`](docs/archive/) — superseded design documents kept for
|
|
1152
1164
|
historical reference (original CONCEPT, session-linking experiments,
|
|
1153
1165
|
pre-publish action list).
|
package/docs/00_overview.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
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
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は継続中 |
|
|
22
|
+
| [15_windows_ci_release_latency_plan.md](15_windows_ci_release_latency_plan.md) | Windows CI 18分の原因、ACL安全網を維持した短縮、release gateの受入条件 |
|
|
22
23
|
| [BUGHUB_RUNTIME_ERROR_STORE_PLAN.md](BUGHUB_RUNTIME_ERROR_STORE_PLAN.md) | local runtime error aggregate store の契約と実装 TODO |
|
|
23
24
|
|
|
24
25
|
## Supporting Records
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
- [ADR 0001](adr/0001-claude-primary-codex-adapter.md): Claude primaryを維持し、Codexをadapterとして追加する。
|
|
36
37
|
- [ADR 0002](adr/0002-observer-claude-completion-receipt.md): Claude completed turnはThroughline所有のStop receiptで固定する。
|
|
37
38
|
- [ADR 0003](adr/0003-observer-completed-chain-cursor.md): Observer cursorをhost固有のcompleted pair chainとprefix検証へ束縛する。
|
|
39
|
+
- [ADR 0020](adr/0020-windows-ci-release-latency.md): Windows ACL契約を維持し、境界fixtureと実ACL検証を分離する。
|
|
38
40
|
|
|
39
41
|
Observerの公開境界は`throughline observer-read`/`throughline observer-wait`のJSON-only CLIである。
|
|
40
42
|
ThroughlineはClaude Stop receiptとCodex rolloutの`task_complete`だけからcompleted cursorを構築し、
|
|
@@ -142,6 +142,8 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
|
|
|
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
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
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
|
+
| **公開済み (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
|
+
| **release準備 (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は変更しない |
|
|
145
147
|
| **未リリース: 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` も有効化する |
|
|
146
148
|
| **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
|
|
147
149
|
|
|
@@ -456,9 +456,9 @@ Phase 5 implementation status (2026-05-06):
|
|
|
456
456
|
- 修正: Caveat の Codex Stop hook は `async: false` で登録され、nested `codex exec` smoke で Stop -> 次 turn 注入まで確認済みだった。Throughline は Claude Stop と同じ発想で Codex Stop も `async: true` にしていたため、Codex 側だけ Caveat と同じ同期 hook に寄せた。既に `async: true` で登録済みの Throughline Codex Stop hook も次回 `throughline install` で `async: false` に更新される。
|
|
457
457
|
- 再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Throughline Codex Stop は `async: false` になり、Caveat / Spotter hooks は保持された。`codex exec --json -C /home/kite/projects/Throughline "Reply exactly: TL_CODEX_SYNC_STOP_SMOKE_20260506"` で child thread `019dfd4f-93ff-7522-8f89-bd1e1996c8d7` が作られ、直後の `throughline doctor --codex` で latest DB session が `codex:019dfd4f-93ff-7522-8f89-bd1e1996c8d7` に進んだ。Codex Stop hook の自然 capture は同期登録で確認済み。
|
|
458
458
|
- 追加修正: Caveat の Codex hook は bare `caveat` ではなく絶対 node + installed CLI script path で登録されていた。Throughline も Codex App Server / VSCode host の PATH 差分を避けるため、旧 bare `throughline codex-hook stop` を次回 install で絶対 node + `bin/throughline.mjs codex-hook stop` に置換する。`doctor --codex` は Codex hook feature / Throughline Stop hook command / legacy bare command を診断表示する。
|
|
459
|
-
- 絶対パス型の再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Stop 先頭は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false
|
|
459
|
+
- 絶対パス型の再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Stop 先頭は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false`になり、Caveat / Spotter hooks は 2 番目以降に保持された。2026-08-02 correction: 当時記録した`timeoutSec: 300`はCodexに無視される誤keyで、v0.8.8から正規の`timeout: 300`へ修正した。`doctor --codex` は `Codex hooks feature: enabled` / `Codex Stop hook: registered` を表示した。`codex exec --json -C /home/kite/projects/Throughline "Reply exactly: TL_CODEX_ABSOLUTE_STOP_SMOKE_20260506"` で child thread `019dfd5e-1248-7c11-8ddc-97e1b0701e10` が作られ、直後の `throughline doctor --codex` で latest DB session が `codex:019dfd5e-1248-7c11-8ddc-97e1b0701e10` に進んだ。
|
|
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
|
-
- 最終確認: 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
|
|
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`だった。2026-08-02 correction: 当時の`timeoutSec: 300`は無効で、v0.8.8の再install後は`timeout: 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
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 前に拒否する。
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
突出していた。TAP durationはwall timeとして加算せず、job全体の実測約18分をbaselineとする。
|
|
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: 8`を設定する。SLOは5分、目標3分としrunner遅延と分離する。
|
|
37
|
+
4. workflowへevent/ref単位の`concurrency`を設定し、新しいcommitが来た時は同一event/refの
|
|
38
|
+
古いrunだけをcancelする。手動runとpush/PRは相互cancelしない。
|
|
39
|
+
5. OS 3種 × Node `22.13.0`/`22.x`/`24.x`の9 matrixは維持する。互換範囲を速度対策のために
|
|
40
|
+
縮小しない。
|
|
41
|
+
6. `0.8.7`の最終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はunit test stepへ8分で置き、5分SLOにrunner一時遅延の余白を持たせる。
|
|
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 8分上限とevent/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.7`、tag/GitHub Release、registry由来global install、
|
|
72
|
+
`throughline --version = 0.8.7`、配置skill/hooks/doctorを確認する。
|
|
73
|
+
|
|
74
|
+
## 敵対的検証の反映
|
|
75
|
+
|
|
76
|
+
read-only refuterはP0なし、P1を4件報告した。TAP durationをwall timeとして加算しないこと、
|
|
77
|
+
temporary ACL失敗時の旧store保持testを加えること、concurrencyへ`event_name`を含めること、
|
|
78
|
+
5分SLOとtimeoutを分離することを採用した。fixture seedと本体の重複ACL検証削減は、最後の公開API
|
|
79
|
+
mutation、native final-path ACL test、atomic failure testを同時に置く条件で妥当と裁定した。
|
|
80
|
+
|
|
81
|
+
## 工程
|
|
82
|
+
|
|
83
|
+
工程状態と完了証拠の正本はLattice storeとし、この文書は目的、判断、非目標、受入条件を所有する。
|
|
84
|
+
|
|
85
|
+
1. ベースライン計測と原因同定
|
|
86
|
+
2. 敵対的検証と設計裁定
|
|
87
|
+
3. 安全網とfixture分離
|
|
88
|
+
4. workflow・文書統合
|
|
89
|
+
5. push後CI、npm公開、global install
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# ADR 0019: 製品所有DB migrationの公開受入
|
|
2
|
+
|
|
3
|
+
## 状態
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-07-20
|
|
6
|
+
|
|
7
|
+
## 親裁定
|
|
8
|
+
|
|
9
|
+
`throughline migrate --json` の実装と公開版 `0.8.6` を、Throughlineの製品所有DB migration入口として
|
|
10
|
+
受け入れる。`factory-diagnostics`のread-only契約、DB不在時に作成しない契約、future schema拒否を維持する。
|
|
11
|
+
|
|
12
|
+
## 実装・回帰証拠
|
|
13
|
+
|
|
14
|
+
- 設計: [ADR 0018](0018-product-owned-database-migration.md)
|
|
15
|
+
- 実装commit: `6fd9554`
|
|
16
|
+
- clean republish commit: `4f9908d`
|
|
17
|
+
- focused test: DB schema、factory diagnostics、migrate CLIの16件すべてgreen
|
|
18
|
+
- full test: 実装受入時の `npm test` green
|
|
19
|
+
- GitHub CI: run `29724249940`、Node 22.13.0/22.x/24.x × macOS/Ubuntu/Windowsの9/9 green
|
|
20
|
+
- clean pack: 208 files、`src/cli/migrate.mjs`を収録し、並行作業の
|
|
21
|
+
`docs/15_windows_ci_release_latency_plan.md`を含まないことを機械確認
|
|
22
|
+
|
|
23
|
+
## 公開証拠
|
|
24
|
+
|
|
25
|
+
- npm `latest`: `throughline@0.8.6`
|
|
26
|
+
- registry shasum: `b4a9ccda9b69715d51c37408d696a210103ee47a`
|
|
27
|
+
- tag / GitHub Release: `v0.8.6`
|
|
28
|
+
- `0.8.5`はrelease dry-run後に別セッションの未コミット文書1件がworktreeへ書かれ、tarballへ混入した。
|
|
29
|
+
runtime codeは同一だったがpackage scopeを不合格とし、tag / GitHub Releaseを作らずnpm deprecateした。
|
|
30
|
+
`0.8.6`は受理済みcommitから作った隔離clean worktreeで再pack・再公開した。
|
|
31
|
+
|
|
32
|
+
## 4 host受入
|
|
33
|
+
|
|
34
|
+
| host | version | migration | DB schema | Throughline factory check |
|
|
35
|
+
|---|---:|---|---|---|
|
|
36
|
+
| mac-kite | 0.8.6 | `already_current` | 9 / 9 | ready |
|
|
37
|
+
| main-server | 0.8.6 | `already_current` | 9 / 9 | ready |
|
|
38
|
+
| FOX WSL2 | 0.8.6 | `already_current` | 9 / 9 | ready |
|
|
39
|
+
| FOX Windows native | 0.8.6 | `already_current` | 9 / 9 | ready |
|
|
40
|
+
|
|
41
|
+
FOX Windows nativeは旧command形のCodex hook 3本を検出したため、`throughline install`でThroughline管理分だけを
|
|
42
|
+
現行形へ正規化し、再投影後に`database_schema=pass`、`codex_hooks=pass`を確認した。UI trustは人手領域として
|
|
43
|
+
変更していない。
|
|
44
|
+
|
|
45
|
+
dotagentsのfactory reporter全体は、Caveat native diagnostics、Codex routing、toolchain ledger、
|
|
46
|
+
main-server stale ingestという既存の別checkによりredのままである。Throughline migration/schema/hooksとは
|
|
47
|
+
分離し、他製品の修理を本受入へ混入させない。Lattice checkは全hostでpassし、Lattice本体は変更していない。
|
|
48
|
+
|
|
49
|
+
## H操作の記録とrollback
|
|
50
|
+
|
|
51
|
+
目的は更新直後のDB migrationを製品所有入口で確定し、全hostへ配布すること。影響はnpm `latest`、Git tag / Release、
|
|
52
|
+
4hostのglobal Throughline、FOX WindowsのThroughline管理hook/skillである。rollbackはnpm `latest`とglobal installを
|
|
53
|
+
0.8.4へ戻し、hookは`throughline uninstall`または保存済み設定backupから復元する。DB schemaの自動downgradeはせず、
|
|
54
|
+
必要時は更新前DB backupから復元する。オーナーの本戦役H承認に基づき実行した。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ADR 0020: Windows CI release latency
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-07-20
|
|
5
|
+
- Scope: Throughline v0.8.7 release gate
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
GitHub Actions run `29722650046` のWindows jobは約18分を要した。TAP上で突出した3テストは、
|
|
10
|
+
completed-turn receiptの256件境界を作るためproduction APIを257回呼び、mutationごとに
|
|
11
|
+
PowerShellでdirectory、SQLite lock、temporary、final storeのowner-only ACLを適用・検証していた。
|
|
12
|
+
境界計算の反復とWindows実ACLのintegration検証が結合され、短時間のmain pushでは9 job matrixが
|
|
13
|
+
重複起動していた。
|
|
14
|
+
|
|
15
|
+
read-only refuterによる敵対的検証ではP0はなく、次のP1を設計へ反映した。
|
|
16
|
+
|
|
17
|
+
- temporary ACL失敗時に旧storeを保持する回帰testが不足していた。
|
|
18
|
+
- refだけのconcurrency groupはmain pushと`workflow_dispatch`を相互cancelし得る。
|
|
19
|
+
- 5分SLOと同値のstep timeoutは共有runnerの一時遅延でflakeし得る。
|
|
20
|
+
- TAP durationの合計をjob wall timeとして扱ってはならない。
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
1. 大量境界testは、production APIによる最初のprivate store作成後に正規schemaの256件fixtureをseedし、
|
|
25
|
+
257件目だけをproduction APIで追加する。limit、history floor、project isolation、observer cursorの
|
|
26
|
+
公開契約は変えない。
|
|
27
|
+
2. Windows ACLは三層で固定する。mock testで初回3回・継続4回のdistinct transitionを検査し、
|
|
28
|
+
temporary ACL失敗時の旧store保持を失敗注入で検査し、Windows native testでdirectory、lock、
|
|
29
|
+
final storeのowner-only ACLを外部helperから検査する。
|
|
30
|
+
3. completed-turn receipt本体は、apply scriptが同じPowerShell process内でread-back済みの新規
|
|
31
|
+
directory/lock/temporaryを直後に再検証しない。ACL済みtemporaryのrename後はshapeだけを
|
|
32
|
+
in-process検査し、final pathの実ACLはnative integration testで証明する。
|
|
33
|
+
4. CIはOS 3種 × Node `22.13.0`/`22.x`/`24.x`の9 matrixを維持する。unit test SLOは5分、
|
|
34
|
+
目標3分、step timeoutは8分とする。
|
|
35
|
+
5. concurrency groupはworkflow、event、refで分離する。同じpush refまたは同じPR refの旧runだけを
|
|
36
|
+
cancelし、`workflow_dispatch`とpush/PRは相互cancelしない。
|
|
37
|
+
6. 公開commitのCI番号・npm shasum・global install証拠はGitHub Releaseへ置く。証拠追記だけの
|
|
38
|
+
Git commitと追加CIは作らない。
|
|
39
|
+
|
|
40
|
+
## Compatibility
|
|
41
|
+
|
|
42
|
+
- completed-turn receiptのschema、256件limit、pair identity、atomic rename、fail-closed ACLを維持する。
|
|
43
|
+
- Windows、macOS、LinuxおよびNode matrixを減らさない。
|
|
44
|
+
- Claude hooks、Codex adapter、Observer JSON-only CLI、DB migrationの挙動を変更しない。
|
|
45
|
+
|
|
46
|
+
## Rollback
|
|
47
|
+
|
|
48
|
+
公開前はfixture、本体最適化、workflow変更を同時にrevertする。公開後に戻す場合もACL安全網testを
|
|
49
|
+
先に外さず、旧実装へ戻したcandidateの9 matrixを通してから公開する。store migrationは不要である。
|
|
50
|
+
|
|
51
|
+
## Non-goals
|
|
52
|
+
|
|
53
|
+
- Windows testやACL検査のskip
|
|
54
|
+
- matrix縮小
|
|
55
|
+
- npm自動publish、credential保管
|
|
56
|
+
- release番号だけを根拠にした性能成功扱い
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "throughline",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
|
|
6
6
|
"keywords": [
|
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
"context-compression",
|
|
10
10
|
"llm"
|
|
11
11
|
],
|
|
12
|
-
"author":
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Quo / クオ at kitepon.dev",
|
|
14
|
+
"url": "https://kitepon.dev/"
|
|
15
|
+
},
|
|
13
16
|
"license": "MIT",
|
|
14
17
|
"repository": {
|
|
15
18
|
"type": "git",
|
|
16
19
|
"url": "git+https://github.com/kitepon-rgb/Throughline.git"
|
|
17
20
|
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/kitepon-rgb/Throughline/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/kitepon-rgb/Throughline#readme",
|
|
18
25
|
"bin": {
|
|
19
26
|
"throughline": "bin/throughline.mjs"
|
|
20
27
|
},
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -480,6 +480,31 @@ function summarizeHookTrust(hooks, { hooksFeatureEnabled, codexHooksFeatureEnabl
|
|
|
480
480
|
};
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
function hasLegacyCodexHookTimeout(hook) {
|
|
484
|
+
return Object.hasOwn(hook, 'timeoutSec');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function describeCodexHookRegistration(hooks, legacyCommandHooks, expectedTimeout) {
|
|
488
|
+
if (hooks.length === 0) return 'not registered';
|
|
489
|
+
if (legacyCommandHooks.length > 0) return 'legacy command needs reinstall';
|
|
490
|
+
if (hooks.some(hasLegacyCodexHookTimeout)) return 'legacy timeout key needs reinstall';
|
|
491
|
+
if (hooks.length !== 1) return 'configuration needs reinstall';
|
|
492
|
+
const hook = hooks[0];
|
|
493
|
+
return hook.type === 'command' && hook.timeout === expectedTimeout && hook.async === false
|
|
494
|
+
? 'registered'
|
|
495
|
+
: 'configuration needs reinstall';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function printCodexHookDetails(hook) {
|
|
499
|
+
console.log(` command: ${hook.command}`);
|
|
500
|
+
console.log(` async: ${hook.async === false ? 'false' : String(hook.async)}`);
|
|
501
|
+
console.log(` timeout: ${hook.timeout ?? '(default: 600)'}`);
|
|
502
|
+
if (hasLegacyCodexHookTimeout(hook)) {
|
|
503
|
+
console.log(` legacy timeoutSec: ${hook.timeoutSec} (ignored by Codex; reinstall required)`);
|
|
504
|
+
}
|
|
505
|
+
console.log(` trusted: ${hook.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
483
508
|
function readCodexHookDiagnosis(codexHome) {
|
|
484
509
|
const hooksPath = join(codexHome, 'hooks.json');
|
|
485
510
|
const configPath = join(codexHome, 'config.toml');
|
|
@@ -599,47 +624,29 @@ function runCodexDiagnosis({
|
|
|
599
624
|
console.log(` CODEX_HOME: ${codexHome}`);
|
|
600
625
|
console.log(` Codex hooks feature: ${hookDiagnosis.featureEnabled ? 'enabled' : 'not enabled'}`);
|
|
601
626
|
console.log(` Codex hook trust: ${hookDiagnosis.managedHookTrust.status}`);
|
|
602
|
-
console.log(` Codex UserPrompt hook: ${
|
|
603
|
-
hookDiagnosis.managedPromptHooks
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
: 'registered'
|
|
608
|
-
}`);
|
|
627
|
+
console.log(` Codex UserPrompt hook: ${describeCodexHookRegistration(
|
|
628
|
+
hookDiagnosis.managedPromptHooks,
|
|
629
|
+
hookDiagnosis.legacyManagedPromptHooks,
|
|
630
|
+
30,
|
|
631
|
+
)}`);
|
|
609
632
|
if (hookDiagnosis.managedPromptHooks.length > 0) {
|
|
610
|
-
|
|
611
|
-
console.log(` command: ${h.command}`);
|
|
612
|
-
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
613
|
-
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
614
|
-
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
633
|
+
printCodexHookDetails(hookDiagnosis.managedPromptHooks[0]);
|
|
615
634
|
}
|
|
616
|
-
console.log(` Codex PostTool hook: ${
|
|
617
|
-
hookDiagnosis.managedPostToolUseHooks
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
: 'registered'
|
|
622
|
-
}`);
|
|
635
|
+
console.log(` Codex PostTool hook: ${describeCodexHookRegistration(
|
|
636
|
+
hookDiagnosis.managedPostToolUseHooks,
|
|
637
|
+
hookDiagnosis.legacyManagedPostToolUseHooks,
|
|
638
|
+
30,
|
|
639
|
+
)}`);
|
|
623
640
|
if (hookDiagnosis.managedPostToolUseHooks.length > 0) {
|
|
624
|
-
|
|
625
|
-
console.log(` command: ${h.command}`);
|
|
626
|
-
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
627
|
-
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
628
|
-
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
641
|
+
printCodexHookDetails(hookDiagnosis.managedPostToolUseHooks[0]);
|
|
629
642
|
}
|
|
630
|
-
console.log(` Codex Stop hook: ${
|
|
631
|
-
hookDiagnosis.managedStopHooks
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
: 'registered'
|
|
636
|
-
}`);
|
|
643
|
+
console.log(` Codex Stop hook: ${describeCodexHookRegistration(
|
|
644
|
+
hookDiagnosis.managedStopHooks,
|
|
645
|
+
hookDiagnosis.legacyManagedStopHooks,
|
|
646
|
+
300,
|
|
647
|
+
)}`);
|
|
637
648
|
if (hookDiagnosis.managedStopHooks.length > 0) {
|
|
638
|
-
|
|
639
|
-
console.log(` command: ${h.command}`);
|
|
640
|
-
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
641
|
-
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
642
|
-
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
649
|
+
printCodexHookDetails(hookDiagnosis.managedStopHooks[0]);
|
|
643
650
|
}
|
|
644
651
|
console.log(` VSCode monitor task: ${monitorTaskDiagnosis.status}`);
|
|
645
652
|
if (monitorTaskDiagnosis.path) {
|
|
@@ -1024,6 +1031,8 @@ export const _internal = {
|
|
|
1024
1031
|
buildCodexContextRefreshDiagnosis,
|
|
1025
1032
|
readCodexHostPrimitiveDiagnosis,
|
|
1026
1033
|
readCodexHookDiagnosis,
|
|
1034
|
+
describeCodexHookRegistration,
|
|
1035
|
+
printCodexHookDetails,
|
|
1027
1036
|
readVsCodeMonitorTaskDiagnosis,
|
|
1028
1037
|
isPidAlive,
|
|
1029
1038
|
findLatestJsonlInSameDir,
|
package/src/cli/doctor.test.mjs
CHANGED
|
@@ -19,8 +19,42 @@ const {
|
|
|
19
19
|
runTrimDiagnosis,
|
|
20
20
|
buildCodexContextRefreshDiagnosis,
|
|
21
21
|
readCodexHostPrimitiveDiagnosis,
|
|
22
|
+
describeCodexHookRegistration,
|
|
23
|
+
printCodexHookDetails,
|
|
22
24
|
} = _internal;
|
|
23
25
|
|
|
26
|
+
test('Codex hook diagnosis requires reinstall for legacy timeoutSec key', () => {
|
|
27
|
+
const legacy = { type: 'command', command: 'canonical', timeoutSec: 300, async: false };
|
|
28
|
+
assert.equal(
|
|
29
|
+
describeCodexHookRegistration([legacy], [], 300),
|
|
30
|
+
'legacy timeout key needs reinstall',
|
|
31
|
+
);
|
|
32
|
+
assert.equal(
|
|
33
|
+
describeCodexHookRegistration([{ type: 'command', command: 'canonical', timeout: 300, async: false }], [], 300),
|
|
34
|
+
'registered',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Codex hook diagnosis prints timeout and exposes ignored legacy key', () => {
|
|
39
|
+
const lines = [];
|
|
40
|
+
const originalLog = console.log;
|
|
41
|
+
console.log = (line) => lines.push(line);
|
|
42
|
+
try {
|
|
43
|
+
printCodexHookDetails({
|
|
44
|
+
command: 'canonical',
|
|
45
|
+
timeout: 300,
|
|
46
|
+
timeoutSec: 30,
|
|
47
|
+
async: false,
|
|
48
|
+
throughlineDoctorTrusted: true,
|
|
49
|
+
});
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = originalLog;
|
|
52
|
+
}
|
|
53
|
+
const output = lines.join('\n');
|
|
54
|
+
assert.match(output, /timeout:\s+300/);
|
|
55
|
+
assert.match(output, /legacy timeoutSec:\s+30 \(ignored by Codex; reinstall required\)/);
|
|
56
|
+
});
|
|
57
|
+
|
|
24
58
|
// ─── parseArgs ──────────────────────────────────────────────────────
|
|
25
59
|
|
|
26
60
|
test('parseArgs: 引数なしは session null', () => {
|
|
@@ -345,7 +379,7 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
345
379
|
{
|
|
346
380
|
type: 'command',
|
|
347
381
|
command: '/usr/bin/node /pkg/bin/throughline.mjs codex-hook user-prompt-submit',
|
|
348
|
-
|
|
382
|
+
timeout: 30,
|
|
349
383
|
async: false,
|
|
350
384
|
},
|
|
351
385
|
],
|
|
@@ -357,7 +391,7 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
357
391
|
{
|
|
358
392
|
type: 'command',
|
|
359
393
|
command: '/usr/bin/node /pkg/bin/throughline.mjs codex-hook post-tool-use',
|
|
360
|
-
|
|
394
|
+
timeout: 30,
|
|
361
395
|
async: false,
|
|
362
396
|
},
|
|
363
397
|
],
|
|
@@ -369,7 +403,7 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
369
403
|
{
|
|
370
404
|
type: 'command',
|
|
371
405
|
command: 'throughline codex-hook stop',
|
|
372
|
-
|
|
406
|
+
timeout: 300,
|
|
373
407
|
async: true,
|
|
374
408
|
},
|
|
375
409
|
],
|
|
@@ -122,11 +122,11 @@ function hasFactoryDatabaseShape(db) {
|
|
|
122
122
|
return bodiesUnique;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function eventStatus({ hooks, legacyHooks, featureEnabled, expectedCommand,
|
|
125
|
+
function eventStatus({ hooks, legacyHooks, featureEnabled, expectedCommand, timeout }) {
|
|
126
126
|
if (hooks.length === 0) return featureEnabled ? 'not_ready' : 'not_applicable';
|
|
127
127
|
if (!featureEnabled || legacyHooks.length > 0 || hooks.length !== 1) return 'not_ready';
|
|
128
128
|
const hook = hooks[0];
|
|
129
|
-
return hook.type === 'command' && hook.command === expectedCommand && hook.
|
|
129
|
+
return hook.type === 'command' && hook.command === expectedCommand && hook.timeout === timeout &&
|
|
130
130
|
hook.async === false ? 'ready' : 'not_ready';
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -143,21 +143,21 @@ export function inspectFactoryHooks({ codexHome = defaultCodexHome(), readHooks
|
|
|
143
143
|
legacyHooks: diagnosis.legacyManagedPromptHooks,
|
|
144
144
|
featureEnabled: diagnosis.featureEnabled,
|
|
145
145
|
expectedCommand: diagnosis.expectedPromptCommand,
|
|
146
|
-
|
|
146
|
+
timeout: 30,
|
|
147
147
|
}),
|
|
148
148
|
postToolUse: eventStatus({
|
|
149
149
|
hooks: diagnosis.managedPostToolUseHooks,
|
|
150
150
|
legacyHooks: diagnosis.legacyManagedPostToolUseHooks,
|
|
151
151
|
featureEnabled: diagnosis.featureEnabled,
|
|
152
152
|
expectedCommand: diagnosis.expectedPostToolUseCommand,
|
|
153
|
-
|
|
153
|
+
timeout: 30,
|
|
154
154
|
}),
|
|
155
155
|
stop: eventStatus({
|
|
156
156
|
hooks: diagnosis.managedStopHooks,
|
|
157
157
|
legacyHooks: diagnosis.legacyManagedStopHooks,
|
|
158
158
|
featureEnabled: diagnosis.featureEnabled,
|
|
159
159
|
expectedCommand: diagnosis.expectedStopCommand,
|
|
160
|
-
|
|
160
|
+
timeout: 300,
|
|
161
161
|
}),
|
|
162
162
|
};
|
|
163
163
|
const values = Object.values(events);
|
|
@@ -166,11 +166,11 @@ test('factory-diagnostics hook inspection rejects corrupt and false-ready shapes
|
|
|
166
166
|
expectedPromptCommand: 'prompt',
|
|
167
167
|
expectedPostToolUseCommand: 'post',
|
|
168
168
|
expectedStopCommand: 'stop',
|
|
169
|
-
managedPromptHooks: [{ type: 'command', command: 'prompt',
|
|
169
|
+
managedPromptHooks: [{ type: 'command', command: 'prompt', timeout: 30, async: true }],
|
|
170
170
|
legacyManagedPromptHooks: [],
|
|
171
|
-
managedPostToolUseHooks: [{ type: 'command', command: 'post',
|
|
171
|
+
managedPostToolUseHooks: [{ type: 'command', command: 'post', timeout: 30, async: false }],
|
|
172
172
|
legacyManagedPostToolUseHooks: [],
|
|
173
|
-
managedStopHooks: [{ type: 'command', command: 'stop',
|
|
173
|
+
managedStopHooks: [{ type: 'command', command: 'stop', timeout: 300, async: false }],
|
|
174
174
|
legacyManagedStopHooks: [],
|
|
175
175
|
}),
|
|
176
176
|
});
|
|
@@ -188,11 +188,11 @@ test('factory-diagnostics hook inspection summarizes every canonical ready event
|
|
|
188
188
|
expectedPromptCommand: 'prompt',
|
|
189
189
|
expectedPostToolUseCommand: 'post',
|
|
190
190
|
expectedStopCommand: 'stop',
|
|
191
|
-
managedPromptHooks: [{ type: 'command', command: 'prompt',
|
|
191
|
+
managedPromptHooks: [{ type: 'command', command: 'prompt', timeout: 30, async: false }],
|
|
192
192
|
legacyManagedPromptHooks: [],
|
|
193
|
-
managedPostToolUseHooks: [{ type: 'command', command: 'post',
|
|
193
|
+
managedPostToolUseHooks: [{ type: 'command', command: 'post', timeout: 30, async: false }],
|
|
194
194
|
legacyManagedPostToolUseHooks: [],
|
|
195
|
-
managedStopHooks: [{ type: 'command', command: 'stop',
|
|
195
|
+
managedStopHooks: [{ type: 'command', command: 'stop', timeout: 300, async: false }],
|
|
196
196
|
legacyManagedStopHooks: [],
|
|
197
197
|
}),
|
|
198
198
|
});
|
|
@@ -202,6 +202,30 @@ test('factory-diagnostics hook inspection summarizes every canonical ready event
|
|
|
202
202
|
assert.equal(result.reason, 'hooks_inspected');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
test('factory-diagnostics hook inspection rejects legacy timeoutSec keys', () => {
|
|
206
|
+
const result = inspectFactoryHooks({
|
|
207
|
+
readHooks: () => ({
|
|
208
|
+
configExists: true,
|
|
209
|
+
configReadable: true,
|
|
210
|
+
hooksExists: true,
|
|
211
|
+
hooksReadable: true,
|
|
212
|
+
featureEnabled: true,
|
|
213
|
+
expectedPromptCommand: 'prompt',
|
|
214
|
+
expectedPostToolUseCommand: 'post',
|
|
215
|
+
expectedStopCommand: 'stop',
|
|
216
|
+
managedPromptHooks: [{ type: 'command', command: 'prompt', timeoutSec: 30, async: false }],
|
|
217
|
+
legacyManagedPromptHooks: [],
|
|
218
|
+
managedPostToolUseHooks: [{ type: 'command', command: 'post', timeoutSec: 30, async: false }],
|
|
219
|
+
legacyManagedPostToolUseHooks: [],
|
|
220
|
+
managedStopHooks: [{ type: 'command', command: 'stop', timeoutSec: 300, async: false }],
|
|
221
|
+
legacyManagedStopHooks: [],
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
assert.deepEqual(result.events, { userPromptSubmit: 'not_ready', postToolUse: 'not_ready', stop: 'not_ready' });
|
|
226
|
+
assert.equal(result.status, 'not_ready');
|
|
227
|
+
});
|
|
228
|
+
|
|
205
229
|
function createFactorySchema(db) {
|
|
206
230
|
db.exec(`
|
|
207
231
|
PRAGMA user_version = 9;
|
package/src/cli/install.mjs
CHANGED
|
@@ -170,7 +170,7 @@ function createCodexHooks() {
|
|
|
170
170
|
{
|
|
171
171
|
type: 'command',
|
|
172
172
|
command: buildCodexUserPromptSubmitHookCommand(),
|
|
173
|
-
|
|
173
|
+
timeout: 30,
|
|
174
174
|
async: false,
|
|
175
175
|
statusMessage: null,
|
|
176
176
|
},
|
|
@@ -181,7 +181,7 @@ function createCodexHooks() {
|
|
|
181
181
|
{
|
|
182
182
|
type: 'command',
|
|
183
183
|
command: buildCodexPostToolUseHookCommand(),
|
|
184
|
-
|
|
184
|
+
timeout: 30,
|
|
185
185
|
async: false,
|
|
186
186
|
statusMessage: null,
|
|
187
187
|
},
|
|
@@ -192,7 +192,7 @@ function createCodexHooks() {
|
|
|
192
192
|
{
|
|
193
193
|
type: 'command',
|
|
194
194
|
command: buildCodexStopHookCommand(),
|
|
195
|
-
|
|
195
|
+
timeout: 300,
|
|
196
196
|
async: false,
|
|
197
197
|
statusMessage: null,
|
|
198
198
|
},
|
package/src/cli/install.test.mjs
CHANGED
|
@@ -120,19 +120,21 @@ test('global install registers Codex session hooks and enables hooks features',
|
|
|
120
120
|
.find(h => h.command === expectedStopCommand);
|
|
121
121
|
assert.ok(codexHook, 'Codex Stop should have absolute throughline.mjs codex-hook stop');
|
|
122
122
|
assert.equal(codexHook.async, false, 'Codex Stop hook should be synchronous for Codex');
|
|
123
|
-
assert.equal(codexHook.
|
|
123
|
+
assert.equal(codexHook.timeout, 300, 'Codex Stop hook should allow summarizer time');
|
|
124
124
|
const promptHook = hooks.hooks.UserPromptSubmit
|
|
125
125
|
.flatMap(g => g.hooks ?? [])
|
|
126
126
|
.find(h => h.command === expectedPromptCommand);
|
|
127
127
|
assert.ok(promptHook, 'Codex UserPromptSubmit should have absolute throughline.mjs codex-hook user-prompt-submit');
|
|
128
128
|
assert.equal(promptHook.async, false, 'Codex UserPromptSubmit hook should be synchronous for capture/monitor state');
|
|
129
|
-
assert.equal(promptHook.
|
|
129
|
+
assert.equal(promptHook.timeout, 30, 'Codex UserPromptSubmit hook should be short');
|
|
130
130
|
const postToolUseHook = hooks.hooks.PostToolUse
|
|
131
131
|
.flatMap(g => g.hooks ?? [])
|
|
132
132
|
.find(h => h.command === expectedPostToolUseCommand);
|
|
133
133
|
assert.ok(postToolUseHook, 'Codex PostToolUse should have absolute throughline.mjs codex-hook post-tool-use');
|
|
134
134
|
assert.equal(postToolUseHook.async, false, 'Codex PostToolUse hook should be synchronous for capture/monitor state');
|
|
135
|
-
assert.equal(postToolUseHook.
|
|
135
|
+
assert.equal(postToolUseHook.timeout, 30, 'Codex PostToolUse hook should be short');
|
|
136
|
+
const managedHooks = [codexHook, promptHook, postToolUseHook];
|
|
137
|
+
assert.doesNotMatch(JSON.stringify(managedHooks), /timeoutSec/);
|
|
136
138
|
const config = readFileSync(join(home.dir, '.codex', 'config.toml'), 'utf8');
|
|
137
139
|
assert.match(config, /^\[features\]\ncodex_hooks = true/m);
|
|
138
140
|
assert.match(config, /^hooks = true/m);
|
|
@@ -245,7 +247,7 @@ test('global install preserves existing Codex hooks and is idempotent', async ()
|
|
|
245
247
|
{
|
|
246
248
|
type: 'command',
|
|
247
249
|
command: '/usr/bin/node /home/kite/.npm-global/bin/caveat codex-hook stop',
|
|
248
|
-
|
|
250
|
+
timeout: 5,
|
|
249
251
|
async: false,
|
|
250
252
|
statusMessage: null,
|
|
251
253
|
},
|
|
@@ -287,7 +289,7 @@ test('global install preserves existing Codex hooks and is idempotent', async ()
|
|
|
287
289
|
}
|
|
288
290
|
});
|
|
289
291
|
|
|
290
|
-
test('global install
|
|
292
|
+
test('global install replaces legacy timeoutSec Throughline Codex hook shapes', async () => {
|
|
291
293
|
const home = makeTempHome();
|
|
292
294
|
if (home.resolved !== home.dir) {
|
|
293
295
|
home.restore();
|
|
@@ -356,19 +358,22 @@ test('global install updates existing Throughline Codex hook shapes', async () =
|
|
|
356
358
|
.filter(h => h.command === expectedStopCommand);
|
|
357
359
|
assert.equal(codexHooks.length, 1);
|
|
358
360
|
assert.equal(codexHooks[0].async, false);
|
|
359
|
-
assert.equal(codexHooks[0].
|
|
361
|
+
assert.equal(codexHooks[0].timeout, 300);
|
|
362
|
+
assert.equal(Object.hasOwn(codexHooks[0], 'timeoutSec'), false);
|
|
360
363
|
const promptHooks = hooks.hooks.UserPromptSubmit
|
|
361
364
|
.flatMap(g => g.hooks ?? [])
|
|
362
365
|
.filter(h => h.command === expectedPromptCommand);
|
|
363
366
|
assert.equal(promptHooks.length, 1);
|
|
364
367
|
assert.equal(promptHooks[0].async, false);
|
|
365
|
-
assert.equal(promptHooks[0].
|
|
368
|
+
assert.equal(promptHooks[0].timeout, 30);
|
|
369
|
+
assert.equal(Object.hasOwn(promptHooks[0], 'timeoutSec'), false);
|
|
366
370
|
const postToolUseHooks = hooks.hooks.PostToolUse
|
|
367
371
|
.flatMap(g => g.hooks ?? [])
|
|
368
372
|
.filter(h => h.command === expectedPostToolUseCommand);
|
|
369
373
|
assert.equal(postToolUseHooks.length, 1);
|
|
370
374
|
assert.equal(postToolUseHooks[0].async, false);
|
|
371
|
-
assert.equal(postToolUseHooks[0].
|
|
375
|
+
assert.equal(postToolUseHooks[0].timeout, 30);
|
|
376
|
+
assert.equal(Object.hasOwn(postToolUseHooks[0], 'timeoutSec'), false);
|
|
372
377
|
assert.equal(
|
|
373
378
|
hooks.hooks.Stop.flatMap(g => g.hooks ?? []).filter(h => h.command === 'throughline codex-hook stop').length,
|
|
374
379
|
0,
|
|
@@ -426,7 +431,7 @@ test('global uninstall removes only Throughline-managed Codex hook', async () =>
|
|
|
426
431
|
{
|
|
427
432
|
type: 'command',
|
|
428
433
|
command: '/usr/bin/node /home/kite/.npm-global/bin/caveat codex-hook stop',
|
|
429
|
-
|
|
434
|
+
timeout: 5,
|
|
430
435
|
async: false,
|
|
431
436
|
statusMessage: null,
|
|
432
437
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import {
|
|
4
|
+
COMPLETED_TURN_RECEIPT_LIMIT,
|
|
5
|
+
defaultCompletedTurnReceiptStorePath,
|
|
6
|
+
writeCompletedTurnReceipt,
|
|
7
|
+
} from './completed-turn-receipts.mjs';
|
|
8
|
+
|
|
9
|
+
function sha256(value) {
|
|
10
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Seeds a schema-valid receipt store without repeating the Windows ACL subprocess path.
|
|
15
|
+
* One public mutation creates the private directory, lock, and store; direct replacement
|
|
16
|
+
* preserves that file ACL, and the caller still performs the boundary mutation publicly.
|
|
17
|
+
*/
|
|
18
|
+
export function seedCompletedTurnReceiptStore({
|
|
19
|
+
projectPath,
|
|
20
|
+
receiptOptions = {},
|
|
21
|
+
size = COMPLETED_TURN_RECEIPT_LIMIT,
|
|
22
|
+
targetSessionId = 'fixture-target',
|
|
23
|
+
}) {
|
|
24
|
+
if (!Number.isSafeInteger(size) || size < 1 || size > COMPLETED_TURN_RECEIPT_LIMIT) {
|
|
25
|
+
throw new TypeError('fixture receipt size is invalid');
|
|
26
|
+
}
|
|
27
|
+
const first = writeCompletedTurnReceipt({
|
|
28
|
+
projectPath,
|
|
29
|
+
targetSessionId,
|
|
30
|
+
originSessionId: 'fixture-origin-1',
|
|
31
|
+
userBody: 'fixture-user-1',
|
|
32
|
+
assistantBody: 'fixture-assistant-1',
|
|
33
|
+
completedAt: 1,
|
|
34
|
+
}, receiptOptions);
|
|
35
|
+
const storePath = receiptOptions.storePath ||
|
|
36
|
+
defaultCompletedTurnReceiptStorePath(first.project_sha256, receiptOptions.env);
|
|
37
|
+
const store = JSON.parse(readFileSync(storePath, 'utf8'));
|
|
38
|
+
store.next_sequence = size + 1;
|
|
39
|
+
store.history_floor = 1;
|
|
40
|
+
store.receipts = Array.from({ length: size }, (_, offset) => {
|
|
41
|
+
const sequence = offset + 1;
|
|
42
|
+
return {
|
|
43
|
+
...first,
|
|
44
|
+
target_session_id: targetSessionId,
|
|
45
|
+
origin_session_id: `fixture-origin-${sequence}`,
|
|
46
|
+
user_sha256: sha256(`fixture-user-${sequence}`),
|
|
47
|
+
assistant_sha256: sha256(`fixture-assistant-${sequence}`),
|
|
48
|
+
completed_at: sequence,
|
|
49
|
+
sequence,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
writeFileSync(storePath, `${JSON.stringify(store)}\n`, 'utf8');
|
|
53
|
+
return { storePath, projectSha256: first.project_sha256 };
|
|
54
|
+
}
|
|
@@ -142,7 +142,11 @@ function writeStore(store, normalized, options, privateDirectory) {
|
|
|
142
142
|
chmodSync(temporary, 0o600);
|
|
143
143
|
}
|
|
144
144
|
renameSync(temporary, path);
|
|
145
|
-
|
|
145
|
+
// The temporary file ACL is applied and read back before rename. Rename
|
|
146
|
+
// preserves that ACL, so only the new final path shape needs an immediate
|
|
147
|
+
// in-process check; native integration tests verify the external ACL.
|
|
148
|
+
if (isWindows(options.env)) assertPrivateFileShape(lstatSync(path));
|
|
149
|
+
else assertPrivateFile(lstatSync(path), options.env, path);
|
|
146
150
|
} finally {
|
|
147
151
|
rmSync(temporary, { force: true });
|
|
148
152
|
}
|
|
@@ -164,7 +168,8 @@ function withStoreLock(normalized, options, operation) {
|
|
|
164
168
|
if (isWindows(options.env)) applyAndVerifyWindowsAcl(lockPath, false);
|
|
165
169
|
else chmodSync(lockPath, 0o600);
|
|
166
170
|
}
|
|
167
|
-
|
|
171
|
+
if (created && isWindows(options.env)) assertPrivateFileShape(lstatSync(lockPath));
|
|
172
|
+
else assertPrivateFile(lstatSync(lockPath), options.env, lockPath);
|
|
168
173
|
const database = new DatabaseSync(lockPath);
|
|
169
174
|
let active = false;
|
|
170
175
|
try {
|
|
@@ -186,7 +191,8 @@ function ensurePrivateDirectory(directory, env) {
|
|
|
186
191
|
assertPrivateDirectoryShape(info);
|
|
187
192
|
if (isWindows(env)) applyAndVerifyWindowsAcl(directory, true);
|
|
188
193
|
else chmodSync(directory, 0o700);
|
|
189
|
-
|
|
194
|
+
if (isWindows(env)) assertPrivateDirectoryShape(lstatSync(directory));
|
|
195
|
+
else assertPrivateDirectory(directory, env);
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
function privateDirectoryCapability(directory, env) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import
|
|
3
|
+
import childProcess from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
6
7
|
import {
|
|
@@ -10,6 +11,8 @@ import {
|
|
|
10
11
|
readCompletedTurnReceiptSnapshot,
|
|
11
12
|
writeCompletedTurnReceipt,
|
|
12
13
|
} from './completed-turn-receipts.mjs';
|
|
14
|
+
import { seedCompletedTurnReceiptStore } from './completed-turn-receipts-test-fixture.mjs';
|
|
15
|
+
import { verifyWindowsPrivateAcl } from './windows-acl-test-helper.mjs';
|
|
13
16
|
|
|
14
17
|
function sandbox() {
|
|
15
18
|
const root = mkdtempSync(join(tmpdir(), 'throughline-completed-receipts-'));
|
|
@@ -101,9 +104,8 @@ test('completed turn receipt: rejects symlinked store paths', { skip: process.pl
|
|
|
101
104
|
test('completed turn receipt: bounded store drops only oldest receipts', () => {
|
|
102
105
|
const box = sandbox();
|
|
103
106
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
+
seedCompletedTurnReceiptStore({ projectPath: '/repo', receiptOptions: { storePath: box.storePath } });
|
|
108
|
+
writeCompletedTurnReceipt({ ...input(COMPLETED_TURN_RECEIPT_LIMIT + 1), targetSessionId: 'target-final' }, { storePath: box.storePath });
|
|
107
109
|
const stored = JSON.parse(readFileSync(box.storePath, 'utf8'));
|
|
108
110
|
assert.equal(stored.receipts.length, COMPLETED_TURN_RECEIPT_LIMIT);
|
|
109
111
|
assert.equal(stored.history_floor, 2);
|
|
@@ -119,9 +121,11 @@ test('completed turn receipt: noisy project cannot evict another project anchor'
|
|
|
119
121
|
const env = { HOME: box.root, USERPROFILE: box.root, XDG_STATE_HOME: join(box.root, 'state-home') };
|
|
120
122
|
try {
|
|
121
123
|
const quiet = writeCompletedTurnReceipt({ ...input(), projectPath: '/quiet-project' }, { env });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
seedCompletedTurnReceiptStore({ projectPath: '/noisy-project', receiptOptions: { env } });
|
|
125
|
+
writeCompletedTurnReceipt({
|
|
126
|
+
...input(COMPLETED_TURN_RECEIPT_LIMIT + 1),
|
|
127
|
+
projectPath: '/noisy-project', targetSessionId: 'target-final',
|
|
128
|
+
}, { env });
|
|
125
129
|
const quietStorePath = defaultCompletedTurnReceiptStorePath(quiet.project_sha256, env);
|
|
126
130
|
const quietStore = JSON.parse(readFileSync(quietStorePath, 'utf8'));
|
|
127
131
|
assert.equal(quietStore.receipts.length, 1);
|
|
@@ -131,6 +135,70 @@ test('completed turn receipt: noisy project cannot evict another project anchor'
|
|
|
131
135
|
}
|
|
132
136
|
});
|
|
133
137
|
|
|
138
|
+
test('completed turn receipt: one Windows mutation spends ACL processes only on distinct state transitions', (t) => {
|
|
139
|
+
const box = sandbox();
|
|
140
|
+
const env = { OS: 'Windows_NT', HOME: box.root, USERPROFILE: box.root };
|
|
141
|
+
const calls = [];
|
|
142
|
+
t.mock.method(childProcess, 'spawnSync', (command, args, options) => {
|
|
143
|
+
calls.push({ command, args, options });
|
|
144
|
+
return { status: 0, signal: null, error: undefined };
|
|
145
|
+
});
|
|
146
|
+
const options = { env, storePath: box.storePath };
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
writeCompletedTurnReceipt(input(1), options);
|
|
150
|
+
assert.equal(calls.length, 3, 'new directory, lock, and store each require one apply+verify process');
|
|
151
|
+
assert.ok(calls.every((call) => call.command === 'powershell.exe'));
|
|
152
|
+
calls.length = 0;
|
|
153
|
+
|
|
154
|
+
writeCompletedTurnReceipt(input(2), options);
|
|
155
|
+
assert.equal(calls.length, 4, 'directory apply, existing lock/store verify, and replacement store apply are distinct');
|
|
156
|
+
assert.ok(calls.every((call) => call.options.timeout === 15_000));
|
|
157
|
+
} finally {
|
|
158
|
+
rmSync(box.root, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('completed turn receipt: Windows temporary ACL failure leaves the previous atomic store intact', (t) => {
|
|
163
|
+
const box = sandbox();
|
|
164
|
+
const env = { OS: 'Windows_NT', HOME: box.root, USERPROFILE: box.root };
|
|
165
|
+
let calls = 0;
|
|
166
|
+
let failAt = Number.POSITIVE_INFINITY;
|
|
167
|
+
t.mock.method(childProcess, 'spawnSync', () => {
|
|
168
|
+
calls += 1;
|
|
169
|
+
return { status: calls === failAt ? 1 : 0, signal: null, error: undefined };
|
|
170
|
+
});
|
|
171
|
+
const options = { env, storePath: box.storePath };
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
writeCompletedTurnReceipt(input(1), options);
|
|
175
|
+
const before = readFileSync(box.storePath, 'utf8');
|
|
176
|
+
failAt = calls + 4;
|
|
177
|
+
assert.throws(() => writeCompletedTurnReceipt(input(2), options), /ACL verification failed/);
|
|
178
|
+
assert.equal(readFileSync(box.storePath, 'utf8'), before);
|
|
179
|
+
} finally {
|
|
180
|
+
rmSync(box.root, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('completed turn receipt: atomic private store has owner-only modes', () => {
|
|
185
|
+
const box = sandbox();
|
|
186
|
+
try {
|
|
187
|
+
writeCompletedTurnReceipt(input(), { storePath: box.storePath });
|
|
188
|
+
if (process.platform === 'win32') {
|
|
189
|
+
verifyWindowsPrivateAcl(dirname(box.storePath), true);
|
|
190
|
+
verifyWindowsPrivateAcl(`${box.storePath}.lock.sqlite`);
|
|
191
|
+
verifyWindowsPrivateAcl(box.storePath);
|
|
192
|
+
} else {
|
|
193
|
+
assert.equal(statSync(dirname(box.storePath)).mode & 0o777, 0o700);
|
|
194
|
+
assert.equal(statSync(`${box.storePath}.lock.sqlite`).mode & 0o777, 0o600);
|
|
195
|
+
assert.equal(statSync(box.storePath).mode & 0o777, 0o600);
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
rmSync(box.root, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
134
202
|
test('completed turn receipt: explicit store path rejects a second project', () => {
|
|
135
203
|
const box = sandbox();
|
|
136
204
|
try {
|
|
@@ -6,6 +6,7 @@ import test from 'node:test';
|
|
|
6
6
|
import { DatabaseSync } from 'node:sqlite';
|
|
7
7
|
import { decodeObserverCursor, encodeObserverCursor, readObserverTurnPage, resolveObserverTurnFeed } from './observer-turn-feed.mjs';
|
|
8
8
|
import { writeCompletedTurnReceipt } from './completed-turn-receipts.mjs';
|
|
9
|
+
import { seedCompletedTurnReceiptStore } from './completed-turn-receipts-test-fixture.mjs';
|
|
9
10
|
import { hashAuditorBody } from './body-digest.mjs';
|
|
10
11
|
|
|
11
12
|
function fixture() {
|
|
@@ -123,7 +124,8 @@ test('observer feed: Claude history floor, host/thread switch, cross-host tie, a
|
|
|
123
124
|
const box = fixture();
|
|
124
125
|
try {
|
|
125
126
|
const options = box.receiptOptions;
|
|
126
|
-
|
|
127
|
+
seedCompletedTurnReceiptStore({ projectPath: box.project, receiptOptions: options, targetSessionId: 'claude-session' });
|
|
128
|
+
writeCompletedTurnReceipt({ projectPath: box.project, targetSessionId: 'claude-session', originSessionId: 'o257', userBody: 'u257', assistantBody: 'a257', completedAt: 257 }, options);
|
|
127
129
|
const latest = resolveObserverTurnFeed({ projectPath: box.project, receiptOptions: options, codexHome: box.home });
|
|
128
130
|
const decoded = decodeObserverCursor(latest.throughCursor);
|
|
129
131
|
const beforeFloor = encodeObserverCursor({ ...decoded, history_floor: 1 });
|