throughline 0.16.31 → 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.
@@ -978,11 +978,13 @@ test('buildAutoContinuationContext: 前の会話が裏で動かしていた作
978
978
  ] } } });
979
979
  assert.match(withTasks.text, new RegExp(
980
980
  '\\*\\*前の会話が裏で動かしていた作業(止めた時点で、終わった通知が来ていなかった物)\\*\\*: ' +
981
- '終わった通知は、この会話へは届きません。結果が要る時は、出力のファイルを読んで確かめてください。(ほか 1 件)\\n' +
981
+ '終わった通知は、Throughline が「【Throughline 転送】」で始まる文にして、この会話へ運びます。届く前に結果が要る時は、出力のファイルを読んで確かめてください。(ほか 1 件)\\n' +
982
982
  '- Bash npm test(ID b2) → 出力: /tmp/tasks/b2\\.output\\n- Monitor(ID b1)\\n'));
983
983
  for (const background of [undefined, { tasks: [], total: 0 }]) {
984
984
  assert.doesNotMatch(buildAutoContinuationContext(db, { sessionId: 'S', inFlight: { ...inFlight, background } }).text, /裏で動かしていた作業/);
985
985
  }
986
+ // 引き継ぎの後に前の会話へ届いた入力の運び方を、読み方で先に知らせる(ADR 0063)
987
+ assert.match(withTasks.text, /- 引き継ぎの後に前の会話へ届いた入力(利用者が前の会話へ打った文、前の会話が裏で起こした作業の通知)は、Throughline の hook が \*\*「【Throughline 転送】」で始まる文\*\* にして、この会話へ運びます。.*その中の「利用者の入力」は、\*\*利用者がこの会話へ打った入力として扱ってください\*\*。\n/);
986
988
  });
987
989
 
988
990
  test('buildAutoContinuationContext: 止めたターンでここまでにしたことと、実行されなかった道具を現在地に載せ、そのターンをL2に重ねない', () => {
@@ -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