@cocorograph/hub-agent 0.6.15 → 0.6.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -15,7 +15,8 @@
15
15
  import { watch as fsWatch } from "node:fs"
16
16
  import { open, stat } from "node:fs/promises"
17
17
 
18
- const DISPLAY_TYPES = new Set(["user", "assistant", "system", "result"])
18
+ // 履歴 hydrate live watch jsonl→SDK message 整形を共通化 (フィールド取りこぼし防止)
19
+ import { DISPLAY_TYPES, normalizeHistoryEvent } from "./claude-history.mjs"
19
20
  const POLL_INTERVAL_MS = 1500
20
21
 
21
22
  /**
@@ -89,7 +90,7 @@ export function watchSessionFile({ filePath, onEvent, fromEnd = true, logger })
89
90
  continue
90
91
  }
91
92
  if (!obj || !DISPLAY_TYPES.has(obj.type)) continue
92
- const event = normalizeEvent(obj)
93
+ const event = normalizeHistoryEvent(obj)
93
94
  try {
94
95
  onEvent(event)
95
96
  } catch (err) {
@@ -151,21 +152,3 @@ export function watchSessionFile({ filePath, onEvent, fromEnd = true, logger })
151
152
  }
152
153
  }
153
154
 
154
- /** jsonl の生 object を SDK message 風の表示用イベントに正規化 (history.mjs と揃える)。 */
155
- function normalizeEvent(obj) {
156
- const event = { type: obj.type }
157
- if (obj.message !== undefined) event.message = obj.message
158
- if (obj.subtype !== undefined) event.subtype = obj.subtype
159
- if (obj.uuid !== undefined) event.uuid = obj.uuid
160
- if (obj.session_id !== undefined) event.session_id = obj.session_id
161
- else if (obj.sessionId !== undefined) event.session_id = obj.sessionId
162
- if (obj.model !== undefined) event.model = obj.model
163
- if (obj.cwd !== undefined) event.cwd = obj.cwd
164
- if (obj.tools !== undefined) event.tools = obj.tools
165
- if (obj.permissionMode !== undefined) event.permissionMode = obj.permissionMode
166
- if (obj.total_cost_usd !== undefined) event.total_cost_usd = obj.total_cost_usd
167
- if (obj.duration_ms !== undefined) event.duration_ms = obj.duration_ms
168
- if (obj.num_turns !== undefined) event.num_turns = obj.num_turns
169
- if (obj.usage !== undefined) event.usage = obj.usage
170
- return event
171
- }
@@ -20,7 +20,36 @@ import path from "node:path"
20
20
  export const MAX_HISTORY_LINES = 500
21
21
 
22
22
  /** UI 表示対象の SDK message type (それ以外は jsonl 内部メタなので除外)。 */
23
- const DISPLAY_TYPES = new Set(["user", "assistant", "system", "result"])
23
+ export const DISPLAY_TYPES = new Set(["user", "assistant", "system", "result"])
24
+
25
+ /**
26
+ * jsonl の生 object を、Browser が SDK message として受け取れる表示用 shape に正規化する。
27
+ * 余分な内部メタは落とし、表示・集計に必要なフィールドだけを superset で拾う。
28
+ *
29
+ * ⚠️ 履歴 hydrate (readSessionHistory) と live watch (claude-history-watch) の両方が
30
+ * これを使う。フィールドを増やすときは必ずここ 1 箇所に足すこと (過去、2 箇所に分散して
31
+ * いて uuid が片方で取りこぼされるドリフトが発生したため共通化した。2026-05-29)。
32
+ *
33
+ * @param {Record<string, unknown>} obj jsonl 1 行をパースした生 object
34
+ * @returns {Record<string, unknown>} SDK message 風の表示用イベント
35
+ */
36
+ export function normalizeHistoryEvent(obj) {
37
+ const event = { type: obj.type }
38
+ if (obj.message !== undefined) event.message = obj.message
39
+ if (obj.subtype !== undefined) event.subtype = obj.subtype
40
+ if (obj.uuid !== undefined) event.uuid = obj.uuid
41
+ if (obj.session_id !== undefined) event.session_id = obj.session_id
42
+ else if (obj.sessionId !== undefined) event.session_id = obj.sessionId
43
+ if (obj.model !== undefined) event.model = obj.model
44
+ if (obj.cwd !== undefined) event.cwd = obj.cwd
45
+ if (obj.tools !== undefined) event.tools = obj.tools
46
+ if (obj.permissionMode !== undefined) event.permissionMode = obj.permissionMode
47
+ if (obj.total_cost_usd !== undefined) event.total_cost_usd = obj.total_cost_usd
48
+ if (obj.duration_ms !== undefined) event.duration_ms = obj.duration_ms
49
+ if (obj.num_turns !== undefined) event.num_turns = obj.num_turns
50
+ if (obj.usage !== undefined) event.usage = obj.usage
51
+ return event
52
+ }
24
53
 
