throughline 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +79 -4
  2. package/README.md +77 -25
  3. package/bin/throughline.mjs +14 -0
  4. package/docs/00_overview.md +12 -0
  5. package/docs/02_clear_auto_handoff_plan.md +39 -13
  6. package/docs/04_public_release_plan.md +2 -1
  7. package/docs/13_native_factory_diagnostics_plan.md +4 -2
  8. package/docs/14_observer_completed_turn_feed_plan.md +290 -0
  9. package/docs/BUGHUB_RUNTIME_ERROR_STORE_PLAN.md +31 -4
  10. package/docs/adr/0002-observer-claude-completion-receipt.md +42 -0
  11. package/docs/adr/0003-observer-completed-chain-cursor.md +34 -0
  12. package/docs/adr/0004-observer-db-pair-projection.md +71 -0
  13. package/docs/adr/0005-observer-read-pagination.md +51 -0
  14. package/docs/adr/0006-observer-page-offset-proof.md +33 -0
  15. package/docs/adr/0007-observer-read-cli-contract.md +61 -0
  16. package/docs/adr/0008-observer-wait-deadline-cancel.md +81 -0
  17. package/docs/adr/0009-observer-integration-regression-and-docs.md +37 -0
  18. package/docs/adr/0010-observer-o1-phase-acceptance.md +49 -0
  19. package/docs/adr/0011-observer-o1-control-lane-reconciliation.md +34 -0
  20. package/docs/adr/0012-claude-stop-transcript-flush-barrier.md +32 -0
  21. package/docs/adr/0013-observer-read-busy-writer-gate.md +46 -0
  22. package/docs/adr/0014-two-phase-handoff-ghost-baton.md +112 -0
  23. package/docs/adr/0015-l1-summarizer-model-effort-ratio.md +81 -0
  24. package/package.json +1 -1
  25. package/rag/01-hooks/hook-stdout-10k-persisted-output.md +48 -0
  26. package/rag/INDEX.md +4 -0
  27. package/src/auditor-context.mjs +92 -11
  28. package/src/auditor-context.test.mjs +116 -1
  29. package/src/baton.mjs +27 -7
  30. package/src/baton.test.mjs +44 -0
  31. package/src/body-digest.mjs +9 -0
  32. package/src/cli/auditor-context.test.mjs +1 -1
  33. package/src/cli/factory-diagnostics.mjs +5 -2
  34. package/src/cli/factory-diagnostics.test.mjs +31 -2
  35. package/src/cli/observer-read.mjs +73 -0
  36. package/src/cli/observer-read.test.mjs +93 -0
  37. package/src/cli/observer-wait.mjs +123 -0
  38. package/src/cli/observer-wait.test.mjs +167 -0
  39. package/src/codex-rollout-memory.mjs +13 -0
  40. package/src/codex-rollout-memory.test.mjs +27 -0
  41. package/src/codex-thread-index.mjs +1 -1
  42. package/src/codex-thread-index.test.mjs +18 -0
  43. package/src/completed-turn-receipts.mjs +373 -0
  44. package/src/completed-turn-receipts.test.mjs +186 -0
  45. package/src/db-schema.test.mjs +9 -2
  46. package/src/db.mjs +20 -1
  47. package/src/decision-log.mjs +24 -0
  48. package/src/factory-diagnostics.mjs +0 -1
  49. package/src/factory-diagnostics.test.mjs +18 -0
  50. package/src/haiku-summarizer.mjs +93 -16
  51. package/src/haiku-summarizer.test.mjs +118 -9
  52. package/src/handoff-executor.mjs +159 -0
  53. package/src/hook-entrypoints.test.mjs +192 -12
  54. package/src/observer-codex-projection.test.mjs +49 -0
  55. package/src/observer-turn-feed.mjs +392 -0
  56. package/src/observer-turn-feed.test.mjs +339 -0
  57. package/src/observer-turn-wait.mjs +102 -0
  58. package/src/observer-turn-wait.test.mjs +122 -0
  59. package/src/pending-handoff.mjs +96 -0
  60. package/src/pending-handoff.test.mjs +107 -0
  61. package/src/prompt-submit.mjs +46 -1
  62. package/src/resume-context.mjs +226 -62
  63. package/src/resume-context.test.mjs +134 -1
  64. package/src/runtime-error-store.mjs +74 -32
  65. package/src/runtime-error-store.test.mjs +51 -3
  66. package/src/session-start.mjs +70 -233
  67. package/src/transcript-reader.mjs +32 -0
  68. package/src/turn-backfill.mjs +3 -2
  69. package/src/turn-backfill.test.mjs +10 -4
  70. package/src/turn-processor.mjs +90 -1
  71. package/src/turn-processor.test.mjs +141 -0
  72. package/src/windows-acl-test-helper.mjs +29 -0
