throughline 0.16.13 → 0.16.14
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,28 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.16.14] — 2026-10-10
|
|
14
|
+
|
|
15
|
+
### 修正
|
|
16
|
+
|
|
17
|
+
- Codex Desktop(同梱のCodex 0.162.0-alpha.17.2)が、内部の文脈(ターンのIDが`auto-compact-N`)から`PreCompact(auto)`のhookを呼ぶ時、Throughlineが`handoff_hook_identity_invalid`の失敗として記録していた。macOSの実機で3回。圧縮は止まっていて、タスクにも影響は無かったが、起きていない失敗がhighで記録された。この形の呼び出しは、失敗にせず圧縮だけを止め、`~/.throughline/codex-auto-handoff/internal-compactions.jsonl`に1行残す([ADR 0047](docs/adr/0047-codex-internal-compaction-is-stopped-without-error.md))。
|
|
18
|
+
- 圧縮を止める動きは今までと同じ。引き継ぎは、文脈が上限に近づいた利用者のターンで今までどおり起きる。
|
|
19
|
+
- それ以外の不正なID(会話のIDがUUIDでない、記録の場所が無い、知らない形のターンのID)は、今までどおり失敗として記録する。
|
|
20
|
+
|
|
21
|
+
### 対応の範囲
|
|
22
|
+
|
|
23
|
+
| | macOS | Linux | Windows |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| Codex(Desktop) | 直した。単体試験と、切り離した置き場での本物のhookの命令で確認(内部のIDは失敗0件・記録1行、不正なIDは失敗1件)。CIのmacOSも成功。**本物のCodex Desktopが内部の文脈からhookを呼ぶ場面は、狙って起こせないので未確認** | 直した。単体試験とCI。実機(同梱0.162.0-alpha.2)では、この呼び出しが起きていない | 直した。単体試験とCI。実機では、この失敗の記録が無い |
|
|
26
|
+
| Claude Code | 対象外。Claude Codeの`PreCompact`は別の実装で、ターンのIDを検査しない | 同じ | 同じ |
|
|
27
|
+
| Grok | 対象外。自動継続の機能が無く、`PreCompact`のhookを登録していない | 同じ | 同じ |
|
|
28
|
+
| Cursor | 対象外。自動継続の機能が無く、`PreCompact`のhookを登録していない | 同じ | 同じ |
|
|
29
|
+
|
|
30
|
+
### 確認していない範囲
|
|
31
|
+
|
|
32
|
+
- 本物のCodex Desktopが内部の文脈からhookを呼ぶ場面での、新しい版の動き(次に起きた時に、失敗の記録が増えず、`internal-compactions.jsonl`に行が出る事で確かめる)。
|
|
33
|
+
- 内部の文脈からの圧縮が、Codexのどの処理から始まるか(公開ソースでIDの形と渡り方だけを確かめた)。
|
|
34
|
+
|
|
13
35
|
## [0.16.13] — 2026-10-10
|
|
14
36
|
|
|
15
37
|
### 修正
|
|
@@ -2173,7 +2195,8 @@ two attempts, instrument first instead of patching again.
|
|
|
2173
2195
|
|
|
2174
2196
|
---
|
|
2175
2197
|
|
|
2176
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.
|
|
2198
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.14...HEAD
|
|
2199
|
+
[0.16.14]: https://github.com/kitepon/Throughline/compare/v0.16.13...v0.16.14
|
|
2177
2200
|
[0.16.13]: https://github.com/kitepon/Throughline/compare/v0.16.12...v0.16.13
|
|
2178
2201
|
[0.16.12]: https://github.com/kitepon/Throughline/compare/v0.16.11...v0.16.12
|
|
2179
2202
|
[0.16.11]: https://github.com/kitepon/Throughline/compare/v0.16.10...v0.16.11
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ADR 0047: Codex の内部の文脈からの自動圧縮は、失敗にせず止める
|
|
2
|
+
|
|
3
|
+
日付: 2026-10-10
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Codex Desktop の自動継続は、`PreCompact(auto)` の hook が受け取った会話の ID とターンの ID が UUID の形でない時、
|
|
8
|
+
`handoff_hook_identity_invalid` の例外にしていた。例外になった hook は、失敗の記録(`HOOK_CODEX_FAILED`、high)を残し、
|
|
9
|
+
`continue: false` を返して圧縮を止める。
|
|
10
|
+
|
|
11
|
+
2026-10-10、macOS の実機(Codex Desktop 26.1007、同梱の Codex 0.162.0-alpha.17.2)で、この失敗が3回記録された。
|
|
12
|
+
|
|
13
|
+
- 13:04:46: 引き継ぎのためにターンを止めた 2.5 秒後。
|
|
14
|
+
- 13:24:01: 別のタスクのターンが始まった 3.3 秒後。
|
|
15
|
+
- 13:25:25: そのタスクを引き継ぎのために止めた 4.7 秒後。
|
|
16
|
+
|
|
17
|
+
Codex は、利用者のターンとは別に、自分で作る内部の文脈を持つ。その文脈のターンの ID は `auto-compact-N` の形になる
|
|
18
|
+
(`codex-rs/core/src/session/mod.rs` の `next_internal_sub_id`)。`PreCompact` の hook へ渡るターンの ID は、その時の文脈の ID
|
|
19
|
+
(`codex-rs/core/src/hook_runtime.rs` の `run_pre_compact_hooks`)なので、内部の文脈から圧縮が始まると、UUID でない ID が届く。
|
|
20
|
+
同梱の Codex が 0.162.0-alpha.2 の端末(Linux)では、この呼び出しは起きていない。
|
|
21
|
+
|
|
22
|
+
3回とも、hook は圧縮を止めていた。2つのタスクの rollout に圧縮の行は無く、使用量も減っていない。13:24:01 に止めた後、
|
|
23
|
+
ターンはそのまま進み、80 秒後の `PreCompact`(利用者のターンの ID)で引き継いで `continued` になった。
|
|
24
|
+
起きていた害は、起きていない失敗が high で記録された事だった。
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
1. 会話の ID と記録の場所が正しく、ターンの ID が `auto-compact-<数字>` の形の `PreCompact(auto)` は、失敗にしない。
|
|
29
|
+
`continue: false` を返して圧縮を止める(今までと同じ結果)。失敗の記録は残さない。
|
|
30
|
+
2. その時に引き継ぎは作らない。内部の文脈は利用者のターンではなく、止める所と続ける作業が決まらない。文脈を残しておけば、
|
|
31
|
+
文脈が上限に近づいた利用者のターンの `PreCompact` で、今までどおり引き継ぐ。
|
|
32
|
+
3. 止めた事を `~/.throughline/codex-auto-handoff/internal-compactions.jsonl` に1行残す(時刻・会話の ID・ターンの ID)。
|
|
33
|
+
4. それ以外の形(会話の ID が UUID でない、記録の場所が無い、ターンの ID が知らない形)は、今までどおり
|
|
34
|
+
`handoff_hook_identity_invalid` の失敗にする。
|
|
35
|
+
|
|
36
|
+
## Consequences
|
|
37
|
+
|
|
38
|
+
- 内部の文脈からの圧縮で、失敗の記録が増えなくなる。圧縮を止める動きは変わらない。
|
|
39
|
+
- 内部の文脈からの圧縮が何度も起きるタスクでも、圧縮は通らない。利用者のターンの `PreCompact` が来るまで、文脈はそのまま残る。
|
|
40
|
+
- Codex が内部の ID の形を変えると、また失敗として記録される。その時は記録の message で気づける。
|
package/package.json
CHANGED
|
@@ -103,6 +103,15 @@ function openSuccessor(operation) {
|
|
|
103
103
|
return !result.error && result.status === 0;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Codex が内部の文脈へ付けるターンの ID(`codex-rs/core/src/session/mod.rs` の `next_internal_sub_id`)。
|
|
107
|
+
const INTERNAL_COMPACTION_TURN_ID_PATTERN = /^auto-compact-\d+$/;
|
|
108
|
+
|
|
109
|
+
/** ターンの外の自動圧縮を止めた記録。引き継ぎの行は増えないので、起きた事をここに残す。会話の本文は書かない。 */
|
|
110
|
+
function recordInternalCompactionStop(entry) {
|
|
111
|
+
mkdirSync(stateRoot(), { recursive: true, mode: 0o700 });
|
|
112
|
+
appendFileSync(join(stateRoot(), 'internal-compactions.jsonl'), JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
/** 引き継ぎ済みの旧タスクを止めた記録。引き継ぎの行は増えないので、起きた事をここに残す。会話の本文は書かない。 */
|
|
107
116
|
function recordAutoHandoffRedirect(entry) {
|
|
108
117
|
mkdirSync(stateRoot(), { recursive: true, mode: 0o700 });
|
|
@@ -157,13 +166,20 @@ function settleUndeliveredAutoHandoff(db, threadId, { rolloutPath, processes, re
|
|
|
157
166
|
export async function requestCodexAutoHandoff({ payload, db = null,
|
|
158
167
|
config = readAutoHandoffConfig(), launchWorker = launchAutoHandoffWorker,
|
|
159
168
|
findThread = findCodexThreadCandidate, openThread = openSuccessor, recordRedirect = recordAutoHandoffRedirect,
|
|
169
|
+
recordInternalCompaction = recordInternalCompactionStop,
|
|
160
170
|
processes = readRuntimeProcesses, readTarget = targetState, now = Date.now() } = {}) {
|
|
161
171
|
if (payload.trigger !== 'auto' || !autoHandoffEnabledFor(config, payload.cwd)) return { status: 'skipped' };
|
|
162
172
|
const threadId = payload.session_id, turnId = payload.turn_id;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
const located = CODEX_NATIVE_ID_PATTERN.test(threadId ?? '') &&
|
|
174
|
+
typeof payload.transcript_path === 'string' && isAbsolute(payload.transcript_path) && isAbsolute(payload.cwd ?? '');
|
|
175
|
+
// Codex が自分で作った内部の文脈(ターンの ID が `auto-compact-N`)からの圧縮。利用者のターンではないので、
|
|
176
|
+
// 引き継ぎの起点にできない。例外にせず圧縮だけを止める(文脈を残せば、次の利用者のターンの圧縮の合図で引き継げる)。
|
|
177
|
+
if (located && INTERNAL_COMPACTION_TURN_ID_PATTERN.test(turnId ?? '')) {
|
|
178
|
+
recordInternalCompaction({ at: new Date(now).toISOString(), thread_id: threadId, turn_id: turnId });
|
|
179
|
+
return { status: 'ok', inserted: false, internalCompaction: true, continue: false,
|
|
180
|
+
stopReason: 'Throughlineが、ターンの外の自動圧縮を止めました。文脈が上限に近づいたターンで、新しいタスクへ引き継ぎます。' };
|
|
166
181
|
}
|
|
182
|
+
if (!located || !CODEX_NATIVE_ID_PATTERN.test(turnId ?? '')) throw error('handoff_hook_identity_invalid');
|
|
167
183
|
// JS実装のrealpathは、WindowsのCodex Desktopが起動したhookの中で `EISDIR: lstat 'C:'` で落ちる。OSのrealpathを使う。
|
|
168
184
|
const rolloutPath = realpathSync.native(payload.transcript_path);
|
|
169
185
|
const state = readCodexHandoffState(rolloutPath, { threadId, turnId });
|
|
@@ -287,6 +287,33 @@ test('PreCompactの重複ではworkerを増やさず、manualと別projectでは
|
|
|
287
287
|
assert.equal((await requestCodexAutoHandoff({ ...args, config: { ...config, projects: ['/other'] } })).status, 'skipped');
|
|
288
288
|
}));
|
|
289
289
|
|
|
290
|
+
test('Codexの内部の文脈(auto-compact-N)からの圧縮は、例外にせず止めて記録する。引き継ぎは作らない', async () => withDb(async (db, home) => {
|
|
291
|
+
const thread = '11111111-1111-1111-1111-111111111111';
|
|
292
|
+
const file = join(home, 'rollout.jsonl'); writeFileSync(file, '');
|
|
293
|
+
const payload = { trigger: 'auto', cwd: home, session_id: thread, turn_id: 'auto-compact-3', transcript_path: file };
|
|
294
|
+
const config = { enabled: true, projects: [home], openHost: 'desktop' };
|
|
295
|
+
let launched = 0; const recorded = [];
|
|
296
|
+
const args = { db, config, payload, launchWorker: async () => { launched++; }, recordInternalCompaction: entry => recorded.push(entry), now: Date.parse('2026-10-10T04:24:01.175Z') };
|
|
297
|
+
const result = await requestCodexAutoHandoff(args);
|
|
298
|
+
assert.equal(result.status, 'ok'); assert.equal(result.continue, false); assert.equal(result.internalCompaction, true);
|
|
299
|
+
assert.match(result.stopReason, /ターンの外の自動圧縮を止めました/);
|
|
300
|
+
assert.equal(launched, 0);
|
|
301
|
+
assert.equal(db.prepare('SELECT COUNT(*) n FROM codex_handoffs').get().n, 0, '引き継ぎの行は作らない');
|
|
302
|
+
assert.deepEqual(recorded, [{ at: '2026-10-10T04:24:01.175Z', thread_id: thread, turn_id: 'auto-compact-3' }]);
|
|
303
|
+
|
|
304
|
+
// 無効な project と手動の圧縮は、今までどおり何もしない
|
|
305
|
+
assert.equal((await requestCodexAutoHandoff({ ...args, config: { ...config, projects: ['/other'] } })).status, 'skipped');
|
|
306
|
+
assert.equal((await requestCodexAutoHandoff({ ...args, payload: { ...payload, trigger: 'manual' } })).status, 'skipped');
|
|
307
|
+
assert.equal(recorded.length, 1);
|
|
308
|
+
|
|
309
|
+
// 形の決まっていない ID と、場所の分からない呼び出しは、今までどおり失敗として扱う
|
|
310
|
+
for (const broken of [{ turn_id: 'something-else' }, { turn_id: undefined }, { turn_id: 'auto-compact-' }, { session_id: 'not-a-uuid' },
|
|
311
|
+
{ transcript_path: null }, { transcript_path: 'relative/rollout.jsonl' }]) {
|
|
312
|
+
await assert.rejects(requestCodexAutoHandoff({ ...args, payload: { ...payload, ...broken } }), /handoff_hook_identity_invalid/);
|
|
313
|
+
}
|
|
314
|
+
assert.equal(recorded.length, 1);
|
|
315
|
+
}));
|
|
316
|
+
|
|
290
317
|
test('PreCompactの入口は、JS実装のrealpathが使えない環境(WindowsのCodex Desktopのhook)でも引き継ぎを作る', async () => withDb(async (db, home) => {
|
|
291
318
|
const thread = '11111111-1111-1111-1111-111111111111', turn = '22222222-2222-2222-2222-222222222222';
|
|
292
319
|
const dir = join(home, 'codex', 'sessions'); mkdirSync(dir, { recursive: true });
|