throughline 0.8.7 → 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 CHANGED
@@ -10,6 +10,20 @@ 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
+
13
27
  ## [0.8.7] — 2026-07-20
14
28
 
15
29
  ### Fixed
@@ -1177,7 +1191,8 @@ two attempts, instrument first instead of patching again.
1177
1191
 
1178
1192
  ---
1179
1193
 
1180
- [Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.7...HEAD
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
1181
1196
  [0.8.7]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.6...v0.8.7
1182
1197
  [0.8.6]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.5...v0.8.6
1183
1198
  [0.8.5]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.4...v0.8.5
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
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
@@ -142,7 +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
- | **release準備 (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) |
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は変更しない |
146
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` も有効化する |
147
148
  | **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
148
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`、`timeoutSec: 300` になり、Caveat / Spotter hooks は 2 番目以降に保持された。`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` に進んだ。
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`、`timeoutSec: 300` だった。VSCode-origin の自然 Stop hook による DB capture も解決済みとして扱う。
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 前に拒否する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.8.7",
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": "kitepon",
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
  },
@@ -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.length === 0
604
- ? 'not registered'
605
- : hookDiagnosis.legacyManagedPromptHooks.length > 0
606
- ? 'legacy command needs reinstall'
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
- const h = hookDiagnosis.managedPromptHooks[0];
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.length === 0
618
- ? 'not registered'
619
- : hookDiagnosis.legacyManagedPostToolUseHooks.length > 0
620
- ? 'legacy command needs reinstall'
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
- const h = hookDiagnosis.managedPostToolUseHooks[0];
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.length === 0
632
- ? 'not registered'
633
- : hookDiagnosis.legacyManagedStopHooks.length > 0
634
- ? 'legacy command needs reinstall'
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
- const h = hookDiagnosis.managedStopHooks[0];
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,
@@ -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
- timeoutSec: 30,
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
- timeoutSec: 30,
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
- timeoutSec: 300,
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, timeoutSec }) {
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.timeoutSec === timeoutSec &&
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
- timeoutSec: 30,
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
- timeoutSec: 30,
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
- timeoutSec: 300,
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', timeoutSec: 30, async: true }],
169
+ managedPromptHooks: [{ type: 'command', command: 'prompt', timeout: 30, async: true }],
170
170
  legacyManagedPromptHooks: [],
171
- managedPostToolUseHooks: [{ type: 'command', command: 'post', timeoutSec: 30, async: false }],
171
+ managedPostToolUseHooks: [{ type: 'command', command: 'post', timeout: 30, async: false }],
172
172
  legacyManagedPostToolUseHooks: [],
173
- managedStopHooks: [{ type: 'command', command: 'stop', timeoutSec: 300, async: false }],
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', timeoutSec: 30, async: false }],
191
+ managedPromptHooks: [{ type: 'command', command: 'prompt', timeout: 30, async: false }],
192
192
  legacyManagedPromptHooks: [],
193
- managedPostToolUseHooks: [{ type: 'command', command: 'post', timeoutSec: 30, async: false }],
193
+ managedPostToolUseHooks: [{ type: 'command', command: 'post', timeout: 30, async: false }],
194
194
  legacyManagedPostToolUseHooks: [],
195
- managedStopHooks: [{ type: 'command', command: 'stop', timeoutSec: 300, async: false }],
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;
@@ -170,7 +170,7 @@ function createCodexHooks() {
170
170
  {
171
171
  type: 'command',
172
172
  command: buildCodexUserPromptSubmitHookCommand(),
173
- timeoutSec: 30,
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
- timeoutSec: 30,
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
- timeoutSec: 300,
195
+ timeout: 300,
196
196
  async: false,
197
197
  statusMessage: null,
198
198
  },
@@ -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.timeoutSec, 300, 'Codex Stop hook should allow summarizer time');
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.timeoutSec, 30, 'Codex UserPromptSubmit hook should be short');
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.timeoutSec, 30, 'Codex PostToolUse hook should be short');
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
- timeoutSec: 5,
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 updates existing Throughline Codex hook shapes', async () => {
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].timeoutSec, 300);
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].timeoutSec, 30);
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].timeoutSec, 30);
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
- timeoutSec: 5,
434
+ timeout: 5,
430
435
  async: false,
431
436
  statusMessage: null,
432
437
  },