@@ -0,0 +1,61 @@
1
+ # ADR 0007: observer-read CLIはJSON-only read境界と固定error codeを持つ
2
+
3
+ 日付: 2026-07-15
4
+
5
+ ## Context
6
+
7
+ ObserverはThroughlineの内部moduleやDBを直接読まず、公開CLIだけを子processとして呼ぶ。Libraryの
8
+ `readObserverTurnPage`は既知状態を値で返す一方、引数、page token、project、DB schema/project/I/Oの
9
+ hard failureは例外で拒否する。CLIが例外本文、path、session、cursor、本文をstderrへ出すとprivacy境界を
10
+ 壊し、既知状態までnon-zeroへ丸めるとObserverがretry/resyncを正しく選べない。
11
+
12
+ この契約はControlのDecision証拠に使うため、追記可能な計画書ではなく不変ADRとして置く。
13
+
14
+ ## Decision
15
+
16
+ 1. 公開入口は次とし、`--project`と`--json`を必須にする。`--after-cursor`、`--through-cursor`、
17
+ `--page-token`、`--limit`は各0回または1回だけ受理し、未知option、重複、値欠落、余剰位置引数を拒否する。
18
+
19
+ ```text
20
+ throughline observer-read --project <absolute-existing-directory>
21
+ [--after-cursor <opaque>] [--through-cursor <opaque>]
22
+ [--page-token <opaque>] [--limit <1..100>] --json
23
+ ```
24
+
25
+ 2. `--page-token`は`--after-cursor`と`--through-cursor`の両方がある場合だけ受理する。cursor/tokenの
26
+ decode、project canonicalization、固定series検証はCLIへ複製せず`readObserverTurnPage`へ委ねる。
27
+ 3. `snapshot`、`delta`、`thread_switched`、`host_switched`、`resync_required`、
28
+ `projection_pending`、`ambiguous_parent`はすべて既知状態である。stdoutへ
29
+ `throughline.observer_read.v1`を単一行JSONとして一度だけ出し、stderr空、exit 0とする。
30
+ 4. hard failureはstdoutを空に保ち、stderrへ次の固定shapeを単一行JSONとして一度だけ出し、exit 1とする。
31
+ error messageへ例外本文、path、body、cursor/token、hash、session/thread/origin IDを転記しない。
32
+
33
+ ```json
34
+ {"schema":"throughline.observer_read.v1","status":"error","code":"E_OBSERVER_READ_ARGS","message":"invalid observer-read arguments"}
35
+ ```
36
+
37
+ 5. 固定codeは次へ限定する。
38
+
39
+ | 条件 | code | message |
40
+ |---|---|---|
41
+ | CLI構文、必須/重複option、limit形式 | `E_OBSERVER_READ_ARGS` | `invalid observer-read arguments` |
42
+ | project/page tokenのhard input拒否 | `E_OBSERVER_READ_INPUT` | `observer read input is invalid` |
43
+ | DB schema不一致 | `E_OBSERVER_READ_DB_SCHEMA` | `observer read database schema is unsupported` |
44
+ | DB project不一致 | `E_OBSERVER_READ_DB_PROJECT` | `observer read database project does not match` |
45
+ | DB open/query/I/O失敗 | `E_OBSERVER_READ_DB_IO` | `observer read database could not be read` |
46
+ | 上記以外の内部失敗 | `E_OBSERVER_READ_INTERNAL` | `observer read failed` |
47
+
48
+ 6. 公開CLIへ`--db`、`--codex-home`、receipt store path、raw session指定を追加しない。製品標準pathと
49
+ host固有indexをlibrary既定で解決し、testはdependency injectionまたは隔離HOME/CODEX_HOMEを使う。
50
+ 7. `bin/throughline.mjs`はsubcommandを明示dispatchし、返却exit codeだけを`process.exitCode`へ写す。
51
+ import時にCLIを自動実行せず、parserと`run`はunit test可能に保つ。
52
+ 8. cursorのversion、project、prefix、rollback不一致はLibrary既定どおり`resync_required`の既知状態で
53
+ exit 0とする。page tokenのversion、binding、offset/prefix不一致だけはsilent skipを防ぐhard input
54
+ errorであり、`E_OBSERVER_READ_INPUT`へ写す。
55
+
56
+ ## Consequences
57
+
58
+ - Observerはstdoutだけを成功/既知状態wireとしてparseでき、stderrは固定codeのhard failureだけになる。
59
+ - DB遅延やresyncをprocess failureと誤認せず、page token改変やDB破損を成功JSONへ丸めない。
60
+ - 内部例外や端末固有pathがadapter境界から漏れない。
61
+ - waitのdeadline、poll、signal/parent disconnect契約は別ADR/Taskで扱い、read CLIへ混ぜない。
@@ -0,0 +1,81 @@
1
+ # ADR 0008: observer-waitはmonotonic deadlineの最終再確認と明示cancelを持つ
2
+
3
+ 日付: 2026-07-15
4
+
5
+ ## Context
6
+
7
+ Observerは完了turnが無い間、最大一時間Throughlineを待つ。DB/WAL/mtimeは完了証拠ではなく、
8
+ Claude receiptまたはCodex `task_complete`から再構成したcompleted cursorだけがchangedを決める。
9
+ deadline到達時に再確認せずtimeoutすると境界直前の完了を取りこぼし、cancel時にtimerやsignal listenerを
10
+ 残すと次のwaitへ副作用が漏れる。wall clockの変更で一時間を過不足させてもならない。
11
+
12
+ この契約はControlのDecision証拠に使うため、追記可能な計画書ではなく不変ADRとして置く。
13
+
14
+ ## Decision
15
+
16
+ 1. Libraryは`waitForObserverTurnChange`を公開し、`projectPath`、必須`afterCursor`、
17
+ `timeoutSeconds`(既定3600、1以上3600以下)、`AbortSignal`と既存host read optionsを受ける。
18
+ test用のclock/sleep/resolver injectionは非公開または明示的なdependency引数としてよいが、公開CLI
19
+ optionにはしない。
20
+ 2. 呼出直後に一度completed-only cursorを再計算する。after cursorが無効なら`resync_required`、
21
+ cross-host tieなら`ambiguous_parent`、cursorが変化済みなら待たず`changed`を返す。
22
+ 3. 変化が無い時だけ短いintervalで再計算する。deadlineはmonotonic clockで固定し、wall clockの進退を
23
+ duration判定へ使わない。DB transaction、file handle、host固有index snapshotをsleep中に保持しない。
24
+ 4. 各wake後はtimeout判定より先にcompleted cursorを再計算する。deadline以上でも最後の一回がchanged、
25
+ `resync_required`、`ambiguous_parent`ならその状態を返し、最後までunchangedの時だけ`timeout`を返す。
26
+ deadline後に追加のpoll cycleは開始しない。
27
+ 5. wait wireは`throughline.observer_wait.v1`とし、次のshapeだけを返す。raw project path、本文、session/
28
+ thread/origin ID、host index pathを含めない。
29
+
30
+ ```json
31
+ {
32
+ "schema": "throughline.observer_wait.v1",
33
+ "status": "changed",
34
+ "afterCursor": "tlc1....",
35
+ "throughCursor": "tlc1...."
36
+ }
37
+ ```
38
+
39
+ | status | throughCursor |
40
+ |---|---|
41
+ | `changed` | 呼出seriesの新しいcompleted cursor |
42
+ | `timeout` | 入力`afterCursor`と同値 |
43
+ | `resync_required` | `null` |
44
+ | `ambiguous_parent` | `null` |
45
+
46
+ 6. AbortSignalが呼出前または待機中にabortされたら成功wireを返さず、固定codeを持つcancel errorで終了する。
47
+ signalと完了が同じwakeで競合した時は、abort観測後に新しい成功pollを開始しない。pending timerを解除し、
48
+ Libraryは呼出後にlistener/handleを保持しない。
49
+ 7. 公開CLIは次とし、`--project`、`--after-cursor`、`--json`を必須にする。未知/重複option、値欠落、
50
+ 範囲外timeoutを拒否する。`--timeout-seconds`既定は3600である。
51
+
52
+ ```text
53
+ throughline observer-wait --project <absolute-existing-directory>
54
+ --after-cursor <opaque> [--timeout-seconds <1..3600>] --json
55
+ ```
56
+
57
+ 8. 4種の既知状態はstdout単一行JSON、stderr空、exit 0とする。hard failureはstdout空、stderr単一行の
58
+ 固定error JSON、exit 1とし、例外本文やpath/cursor/hash/raw identityを転記しない。
59
+
60
+ | 条件 | code | message |
61
+ |---|---|---|
62
+ | CLI構文/timeout形式 | `E_OBSERVER_WAIT_ARGS` | `invalid observer-wait arguments` |
63
+ | projectのhard input拒否 | `E_OBSERVER_WAIT_INPUT` | `observer wait input is invalid` |
64
+ | signal/parent disconnect | `E_OBSERVER_WAIT_CANCELLED` | `observer wait was cancelled` |
65
+ | その他の内部失敗 | `E_OBSERVER_WAIT_INTERNAL` | `observer wait failed` |
66
+
67
+ 9. CLIは`SIGINT`、`SIGTERM`、Node IPCの`disconnect`を一つのAbortControllerへ写す。通常の子processが
68
+ IPC無しで親だけ終了する場合も一時間孤児化しないよう、起動時`ppid`を保存し、poll interval以下の
69
+ 周期で`process.ppid`変化または`process.kill(parentPid, 0)`の`ESRCH`を検出してabortする。`EPERM`は
70
+ 親不在の証拠にしない。終了時にsignal listenerと親watch timerを必ず外す。stdout/stderrのpipe断は
71
+ 成功扱いにせず、`EPIPE`を握りつぶして別経路へfallbackしない。
72
+ 10. cursorのversion、project、prefix、rollback不一致はLibrary既定どおり`resync_required`でexit 0とする。
73
+ waitはDB本文freshnessを待たず、`changed`後の`observer-read`が`projection_pending`を裁定する。
74
+
75
+ ## Consequences
76
+
77
+ - 呼出前に完了済みなら即時changed、待機中なら次poll、deadline境界なら最終再確認で回収できる。
78
+ - timeout、resync、曖昧親、cancelを混同せず、Observerは保存cursorを安全に据え置ける。
79
+ - 一時間waitでもtransactionやtimer leakを残さず、read側のDB freshness責務を重複しない。
80
+ - OSがSIGKILLした場合のcleanupや親死活監視daemonは非目標であり、通常signal/IPC/pipe契約を越えて
81
+ 成功を偽装しない。
@@ -0,0 +1,37 @@
1
+ # ADR 0009: Observer統合の関連回帰と公開文書を受け入れる
2
+
3
+ 日付: 2026-07-15
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ ## Context
10
+
11
+ Observer向けcompleted-turn feedは、追加したread/wait境界だけでなく、既存のClaude Stop hook、Codex
12
+ capture、auditor-context、token monitorとの互換を維持する必要がある。また、実装済みの公開CLI契約を
13
+ README、AI向け正典、docs overview、CHANGELOGへ同期し、DB直接監視をfallbackとして案内してはならない。
14
+
15
+ ## Decision
16
+
17
+ 1. 次の関連gateをO1統合回帰として受け入れる。
18
+
19
+ ```text
20
+ node --import ./src/test-env.mjs --test \
21
+ src/hook-entrypoints.test.mjs src/codex-capture.test.mjs \
22
+ src/auditor-context.test.mjs src/cli/auditor-context.test.mjs \
23
+ src/cli/codex-hook.test.mjs src/token-monitor.test.mjs
24
+ ```
25
+
26
+ 結果は130件成功、失敗・skip・cancel・todo各0、実行時間807.298msだった。
27
+ 2. commit `fb558d7`のREADME、CLAUDE.md、`docs/00_overview.md`、CHANGELOG同期を受け入れる。
28
+ 3. 文書は`observer-read`/`observer-wait`をJSON-only CLIとして説明し、opaque cursor、read/wait状態、
29
+ `projection_pending`、最大3600秒wait、Claude receiptとCodex `task_complete`のcompleted境界を記録する。
30
+ 4. ThroughlineはMCP serverを所有せず、ObserverへDB、WAL、rolloutの直接監視fallbackを案内しない。
31
+ 5. 未公開、Phase full regression、pack gate、独立監査は完了扱いせず、次のO1 gateへ残す。
32
+
33
+ ## Consequences
34
+
35
+ - O1の関連回帰と文書同期TODOを完了できる。
36
+ - full `npm test`、`npm pack --dry-run --json`、独立監査はPhase完了時に一度だけ実行する。
37
+ - このADRはTask `observer-feed-doc-sync`のfinalization Decisionとして使い、追記可能なplanは使わない。
@@ -0,0 +1,49 @@
1
+ # ADR 0010: Observer O1 Phaseを監査修正込みで受け入れる
2
+
3
+ 日付: 2026-07-15
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ ## Context
10
+
11
+ Observer向けcompleted-turn feedのPhase O1は、Claude receiptとCodex `task_complete`から
12
+ host-neutralなcompleted chainを構築し、rollback、prefix差替え、pagination、deadline、cancel、
13
+ privacyの境界を維持する。Phase完了候補でfull regressionと独立監査を一度ずつ実施したところ、
14
+ 監査は二つの実欠陥を発見し、監査時点の製品判定をFAILEDとした。
15
+
16
+ 1. Claude Stopが複数の過去turnを一括backfillした時、最後のpairしかreceiptへ公開しない。
17
+ 2. Codex project resolverがPOSIX pathまで小文字化し、caseだけ異なるprojectを誤照合し得る。
18
+
19
+ ## Decision
20
+
21
+ 1. 独立監査の製品判定FAILEDをControl revision 67でrejectとして保持する。監査の指摘を
22
+ 成功扱いへ書き換えず、同じTODOへの独立監査も反復しない。
23
+ 2. P1はcommit `02a809f`で、backfillが返す全logical turn numberを時系列でClaude receiptへ
24
+ publishする。receipt storeのsame-pair冪等性により、DBだけ回収済みだった過去pairも穴埋めする。
25
+ 3. P2はcommit `88fafaf`で、POSIX pathのcaseを保持し、Windows drive pathだけを従来どおり
26
+ case-insensitiveに扱う。
27
+ 4. P1/P2修正後のfocused gateを受け入れる。
28
+
29
+ ```text
30
+ node --import ./src/test-env.mjs --test \
31
+ src/turn-backfill.test.mjs src/hook-entrypoints.test.mjs \
32
+ src/codex-thread-index.test.mjs
33
+ ```
34
+
35
+ 結果は28件成功、失敗・skip・cancel・todo各0、実行時間693.58925msだった。
36
+ 5. Phase完了候補のfull `npm test`は監査前HEAD `c5d6f2d`で一度実施し、661件中660件成功、
37
+ 失敗0、Windows限定1件skip、cancel・todo各0、実行時間32983.729625msだった。
38
+ 監査後の変更範囲は上記focused gateで全て再検証した。fullを現HEADで再実行したとは扱わない。
39
+ 6. 修正後HEADの`npm pack --dry-run --json`は`throughline@0.6.3`、entryCount 190、shasum
40
+ `0d27e31c334f1f1941c27383e7f2a61f20c2e370`で成功した。`git diff --check`も成功した。
41
+ 7. Phase O1の実装・関連回帰・文書同期・full/pack・独立監査とその二指摘の修正を完了とする。
42
+ publish、registry、実端末展開は別Waveであり、このDecisionには含めない。
43
+
44
+ ## Consequences
45
+
46
+ - Claude receiptとCodex `task_complete`のcompleted chainは、複数turn回収とPOSIX case境界を含めて
47
+ Observerのread/wait契約へ渡せる。
48
+ - full regressionを細かな修正ごとに反復せず、監査後deltaをfocused gateで検証した証拠が残る。
49
+ - このADRをTask `observer-feed-phase-audit`、Phase gate、Control finalizationの不変Decision証拠に使う。
@@ -0,0 +1,34 @@
1
+ # ADR 0011: Observer O1 Controlのclosure laneを補正する
2
+
3
+ 日付: 2026-07-15
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ ## Context
10
+
11
+ Control `observer-feed-20260715`のPhase gateは、O1の主要実装Taskが完了したrevision 51の後、
12
+ revision 52で`behavior-preserving`として宣言された。したがって、このgateが対象にするのは
13
+ 文書同期、full/pack、独立監査、最終統合からなるclosure laneであり、先行するO1実装全体を
14
+ behavior-preservingと再分類するものではない。
15
+
16
+ 独立監査後、P1/P2の挙動修正がclosure中に必要になった。元gateを改変したり
17
+ `behavior_change=not-applicable`のまま修正を隠したりしてはならない。
18
+
19
+ ## Decision
20
+
21
+ 1. P1/P2の修正受入はcorrective Control `observer-feed-o1-audit-fixes-20260715`が所有する。
22
+ 2. corrective Controlは`behavior-change` laneで、二つの修正受入Task、focused gate、
23
+ [ADR 0010](0010-observer-o1-phase-acceptance.md)をDecision証拠としてrevision 15でfinalize済みである。
24
+ 3. 元ControlのPhase gateはrevision 52以降のclosure統合だけを対象とし、
25
+ `behavior_change=not-applicable`はこのclosure laneに対してのみ記録する。
26
+ 4. 元Controlの独立監査FAILEDとworker rejectは保持し、corrective Controlのfinalizationを受けて
27
+ Phase O1全体を最終受入する。
28
+ 5. Phase gateを実装後に追加した事実は消さない。今後のPhaseは実装前にgateを宣言する。
29
+
30
+ ## Consequences
31
+
32
+ - 挙動修正をbehavior-preservingへ偽装せず、二つのControlの所有境界が明示される。
33
+ - 元Controlはclosure laneとして完了でき、O1の実装・監査・修正証拠を一つのDecision鎖へ統合できる。
34
+ - このADRを元ControlのPhase gateとControl finalizationの不変Decision証拠に使う。
@@ -0,0 +1,32 @@
1
+ # ADR 0012: Claude Stop receiptの前にtranscript flush barrierを置く
2
+
3
+ 日付: 2026-07-16
4
+
5
+ ## Status
6
+
7
+ Accepted for implementation。Observer queue 19eを保持し、次のlive turnより先に修理する。
8
+
9
+ ## Context
10
+
11
+ 実Claude turnはassistant `end_turn`、Stop hook 4本・hook error 0、Throughline state更新まで成立したが、
12
+ hook実行時の最新sessionにはuser/assistant bodyがなくcompletion receiptも作られなかった。同じtranscriptを
13
+ turn後に現行parserで読むとlatest logical groupは1件だった。Claudeのasync Stop hookがfinal assistant行の
14
+ 永続化可視化より先に一度だけtranscriptを読み、`lastTurnNumber === null`で正常no-opしたflush raceである。
15
+
16
+ ## Decision
17
+
18
+ 1. Stop payloadに非空`last_assistant_message`がある場合、latest user groupの非junk assistant本文が
19
+ その値と一致するまで短いbounded intervalでtranscriptを再読する。
20
+ 2. `last_assistant_message`はcompletion identity/flush barrierにだけ使い、L2本文やreceipt digestの
21
+ ソースにはしない。本文は従来どおりtranscriptからDBへcommitしたpairだけを使う。
22
+ 3. latest user groupを必須にし、過去の同文assistantや前turnを一致として採用しない。
23
+ 4. deadlineまで一致しなければ明示errorと`HOOK_PROCESS_TURN_FAILED`を返し、completionなしへ丸めない。
24
+ 5. markerを持たない旧Claude hostは既存one-shot transcript parser契約を維持する。
25
+ 6. Claude Stop hookの`async: true`、DB schema、receipt wire、Observer cursorは変更しない。
26
+
27
+ ## Acceptance
28
+
29
+ - user行だけの状態でhookを開始し、assistant行を遅延appendしても同じ実行でbody pairとreceiptを作る。
30
+ - 過去turnのassistantがcurrent markerと同文でも、latest userが未完なら待機し、誤ったpairをpublishしない。
31
+ - marker不一致のdeadlineはexplicit failureになり、runtime error ownerは一回だけ記録する。
32
+ - 通常同期flush、markerなし旧payload、複数turn backfillの既存挙動を維持する。
@@ -0,0 +1,46 @@
1
+ # ADR 0013: Observer readは同時writerをboundedに待つ
2
+
3
+ 日付: 2026-07-16
4
+
5
+ ## Status
6
+
7
+ Accepted。
8
+
9
+ ## Context
10
+
11
+ queue 19eの実Codexで、2件目の`task_complete`とThroughline captureは成功した一方、同時に走った
12
+ Observer production callerの`observer-read`が一度nonzeroとなり`E_THROUGHLINE_EXEC`で終了した。
13
+ 直後の同じ公開readは2件のcompleted turnを返した。campaign-private driverでも同じ書込み瞬間の
14
+ 単発nonzeroを観測しており、caller固有の失敗ではない。
15
+
16
+ completed feedはDB lagを`projection_pending`として扱うが、SQLite lockをstale成功へ丸めてはならない。
17
+ 一方、通常のStop captureとreadの短い競合を即時hard failureにすると、正規の継続監視が不安定になる。
18
+
19
+ ## Decision
20
+
21
+ 1. `readCompletedPairProjection`のread-only SQLite connectionだけにbounded busy waitを設定する。
22
+ 2. lock解消後は同じDBの整合したsnapshotを読み、pair不足は従来どおり`projection_pending`にする。
23
+ 3. busy上限超過、schema不一致、project不一致、その他I/Oは従来どおりhard failureにする。
24
+ 4. CLI再spawn、別DB、古い本文、cursor進行へのfallbackは追加しない。
25
+ 5. Spotter向け`readAuditorContext`の既存lock failure契約は変更しない。
26
+
27
+ ## Acceptance
28
+
29
+ - 別processがexclusive writer lockを保持中でも、上限内に解放すればcompleted projectionが同じpairを返す。
30
+ - 上限を越えるlockとその他hard failureはnonzero契約を維持する。
31
+ - Observer read/wait、auditor-context、Codex captureの関連gateを一度通す。
32
+ - 修理済みcandidateでqueue 19e Codex 2-cycle liveを再確認する。
33
+
34
+ 実装gate(2026-07-16):
35
+
36
+ - focused `src/auditor-context.test.mjs`: 修正前15/16、修正後16/16 PASS。
37
+ - related Observer read/wait、auditor、receipt、Codex hook/capture: 78/78 PASS。
38
+ - 変更source/testの構文検査、新規ADR lint、`git diff --check`: PASS。
39
+
40
+ Live acceptance(2026-07-16):
41
+
42
+ - 修理済みcandidateのqueue 19e実Codex r11で、親completed-turn 2件と同じObserver generationの
43
+ completed cycle 2件を確認した。
44
+ - 初回cycle後65秒超の継続、pending reservation/cycle/model operation残留なしを確認した。
45
+ - caller SIGINTはcancelled/exit 130、managed app-serverと親app-serverはterminalとなった。
46
+ - projectは空のまま。raw ID、prompt、model output、credentialは保存していない。
@@ -0,0 +1,112 @@
1
+ # ADR 0014: 二相ハンドオフ — 幽霊SessionStartによるバトン奪取の排除と注入10k予算
2
+
3
+ 日付: 2026-07-17
4
+
5
+ ## Status
6
+
7
+ Accepted(実装済み・live acceptance 済み — 本ADR末尾の記録参照)。
8
+
9
+ ## Context
10
+
11
+ ### Incident: バトンが幽霊セッションに奪われ、実セッションが記憶ゼロで始まる
12
+
13
+ 2026-07-17、dotagentsで同日2回(10:33 JST / 16:39 JST)、`/tl` バトンによる引き継ぎが
14
+ 沈黙して失敗した。`~/.throughline/logs/inheritance-decision.log` と DB の実測:
15
+
16
+ - Claude Code は同一 project_path に対し短時間(実測 315ms / 488ms 差)に複数の
17
+ SessionStart hook を発火させることがある。うち一部は **transcript を一度も生成せず、
18
+ sessions 行の updated_at が誕生時刻のまま二度と動かない「幽霊セッション」** になる。
19
+ - 旧実装は SessionStart で `consumeBaton`(BEGIN IMMEDIATE の SELECT+DELETE)→ merge →
20
+ stdout 注入を行っていたため、**先着した幽霊がバトンを消費して前任の全記憶
21
+ (skeletons 16件 / bodies 84件等)を吸い込み**、数百ms後の実セッションは
22
+ `baton_skip_reason: "missing"` で記憶ゼロで始まった。
23
+ - 全期間実測: 同一projectで2秒以内の SessionStart ペアは 3,222 イベント中 56 ペア
24
+ (= 近接ダブル発火は日常的)。バトン存在時に近接ペアが起きた 2 回は 2 回とも幽霊が勝った。
25
+ - 2026-05 の auto path でも、受け取り側 transcript が存在しない merge が 6 件あり、
26
+ 幽霊 twin(source='clear' の二重発火)が前任に選ばれた形と整合する。
27
+
28
+ **SessionStart 時点では実体と幽霊を判別する情報が原理的に存在しない。**
29
+ 実測で、本物のセッションの transcript ファイルも SessionStart hook 発火の約 461ms
30
+ **後** に作られる(payload の `transcript_path` は「これから作られる予定のパス」)。
31
+ したがって「消費前に transcript 実在を検査する」案は本物も全部弾いて成立しない。
32
+
33
+ ### 第二の欠陥: hook stdout 注入は10k超で file 化され、モデル可視が先頭2KBに劣化する
34
+
35
+ 対策検証中の実測(Claude Code 2.1.211、tracer 実験 + 全 transcript 掃引):
36
+
37
+ - SessionStart / UserPromptSubmit の hook stdout は約 10,000 字を超えると
38
+ `<persisted-output>`(保存ファイルパス + 先頭 2KB preview)に置換され、
39
+ **モデルに inline で見えるのは先頭 2KB だけ** になる。
40
+ 9,501 字は inline 通過、15,286 字は file 化を確認。
41
+ - 実運用の SessionStart 注入で 10k 超だった 12 件(2026-06-28 / v2.1.195 以降の全件)は
42
+ **12 件全部が劣化していた**(例: 64,148 字 emit → 可視 2,054 字)。
43
+ L1+L2 本体はモデルに読まれておらず、ヘッダ + 現在地アンカーが偶然 2KB 内に
44
+ 収まっていたため引き継ぎが機能している風に見えていた。
45
+
46
+ ## Decision
47
+
48
+ ### 二相ハンドオフ(merge・注入を「実体の証明」まで遅延する)
49
+
50
+ 1. **SessionStart(第一相)は intent 登録のみ**。sessions INSERT と
51
+ `pending_handoffs`(schema v9)への登録だけを行い、consumeBaton / merge / 注入を
52
+ 一切しない。auto path(source='clear')の前任はこの時点で解決して
53
+ `auto_predecessor_id` に凍結する。
54
+ 2. **最初の UserPromptSubmit(第二相)が consume + merge + 注入を行う**。
55
+ プロンプト到達 = セッション実在の証明であり、幽霊は構造上ここに到達できない。
56
+ pending 行の consume は BEGIN IMMEDIATE で atomic(1 セッション 1 回)。
57
+ 3. **baton 適格性はセッション誕生時刻基準**: `age = pending.created_at - baton.created_at`
58
+ が `0 ≤ age ≤ TTL(1h)` のときだけ消費。負 age(自分の誕生後に書かれたバトン)は
59
+ **削除せず残置**(`future_baton`)— 走行中セッションの横取りと、multi-window で
60
+ 本来の後継のバトンを先食いする穴を同時に塞ぐ。TTL の意味論
61
+ 「/tl から新セッション開始までの猶予」は消費が遅延しても保存される。
62
+ 4. **auto path の前任候補に transcript 実在フィルタ**(導出パス or state file)。
63
+ 幽霊 twin は transcript を持たないため前任に選ばれない(2026-05 型の再発防止)。
64
+ 実前任は /clear 前に活動していた実体なので必ず transcript を持つ。
65
+ 5. **注入は10k予算内レンダリング** (`buildBudgetedResumeContext`, 上限 9,500 字):
66
+ ヘッダ + 現在地アンカーは常に全文、L1 → L2 の順に新しい側から予算まで詰め、
67
+ 省略した行数は**注入文内に明示**し decision log にも記録する(黙って切らない)。
68
+ 最新 L2 行が単体で予算超過なら切り詰めて `throughline detail` 参照を付す。
69
+ 6. 幽霊の pending 行は誰にも consume されず無害に残る(数百バイト/行)。
70
+ TTL ベースの GC は**入れない** — 長時間 idle 後の初回プロンプトから引き継ぎを
71
+ silent に奪う fallback になるため。
72
+ 7. 判定ログは `phase: 'session-start' | 'prompt-submit'` の2種を同じ
73
+ inheritance-decision.log に記録する(本 incident の一次証拠となった実績を保つ)。
74
+
75
+ ### 廃止・撤去
76
+
77
+ - SessionStart の注入経路と、それにぶら下がっていた Phase 0-2 spike /
78
+ Phase 0-6 `initialUserMessage` テスト分岐(docs/10 で両 no-go 確定済みの実験残骸)。
79
+ - 「UserPromptSubmit は注入しない」規約 — 理由だった「SessionStart との二重注入」が
80
+ SessionStart 注入の廃止で消滅したため、注入責務ごと UserPromptSubmit へ移す。
81
+
82
+ ## Acceptance
83
+
84
+ 実装 gate(2026-07-17、このMacで実施済み):
85
+
86
+ - `npm test` 680/681 PASS(1 skip は既存の win32 契約 skip)。
87
+ - 新規回帰テスト: 幽霊先着 SessionStart がバトンを奪えず、実セッションの初回プロンプトが
88
+ 記憶を受け取る(incident 再現形)/ 走行中セッションが future baton を奪えない /
89
+ bornAt 基準 TTL / pending consume の1回性 / 予算レンダリングの省略告知・切り詰め。
90
+ - 10k 実測: 一時 UserPromptSubmit hook + `claude -p`(Haiku)の tracer 実験で、
91
+ 11,953 字 stdout の 10k 境界後 tracer がモデル不可視、`<persisted-output>` 化を確認。
92
+
93
+ Live acceptance(2026-07-17、このMacで dev 版 global install 後に実施):
94
+
95
+ - [x] 実 Claude Code(headless、実 hooks 経由)で、記憶セッション → `/tl` → 新セッション
96
+ 初回プロンプトの引き継ぎを確認。後継が合言葉を即答し、decision log に
97
+ `phase=session-start`(pending_registered)→ `phase=prompt-submit`
98
+ (`triggered_path=baton`, `merged=true`, injection 2,095 字・省略ゼロ)が刻まれた。
99
+ DB は初回 hook 発火で v9 へ migrate(session `8452e936`、前任 `c8b485bd`)。
100
+ - [x] 自己バトン食いの不在を実機確認: `/tl` を打った resume セッション自身の pending は
101
+ `baton missing` で消化され(消費が baton 書込より先)、バトンは後継まで残った。
102
+ `baton_age_ms` は誕生時刻基準(21,200ms)で記録された。
103
+ - [x] 幽霊2体(c4f05b96 / 90cb0c0e)の記憶回収済み(skeletons 30 / bodies 166 /
104
+ details 2,247 を実セッション `9fb15563` へ。DB バックアップ
105
+ `throughline.db.backup-20260717-ghost-recovery` 取得済み)。
106
+ - [ ] multi-window での近接 SessionStart 実機再現は未実施(幽霊の発生タイミングを
107
+ 任意に誘発できないため。回帰は subprocess テストの incident 再現形で担保)。
108
+
109
+ ## 関連
110
+
111
+ - 幽霊 SessionStart 自体は Claude Code 側の挙動(upstream 報告は別トラック)。
112
+ - 10k persisted-output は third-party 仕様として rag/01-hooks に実測記録。
@@ -0,0 +1,81 @@
1
+ # ADR 0015: L1 要約の既定を gpt-5.6-luna / effort low / 削減割合 1/5 にする
2
+
3
+ 日付: 2026-07-17
4
+
5
+ ## Status
6
+
7
+ Accepted(実装済み・全テスト green。実運用での要約品質の継続観察は運用に委ねる)。
8
+
9
+ ## Context
10
+
11
+ - オーナー方針: 裏方で動く AI は Codex に寄せる。GPT-5.6 系列(sol / terra / luna)の
12
+ 登場を受け、コスト効率を含めてモデルを選定する。要約の目標量は文字数指定ではなく
13
+ **削減割合**で決めたい(割合はオーナーが設定で動かせること)。
14
+ - 現行実装の問題: `summarizeWithCodexCli` は isolation のため `--ignore-user-config` で
15
+ `codex exec` を呼ぶが、これにより `~/.codex/config.toml` のモデル選択も読まれず、
16
+ **CLI 内蔵デフォルトで走っていた**(明示 `-m` なし)。
17
+ - 基準線: 現行 Claude Haiku 経路は削減割合 1/5(`l2Text.length / 5` を「約N文字」に
18
+ 換算してプロンプトへ渡す割合ベース。docs の「L1 = one-liner」記述は実装と乖離)。
19
+
20
+ ## 実測評価(2026-07-17、このMac)
21
+
22
+ 方法: 実 DB の L2 ターンをソースに、本番と同型のプロンプト・`codex exec` flag で要約を
23
+ 生成。**要約を見る前に**各ソースから要点チェックリスト(固有名詞・数値・因果・決定・
24
+ 未解決)を作成し、要約ごとの「要点拾い数」で採点。原文に無い内容の混入(捏造)は別枠で
25
+ 全数検査。判定者は親セッション(単一判定者、n は下記のとおり=既定値選定には十分、
26
+ 論文品質ではない)。
27
+
28
+ ### Round 1: モデル × 削減割合(5ソース × 7構成 = 35ラン)
29
+
30
+ | 構成 | 拾い率 | 備考 |
31
+ |---|---|---|
32
+ | Haiku@1/5(現行基準線) | 26/35 = 74%(timeout 1件込み。完走分のみ 93%) | **5本中1本が90秒 timeout で完全失敗**。本番はリトライ後 raw L2 fallback = 実質無圧縮 |
33
+ | luna@1/5 | 32/35 = 91% | 6〜16秒 |
34
+ | luna@1/10 | 80% / luna@1/15 | 64% |
35
+ | terra@1/5 | 32.5/35 = 93% | luna+2%のために上位量を払う価値なし |
36
+ | terra@1/10 | 73% / terra@1/15 | 60% |
37
+
38
+ **結論1: 圧縮を 1/5 より強めると要点が死ぬ(93%→73〜80%)。モデルを賢くしても
39
+ 救えない=情報量の物理であり、既定割合は 1/5 のまま。**
40
+
41
+ ### Round 2: luna の effort 比較(8ソース × {none, low, medium} × 2反復 = 48ラン)
42
+
43
+ ソースは別プロジェクト由来3本(Caveat 技術報告 / OpenCClaw 指示書散文 / 6.2k字
44
+ subagent 報告)を追加。`minimal` は luna 非対応(400 error。サポートは
45
+ none/low/medium/high/xhigh)のため none を下端とした。
46
+
47
+ | effort | 拾い率 | 中央値レイテンシ | 特徴的な欠落 |
48
+ |---|---|---|---|
49
+ | none | 103/116 = 89% | 11.8s | **物語の現在地を落とす**(親の裁定で再開済み→「裁定待ち」と書く ×2、設計矛盾の同型性、「効果検証はこれから」) |
50
+ | **low** | **107.5/116 = 93%** | 12.4s | 最もバランス。反復ブレ ±1〜2項目 |
51
+ | medium | 107.5/116 = 93% | 16.3s | low と同点。**事故の白状を2回とも落とした**(整理しすぎて都合の悪い脱線を刈る傾向) |
52
+
53
+ 捏造は Round 1+2 の全 82 有効ランで 0 件。
54
+
55
+ **結論2: low が既定。medium は同点で 1.4 倍遅くコスト高。none は事実の羅列は拾えるが
56
+ 「誰が何を決めて今どこか」を落とし、再開用 L1 として痛い欠落をする。**
57
+
58
+ ## Decision
59
+
60
+ 1. **既定: `gpt-5.6-luna` / `model_reasoning_effort="low"` / 削減割合 0.2 (=1/5)**。
61
+ 2. `summarizeWithCodexCli` は明示 `-m` / `-c model_reasoning_effort=...` を渡す
62
+ (`--ignore-user-config` によるモデル未指定の穴を修理)。
63
+ 3. env で設定可能: `THROUGHLINE_L1_MODEL` / `THROUGHLINE_L1_EFFORT` /
64
+ `THROUGHLINE_L1_RATIO`(割合形式 0 < r ≤ 1)。**不正な RATIO は黙って既定へ
65
+ 落とさず explicit error**(フォールバック禁止原則)。
66
+ 4. claude-primary の backend 順序を **codex-sidecar → Codex CLI (luna@low) → Haiku →
67
+ raw L2** にする。各段の失敗理由は結果 (`sidecarReason` / `codexCliReason`) に記録
68
+ する宣言済み fallback。codex-primary は従来どおり Codex CLI 一本で explicit error。
69
+ (実測で Haiku は timeout 完全失敗があり、luna は同等品質・半分以下のレイテンシ・
70
+ 失敗ゼロのため、Haiku より先に置く。)
71
+ 5. sidecar `summarize-l1` preset のモデルは codex-sidecar(別repo)の所有。本 ADR では
72
+ 変更しない。
73
+ 6. docs の「L1 = one-liner」記述は「割合ベース(既定 1/5)」へ修正する。
74
+
75
+ ## Acceptance
76
+
77
+ - `npm test` 684/685 PASS(1 skip は既存 win32 契約)。
78
+ - 新規テスト: 明示 `-m`/effort 引数、env 上書き、RATIO のプロンプト反映、不正 RATIO の
79
+ explicit error、claude-primary の sidecar→codex-cli→haiku 梯子。
80
+ - 評価の生データ: 実測スクリプト・全要約・採点はセッション scratchpad(使い捨て)。
81
+ 要旨は本 ADR の表が正本。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -0,0 +1,48 @@
1
+ # Hook stdout は約 10k 字で `<persisted-output>` に file 化され、モデル可視は先頭 2KB に劣化する
2
+
3
+ - **出典**: 自前実測(tracer 実験 + 全 transcript 掃引)。このMac、Claude Code 2.1.211
4
+ - **取得日**: 2026-07-17
5
+ - **確度**: confirmed(境界2点 + 実運用12件で再現)
6
+
7
+ ## 事実
8
+
9
+ SessionStart / UserPromptSubmit の hook **plain stdout**(および文書上は
10
+ `hookSpecificOutput` の context string)は、約 10,000 字を超えると inline 注入されず、
11
+ transcript の `attachment.content` が以下に置換される:
12
+
13
+ ```
14
+ <persisted-output>
15
+ Output too large (11.7KB). Full output saved to: ~/.claude/projects/<proj>/<session>/tool-results/hook-<uuid>...
16
+ <先頭 ~2KB の preview>
17
+ ...
18
+ </persisted-output>
19
+ ```
20
+
21
+ - 全文は attachment の `stdout` field に保存されるが、**モデル可視は `content`
22
+ (= path + 先頭 2KB preview) だけ**。モデルがツールでファイルを読まない限り本文は届かない。
23
+ - 境界実測: **9,501 字は inline 通過 / 15,286 字は file 化**(公称上限は hooks reference の
24
+ 「10,000 characters per context string」と整合)。
25
+ - tracer 実験: 11,953 字 stdout の 10k 境界後 tracer は Haiku から不可視、境界前 tracer は可視。
26
+
27
+ ## 影響(Throughline での実害)
28
+
29
+ 実運用 transcript 掃引の結果、SessionStart 注入で 10k 超だった **12 件(2026-06-28 /
30
+ v2.1.195 以降の全件)が 12 件とも劣化**していた(例: 64,148 字 emit → 可視 2,054 字)。
31
+ 記憶注入の L1+L2 本体はモデルに読まれておらず、ヘッダ + 現在地アンカーが偶然先頭 2KB に
32
+ 収まっていたため「機能している風」に見えていた。
33
+
34
+ v2.1.195 より前に 10k 超を emit した実績が手元に無いため、「いつ導入されたか」は未確定。
35
+ 運用上は「現行版では 10k 超 = 劣化」で確定。
36
+
37
+ ## 対処
38
+
39
+ 注入は予算内レンダリングで行う(Throughline は `buildBudgetedResumeContext`、上限 9,500 字。
40
+ ヘッダ + アンカー常時全文、L1 → L2 を新しい側から詰め、省略は注入文へ明示)。
41
+ 詳細は [ADR 0014](../../docs/adr/0014-two-phase-handoff-ghost-baton.md)。
42
+
43
+ ## 検証手順(再現用)
44
+
45
+ 1. 一時 project に `.claude/settings.json` で UserPromptSubmit hook を登録し、
46
+ 10k 境界の前後に tracer を置いた ~12k 字を stdout に emit する
47
+ 2. `claude -p --model claude-haiku-4-5-*` で「見えている tracer を列挙して」と問う
48
+ 3. transcript の `attachment.content` / `attachment.stdout` を比較する
package/rag/INDEX.md CHANGED
@@ -86,6 +86,10 @@ Verified via [openclaude source](04-skills/raw/initialUserMessage-investigation.
86
86
 
87
87
  SessionEnd reason enum: `clear|resume|logout|prompt_input_exit|bypass_permissions_disabled|other`、default timeout 1.5s(/clear にも適用)— [session-end-reasons.md](01-hooks/raw/session-end-reasons.md)。実測: ビルトイン /clear はどのクライアントでも UserPromptSubmit に届かない(同一セッション /tl 対照実験 ×2 + VSCode 2.1.207)。VSCode は `source:"clear"` を送るが Desktop 2.1.205 は `source:"startup"`(クライアント実装差・バージョン交絡棄却済み)。→ Desktop の /clear 検知は SessionEnd(reason='clear') が唯一の hook 経路候補(実機検証は [docs/12](../docs/12_desktop_clear_handoff_plan.md) A Phase 1)。
88
88
 
89
+ ### Finding 9: hook stdout は ~10k 字で persisted-output に file 化、モデル可視は先頭 2KB のみ (2026-07-17)
90
+
91
+ SessionStart / UserPromptSubmit の hook stdout は約 10,000 字超で `<persisted-output>`(保存ファイルパス + 先頭 2KB preview)に置換され、モデルには preview しか届かない(silent degradation、hook は exit 0 のまま)。境界実測 9,501 字 inline / 15,286 字 file 化。実運用の 10k 超注入 12 件(v2.1.195 以降)は 12/12 劣化 — 記憶注入の L1+L2 本体は読まれていなかった。対処は 9,500 字予算内レンダリング。詳細: [hook-stdout-10k-persisted-output.md](01-hooks/hook-stdout-10k-persisted-output.md)、[ADR 0014](../docs/adr/0014-two-phase-handoff-ghost-baton.md)。
92
+
89
93
  ---
90
94
 
91
95
  ## Throughline 仮説の見直し