throughline 0.12.5 → 0.12.6
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 +17 -1
- package/docs/adr/0015-l1-summarizer-model-effort-ratio.md +2 -0
- package/docs/adr/0028-l1-summarizer-backend-failure-code.md +59 -0
- package/package.json +1 -1
- package/src/cli/codex-hook.mjs +19 -2
- package/src/cli/codex-hook.test.mjs +14 -0
- package/src/hook-failure-log.mjs +6 -0
- package/src/hook-failure-log.test.mjs +18 -0
- package/src/observer-turn-feed.test.mjs +7 -2
- package/src/runtime-error-hook.test.mjs +73 -1
- package/src/runtime-error-store.mjs +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.12.6] — 2026-10-04
|
|
14
|
+
|
|
15
|
+
### 修正
|
|
16
|
+
|
|
17
|
+
- Codex Stop hookで、会話の取り込みが済んだ後のL1要約backend(Codex CLI)の失敗を、`HOOK_CODEX_FAILED`(Codex hook processing failed・high)ではなく`L1_SUMMARIZER_BACKEND_FAILED`(L1 summarizer backend failed・warn)として数える([ADR 0028](docs/adr/0028-l1-summarizer-backend-failure-code.md))。Codex CLIが利用上限や認証切れで非0終了すると、取り込みは成功しているのにhook処理の失敗として記録されていた。hookの終了codeとstderrは変えない。要約は次のStopが同じturnからやり直す。
|
|
18
|
+
- 0.9.0と0.12.5の両方で、20 turnを超えるスレッドのStopがCodex CLIの失敗で`HOOK_CODEX_FAILED`を記録することを再現した。Macに残っていた1151回の最後の1回は、そのスレッドが利用上限に当たった時刻と一致する。1151回すべてがこの形だったことを示す記録は無い。
|
|
19
|
+
|
|
20
|
+
### 変更
|
|
21
|
+
|
|
22
|
+
- `~/.throughline/logs/hook-failures.log`に、errorが持つ`reason`と、外部CLIの`stderr`の末尾(1000字まで)を足す。`Codex CLI summarizer failed: exit 1`だけでは、利用上限か認証切れかが分からないため。端末内にだけ残し、外へは送らない。
|
|
23
|
+
|
|
24
|
+
### 試験
|
|
25
|
+
|
|
26
|
+
- Observerのcursorがprojectのpath・session id・本文を持たないことを確かめる試験が、偶然落ちることがあった。sha256の16進に目印の`a257`が現れると失敗し、Windowsではpathをそのまま正規表現にしていた。目印を16進に現れない文字列にし、値をそのまま探す形に直す。製品の動作は変えていない。
|
|
27
|
+
|
|
13
28
|
## [0.12.5] — 2026-10-04
|
|
14
29
|
|
|
15
30
|
### 修正
|
|
@@ -1588,7 +1603,8 @@ two attempts, instrument first instead of patching again.
|
|
|
1588
1603
|
|
|
1589
1604
|
---
|
|
1590
1605
|
|
|
1591
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.12.
|
|
1606
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.12.6...HEAD
|
|
1607
|
+
[0.12.6]: https://github.com/kitepon/Throughline/compare/v0.12.5...v0.12.6
|
|
1592
1608
|
[0.12.5]: https://github.com/kitepon/Throughline/compare/v0.12.4...v0.12.5
|
|
1593
1609
|
[0.12.4]: https://github.com/kitepon/Throughline/compare/v0.12.3...v0.12.4
|
|
1594
1610
|
[0.12.3]: https://github.com/kitepon/Throughline/compare/v0.12.2...v0.12.3
|
|
@@ -66,6 +66,8 @@ none/low/medium/high/xhigh)のため none を下端とした。
|
|
|
66
66
|
4. claude-primary の backend 順序を **codex-sidecar → Codex CLI (luna@low) → Haiku →
|
|
67
67
|
raw L2** にする。各段の失敗理由は結果 (`sidecarReason` / `codexCliReason`) に記録
|
|
68
68
|
する宣言済み fallback。codex-primary は従来どおり Codex CLI 一本で explicit error。
|
|
69
|
+
(2026-10-04 [ADR 0028](0028-l1-summarizer-backend-failure-code.md)で、この explicit error を runtime error store では
|
|
70
|
+
`L1_SUMMARIZER_BACKEND_FAILED` として数えることにした。hook の終了 code と stderr は変えていない。)
|
|
69
71
|
(実測で Haiku は timeout 完全失敗があり、luna は同等品質・半分以下のレイテンシ・
|
|
70
72
|
失敗ゼロのため、Haiku より先に置く。)
|
|
71
73
|
5. sidecar `summarize-l1` preset のモデルは codex-sidecar(別repo)の所有。本 ADR では
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ADR 0028: Codex Stop の L1 要約 backend の失敗は、hook 処理の失敗と別の code で数える
|
|
2
|
+
|
|
3
|
+
日付: 2026-10-04
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Codex Stop hook は、rollout を DB へ取り込んで commit した後、bodies が L2 window(20 turn)を超えていれば、
|
|
8
|
+
最古の未要約 turn を Codex CLI(`codex exec`)で L1 に要約する。codex-primary の要約は Codex CLI 一本で、
|
|
9
|
+
CLI の失敗は explicit error にする(ADR 0015 Decision 4)。
|
|
10
|
+
|
|
11
|
+
この error は hook の一番外の catch まで届き、`HOOK_CODEX_FAILED`(`Throughline Codex hook processing failed`、
|
|
12
|
+
severity `high`)として runtime error store に数えられていた。
|
|
13
|
+
|
|
14
|
+
Mac の runtime error store には `HOOK_CODEX_FAILED` が 1151 回あった(2026-07-13 〜 2026-08-07、最後の版は 0.9.0)。
|
|
15
|
+
最後の1回は 2026-08-07T06:23:58.298Z で、その Codex スレッドは同じ秒に次の turn が
|
|
16
|
+
`You've hit your usage limit` で終わっている。当時の stderr はどこにも残っていない。
|
|
17
|
+
|
|
18
|
+
0.9.0 と 0.12.5 の両方で、次を再現した。20 turn を超えるスレッドに Stop を送り、Codex CLI が非0で終わると、
|
|
19
|
+
取り込みは成功したまま hook が `Codex CLI summarizer failed: exit N`(reason `codex_cli_failed`)で失敗し、
|
|
20
|
+
`HOOK_CODEX_FAILED` が1回増える。UserPromptSubmit と PostToolUse は要約をしないので失敗しない。
|
|
21
|
+
|
|
22
|
+
`HOOK_CODEX_FAILED` という code からは、取り込みが壊れたのか、外部 CLI が使えなかっただけなのかを区別できない。
|
|
23
|
+
後者は Codex 側の利用上限・認証切れ・model の失敗で起き、Throughline の修理では直らない。
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
1. Codex hook の失敗のうち、L1 要約 backend の失敗だけを `L1_SUMMARIZER_BACKEND_FAILED` として数える。
|
|
28
|
+
component は `codex_l1_summarizer`、template は `Throughline L1 summarizer backend (Codex CLI) failed`、
|
|
29
|
+
severity は `warn`。
|
|
30
|
+
2. 対象は `source` が `codex-cli` で、`reason` が `codex_cli_failed`(非0終了・timeout・起動失敗)か
|
|
31
|
+
`empty_output` の error だけ。`missing_project_path` と `recursion_guard` は製品側の誤りなので、
|
|
32
|
+
`HOOK_CODEX_FAILED` のままにする。要約以外の失敗も `HOOK_CODEX_FAILED` のまま。
|
|
33
|
+
3. hook の終了 code(1)と stderr(`[codex-hook] Codex CLI summarizer failed: exit N`)は変えない。
|
|
34
|
+
ADR 0015 の explicit error 契約と、失敗を隠さない原則はそのまま。変えるのは数える code だけ。
|
|
35
|
+
4. `hook-failures.log`(ADR 0027)に、error が持つ `reason` と、外部 CLI の `stderr` の末尾(1000字まで)を足す。
|
|
36
|
+
`Codex CLI summarizer failed: exit 1` だけでは、利用上限なのか認証切れなのかが分からないため。
|
|
37
|
+
このログは端末内にだけ残し、外へは送らない。runtime error store には今までどおり定型 code と回数だけを入れる。
|
|
38
|
+
5. severity を `warn` にする理由: 会話の取り込みは commit 済みで、記憶は失われていない。要約は次の Stop が
|
|
39
|
+
同じ turn からやり直す。L1 が無い間、recall は L2 の本文を返す(ADR 0016)。
|
|
40
|
+
BugHub が受ける severity は `fatal`・`high`・`warn`・`info` の4つで、他の値は report 全体が 422 になる
|
|
41
|
+
(2026-10-04、BugHub の `PRODUCT_REPORTING.md` と `factory-report-v1.schema.json` をBugHubの担当が確認)。
|
|
42
|
+
|
|
43
|
+
## Consequences
|
|
44
|
+
|
|
45
|
+
- 外部 CLI が使えない端末は、BugHub で「L1 summarizer backend failed」(warn、日次のまとめ)として見える。
|
|
46
|
+
「Codex hook processing failed」(high、即時通知)は、取り込みや hook 処理そのものの失敗だけになる。
|
|
47
|
+
- fingerprint は code ごとに決まるので、新しい code は BugHub で新しい issue になる。過去の `HOOK_CODEX_FAILED` の
|
|
48
|
+
回数は移らない。
|
|
49
|
+
- runtime error store は、知らない code を持つ記録を schema invalid として拒否する。この版で
|
|
50
|
+
`L1_SUMMARIZER_BACKEND_FAILED` を記録した端末を 0.12.5 以前へ戻すと、store を読めなくなる。
|
|
51
|
+
- Claude Stop は対象外。claude-primary の要約は Codex CLI → Haiku → raw L2 の宣言済み fallback で、
|
|
52
|
+
backend の失敗では throw しない。
|
|
53
|
+
|
|
54
|
+
## 確認できていないこと
|
|
55
|
+
|
|
56
|
+
- Mac の 1151 回がすべてこの形だったことを示す記録は無い。確認できたのは、同じ code を同じ版で再現できたことと、
|
|
57
|
+
最後の1回が利用上限に当たった時刻と一致することだけ。外した仮説は、製品の試験からの漏れ、rollout の大きさ、
|
|
58
|
+
入力の形、同時実行、スレッド全体の順次再生。
|
|
59
|
+
- 要約が失敗し続ける端末では、Codex の Stop hook が毎回 exit 1 になる。Codex 側の表示への影響は調べていない。
|
package/package.json
CHANGED
package/src/cli/codex-hook.mjs
CHANGED
|
@@ -360,6 +360,22 @@ async function runCodexContextRefreshInstructionHook({
|
|
|
360
360
|
};
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
const L1_SUMMARIZER_BACKEND_REASONS = new Set(['codex_cli_failed', 'empty_output']);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Codex hook が失敗した時に記録する runtime error の code を決める(ADR 0028)。
|
|
367
|
+
* L1 要約の backend(Codex CLI)が非0で終わった、または空を返した失敗は、取り込みの後に起きる
|
|
368
|
+
* 外部 CLI の失敗なので、hook 処理の失敗とは別の code で数える。hook の終了 code と stderr は変えない。
|
|
369
|
+
* @param {unknown} error
|
|
370
|
+
* @returns {'HOOK_CODEX_FAILED' | 'L1_SUMMARIZER_BACKEND_FAILED'}
|
|
371
|
+
*/
|
|
372
|
+
export function codexHookFailureCode(error) {
|
|
373
|
+
if (error?.source === 'codex-cli' && L1_SUMMARIZER_BACKEND_REASONS.has(error?.reason)) {
|
|
374
|
+
return 'L1_SUMMARIZER_BACKEND_FAILED';
|
|
375
|
+
}
|
|
376
|
+
return 'HOOK_CODEX_FAILED';
|
|
377
|
+
}
|
|
378
|
+
|
|
363
379
|
export async function run(argv = []) {
|
|
364
380
|
suppressExperimentalWarnings();
|
|
365
381
|
let parsed;
|
|
@@ -417,8 +433,9 @@ export async function run(argv = []) {
|
|
|
417
433
|
// continue=falseは、明示した失敗理由を含めてhostへ停止を届けた返答。背景処理の成否は引き継ぎ状態へ記録する。
|
|
418
434
|
process.exit(result.continue === false || result.status === 'ok' || result.status === 'skipped' ? 0 : 1);
|
|
419
435
|
} catch (err) {
|
|
420
|
-
|
|
421
|
-
|
|
436
|
+
const code = codexHookFailureCode(err);
|
|
437
|
+
recordRuntimeErrorBestEffort(code, { env: process.env });
|
|
438
|
+
logHookFailure(code, err);
|
|
422
439
|
const msg = err instanceof Error ? err.message : 'unknown';
|
|
423
440
|
if (parsed.json) {
|
|
424
441
|
process.stdout.write(
|
|
@@ -6,6 +6,7 @@ import { test } from 'node:test';
|
|
|
6
6
|
import { DatabaseSync } from 'node:sqlite';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
codexHookFailureCode,
|
|
9
10
|
runCodexPostToolUseHook,
|
|
10
11
|
runCodexStopHook,
|
|
11
12
|
runCodexUserPromptSubmitHook,
|
|
@@ -497,6 +498,19 @@ test('codex-hook stop skips cleanly when Codex thread id is unavailable', async
|
|
|
497
498
|
}
|
|
498
499
|
});
|
|
499
500
|
|
|
501
|
+
test('codexHookFailureCodeは、L1要約backendの失敗だけを別のcodeにする', () => {
|
|
502
|
+
const backend = (reason) => Object.assign(new Error('x'), { source: 'codex-cli', reason });
|
|
503
|
+
assert.equal(codexHookFailureCode(backend('codex_cli_failed')), 'L1_SUMMARIZER_BACKEND_FAILED');
|
|
504
|
+
assert.equal(codexHookFailureCode(backend('empty_output')), 'L1_SUMMARIZER_BACKEND_FAILED');
|
|
505
|
+
// 製品側の誤り(projectPath 欠落・再帰)と、要約以外の失敗は hook 処理の失敗のまま。
|
|
506
|
+
assert.equal(codexHookFailureCode(backend('missing_project_path')), 'HOOK_CODEX_FAILED');
|
|
507
|
+
assert.equal(codexHookFailureCode(backend('recursion_guard')), 'HOOK_CODEX_FAILED');
|
|
508
|
+
assert.equal(codexHookFailureCode(Object.assign(new Error('x'), { reason: 'codex_cli_failed' })), 'HOOK_CODEX_FAILED');
|
|
509
|
+
assert.equal(codexHookFailureCode(new Error('database is locked')), 'HOOK_CODEX_FAILED');
|
|
510
|
+
assert.equal(codexHookFailureCode('thrown string'), 'HOOK_CODEX_FAILED');
|
|
511
|
+
assert.equal(codexHookFailureCode(null), 'HOOK_CODEX_FAILED');
|
|
512
|
+
});
|
|
513
|
+
|
|
500
514
|
test('codexHomeFromTranscriptPath infers CODEX_HOME from rollout path', () => {
|
|
501
515
|
const path = '/tmp/codex-home/sessions/2026/05/06/rollout-2026-05-06T09-40-50-id.jsonl';
|
|
502
516
|
assert.equal(_internal.codexHomeFromTranscriptPath(path), '/tmp/codex-home');
|
package/src/hook-failure-log.mjs
CHANGED
|
@@ -36,6 +36,12 @@ export function logHookFailure(code, error, { home, now = Date.now(), stderr = p
|
|
|
36
36
|
name: error instanceof Error ? error.name : typeof error,
|
|
37
37
|
message: message.slice(0, HOOK_FAILURE_MESSAGE_LIMIT),
|
|
38
38
|
};
|
|
39
|
+
// 外部 CLI の失敗は、message が終了 code だけになる。理由(reason)と CLI の stderr の末尾も残す
|
|
40
|
+
// (CLI は先頭に起動情報を出し、失敗の理由は末尾に出す)。
|
|
41
|
+
if (typeof error?.reason === 'string' && error.reason) entry.reason = error.reason;
|
|
42
|
+
if (typeof error?.stderr === 'string' && error.stderr) {
|
|
43
|
+
entry.stderr = error.stderr.slice(-HOOK_FAILURE_MESSAGE_LIMIT);
|
|
44
|
+
}
|
|
39
45
|
const path = hookFailureLogPath(home === undefined ? {} : { home });
|
|
40
46
|
try {
|
|
41
47
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -23,6 +23,24 @@ test('logHookFailureは、失敗の理由を1行のJSONで追記する', () => {
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test('logHookFailureは、外部CLIの失敗の理由とstderrの末尾を残す', () => {
|
|
27
|
+
const home = mkdtempSync(join(tmpdir(), 'tl-hook-failure-log-'));
|
|
28
|
+
try {
|
|
29
|
+
const err = new Error('Codex CLI summarizer failed: exit 1');
|
|
30
|
+
err.reason = 'codex_cli_failed';
|
|
31
|
+
err.stderr = `${'banner '.repeat(300)}ERROR: usage limit`;
|
|
32
|
+
logHookFailure('L1_SUMMARIZER_BACKEND_FAILED', err, { home });
|
|
33
|
+
const [entry] = readFileSync(hookFailureLogPath({ home }), 'utf8').trim().split('\n').map((line) => JSON.parse(line));
|
|
34
|
+
assert.equal(entry.code, 'L1_SUMMARIZER_BACKEND_FAILED');
|
|
35
|
+
assert.equal(entry.message, 'Codex CLI summarizer failed: exit 1');
|
|
36
|
+
assert.equal(entry.reason, 'codex_cli_failed');
|
|
37
|
+
assert.equal(entry.stderr.length, HOOK_FAILURE_MESSAGE_LIMIT);
|
|
38
|
+
assert.match(entry.stderr, /ERROR: usage limit$/);
|
|
39
|
+
} finally {
|
|
40
|
+
rmSync(home, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
26
44
|
test('logHookFailureは、長い文面を上限で切る', () => {
|
|
27
45
|
const home = mkdtempSync(join(tmpdir(), 'tl-hook-failure-log-'));
|
|
28
46
|
try {
|
|
@@ -126,7 +126,7 @@ test('observer feed: Claude history floor, host/thread switch, cross-host tie, a
|
|
|
126
126
|
try {
|
|
127
127
|
const options = box.receiptOptions;
|
|
128
128
|
seedCompletedTurnReceiptStore({ projectPath: box.project, receiptOptions: options, targetSessionId: 'claude-session' });
|
|
129
|
-
writeCompletedTurnReceipt({ projectPath: box.project, targetSessionId: 'claude-session', originSessionId: 'o257', userBody: '
|
|
129
|
+
writeCompletedTurnReceipt({ projectPath: box.project, targetSessionId: 'claude-session', originSessionId: 'o257', userBody: 'user-body-257', assistantBody: 'assistant-body-257', completedAt: 257 }, options);
|
|
130
130
|
const latest = resolveObserverTurnFeed({ projectPath: box.project, receiptOptions: options, codexHome: box.home });
|
|
131
131
|
const decoded = decodeObserverCursor(latest.throughCursor);
|
|
132
132
|
const beforeFloor = encodeObserverCursor({ ...decoded, history_floor: 1 });
|
|
@@ -137,7 +137,12 @@ test('observer feed: Claude history floor, host/thread switch, cross-host tie, a
|
|
|
137
137
|
const changedPrefix = encodeObserverCursor({ ...decoded, prefix_sha256: 'f'.repeat(64) });
|
|
138
138
|
assert.equal(resolveObserverTurnFeed({ projectPath: box.project, cursor: changedPrefix, receiptOptions: options, codexHome: box.home }).status, 'resync_required');
|
|
139
139
|
assert.throws(() => encodeObserverCursor({ ...decoded, length: 0 }), /cursor invalid/);
|
|
140
|
-
|
|
140
|
+
// cursor は project の path・session id・本文を持たない。目印は16進に現れない文字列にし、正規表現を使わず
|
|
141
|
+
// 値をそのまま探す(sha256 の16進に `a257` が偶然現れる、Windows の path が正規表現として壊れる、の2つを避ける)。
|
|
142
|
+
const cursorValues = Object.values(decoded).map(String).join('\n');
|
|
143
|
+
for (const secret of [box.project, 'claude-session', 'user-body-257', 'assistant-body-257']) {
|
|
144
|
+
assert.equal(cursorValues.includes(secret), false, secret);
|
|
145
|
+
}
|
|
141
146
|
const id = '019dfaba-f87e-7f41-a144-d5ca7c6dd7f9';
|
|
142
147
|
writeRollout(box.home, box.project, id, completeEvents('2026-07-15T00:03:00.000Z'));
|
|
143
148
|
const switched = resolveObserverTurnFeed({ projectPath: box.project, cursor: latest.throughCursor, receiptOptions: options, codexHome: box.home });
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
6
|
import { tmpdir } from 'node:os';
|
|
6
7
|
import { dirname, join } from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -69,6 +70,77 @@ test('top-level hook owners record one fixed aggregate per failure without repla
|
|
|
69
70
|
assert.equal(store.records.find((record) => record.error_code === 'HOOK_PROCESS_TURN_FAILED').count, 2);
|
|
70
71
|
});
|
|
71
72
|
|
|
73
|
+
function makeBin(dir, name, body) {
|
|
74
|
+
const script = join(dir, `${name}.mjs`);
|
|
75
|
+
writeFileSync(script, body);
|
|
76
|
+
if (process.platform === 'win32') {
|
|
77
|
+
const command = join(dir, `${name}.cmd`);
|
|
78
|
+
writeFileSync(command, `@echo off\r\n${JSON.stringify(process.execPath)} ${JSON.stringify(script)} %*\r\n`);
|
|
79
|
+
writeFileSync(join(dir, `${name}.ps1`), `& ${JSON.stringify(process.execPath)} ${JSON.stringify(script)} @args\nexit $LASTEXITCODE\n`);
|
|
80
|
+
return command;
|
|
81
|
+
}
|
|
82
|
+
const command = join(dir, name);
|
|
83
|
+
writeFileSync(command, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(script)} "$@"\n`);
|
|
84
|
+
chmodSync(command, 0o755);
|
|
85
|
+
return command;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test('Codex StopでL1要約backendが失敗した時、取り込みは残り、別のcodeで1回数える', () => {
|
|
89
|
+
const { root, env } = createEnabledEnvironment('throughline-runtime-hook-summarizer-');
|
|
90
|
+
const threadId = '019dfaba-f87e-7f41-a144-d5ca7c6dd7f9';
|
|
91
|
+
const project = join(root, 'project');
|
|
92
|
+
const codexHome = join(root, 'codex');
|
|
93
|
+
const rolloutDir = join(codexHome, 'sessions', '2026', '05', '06');
|
|
94
|
+
mkdirSync(project, { recursive: true });
|
|
95
|
+
mkdirSync(rolloutDir, { recursive: true });
|
|
96
|
+
const at = '2026-05-06T00:41:00.000Z';
|
|
97
|
+
const event = (type, payload = {}) => ({ timestamp: at, type: 'event_msg', payload: { type, ...payload } });
|
|
98
|
+
const rows = [{ timestamp: at, type: 'session_meta',
|
|
99
|
+
payload: { id: threadId, timestamp: at, cwd: project, source: 'vscode', cli_version: '0.128.0-alpha.1' } }];
|
|
100
|
+
// L2 window(20 turn)を超えると、Stop は最古の turn を Codex CLI で要約する。
|
|
101
|
+
for (let turn = 0; turn < 22; turn += 1) {
|
|
102
|
+
rows.push(event('user_message', { message: `request ${turn}` }), event('task_started'),
|
|
103
|
+
{ timestamp: at, type: 'turn_context', payload: { model: 'gpt-5.5', cwd: project } },
|
|
104
|
+
event('agent_message', { message: `answer ${turn}` }), event('task_complete'));
|
|
105
|
+
}
|
|
106
|
+
const rolloutPath = join(rolloutDir, `rollout-2026-05-06T09-40-50-${threadId}.jsonl`);
|
|
107
|
+
writeFileSync(rolloutPath, rows.map((row) => JSON.stringify(row)).join('\n') + '\n');
|
|
108
|
+
const binDir = join(root, 'bin');
|
|
109
|
+
mkdirSync(binDir, { recursive: true });
|
|
110
|
+
const codex = makeBin(binDir, 'codex', "process.stderr.write('ERROR: usage limit reached\\n');\nprocess.exit(42);\n");
|
|
111
|
+
|
|
112
|
+
const result = spawnSync(process.execPath, [BIN, 'codex-hook', 'stop'], {
|
|
113
|
+
env: { ...env, CODEX_HOME: codexHome, THROUGHLINE_CODEX_CLI_BIN: codex },
|
|
114
|
+
input: JSON.stringify({ session_id: threadId, transcript_path: rolloutPath, cwd: project }),
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// hook は明示して失敗する(終了 code と stderr は変えない)。
|
|
119
|
+
assert.equal(result.status, 1, result.stderr);
|
|
120
|
+
assert.match(result.stderr, /\[codex-hook\] Codex CLI summarizer failed: exit 42/);
|
|
121
|
+
|
|
122
|
+
const store = JSON.parse(readFileSync(defaultRuntimeErrorStorePath(env), 'utf8'));
|
|
123
|
+
assert.deepEqual(store.records.map((record) => [record.error_code, record.component, record.severity, record.count]), [
|
|
124
|
+
['L1_SUMMARIZER_BACKEND_FAILED', 'codex_l1_summarizer', 'warn', 1],
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
// 取り込みは要約の前に commit 済み。
|
|
128
|
+
const db = new DatabaseSync(join(root, '.throughline', 'throughline.db'));
|
|
129
|
+
try {
|
|
130
|
+
const { n } = db.prepare("SELECT COUNT(*) AS n FROM bodies WHERE session_id = ? AND role = 'assistant'")
|
|
131
|
+
.get(`codex:${threadId}`);
|
|
132
|
+
assert.equal(n, 22);
|
|
133
|
+
} finally {
|
|
134
|
+
db.close();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [entry] = readFileSync(join(root, '.throughline', 'logs', 'hook-failures.log'), 'utf8')
|
|
138
|
+
.trim().split('\n').map((line) => JSON.parse(line));
|
|
139
|
+
assert.equal(entry.code, 'L1_SUMMARIZER_BACKEND_FAILED');
|
|
140
|
+
assert.equal(entry.reason, 'codex_cli_failed');
|
|
141
|
+
assert.match(entry.stderr, /usage limit reached/);
|
|
142
|
+
});
|
|
143
|
+
|
|
72
144
|
test('store failure preserves product failure and emits only fixed storage diagnostic', () => {
|
|
73
145
|
const { env } = createEnabledEnvironment('throughline-runtime-hook-store-fail-');
|
|
74
146
|
const storePath = defaultRuntimeErrorStorePath(env);
|
|
@@ -52,6 +52,13 @@ const DEFINITIONS = Object.freeze({
|
|
|
52
52
|
severity: 'high',
|
|
53
53
|
template: 'Throughline Codex hook processing failed',
|
|
54
54
|
}),
|
|
55
|
+
// Codex Stop の取り込みは済み、L1 要約の backend(Codex CLI)だけが失敗した(ADR 0028)。
|
|
56
|
+
L1_SUMMARIZER_BACKEND_FAILED: Object.freeze({
|
|
57
|
+
component: 'codex_l1_summarizer',
|
|
58
|
+
// BugHub が受ける severity は fatal・high・warn・info だけ。他の値は report 全体が 422 になる。
|
|
59
|
+
severity: 'warn',
|
|
60
|
+
template: 'Throughline L1 summarizer backend (Codex CLI) failed',
|
|
61
|
+
}),
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
export function defaultRuntimeErrorConfigPath(env = process.env) {
|