25
54
  /**
26
55
  * cwd 文字列を Claude Code の project dir 名に変換する。
@@ -82,21 +111,8 @@ export async function readSessionHistory(filePath, { maxLines = MAX_HISTORY_LINE
82
111
  }
83
112
  if (!obj || typeof obj !== "object") continue
84
113
  if (!DISPLAY_TYPES.has(obj.type)) continue
85
- // SDK message と同じ shape にする (余分な meta は落とす)
86
- const event = { type: obj.type }
87
- if (obj.message !== undefined) event.message = obj.message
88
- if (obj.subtype !== undefined) event.subtype = obj.subtype
89
- if (obj.session_id !== undefined) event.session_id = obj.session_id
90
- else if (obj.sessionId !== undefined) event.session_id = obj.sessionId
91
- if (obj.model !== undefined) event.model = obj.model
92
- if (obj.cwd !== undefined) event.cwd = obj.cwd
93
- if (obj.tools !== undefined) event.tools = obj.tools
94
- if (obj.permissionMode !== undefined) event.permissionMode = obj.permissionMode
95
- if (obj.total_cost_usd !== undefined) event.total_cost_usd = obj.total_cost_usd
96
- if (obj.duration_ms !== undefined) event.duration_ms = obj.duration_ms
97
- if (obj.num_turns !== undefined) event.num_turns = obj.num_turns
98
- if (obj.usage !== undefined) event.usage = obj.usage
99
- events.push(event)
114
+ // SDK message と同じ shape にする (余分な meta は落とす)。整形は共通関数に集約。
115
+ events.push(normalizeHistoryEvent(obj))
100
116
  }
101
117
  return { events, total_lines, truncated }
102
118
  }
@@ -34,9 +34,29 @@ const IDLE_DETACH_TTL_MS = 7 * 24 * 60 * 60 * 1000
34
34
  * 従来の「1メッセージ=1query」へ即ロールバックできる。デフォルト有効。 */
35
35
  const CHAT_RESIDENT_ENABLED = process.env.HUB_AGENT_CHAT_RESIDENT !== "0"
36
36
 
37
- /** 文字列を SDK streaming input SDKUserMessage に包む。 */
37
+ /** 改修4 (2026-05-29): resume セッションも常駐query化する (init を毎ターン送らず、
38
+ * 1 セッション=1 query で system/init を初回 1 回のみにする。VS Code 拡張 / Claude Web
39
+ * と同じ挙動)。query 起動時に options.resume で過去文脈を引き継ぐ。
40
+ * env HUB_AGENT_CHAT_RESIDENT_RESUME="0" で従来の per-message (毎ターン init) に
41
+ * 個別ロールバック可能 (新規セッションの常駐化は CHAT_RESIDENT_ENABLED 側で独立制御)。
42
+ * デフォルト有効。
43
+ * 注意: 過去に「resume + 常駐」で『Continue from where you left off』暴走が疑われたが、
44
+ * 原因は continue オプション (直近会話の自動継続) の誤用と Hub 不調の重なりと推定。
45
+ * 本実装では continue は使わず resume (明示 session 指定) のみを用い、初回 input は
46
+ * ユーザーの実メッセージのみとすることで自動継続の暴走を回避する。 */
47
+ const CHAT_RESIDENT_RESUME_ENABLED =
48
+ process.env.HUB_AGENT_CHAT_RESIDENT_RESUME !== "0"
49
+
50
+ /** 文字列を SDK streaming input の SDKUserMessage に包む。
51
+ * SDKUserMessage は parent_tool_use_id: string|null が必須フィールド。現行 SDK は入力側で
52
+ * 寛容なので省略しても動くが、将来の型厳格化に備えて明示する。トップレベルのユーザー入力
53
+ * なので親 tool_use は無く null。 */
38
54
  function toSDKUserMessage(text) {
39
- return { type: "user", message: { role: "user", content: text } }
55
+ return {
56
+ type: "user",
57
+ message: { role: "user", content: text },
58
+ parent_tool_use_id: null,
59
+ }
40
60
  }
