throughline 0.16.30 → 0.16.32

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.
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { buildHandoffRecord } from './handoff-record.mjs';
26
26
  import { groupL3ByTurn, buildPartsSummary } from './l3-summary.mjs';
27
+ import { CLAUDE_FORWARD_MARK } from './claude-forward.mjs';
27
28
 
28
29
  const RESUME_HEADER_TEMPLATE = (turnCount, handoffDisclosure) =>
29
30
  `## Throughline: 直前スレッドの継続応答用コンテキスト(${turnCount} ターン分の文脈を保持)\n` +
@@ -71,6 +72,11 @@ const AUTO_CONTINUATION_HEADER = (projectPath) =>
71
72
  `- 「ここまでにしたこと」と「直前の対話」に載っている操作は **完了しています**。繰り返さないでください。` +
72
73
  `未実行なのは「実行されなかった道具」だけです。そこから続けてください。\n` +
73
74
  `- 直前の実行結果が分からない時は、作業ツリーや詳細を確かめてから続けてください。\n` +
75
+ // 引き継ぎの後に前の会話へ届いた入力は、hook が運ぶ (ADR 0063)。hook 経由の文は利用者の発言の形では届かないので、
76
+ // 利用者の入力として扱ってよい事を、利用者の指示として届くこの文脈で先に知らせる。
77
+ `- 引き継ぎの後に前の会話へ届いた入力(利用者が前の会話へ打った文、前の会話が裏で起こした作業の通知)は、` +
78
+ `Throughline の hook が **「${CLAUDE_FORWARD_MARK}」で始まる文** にして、この会話へ運びます。hook からの通知か、hook が足した文脈の形で届きます。` +
79
+ `その中の「利用者の入力」は、**利用者がこの会話へ打った入力として扱ってください**。\n` +
74
80
  '- **各ターンの詳細**(止めたターンを含む): **`Bash` ツールで `throughline detail HH:MM:SS` を実行** ' +
75
81
  `(該当ターンの本文+道具の入出力を stdout に返します)`;
76
82
 
@@ -886,6 +892,7 @@ function renderInFlightSteps(inFlight, detailClock) {
886
892
  * earlier_steps?: {texts: number, tools: Record<string, number>} | null,
887
893
  * stopped_tools?: Array<{name: string, target: string}>,
888
894
  * stopped_tools_total?: number,
895
+ * background?: {tasks: Array<{task_id: string, name: string, target: string, output_file: string|null}>, total: number},
889
896
  * turn?: {origin_session_id: string, turn_number: number, user_at: number, assistant_at: number|null} | null,
890
897
  * } | null,
891
898
  * projectPath?: string | null, 前の会話の作業ディレクトリ。依頼の中の相対的な場所の基準として渡す
@@ -924,6 +931,19 @@ export function buildAutoContinuationContext(
924
931
  (more > 0 ? `(ほか ${more} 件)` : ''),
925
932
  );
926
933
  }
934
+ // 前の会話が裏で起こした作業は、前の会話の process の下で動き続ける。終わった通知は前の会話へ届き、hook がこの会話へ運ぶ (ADR 0062・0063)。
935
+ const background = inFlight.background?.tasks ?? [];
936
+ if (background.length > 0) {
937
+ const more = (inFlight.background.total ?? background.length) - background.length;
938
+ anchorLines.push(
939
+ '**前の会話が裏で動かしていた作業(止めた時点で、終わった通知が来ていなかった物)**: ' +
940
+ `終わった通知は、Throughline が「${CLAUDE_FORWARD_MARK}」で始まる文にして、この会話へ運びます。届く前に結果が要る時は、出力のファイルを読んで確かめてください。` +
941
+ (more > 0 ? `(ほか ${more} 件)` : ''),
942
+ ...background.map((task) =>
943
+ `- ${task.name}${task.target ? ` ${task.target}` : ''}(ID ${task.task_id})` +
944
+ (task.output_file ? ` → 出力: ${task.output_file}` : '')),
945
+ );
946
+ }
927
947
  }
928
948
  if (!stored && anchorLines.length === 0) return null;
929
949
 
