@cocorograph/hub-agent 0.6.10 → 0.6.12
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-stream-bridge.mjs +128 -13
- package/src/main.mjs +6 -0
package/package.json
CHANGED
|
@@ -24,6 +24,12 @@ import { randomUUID } from "node:crypto"
|
|
|
24
24
|
import { jsonlPath } from "./claude-history.mjs"
|
|
25
25
|
import { watchSessionFile } from "./claude-history-watch.mjs"
|
|
26
26
|
|
|
27
|
+
/** browser 切断後、セッションを生かしたまま再接続を待つ idle TTL (ミリ秒)。これを過ぎ、
|
|
28
|
+
* かつ走行中でなければ撤去する。端末スリープ/長期離席後の再接続も吸収できるよう 7 日に
|
|
29
|
+
* 設定 (放置セッションのメモリ蓄積は idle 時のみで小さい)。走行中ターンは TTL を過ぎても
|
|
30
|
+
* 完走するまで絶対に撤去しない。明示終了 / 新規セッションでは即時撤去される。 */
|
|
31
|
+
const IDLE_DETACH_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
32
|
+
|
|
27
33
|
/** message (`{role, content}` or string) からプロンプト文字列を取り出す。 */
|
|
28
34
|
function extractPromptText(message) {
|
|
29
35
|
if (typeof message === "string") return message
|
|
@@ -48,6 +54,8 @@ class ClaudeStreamSession {
|
|
|
48
54
|
cwd,
|
|
49
55
|
model,
|
|
50
56
|
permissionMode,
|
|
57
|
+
maxTurns,
|
|
58
|
+
maxThinkingTokens,
|
|
51
59
|
resumeSessionId,
|
|
52
60
|
sdk,
|
|
53
61
|
logger,
|
|
@@ -61,6 +69,9 @@ class ClaudeStreamSession {
|
|
|
61
69
|
this.cwd = cwd
|
|
62
70
|
this.model = model || null
|
|
63
71
|
this.permissionMode = permissionMode || null
|
|
72
|
+
this.maxTurns = typeof maxTurns === "number" ? maxTurns : null
|
|
73
|
+
this.maxThinkingTokens =
|
|
74
|
+
typeof maxThinkingTokens === "number" ? maxThinkingTokens : null
|
|
64
75
|
this.sdk = sdk
|
|
65
76
|
this.logger = logger
|
|
66
77
|
this.onEvent = onEvent
|
|
@@ -73,6 +84,11 @@ class ClaudeStreamSession {
|
|
|
73
84
|
/** 次の query() で resume に使う session_id。各ターンの system/init で更新。 */
|
|
74
85
|
this.sessionId = resumeSessionId || null
|
|
75
86
|
|
|
87
|
+
/** browser 切断中フラグ。true でも query は中断せず継続する (絶対に落とさない)。 */
|
|
88
|
+
this._detached = false
|
|
89
|
+
/** detached のまま放置されたセッションを撤去する idle タイマー。再アタッチでキャンセル。 */
|
|
90
|
+
this._idleTimer = null
|
|
91
|
+
|
|
76
92
|
/** @type {Map<string, {resolve: (decision: object) => void}>} permission 応答待ち */
|
|
77
93
|
this._permissionResolvers = new Map()
|
|
78
94
|
/** 現在ターン実行中か (多重 query 防止) */
|
|
@@ -157,6 +173,41 @@ class ClaudeStreamSession {
|
|
|
157
173
|
return true
|
|
158
174
|
}
|
|
159
175
|
|
|
176
|
+
/** 再アタッチ: 走行中(または生存中)セッションに新しい stream_id を紐付け直し、
|
|
177
|
+
* idle 撤去タイマーを止める。以降のターンイベントはこの stream_id 経由で新しい
|
|
178
|
+
* browser 接続へライブに流れる (= 通常の生成中表示と同じ)。再アタッチ前の確定分は
|
|
179
|
+
* browser 側の jsonl hydrate (history.request) で復元するため、ここでは replay しない。 */
|
|
180
|
+
reattach(stream_id) {
|
|
181
|
+
this.stream_id = stream_id
|
|
182
|
+
this._detached = false
|
|
183
|
+
if (this._idleTimer) {
|
|
184
|
+
clearTimeout(this._idleTimer)
|
|
185
|
+
this._idleTimer = null
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** soft detach: browser 切断時にターンを中断せずセッションを生かしたまま detached に
|
|
190
|
+
* する。idle TTL 経過後に初めて onTimeout (= reap) を呼ぶ。再アタッチでキャンセル。 */
|
|
191
|
+
softDetach(ttlMs, onTimeout) {
|
|
192
|
+
this._detached = true
|
|
193
|
+
if (this._idleTimer) clearTimeout(this._idleTimer)
|
|
194
|
+
const tick = () => {
|
|
195
|
+
// 再アタッチ済みなら撤去しない (reattach が _detached=false にする)
|
|
196
|
+
if (!this._detached) {
|
|
197
|
+
this._idleTimer = null
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
// 走行中ターンは完走を待ち TTL 後に再チェック (走行中は絶対に撤去しない)
|
|
201
|
+
if (this._busy) {
|
|
202
|
+
this._idleTimer = setTimeout(tick, ttlMs)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
this._idleTimer = null
|
|
206
|
+
onTimeout?.()
|
|
207
|
+
}
|
|
208
|
+
this._idleTimer = setTimeout(tick, ttlMs)
|
|
209
|
+
}
|
|
210
|
+
|
|
160
211
|
/**
|
|
161
212
|
* ユーザーメッセージ 1 件を 1 query() として実行する。
|
|
162
213
|
* 既存ターン実行中は無視 (UI 側で送信を抑止する想定だが念のため)。
|
|
@@ -185,6 +236,9 @@ class ClaudeStreamSession {
|
|
|
185
236
|
}
|
|
186
237
|
if (this.model) options.model = this.model
|
|
187
238
|
if (this.permissionMode) options.permissionMode = this.permissionMode
|
|
239
|
+
// Phase B: チャット SDK に効くオプション (拡張思考予算 / ツール往復上限)。
|
|
240
|
+
if (this.maxTurns != null) options.maxTurns = this.maxTurns
|
|
241
|
+
if (this.maxThinkingTokens != null) options.maxThinkingTokens = this.maxThinkingTokens
|
|
188
242
|
// 直前ターンまでの session_id があれば resume チェーン
|
|
189
243
|
if (this.sessionId) options.resume = this.sessionId
|
|
190
244
|
|
|
@@ -295,6 +349,10 @@ class ClaudeStreamSession {
|
|
|
295
349
|
close() {
|
|
296
350
|
this._closed = true
|
|
297
351
|
this.abortTurn()
|
|
352
|
+
if (this._idleTimer) {
|
|
353
|
+
clearTimeout(this._idleTimer)
|
|
354
|
+
this._idleTimer = null
|
|
355
|
+
}
|
|
298
356
|
if (this._watcher) {
|
|
299
357
|
try {
|
|
300
358
|
this._watcher.stop()
|
|
@@ -330,6 +388,9 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
330
388
|
this.logger = logger
|
|
331
389
|
/** @type {Map<string, ClaudeStreamSession>} */
|
|
332
390
|
this.sessions = new Map()
|
|
391
|
+
/** session_id → 生存セッション。再接続 (再アタッチ) で同一セッションを引く索引。
|
|
392
|
+
* browser が切れてもここに残し、走行中ターンの継続 + 再接続でのライブ追従を可能にする。 */
|
|
393
|
+
this._liveBySession = new Map()
|
|
333
394
|
}
|
|
334
395
|
|
|
335
396
|
/**
|
|
@@ -340,39 +401,80 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
340
401
|
* cwd?: string,
|
|
341
402
|
* model?: string|null,
|
|
342
403
|
* permissionMode?: string|null,
|
|
404
|
+
* maxTurns?: number|null,
|
|
405
|
+
* maxThinkingTokens?: number|null,
|
|
343
406
|
* resumeSessionId?: string|null,
|
|
344
407
|
* }} args
|
|
345
408
|
* @returns {{ stream_id: string, resuming: boolean }}
|
|
346
409
|
*/
|
|
347
|
-
attach({
|
|
410
|
+
attach({
|
|
411
|
+
stream_id,
|
|
412
|
+
cwd,
|
|
413
|
+
model,
|
|
414
|
+
permissionMode,
|
|
415
|
+
maxTurns,
|
|
416
|
+
maxThinkingTokens,
|
|
417
|
+
resumeSessionId,
|
|
418
|
+
}) {
|
|
348
419
|
if (!stream_id) throw new TypeError("attach requires stream_id")
|
|
349
420
|
if (this.sessions.has(stream_id)) {
|
|
350
421
|
throw new Error(`stream_id "${stream_id}" は既に attach 済みです`)
|
|
351
422
|
}
|
|
423
|
+
// 常駐化: resume_session_id が示すセッションが (走行中 / idle 問わず) まだ生存していれば、
|
|
424
|
+
// 新規 query を起動せずそのセッションへ再アタッチする。これにより端末スリープ/再接続でも
|
|
425
|
+
// ターンは落ちず、以降のイベントは新しい stream_id 経由でライブに流れる
|
|
426
|
+
// (= 通常の生成中表示と同じ)。再接続前の確定分は browser の jsonl hydrate で復元する。
|
|
427
|
+
if (resumeSessionId) {
|
|
428
|
+
const live = this._liveBySession.get(resumeSessionId)
|
|
429
|
+
if (live && !live._closed) {
|
|
430
|
+
this.sessions.delete(live.stream_id)
|
|
431
|
+
live.reattach(stream_id)
|
|
432
|
+
this.sessions.set(stream_id, live)
|
|
433
|
+
this.logger?.info(
|
|
434
|
+
{ stream_id, resume: resumeSessionId, busy: live._busy },
|
|
435
|
+
"claude stream reattached to live session",
|
|
436
|
+
)
|
|
437
|
+
return { stream_id, resuming: true, reattached: true }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
352
440
|
const session = new ClaudeStreamSession({
|
|
353
441
|
stream_id,
|
|
354
442
|
cwd: cwd || process.env.HOME || process.cwd(),
|
|
355
443
|
model: model || null,
|
|
356
444
|
permissionMode: permissionMode || null,
|
|
445
|
+
maxTurns: typeof maxTurns === "number" ? maxTurns : null,
|
|
446
|
+
maxThinkingTokens:
|
|
447
|
+
typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
|
|
357
448
|
resumeSessionId: resumeSessionId || null,
|
|
358
449
|
sdk: this.sdk,
|
|
359
450
|
logger: this.logger,
|
|
360
451
|
onEvent: (event) => {
|
|
361
|
-
|
|
452
|
+
// stream_id は再アタッチで変わるため session.stream_id (最新) を使う。
|
|
453
|
+
this.emit("event", { stream_id: session.stream_id, session_id: session.sessionId, event })
|
|
454
|
+
// session_id 確定後は索引に登録 (冪等)。再接続時の再アタッチに使う。
|
|
455
|
+
if (session.sessionId) this._liveBySession.set(session.sessionId, session)
|
|
362
456
|
},
|
|
363
457
|
onPermission: ({ tool_name, input, request_id }) => {
|
|
364
|
-
this.emit("permission", {
|
|
458
|
+
this.emit("permission", {
|
|
459
|
+
stream_id: session.stream_id,
|
|
460
|
+
request_id,
|
|
461
|
+
tool_name,
|
|
462
|
+
input,
|
|
463
|
+
})
|
|
365
464
|
},
|
|
366
465
|
onError: (err) => {
|
|
367
|
-
this.emit("error", { stream_id, error: err?.message || String(err) })
|
|
466
|
+
this.emit("error", { stream_id: session.stream_id, error: err?.message || String(err) })
|
|
368
467
|
},
|
|
369
468
|
onReap: () => {
|
|
370
|
-
// ターン完走後の遅延クローズ。Map
|
|
371
|
-
if (this.sessions.get(stream_id) === session) {
|
|
372
|
-
this.sessions.delete(stream_id)
|
|
469
|
+
// ターン完走後の遅延クローズ。Map / 索引から撤去し exit を emit する。
|
|
470
|
+
if (this.sessions.get(session.stream_id) === session) {
|
|
471
|
+
this.sessions.delete(session.stream_id)
|
|
472
|
+
}
|
|
473
|
+
if (session.sessionId && this._liveBySession.get(session.sessionId) === session) {
|
|
474
|
+
this._liveBySession.delete(session.sessionId)
|
|
373
475
|
}
|
|
374
476
|
this.emit("exit", {
|
|
375
|
-
stream_id,
|
|
477
|
+
stream_id: session.stream_id,
|
|
376
478
|
code: 0,
|
|
377
479
|
reason: "detached-after-turn",
|
|
378
480
|
session_id: session.sessionId,
|
|
@@ -436,11 +538,22 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
436
538
|
detach({ stream_id }) {
|
|
437
539
|
const s = this.sessions.get(stream_id)
|
|
438
540
|
if (!s) return false
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
541
|
+
// 常駐化: browser 切断ではセッションを止めない (reap しない)。走行中ターンは完走し、
|
|
542
|
+
// idle のまま IDLE_DETACH_TTL_MS 経過して初めて撤去する。TTL 内に再接続 (再アタッチ)
|
|
543
|
+
// されればキャンセルされる。明示的に止めたい場合は interrupt()/明示終了を使う。
|
|
544
|
+
s.softDetach(IDLE_DETACH_TTL_MS, () => {
|
|
545
|
+
if (this.sessions.get(s.stream_id) === s) this.sessions.delete(s.stream_id)
|
|
546
|
+
if (s.sessionId && this._liveBySession.get(s.sessionId) === s) {
|
|
547
|
+
this._liveBySession.delete(s.sessionId)
|
|
548
|
+
}
|
|
549
|
+
s.close()
|
|
550
|
+
this.emit("exit", {
|
|
551
|
+
stream_id: s.stream_id,
|
|
552
|
+
code: 0,
|
|
553
|
+
reason: "idle-reaped",
|
|
554
|
+
session_id: s.sessionId,
|
|
555
|
+
})
|
|
556
|
+
})
|
|
444
557
|
return true
|
|
445
558
|
}
|
|
446
559
|
|
|
@@ -453,6 +566,8 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
453
566
|
this.sessions.delete(stream_id)
|
|
454
567
|
this.emit("exit", { stream_id, code: 0, reason: "shutdown", session_id: s.sessionId })
|
|
455
568
|
}
|
|
569
|
+
// 常駐化: 再アタッチ索引も全消去 (プロセス終了時の後始末)。
|
|
570
|
+
this._liveBySession.clear()
|
|
456
571
|
}
|
|
457
572
|
|
|
458
573
|
/** 現在 attach 中の stream_id 一覧 (debug 用)。 */
|
package/src/main.mjs
CHANGED
|
@@ -628,6 +628,12 @@ async function dispatch(msg, ctx) {
|
|
|
628
628
|
msg.permission_mode ||
|
|
629
629
|
ctx.config?.claude_permission_mode ||
|
|
630
630
|
null,
|
|
631
|
+
// Phase B: チャット SDK に効くオプション (session 単位 override)。
|
|
632
|
+
maxTurns: typeof msg.max_turns === "number" ? msg.max_turns : null,
|
|
633
|
+
maxThinkingTokens:
|
|
634
|
+
typeof msg.max_thinking_tokens === "number"
|
|
635
|
+
? msg.max_thinking_tokens
|
|
636
|
+
: null,
|
|
631
637
|
resumeSessionId: msg.resume_session_id || null,
|
|
632
638
|
})
|
|
633
639
|
ctx.client.send({
|