41
61
 
42
62
  /** 常駐 query の streaming input キュー。push された SDKUserMessage を AsyncIterable として
@@ -136,13 +156,17 @@ class ClaudeStreamSession {
136
156
  /** detached のまま放置されたセッションを撤去する idle タイマー。再アタッチでキャンセル。 */
137
157
  this._idleTimer = null
138
158
 
139
- /** 改修2: 常駐query対象か。明示 resident 指定があれば優先 (テスト/将来の per-session 設定用)。
140
- * 未指定なら resume なしの新規セッションのみ常駐 (既存 resume セッションは過去の
141
- * 「Continue from where you left off」暴走を避けて従来 per-message を維持)。 */
159
+ /** 改修2+4: 常駐query対象か。明示 resident 指定があれば優先 (テスト/将来の per-session 設定用)。
160
+ * 未指定の場合:
161
+ * - 新規セッション (resume なし): CHAT_RESIDENT_ENABLED で常駐 (改修2)。
162
+ * - 既存セッション (resume あり): CHAT_RESIDENT_ENABLED かつ CHAT_RESIDENT_RESUME_ENABLED
163
+ * なら常駐 (改修4)。query 起動時に options.resume で文脈を引き継ぎ init は初回 1 回のみ。
164
+ * どちらも無効なら従来 per-message (毎ターン init) にフォールバックする。 */
142
165
  this._residentEligible =
143
166
  typeof resident === "boolean"
144
167
  ? resident
145
- : !resumeSessionId && CHAT_RESIDENT_ENABLED
168
+ : CHAT_RESIDENT_ENABLED &&
169
+ (!resumeSessionId || CHAT_RESIDENT_RESUME_ENABLED)
146
170
  /** 常駐query の input キュー (streaming input)。初回 input 時に生成・query 起動。 */
147
171
  this._inputQueue = null
148
172
  /** 起動済みの常駐 query ハンドル (interrupt() 用)。 */
