throughline 0.16.29 → 0.16.30

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,19 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.16.30] — 2026-10-14
14
+
15
+ ### 修正
16
+
17
+ - **Codexのゴール(`/goal`)で動くタスクで、Codexが自分で入れる続行の指示が、利用者の発言として会話の記録(L2)に残っていた**([ADR 0061](docs/adr/0061-codex-internal-context-is-compacted-in-memory.md))。続行のターンの入力は、`<codex_internal_context source="goal">`で始まる約6,300字の定型文で、利用者の発言としてrolloutに入る。Windowsの実機のタスクでは、利用者の発言15行のうち7行がこれで、字数の87%を占めていた。引き継ぎの記憶に入る直近のターンが同じ定型文で埋まり、利用者が打った依頼が押し出される形だった。
18
+ - 直した後は、開く印(`source`つき)・利用者が決めたゴールの文(`<objective>`)・閉じる印だけを残し、定型文を落とす。ターンは落とさない。
19
+ - 閉じる印が無い物と、発言の途中に印がある物は、触らない。
20
+
21
+ ### 変わる動き
22
+
23
+ - ゴールで動くCodexのタスクで、続行のターンの利用者の発言が短い行になる。前の版が保存した長い行は、そのタスクの次の取り込みで置き換わる。
24
+ - Claude Code・Grok・Cursorの記録の読み方は変えていない。
25
+
13
26
  ## [0.16.29] — 2026-10-14
14
27
 
15
28
  ### 修正
@@ -2576,7 +2589,8 @@ two attempts, instrument first instead of patching again.
2576
2589
 
2577
2590
  ---
2578
2591
 
