@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 +1 -1
- package/src/claude-history-watch.mjs +3 -20
- package/src/claude-history.mjs +32 -16
- package/src/claude-stream-bridge.mjs +197 -24
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
}
|
package/src/claude-history.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
*
|
|
141
|
-
*
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
/** 改修
|
|
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 が完走する。
|
|
467
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
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 }
|