@@ -244,14 +268,89 @@ class ClaudeStreamSession {
244
268
  /** 再アタッチ: 走行中(または生存中)セッションに新しい stream_id を紐付け直し、
245
269
  * idle 撤去タイマーを止める。以降のターンイベントはこの stream_id 経由で新しい
246
270
  * browser 接続へライブに流れる (= 通常の生成中表示と同じ)。再アタッチ前の確定分は
247
- * browser 側の jsonl hydrate (history.request) で復元するため、ここでは replay しない。 */
248
- reattach(stream_id) {
271
+ * browser 側の jsonl hydrate (history.request) で復元するため、ここでは replay しない。
272
+ *
273
+ * 改修5 (2026-05-29): モデル/権限/拡張思考のターン切替。再アタッチ時に opts で
274
+ * 新しい値が渡され、現在値と異なれば applyRuntimeOptions で反映する。これにより
275
+ * 入力欄下バッジの変更が「同一セッション(常駐 query)を維持したまま次ターンから」
276
+ * 効くようになる。従来は reattach が stream_id だけ差し替えていたため、起動済み
277
+ * query の model/permission/thinking は初期値のまま変わらなかった (バッジが効かない
278
+ * 不具合の原因)。 */
279
+ reattach(stream_id, opts = undefined) {
249
280
  this.stream_id = stream_id
250
281
  this._detached = false
251
282
  if (this._idleTimer) {
252
283
  clearTimeout(this._idleTimer)
253
284
  this._idleTimer = null
254
285
  }
286
+ if (opts) this.applyRuntimeOptions(opts)
287
+ }
288
+
289
+ /** 改修5: モデル/権限/拡張思考をランタイムに切り替える。
290
+ *
291
+ * - 保持フィールド (this.model/permissionMode/maxThinkingTokens) を更新する。これは
292
+ * 常駐 query が異常終了して resume 再起動する際 (_runResidentQuery) に最新値で
293
+ * 再 spawn させるため、および per-message セッションが次ターンの options に反映する
294
+ * ため。
295
+ * - 起動済みの常駐 query があれば SDK の制御メソッド (setModel / setPermissionMode /
296
+ * setMaxThinkingTokens) を呼び、プロセス再起動なしで次ターンから即反映する
297
+ * (公式 streaming input mode のランタイム制御。stdin に control_request を流す)。
298
+ *
299
+ * 値が undefined のキーは「変更なし」として無視する (バッジ未送出時に既存値を消さない)。
300
+ * model に空文字/null が来たら setModel(undefined) でデフォルトへ戻す。
301
+ * maxThinkingTokens に 0/null が来たら setMaxThinkingTokens(null) でオフにする。 */
302
+ applyRuntimeOptions({ model, permissionMode, maxThinkingTokens } = {}) {
303
+ const q = this._residentQuery
304
+ // モデル
305
+ if (model !== undefined) {
306
+ const next = model || null
307
+ if (next !== this.model) {
308
+ this.model = next
309
+ if (q && typeof q.setModel === "function") {
310
+ q.setModel(next || undefined).catch((err) =>
311
+ this.logger?.warn(
312
+ { err: err?.message, stream_id: this.stream_id },
313
+ "setModel failed",
314
+ ),
315
+ )
316
+ }
317
+ }
318
+ }
319
+ // 権限モード
320
+ if (permissionMode !== undefined) {
321
+ const next = permissionMode || null
322
+ if (next !== this.permissionMode) {
323
+ this.permissionMode = next
324
+ // setPermissionMode は有効な mode を要求する。null/空 (=未指定へ戻す) は
325
+ // SDK 側に「解除」API が無いため、保持値の更新のみ (次回 spawn で既定に従う)。
326
+ if (next && q && typeof q.setPermissionMode === "function") {
327
+ q.setPermissionMode(next).catch((err) =>
328
+ this.logger?.warn(
329
+ { err: err?.message, stream_id: this.stream_id },
330
+ "setPermissionMode failed",
331
+ ),
332
+ )
333
+ }
334
+ }
335
+ }
336
+ // 拡張思考予算
337
+ if (maxThinkingTokens !== undefined) {
338
+ const next =
339
+ typeof maxThinkingTokens === "number" && maxThinkingTokens > 0
340
+ ? maxThinkingTokens
341
+ : null
342
+ if (next !== this.maxThinkingTokens) {
343
+ this.maxThinkingTokens = next
344
+ if (q && typeof q.setMaxThinkingTokens === "function") {
345
+ q.setMaxThinkingTokens(next).catch((err) =>
346
+ this.logger?.warn(
347
+ { err: err?.message, stream_id: this.stream_id },
348
+ "setMaxThinkingTokens failed",
349
+ ),
350
+ )
351
+ }
352
+ }
353
+ }
255
354
  }
256
355
 
257
356
  /** soft detach: browser 切断時にターンを中断せずセッションを生かしたまま detached に
@@ -286,20 +385,30 @@ class ClaudeStreamSession {
286
385
  const prompt = extractPromptText(message)
287
386
  if (!prompt) return
288
387
 
289
- // 改修2: 常駐query対象セッションは streaming input キューへ積む (busy 中でも積み、
290
- // SDK が順次処理する)。初回 input で常駐 query を 1 回だけ起動する。
388
+ // 改修2+4: 常駐query対象セッション。
291
389
  if (this._residentEligible) {
292
390
  if (!this._inputQueue) this._inputQueue = new InputQueue()
391
+ // 改修4 (A): ターンのシリアライズ。実行中ターン (busy) は InputQueue へ即 push せず
392
+ // pending へ退避し、現ターンの result 後に 1 件ずつ drain する。前ターン未完了のまま
393
+ // 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
394
+ // pending は queue_state で UI (送信待ちチップ) に出す (per-message と同じ体験)。
395
+ if (this._busy) {
396
+ this._pendingMessages.push(prompt)
397
+ this.logger?.info(
398
+ { stream_id: this.stream_id, queued: this._pendingMessages.length },
399
+ "resident busy, message queued",
400
+ )
401
+ this._emitQueueState()
402
+ return
403
+ }
293
404
  this._busy = true
294
405
  this._inputQueue.push(toSDKUserMessage(prompt))
295
- if (!this._residentStarted) {
406
+ // 改修4 (A): 死亡ガード。常駐 query が未起動 or (エラー等で) 終了済み (_residentQuery=null)
407
+ // なら (再)起動する。_runResidentQuery は起動時 options.resume=this.sessionId で文脈を
408
+ // 復元するため、途中死からの復活でも過去コンテキストは失われない。
409
+ if (!this._residentQuery) {
296
410
  this._residentStarted = true
297
- this._runResidentQuery().catch((err) => {
298
- this.logger?.error(
299
- { stream_id: this.stream_id, err: err?.message },
300
- "resident query threw",
301
- )
302
- })
411
+ this._startResidentQuery()
303
412
  }
304
413
  return
305
414
  }
@@ -341,7 +450,13 @@ class ClaudeStreamSession {
341
450
  if (this.sessionId) options.resume = this.sessionId
342
451
 
343
452
  try {
344
- const generator = this.sdk.query({ prompt, options })
453
+ // 改修4 (A): canUseTool は streaming input (AsyncIterable prompt) でのみ確実に機能する。
454
+ // per-message でも単発 InputQueue (1 件 push → 即 close) を prompt に渡し、1 query=1 ターンの
455
+ // resume チェーン挙動は維持しつつ、文字列 prompt + canUseTool の SDK 制約リスクを回避する。
456
+ const inputQueue = new InputQueue()
457
+ inputQueue.push(toSDKUserMessage(prompt))
458
+ inputQueue.close()
459
+ const generator = this.sdk.query({ prompt: inputQueue, options })
345
460
  for await (const msg of generator) {
346
461
  if (
347
462
  msg?.type === "system" &&
@@ -460,11 +575,40 @@ class ClaudeStreamSession {
460
575
  }
461
576
  }
462
577
 
463
- /** 改修2: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
578
+ /** 改修4 (A): 常駐 query を resume 付きで (再)起動する。_runResidentQuery 起動時に
579
+ * options.resume=this.sessionId で過去文脈を復元するため、途中死からの復活でも安全。 */
580
+ _startResidentQuery() {
581
+ this._runResidentQuery().catch((err) => {
582
+ this.logger?.error(
583
+ { stream_id: this.stream_id, err: err?.message },
584
+ "resident query threw",
585
+ )
586
+ })
587
+ }
588
+
589
+ /** 改修4 (A): ターン完了時に pending の先頭 1 件を InputQueue へ流す (ターンのシリアライズ)。
590
+ * queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。 */
591
+ _drainResidentPending() {
592
+ if (this._closed) return
593
+ if (this._pendingMessages.length === 0) {
594
+ this._emitQueueState()
595
+ return
596
+ }
597
+ const next = this._pendingMessages.shift()
598
+ this._busy = true
599
+ this._inputQueue.push(toSDKUserMessage(next))
600
+ this._emitQueueState()
601
+ }
602
+
603
+ /** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
464
604
  * system/init は最初の 1 回のみ (毎ターン init = 文脈/キャッシュ温め直しを解消)。各ターンは
465
605
  * result で _busy=false にするが query は継続し次 input を待つ。close (inputQueue.close) で
466
- * generator が終わり query が完走する。resume は使わない (新規セッションのみが対象)。
467
- * 常駐では query stream が唯一のイベント源のため watcher は張らない。 */
606
+ * generator が終わり query が完走する。
607
+ * 改修4: resume セッションも対象。query 起動時 options.resume に過去 session_id を渡して
608
+ * 文脈を引き継ぐ (init は初回 1 回のみ)。continue は使わない (自動継続の暴走回避)。初回 input は
609
+ * sendMessage が積むユーザーの実メッセージなので「Continue from where you left off」は発生しない。
610
+ * 常駐では query stream が唯一のイベント源のため watcher は張らない (過去確定分は browser が
611
+ * history.request で hydrate 済み。resume で query stream が過去行を replay しない前提)。 */
468
612
  async _runResidentQuery() {
469
613
  const options = {
470
614
  cwd: this.cwd,
@@ -475,6 +619,9 @@ class ClaudeStreamSession {
475
619
  if (this.permissionMode) options.permissionMode = this.permissionMode
476
620
  if (this.maxTurns != null) options.maxTurns = this.maxTurns
477
621
  if (this.maxThinkingTokens != null) options.maxThinkingTokens = this.maxThinkingTokens
622
+ // 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
623
+ // query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
624
+ if (this.sessionId) options.resume = this.sessionId
478
625
  const denyPending = (reason) => {
479
626
  for (const [, resolver] of this._permissionResolvers) {
480
627
  try {
@@ -501,6 +648,8 @@ class ClaudeStreamSession {
501
648
  // ターン完了: 未解決 permission を閉じ、次 input 受付へ (query は継続)。
502
649
  denyPending("turn ended")
503
650
  this._busy = false
651
+ // 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
652
+ this._drainResidentPending()
504
653
  }
505
654
  try {
506
655
  this.onEvent?.(msg)
@@ -523,13 +672,22 @@ class ClaudeStreamSession {
523
672
  this._residentQuery = null
524
673
  this._busy = false
525
674
  denyPending("closed")
526
- // 常駐 query の終了 = セッション終了。detached 後にここへ来たら reap する。
675
+ // 常駐 query の終了。detached 後にここへ来たら reap する。
527
676
  if (this._reapAfterTurn && !this._closed) {
528
677
  try {
529
678
  this.close()
530
679
  } finally {
531
680
  this.onReap?.()
532
681
  }
682
+ } else if (!this._closed && this._pendingMessages.length > 0) {
683
+ // 改修4 (A): 異常終了 (close 以外) で pending が残っていれば resume 付きで再起動し
684
+ // 取りこぼしを防ぐ。_runResidentQuery が options.resume=this.sessionId で文脈を復元する。
685
+ const next = this._pendingMessages.shift()
686
+ this._busy = true
687
+ this._residentStarted = true
688
+ this._inputQueue.push(toSDKUserMessage(next))
689
+ this._emitQueueState()
690
+ this._startResidentQuery()
533
691
  }
534
692
  }
535
693
  }
@@ -657,10 +815,25 @@ export class ClaudeStreamBridge extends EventEmitter {
657
815
  const live = this._liveBySession.get(resumeSessionId)
658
816
  if (live && !live._closed) {
659
817
  this.sessions.delete(live.stream_id)
660
- live.reattach(stream_id)
818
+ // 改修5: 再アタッチ時に model/permission/maxThinkingTokens を引き継ぎ反映する。
819
+ // browser はバッジ変更時に新しい値を載せた claude.attach を同一 resume で送る
820
+ // ため、ここで適用すると常駐 query を維持したまま次ターンから切り替わる。
821
+ live.reattach(stream_id, {
822
+ model,
823
+ permissionMode,
824
+ maxThinkingTokens:
825
+ typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
826
+ })
661
827
  this.sessions.set(stream_id, live)
662
828
  this.logger?.info(
663
- { stream_id, resume: resumeSessionId, busy: live._busy },
829
+ {
830
+ stream_id,
831
+ resume: resumeSessionId,
832
+ busy: live._busy,
833
+ model: live.model,
834
+ permissionMode: live.permissionMode,
835
+ maxThinkingTokens: live.maxThinkingTokens,
836
+ },
664
837
  "claude stream reattached to live session",
665
838
  )
666
839
  return { stream_id, resuming: true, reattached: true }