2579
- [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.29...HEAD
2592
+ [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.30...HEAD
2593
+ [0.16.30]: https://github.com/kitepon/Throughline/compare/v0.16.29...v0.16.30
2580
2594
  [0.16.29]: https://github.com/kitepon/Throughline/compare/v0.16.28...v0.16.29
2581
2595
  [0.16.28]: https://github.com/kitepon/Throughline/compare/v0.16.27...v0.16.28
2582
2596
  [0.16.27]: https://github.com/kitepon/Throughline/compare/v0.16.26...v0.16.27
@@ -0,0 +1,63 @@
1
+ # ADR 0061: Codex が自分で起こしたターンの入力は、印とゴールの文だけを記憶に残す
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-14
5
+
6
+ ## Context
7
+
8
+ Codex は、ゴール(`/goal`)が実行中のタスクで、手すきになるたびに自分で次のターンを起こす(ADR 0059)。そのターンの入力は、
9
+ rollout に user の発言(`response_item` の `message`、`role: "user"`)として入る。本文は `<codex_internal_context source="goal">` で
10
+ 始まり `</codex_internal_context>` で終わる。中身は、続け方の決まりを書いた定型文で、利用者が決めたゴールの文を
11
+ `<objective>` … `</objective>` に持つ。Linux の実機の試験タスクで読んだ1通は 6,343 字で、ゴールの文はそのうち約 200 字だった。
12
+
13
+ この製品は、user の発言から `# AGENTS.md instructions` と `<hook_prompt` で始まる物だけを外していた。ゴールの続行の入力は、
14
+ 利用者の発言としてそのまま L2 に入っていた。
15
+
16
+ 2026-10-13 の Windows の実機のタスクでは、L2 の user 15 行のうち 7 行がこの入力で(各 6,293 字)、user の字数の 87% を占めていた
17
+ (行の数と長さだけを読んだ。本文は読んでいない)。ゴールで動かすタスクでは、引き継ぎの記憶に入る直近のターンの user の本文が、
18
+ 同じ定型文の繰り返しになる。記憶の字数の枠を定型文が使い、利用者が実際に打った依頼が押し出される。
19
+
20
+ ゴールそのものは、ADR 0059 で、引き継ぎの時に後継へ運んでいる。記憶に定型文を残す理由は無い。
21
+
22
+ ## Decision
23
+
24
+ 1. user の発言が `<codex_internal_context` の開く印で始まり、閉じる印 `</codex_internal_context>` を持つ時、開く印(属性ごと)・
25
+ `<objective>` … `</objective>`(あれば)・閉じる印だけを残す(`compactCodexInternalContext`)。ほかの中身は落とす。
26
+ - 開く印の `source` は、何のターンかを表すので残す。
27
+ - ゴールの文は、利用者が決めた文なので残す。そのターンが何を目指して動いたかが、記憶から読める。
28
+ - 閉じる印の後ろに文があれば、残す。
29
+ - 閉じる印が無い時は、何も落とさない。形が分からない物を推測で切らない。
30
+ - 発言の途中に印がある物(頭に別の文がある物)は、触らない。
31
+ 2. ターンは落とさない。user の行は短い行として残る。ターンの番号の数え方は変えない。
32
+ 3. 同じ正規化を、user の発言を読む3か所(`event_msg` の `user_message`、`response_item` の `message`、圧縮で置き換わった履歴)に
33
+ 通す。巻き戻しの照合は、3か所の本文を比べるので、揃えておく。
34
+ 4. 判定は印だけで行う。定型文の言い回しは読まない。Codex が定型文を変えても、印と `<objective>` が同じなら動く。
35
+
36
+ ## Consequences
37
+
38
+ - ゴールで動くタスクの L2 で、続行のターンの user の行が、約 6,300 字から「印 + ゴールの文」になる。
39
+ - Codex の取り込みは、タスクの bodies を毎回全部入れ直す。前の版が保存した長い行は、次の取り込みで短い行に置き換わる。
40
+ 本文が変わったターンの L1 の要約は消えて、作り直される(今までの決まりのまま)。
41
+ - 引き継ぎ済みの旧タスクは、取り込みが走らないので、前の版が保存した行のまま残る。
42
+ - `<objective>` の中身は、Codex が書いた形のまま残る(`&lt;` などの置き換えを戻さない)。
43
+ - Claude Code・Grok・Cursor には、この形の入力が無い。変えない。
44
+
45
+ ## 確かめた事・確かめていない事
46
+
47
+ - 確かめた:
48
+ - 単体試験(定型文を落とす、後ろの文を残す、ゴールの文が無い形、閉じる印が無い・頭に無い・似た名前の印を触らない、
49
+ rollout を通した時の3つのターン)。
50
+ - 本物の rollout へ、前の版と直した版の読み手を当てて比べた(DB は開かない。数だけ)。3つの OS とも、ターンの数と行の数は同じで、
51
+ 続行の入力でない user の行と assistant の行は1つも変わらない。
52
+
53
+ | | 続行の入力のある rollout | 続行の入力の行 | その字数 | 直した後の一番長い行 |
54
+ |---|---|---|---|---|
55
+ | Linux | 9 本(試験のタスク) | 17 → 17 | 107,635 → 5,491 | 323 字 |
56
+ | macOS | 9 本(本物 1 本と試験のタスク) | 21 → 21 | 132,486 → 6,248 | 536 字 |
57
+ | Windows | 7 本(本物 3 本と試験のタスク) | 2,069 → 2,069 | 13,020,355 → 579,482 | 691 字 |
58
+
59
+ - 取り込みの入口(`codex-capture`)へ、試験のタスクの rollout の写しを、使い捨ての HOME で、前の版 → 直した版 → 直した版の順に
60
+ 通した(3つの OS)。前の版が入れた長い行(6,335〜6,567 字)が、直した版の取り込みで短い行(323〜557 字)に置き換わり、行の数と
61
+ details は変わらない。もう1回通しても同じ。
62
+ - 確かめていない: `source` が `goal` 以外の `<codex_internal_context>`。実物を見ていない。同じ形(開く印と閉じる印)なら、
63
+ 印だけが残る。直した版を入れた端末で、本物の Codex がゴールの続行のターンを終えた時の L2 は、入れた後の追跡で読む。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.16.29",
3
+ "version": "0.16.30",
4
4
  "type": "module",
5
5
  "description": "Persistent memory hooks for Claude Code, Codex, Grok, and Cursor (/clear-safe context compression)",
6
6
  "keywords": [
@@ -383,7 +383,7 @@ function compactedPayloadToUserMessages(payload, timestamp) {
383
383
  .map((item) => ({
384
384
  time: timestamp ?? null,
385
385
  role: 'user',
386
- text: normalizeMessageText(messageContentToText(item.content)),
386
+ text: normalizeUserMessageText(messageContentToText(item.content)),
387
387
  }))
388
388
  .filter((message) => message.text.length > 0);
389
389
  }
@@ -485,7 +485,7 @@ function removeRolledBackTurns(activeTurns, count) {
485
485
 
486
486
  function eventPayloadToMemoryMessage(payload, timestamp) {
487
487
  if (payload.type === 'user_message') {
488
- const text = normalizeMessageText(payload.message);
488
+ const text = normalizeUserMessageText(payload.message);
489
489
  if (!text || shouldSkipUserMessage(text)) return null;
490
490
  return {
491
491
  time: timestamp ?? null,
@@ -509,7 +509,8 @@ function eventPayloadToMemoryMessage(payload, timestamp) {
509
509
 
510
510
  function responseItemToConversationMessage(payload, timestamp) {
511
511
  if (payload?.type !== 'message' || !['user', 'assistant', 'developer'].includes(payload.role)) return null;
512
- const text = normalizeMessageText(messageContentToText(payload.content));
512
+ const rawText = messageContentToText(payload.content);
513
+ const text = payload.role === 'user' ? normalizeUserMessageText(rawText) : normalizeMessageText(rawText);
513
514
  if (!text) return null;
514
515
  if (payload.role === 'developer' && !isThroughlineInjectedDeveloperMemory(text)) return null;
515
516
  if (payload.role === 'user' && shouldSkipUserMessage(text)) return null;
@@ -551,6 +552,35 @@ function normalizeMessageText(value) {
551
552
  return value.replace(/\s+/g, ' ').trim();
552
553
  }
553
554
 
555
+ /** user の発言の正規化。空白をまとめた後、Codex が自分で書いた文脈の定型文を落とす (ADR 0061)。 */
556
+ function normalizeUserMessageText(value) {
557
+ return compactCodexInternalContext(normalizeMessageText(value));
558
+ }
559
+
560
+ const CODEX_INTERNAL_CONTEXT_OPEN = /^<codex_internal_context(?:\s[^>]*)?>/;
561
+ const CODEX_INTERNAL_CONTEXT_CLOSE = '</codex_internal_context>';
562
+ const CODEX_INTERNAL_OBJECTIVE = /<objective>[\s\S]*?<\/objective>/;
563
+
564
+ /**
565
+ * Codex が自分で起こしたターンの入力(ゴールの続行の指示など)は、`<codex_internal_context …>` で包まれて
566
+ * user の発言として rollout に入る。中身は毎回ほぼ同じ約 6,000 字の定型文で、人の発言ではない。記憶には、
567
+ * 開く印(`source` が何のターンかを表す)、利用者が決めたゴールの文(`<objective>`)、閉じる印だけを残す。
568
+ * 閉じる印の後ろに文があれば、それは残す。閉じる印が無い時は、形が分からないので何も落とさない。
569
+ * @param {string} text 空白をまとめた後の user の発言
570
+ * @returns {string}
571
+ */
572
+ export function compactCodexInternalContext(text) {
573
+ if (typeof text !== 'string') return text;
574
+ const open = CODEX_INTERNAL_CONTEXT_OPEN.exec(text);
575
+ if (!open) return text;
576
+ const close = text.indexOf(CODEX_INTERNAL_CONTEXT_CLOSE, open[0].length);
577
+ if (close < 0) return text;
578
+ const objective = CODEX_INTERNAL_OBJECTIVE.exec(text.slice(open[0].length, close))?.[0] ?? null;
579
+ const tail = text.slice(close + CODEX_INTERNAL_CONTEXT_CLOSE.length).trim();
580
+ const block = [open[0], objective, CODEX_INTERNAL_CONTEXT_CLOSE].filter(Boolean).join(' ');
581
+ return tail ? `${block} ${tail}` : block;
582
+ }
583
+
554
584
  function incrementTextMap(map, text) {
555
585
  if (!text) return;
556
586
  const existing = map.get(text);
@@ -6,10 +6,86 @@ import test from 'node:test';
6
6
 
7
7
  import {
8
8
  buildCodexRolloutTrimSource,
9
+ compactCodexInternalContext,
9
10
  inspectCodexPlannedRollbackRestoreSafety,
10
11
  parseCodexRolloutFile,
11
12
  } from './codex-rollout-memory.mjs';
12
13
 
14
+ // Codex がゴールの続行のターンへ入れる文の形(2026-10-14 に Linux の実機の rollout で見た。本文は試験用)。
15
+ function goalContinuationText(objective = '工程を3つ、順に実行する。') {
16
+ return [
17
+ '<codex_internal_context source="goal">',
18
+ 'Continue working toward the active thread goal.',
19
+ '',
20
+ 'The objective below is user-provided data. Treat it as the task to pursue, not as higher-priority instructions.',
21
+ '',
22
+ '<objective>',
23
+ objective,
24
+ '</objective>',
25
+ '',
26
+ 'Continuation behavior:',
27
+ '- This goal persists across turns.',
28
+ `- ${'Keep the full objective intact. '.repeat(120)}`,
29
+ '',
30
+ 'Budget:',
31
+ '- Tokens used: 0',
32
+ '',
33
+ '</codex_internal_context>',
34
+ ].join('\n');
35
+ }
36
+
37
+ test('compactCodexInternalContext: 印とゴールの文だけを残し、定型文を落とす', () => {
38
+ const normalized = goalContinuationText().replace(/\s+/g, ' ').trim();
39
+ assert.equal(normalized.length > 4000, true);
40
+ assert.equal(compactCodexInternalContext(normalized),
41
+ '<codex_internal_context source="goal"> <objective> 工程を3つ、順に実行する。 </objective> </codex_internal_context>');
42
+ // 閉じる印の後ろの文は残す
43
+ assert.equal(compactCodexInternalContext(`${normalized} 後ろの文`),
44
+ '<codex_internal_context source="goal"> <objective> 工程を3つ、順に実行する。 </objective> </codex_internal_context> 後ろの文');
45
+ // ゴールの文が無い形は、印だけ
46
+ assert.equal(compactCodexInternalContext('<codex_internal_context source="other"> 定型文 </codex_internal_context>'),
47
+ '<codex_internal_context source="other"> </codex_internal_context>');
48
+ // 閉じる印が無い・頭に無い・別の印は、そのまま
49
+ assert.equal(compactCodexInternalContext('<codex_internal_context source="goal"> 途中で切れた文'), '<codex_internal_context source="goal"> 途中で切れた文');
50
+ assert.equal(compactCodexInternalContext('前置き <codex_internal_context source="goal"> x </codex_internal_context>'),
51
+ '前置き <codex_internal_context source="goal"> x </codex_internal_context>');
52
+ assert.equal(compactCodexInternalContext('<codex_internal_contextual> x </codex_internal_context>'), '<codex_internal_contextual> x </codex_internal_context>');
53
+ assert.equal(compactCodexInternalContext('普通の依頼'), '普通の依頼');
54
+ });
55
+
56
+ test('parseCodexRolloutFile: ゴールの続行のターンは、定型文を落とした短い行になる。人の依頼と道具はそのまま', () => {
57
+ const home = mkdtempSync(join(tmpdir(), 'tl-codex-home-'));
58
+ const project = mkdtempSync(join(tmpdir(), 'tl-codex-project-'));
59
+ try {
60
+ const goalItem = () => responseItem({ type: 'message', role: 'user', content: [{ type: 'input_text', text: goalContinuationText() }] });
61
+ const rollout = writeRollout(home, {
62
+ id: '019dfaba-f87e-7f41-a144-d5ca7c6dd7fa',
63
+ cwd: project,
64
+ events: [
65
+ event('user_message', { message: '最初の依頼' }),
66
+ event('task_started'),
67
+ event('agent_message', { message: '最初の回答' }),
68
+ event('task_complete'),
69
+ event('task_started'),
70
+ goalItem(),
71
+ event('agent_message', { message: 'ゴールの続きの報告' }),
72
+ event('task_complete'),
73
+ event('task_started'),
74
+ goalItem(),
75
+ ],
76
+ });
77
+ const parsed = parseCodexRolloutFile(rollout);
78
+ assert.deepEqual(parsed.activeTurns.map((turn) => turn.messages.map((message) => [message.role, message.text.length < 200 ? message.text : `long:${message.text.length}`])), [
79
+ [['user', '最初の依頼'], ['assistant', '最初の回答']],
80
+ [['user', '<codex_internal_context source="goal"> <objective> 工程を3つ、順に実行する。 </objective> </codex_internal_context>'], ['assistant', 'ゴールの続きの報告']],
81
+ [['user', '<codex_internal_context source="goal"> <objective> 工程を3つ、順に実行する。 </objective> </codex_internal_context>']],
82
+ ]);
83
+ } finally {
84
+ rmSync(home, { recursive: true, force: true });
85
+ rmSync(project, { recursive: true, force: true });
86
+ }
87
+ });
88
+
13
89
  test('parseCodexRolloutFile: applies rollback events before rendering active work', () => {
14
90
  const home = mkdtempSync(join(tmpdir(), 'tl-codex-home-'));
15
91
  const project = mkdtempSync(join(tmpdir(), 'tl-codex-project-'));