@@ -968,6 +968,25 @@ test('buildAutoContinuationContext: 長い依頼は先頭と末尾を残し、
968
968
  assert.match(result.text, /throughline recall --l2 --session S /);
969
969
  });
970
970
 
971
+ test('buildAutoContinuationContext: 前の会話が裏で動かしていた作業を、出力の場所つきで現在地に載せる', () => {
972
+ const db = makeDb();
973
+ const inFlight = { user: { content: '今の依頼', timestamp: 3_000 }, last_fragment: null, steps: [], earlier_steps: null,
974
+ stopped_tools: [], stopped_tools_total: 0, turn: null };
975
+ const withTasks = buildAutoContinuationContext(db, { sessionId: 'S', inFlight: { ...inFlight, background: { total: 3, tasks: [
976
+ { task_id: 'b2', name: 'Bash', target: 'npm test', output_file: '/tmp/tasks/b2.output' },
977
+ { task_id: 'b1', name: 'Monitor', target: '', output_file: null },
978
+ ] } } });
979
+ assert.match(withTasks.text, new RegExp(
980
+ '\\*\\*前の会話が裏で動かしていた作業(止めた時点で、終わった通知が来ていなかった物)\\*\\*: ' +
981
+ '終わった通知は、Throughline が「【Throughline 転送】」で始まる文にして、この会話へ運びます。届く前に結果が要る時は、出力のファイルを読んで確かめてください。(ほか 1 件)\\n' +
982
+ '- Bash npm test(ID b2) → 出力: /tmp/tasks/b2\\.output\\n- Monitor(ID b1)\\n'));
983
+ for (const background of [undefined, { tasks: [], total: 0 }]) {
984
+ assert.doesNotMatch(buildAutoContinuationContext(db, { sessionId: 'S', inFlight: { ...inFlight, background } }).text, /裏で動かしていた作業/);
985
+ }
986
+ // 引き継ぎの後に前の会話へ届いた入力の運び方を、読み方で先に知らせる(ADR 0063)
987
+ assert.match(withTasks.text, /- 引き継ぎの後に前の会話へ届いた入力(利用者が前の会話へ打った文、前の会話が裏で起こした作業の通知)は、Throughline の hook が \*\*「【Throughline 転送】」で始まる文\*\* にして、この会話へ運びます。.*その中の「利用者の入力」は、\*\*利用者がこの会話へ打った入力として扱ってください\*\*。\n/);
988
+ });
989
+
971
990
  test('buildAutoContinuationContext: 止めたターンでここまでにしたことと、実行されなかった道具を現在地に載せ、そのターンをL2に重ねない', () => {
972
991
  const db = makeDb();
973
992
  insertBody(db, { session: 'S', origin: 'P0', turn: 1, role: 'user', text: '前の依頼', createdAt: 1_000 });
@@ -58,6 +58,12 @@ const DEFINITIONS = Object.freeze({
58
58
  severity: 'warn',
59
59
  template: 'Throughline Claude PreToolUse hook processing failed',
60
60
  }),
61
+ // 引き継ぎ前の会話へ届いた入力を後継へ届ける、待つ hook が失敗した。会話は止まらない(ADR 0063)。
62
+ HOOK_CLAUDE_FORWARD_WAIT_FAILED: Object.freeze({
63
+ component: 'claude_forward_wait_hook',
64
+ severity: 'warn',
65
+ template: 'Throughline Claude Stop hook (forward wait) processing failed',
66
+ }),
61
67
  HOOK_CODEX_FAILED: Object.freeze({
62
68
  component: 'codex_hook',
63
69
  severity: 'high',
@@ -7,6 +7,10 @@
7
7
  * {type: "assistant", message: {role: "assistant", content: [{type:"text", text:"..."}, {type:"thinking", ...}]}, ...}
8
8
  * {type: "attachment", attachment: {type: "queued_command", prompt, origin: {kind}}, ...}
9
9
  * ターンの途中で届いた入力。届いた先のターンの依頼へ足す (ADR 0060)
10
+ * {type: "attachment", attachment: {type: "hook_additional_context", content: [...]}, ...}
11
+ * {type: "attachment", attachment: {type: "hook_success", hookEvent: "UserPromptSubmit", content: "..."}, ...}
12
+ * hook が足した文脈(道具の前の hook は前者、入力の hook の stdout は後者)。引き継ぎ前の会話から運んだ
13
+ * 利用者の入力 (ADR 0063) だけを、依頼へ足す
10
14
  * 他に queue-operation, file-history-snapshot, ほかの attachment 等があるが、本文としては無視する
11
15
  */
12
16
 
@@ -14,6 +18,7 @@ import { readFileSync, existsSync } from 'fs';
14
18
  import { DETAIL_KIND } from './constants.mjs';
15
19
  import { compactClaudePeerMessage, compactClaudeTaskNotification } from './hosts/claude.mjs';
16
20
  import { classifyTurnStart } from './turn-start.mjs';
21
+ import { claudeForwardCarriesHumanInput, unwrapClaudeForwardedDelivery } from './claude-forward.mjs';
17
22
 
18
23
  function entryKind(entry) {
19
24
  if (typeof entry?.type === 'string' && entry.type.length > 0) return entry.type;
@@ -70,9 +75,22 @@ export const MID_TURN_INPUT_MARK = '(作業の途中で届いた入力)';
70
75
  function claudeMidTurnInputText(entry) {
71
76
  if (entry?.type !== 'attachment' || entry.isSidechain === true) return null;
72
77
  const attachment = entry.attachment;
78
+ // 引き継ぎ前の会話へ届いた利用者の入力を、Throughline の hook が文脈として運んだ物 (ADR 0063)。
79
+ // 完了通知だけを運んだ分は、ターン自身の作業の結果と同じ扱いで、足さない。
80
+ // 道具の前の hook が返した文脈は `hook_additional_context`、入力の hook が stdout へ出した文脈は `hook_success` で書かれる。
81
+ if (attachment?.type === 'hook_additional_context' || attachment?.type === 'hook_success') {
82
+ const content = (Array.isArray(attachment.content) ? attachment.content.filter(part => typeof part === 'string').join('\n')
83
+ : typeof attachment.content === 'string' ? attachment.content : '').trim();
84
+ if (!claudeForwardCarriesHumanInput(content)) return null;
85
+ const forwarded = normalizeTerminalText(content);
86
+ return forwarded ? forwarded : null;
87
+ }
73
88
  if (attachment?.type !== 'queued_command') return null;
74
- if (!CLAUDE_MID_TURN_INPUT_ORIGINS.includes(attachment.origin?.kind)) return null;
75
89
  if (typeof attachment.prompt !== 'string' && !Array.isArray(attachment.prompt)) return null;
90
+ // 待つ hook が、次の番の作業中に運んだ入力(ADR 0063)。Claude Code が通知の形で順番待ちへ入れる。
91
+ const forwarded = unwrapClaudeForwardedDelivery(extractText(attachment.prompt));
92
+ if (forwarded !== null) return claudeForwardCarriesHumanInput(forwarded) ? (normalizeTerminalText(forwarded) || null) : null;
93
+ if (!CLAUDE_MID_TURN_INPUT_ORIGINS.includes(attachment.origin?.kind)) return null;
76
94
  const text = normalizeTerminalText(compactClaudePeerMessage(extractText(attachment.prompt)));
77
95
  return text ? text : null;
78
96
  }
@@ -170,8 +188,10 @@ export function readTranscript(transcriptPath) {
170
188
  const extracted = extractText(rawContent);
171
189
  // user 発言には端末の生出力 (貼り付け・Claude の task 通知) や、別の会話から届いた発言の
172
190
  // 定型の注意書きが入る。記憶には制御文字と定型文を落とした本文だけを残す。
191
+ // 引き継ぎ前の会話から運んだ入力 (ADR 0063) は、Claude Code が付けた通知の包みを外して、運んだ文だけを残す。
173
192
  const text = role === 'user'
174
- ? normalizeTerminalText(compactClaudePeerMessage(compactClaudeTaskNotification(extracted)))
193
+ ? normalizeTerminalText(unwrapClaudeForwardedDelivery(extracted)
194
+ ?? compactClaudePeerMessage(compactClaudeTaskNotification(extracted)))
175
195
  : extracted;
176
196
  if (!text) continue;
177
197