@cocorograph/hub-agent 0.8.108 → 0.8.110
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 +81 -20
- package/src/hub-bundle.mjs +70 -12
- package/src/main.mjs +6 -1
package/package.json
CHANGED
|
@@ -66,10 +66,13 @@ function siblingEnvFor(cwd) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** browser 切断後、セッションを生かしたまま再接続を待つ idle TTL (ミリ秒)。これを過ぎ、
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
const IDLE_DETACH_TTL_MS =
|
|
69
|
+
* かつ実作業が無ければ撤去する。2026-10-06 の実測では使用済み chrome-devtools-mcp が
|
|
70
|
+
* 1 本 0.7〜2.5GB を保持し続け、swap は 26GB に達した。idle 中もメモリが小さいとは
|
|
71
|
+
* 限らないため、再接続の猶予を 3 時間に抑える。実作業中は TTL 後も再チェックする。 */
|
|
72
|
+
export const IDLE_DETACH_TTL_MS = readPositiveIntEnv("HUB_AGENT_CLAUDE_IDLE_DETACH_TTL_MS", 3 * 60 * 60 * 1000)
|
|
73
|
+
|
|
74
|
+
/** 無操作かつ実作業無しが 6 時間続いたら、セッションを残して常駐 query だけ畳む。 */
|
|
75
|
+
export const IDLE_ATTACHED_TTL_MS = readPositiveIntEnv("HUB_AGENT_CLAUDE_IDLE_ATTACHED_TTL_MS", 6 * 60 * 60 * 1000)
|
|
73
76
|
|
|
74
77
|
/** 改修2: チャット常駐query化のフラグ。env HUB_AGENT_CHAT_RESIDENT="0" で無効化し
|
|
75
78
|
* 従来の「1メッセージ=1query」へ即ロールバックできる。デフォルト有効。 */
|
|
@@ -201,7 +204,7 @@ const DEAD_TERMINAL_TTL_MS = readPositiveIntEnv(
|
|
|
201
204
|
|
|
202
205
|
/** 子⑤ (T06544): warm 保持を「各 cwd K 本 × 同時 warm 保持する cwd 数 N(既定 20)」で有界化する
|
|
203
206
|
* 上限(codex-stream-bridge.mjs の WARM_CWD_MAX と対)。claude は従来 per-session の softDetach
|
|
204
|
-
*
|
|
207
|
+
* タイマーだけで件数上限が無かったため、多数の cwd を渡り歩くと warm セッション(= SDK
|
|
205
208
|
* 常駐 query プロセス)が N 個を超えて蓄積し得た。cwd 単位の LRU で最古 cwd から早期 reap する。
|
|
206
209
|
* env HUB_AGENT_WARM_CWD_MAX で調整可能(codex と共有)。 */
|
|
207
210
|
export const WARM_CWD_MAX = readPositiveIntEnv("HUB_AGENT_WARM_CWD_MAX", 20)
|
|
@@ -483,6 +486,8 @@ export class ClaudeStreamSession {
|
|
|
483
486
|
* recency を失う。これは touch() で更新し detach でリセットしないので、warm 化しても「最後に
|
|
484
487
|
* 使った時刻」が残り、最古 cwd から LRU reap できる。 */
|
|
485
488
|
this._lastActivityMs = Date.now()
|
|
489
|
+
// 長いターンの終了直後に、入力時刻だけを見て idle 撤去しないための終了時刻。
|
|
490
|
+
this._lastTurnSettledMs = 0
|
|
486
491
|
/** 子⑤ round5 Finding 1: 生成順の単調増加シーケンス(warm 境界のタイブレーク用。値が大きい =
|
|
487
492
|
* 新しい)。recency 同値時に per-cwd=K が新しいセッションを残せるようにする。
|
|
488
493
|
* ⚠️ カウンタは **claude / codex 共通**(session-seq.mjs)。総 live 上限 M は両 CLI を合算して
|
|
@@ -702,6 +707,8 @@ export class ClaudeStreamSession {
|
|
|
702
707
|
* close()/shutdown() で子プロセスが残留する。per-message と同じく AbortController を
|
|
703
708
|
* options へ渡し、interrupt が reject したときと close() のときに最終手段として abort する。 */
|
|
704
709
|
this._residentAbortController = null
|
|
710
|
+
this._foldedIdle = false
|
|
711
|
+
this._idleFoldAbortController = null
|
|
705
712
|
|
|
706
713
|
/** jsonl ライブ追従 watcher (0.6.5)。外部 (tmux 等) の追記を拾う。 */
|
|
707
714
|
this._watcher = null
|
|
@@ -1338,26 +1345,33 @@ export class ClaudeStreamSession {
|
|
|
1338
1345
|
softDetach(ttlMs, onTimeout) {
|
|
1339
1346
|
this._detached = true
|
|
1340
1347
|
if (this._idleTimer) clearTimeout(this._idleTimer)
|
|
1348
|
+
const detachedAt = Date.now()
|
|
1349
|
+
const schedule = (remaining) => {
|
|
1350
|
+
// Node.js のタイマー上限を超える TTL も、期限まで分割して待つ。
|
|
1351
|
+
this._idleTimer = setTimeout(tick, Math.min(remaining, 2147483647))
|
|
1352
|
+
this._idleTimer.unref?.()
|
|
1353
|
+
}
|
|
1341
1354
|
const tick = () => {
|
|
1342
1355
|
// 再アタッチ済みなら撤去しない (reattach が _detached=false にする)
|
|
1343
|
-
if (!this._detached) {
|
|
1356
|
+
if (!this._detached || this._closed) {
|
|
1344
1357
|
this._idleTimer = null
|
|
1345
1358
|
return
|
|
1346
1359
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1360
|
+
const now = Date.now()
|
|
1361
|
+
const remaining = Math.max(detachedAt, this._lastTurnSettledMs) + ttlMs - now
|
|
1362
|
+
if (remaining > 0) {
|
|
1363
|
+
schedule(remaining)
|
|
1364
|
+
return
|
|
1365
|
+
}
|
|
1366
|
+
// 実作業中は再チェックするが、detach / ターン終了由来の期限自体は延ばさない。
|
|
1367
|
+
if (this._busy || this._pendingMessages.length > 0 || this._hasLiveTask?.(now)) {
|
|
1368
|
+
schedule(ttlMs)
|
|
1354
1369
|
return
|
|
1355
1370
|
}
|
|
1356
1371
|
this._idleTimer = null
|
|
1357
1372
|
onTimeout?.()
|
|
1358
1373
|
}
|
|
1359
|
-
|
|
1360
|
-
this._idleTimer.unref?.()
|
|
1374
|
+
schedule(ttlMs)
|
|
1361
1375
|
}
|
|
1362
1376
|
|
|
1363
1377
|
/**
|
|
@@ -1420,7 +1434,8 @@ export class ClaudeStreamSession {
|
|
|
1420
1434
|
// pending へ退避し、現ターンの result 後に 1 件ずつ drain する。前ターン未完了のまま
|
|
1421
1435
|
// 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
|
|
1422
1436
|
// pending は queue_state で UI (送信待ちチップ) に出す (per-message と同じ体験)。
|
|
1423
|
-
if (this._busy) {
|
|
1437
|
+
if (this._busy || (this._foldedIdle && this._residentQuery)) {
|
|
1438
|
+
// fold の abort が完了するまでの入力は finally の既存 resume/drain 経路へ渡す。
|
|
1424
1439
|
this._enqueuePending(prompt, ultracode, inputId)
|
|
1425
1440
|
this.logger?.info(
|
|
1426
1441
|
{ stream_id: this.stream_id, queued: this._pendingMessages.length },
|
|
@@ -1629,6 +1644,7 @@ export class ClaudeStreamSession {
|
|
|
1629
1644
|
/* ignore */
|
|
1630
1645
|
}
|
|
1631
1646
|
}
|
|
1647
|
+
this._lastTurnSettledMs = Date.now()
|
|
1632
1648
|
this._busy = false
|
|
1633
1649
|
if (aborted) {
|
|
1634
1650
|
this.logger?.info({ stream_id: this.stream_id }, "claude turn aborted")
|
|
@@ -2157,6 +2173,7 @@ export class ClaudeStreamSession {
|
|
|
2157
2173
|
// 中断直後の 1 ターンだけを帰属不明として扱い、それが確定したら次からは信頼に戻す。
|
|
2158
2174
|
if (this._lastTurnTexts?.startedUncertain) this._turnTextsUncertain = false
|
|
2159
2175
|
denyPending?.("turn ended")
|
|
2176
|
+
this._lastTurnSettledMs = Date.now()
|
|
2160
2177
|
this._busy = false
|
|
2161
2178
|
// result の後に同一ターンの idle が遅れて届く場合がある。pending drain は同期的に
|
|
2162
2179
|
// 次ターンの busy を立てるため、その idle を新ターンへ誤帰属させないよう先にガードする。
|
|
@@ -2180,6 +2197,9 @@ export class ClaudeStreamSession {
|
|
|
2180
2197
|
* (await するため async 化。idle ハンドラからは fire-and-forget で呼ばれる)。 */
|
|
2181
2198
|
async _drainResidentPending() {
|
|
2182
2199
|
if (this._closed) return
|
|
2200
|
+
// fold 中の旧 query には渡さず、終了時の finally が resume して pending を拾う。
|
|
2201
|
+
// 通常の interrupt 後の drain は従来どおり進める。
|
|
2202
|
+
if (this._foldedIdle && this._residentQuery) return
|
|
2183
2203
|
if (this._pendingMessages.length === 0) {
|
|
2184
2204
|
// 症状1 自己修復 (2026-07-21): _drainPending と同旨。落ちた drain broadcast で送信待ち
|
|
2185
2205
|
// チップが stale 残留するのを、ターン settle 毎の force 再表明で補正する (空 = count=0)。
|
|
@@ -2210,6 +2230,7 @@ export class ClaudeStreamSession {
|
|
|
2210
2230
|
this._emitQueueState([next.text], { startedIds: [next.id] })
|
|
2211
2231
|
else this._emitQueueState([], { force: true })
|
|
2212
2232
|
} catch (err) {
|
|
2233
|
+
this._lastTurnSettledMs = Date.now()
|
|
2213
2234
|
this._busy = false
|
|
2214
2235
|
this._pendingMessages.unshift(next)
|
|
2215
2236
|
this.logger?.warn(
|
|
@@ -2286,6 +2307,7 @@ export class ClaudeStreamSession {
|
|
|
2286
2307
|
try {
|
|
2287
2308
|
const q = this.sdk.query({ prompt: this._inputQueue, options })
|
|
2288
2309
|
this._residentQuery = q
|
|
2310
|
+
this._foldedIdle = false
|
|
2289
2311
|
for await (const msg of q) {
|
|
2290
2312
|
let suppressEmit = false
|
|
2291
2313
|
this._observeTurnTexts(msg)
|
|
@@ -2369,7 +2391,14 @@ export class ClaudeStreamSession {
|
|
|
2369
2391
|
}
|
|
2370
2392
|
}
|
|
2371
2393
|
} catch (err) {
|
|
2372
|
-
|
|
2394
|
+
// run 固有の controller と照合する。fold フラグだけでは、再開後のエラーや
|
|
2395
|
+
// abort と同時に起きた無関係なエラーまで握り潰してしまう。
|
|
2396
|
+
const idleFoldAbort = this._idleFoldAbortController === myAbort && myAbort.signal.aborted && (
|
|
2397
|
+
err?.name === "AbortError" || err?.code === "ABORT_ERR" ||
|
|
2398
|
+
// SDK の abort 終了エラーは Error 派生で name が AbortError とは限らない。
|
|
2399
|
+
err?.message === "Claude Code process aborted by user" || err?.message === "Operation aborted"
|
|
2400
|
+
)
|
|
2401
|
+
if (!this._closed && !idleFoldAbort) {
|
|
2373
2402
|
try {
|
|
2374
2403
|
this.onError?.(err)
|
|
2375
2404
|
} catch {
|
|
@@ -2383,6 +2412,7 @@ export class ClaudeStreamSession {
|
|
|
2383
2412
|
// generator になっていたことを検知できるようにするため。
|
|
2384
2413
|
this._residentQueryReturned = true
|
|
2385
2414
|
this._residentQuery = null
|
|
2415
|
+
if (this._busy) this._lastTurnSettledMs = Date.now()
|
|
2386
2416
|
this._busy = false
|
|
2387
2417
|
denyPending("closed")
|
|
2388
2418
|
// 常駐 query の終了。detached 後にここへ来たら reap する。
|
|
@@ -2503,6 +2533,7 @@ export class ClaudeStreamSession {
|
|
|
2503
2533
|
// いたが、中断ターンの in-flight イベントが _turnProgressed を立ててしまうため成立して
|
|
2504
2534
|
// いなかった)。
|
|
2505
2535
|
this._turnSettled = true
|
|
2536
|
+
if (this._busy) this._lastTurnSettledMs = Date.now()
|
|
2506
2537
|
this._busy = false
|
|
2507
2538
|
this._armStaleIdleGuard()
|
|
2508
2539
|
// 孤児カード防止 (2026-07-10 監査): 中断したターンに紐づく未応答 permission は
|
|
@@ -2586,9 +2617,25 @@ export class ClaudeStreamSession {
|
|
|
2586
2617
|
}
|
|
2587
2618
|
}
|
|
2588
2619
|
|
|
2620
|
+
/** idle の常駐プロセスだけを畳み、再接続と次入力用にセッションを保持する。 */
|
|
2621
|
+
foldIdleResident() {
|
|
2622
|
+
if (this._closed || !this._residentQuery || this._foldedIdle) return false
|
|
2623
|
+
this._foldedIdle = true
|
|
2624
|
+
// 既に別の理由で abort された run を idle fold のものとして扱わない。
|
|
2625
|
+
if (!this._residentAbortController?.signal.aborted) {
|
|
2626
|
+
this._idleFoldAbortController = this._residentAbortController
|
|
2627
|
+
}
|
|
2628
|
+
this._hardAbortResident()
|
|
2629
|
+
this.logger?.info(
|
|
2630
|
+
{ stream_id: this.stream_id, session_id: this.sessionId, cwd: this.cwd, reason: "inactive" },
|
|
2631
|
+
"claude idle resident query folded",
|
|
2632
|
+
)
|
|
2633
|
+
return true
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2589
2636
|
/** SDK-P0-4 (2026-07-13): 常駐 query の最終手段の強制終了。interrupt() (control request) が
|
|
2590
|
-
* 失敗・無応答のときと close() から呼ぶ。AbortController を落とすと generator
|
|
2591
|
-
*
|
|
2637
|
+
* 失敗・無応答のときと close() / idle fold から呼ぶ。AbortController を落とすと generator が
|
|
2638
|
+
* 終了し、finally が pending を resume 付きで再起動して取りこぼしを防ぐ。 */
|
|
2592
2639
|
_hardAbortResident() {
|
|
2593
2640
|
const ac = this._residentAbortController
|
|
2594
2641
|
if (!ac || ac.signal?.aborted) return
|
|
@@ -2632,6 +2679,8 @@ export class ClaudeStreamSession {
|
|
|
2632
2679
|
* idle セッション (_residentQueryReturned=false) は生存中なので reattach 可。 */
|
|
2633
2680
|
_isReattachable() {
|
|
2634
2681
|
if (this._closed) return false
|
|
2682
|
+
// 意図的に畳んだ常駐 query は、同じセッションで次入力から再開できる。
|
|
2683
|
+
if (this._foldedIdle) return true
|
|
2635
2684
|
if (this._residentQueryReturned && !this._residentQuery) return false
|
|
2636
2685
|
return true
|
|
2637
2686
|
}
|
|
@@ -2803,7 +2852,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2803
2852
|
*
|
|
2804
2853
|
* TUI モードに切り替えた session は tmux ペインの対話 claude が write を担うため、
|
|
2805
2854
|
* SDK セッションを生かしたままだと同一 cwd の jsonl を二重ホストして競合する
|
|
2806
|
-
* (DESIGN.md C7)。soft-detach (
|
|
2855
|
+
* (DESIGN.md C7)。soft-detach (既定 3 時間 resident) では止まらないので、ここで明示 close する。
|
|
2807
2856
|
*
|
|
2808
2857
|
* ⚠️ `reason` は exit イベントにそのまま載る。既定の `tui-bind-stop` は上記 TUI bind 用の
|
|
2809
2858
|
* 名前なので、**TUI 以外の理由で止めるときは必ず渡す**こと (既定のままだと、存在しない
|
|
@@ -4202,6 +4251,18 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
4202
4251
|
return !!session._hasLiveTask?.(now)
|
|
4203
4252
|
}
|
|
4204
4253
|
|
|
4254
|
+
/** 購読数に関係なく、実作業が無く無操作 TTL を過ぎた常駐 query だけを畳む。 */
|
|
4255
|
+
foldInactiveSessions(ttlMs = IDLE_ATTACHED_TTL_MS, now = Date.now()) {
|
|
4256
|
+
let folded = 0
|
|
4257
|
+
for (const { session } of this._liveCandidatesForCwd()) {
|
|
4258
|
+
if (!session._residentQuery || this._hasSessionWork(session, now)) continue
|
|
4259
|
+
const lastActive = Math.max(session._lastActivityMs ?? 0, session._lastTurnSettledMs ?? 0)
|
|
4260
|
+
if (now - lastActive < ttlMs) continue
|
|
4261
|
+
if (session.foldIdleResident()) folded += 1
|
|
4262
|
+
}
|
|
4263
|
+
return folded
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4205
4266
|
/**
|
|
4206
4267
|
* daemon の deferred restart が待つべき実作業があるか。
|
|
4207
4268
|
* 多端末 subscribe は warm 保持には必要だが作業ではないため、ここでは数えない。
|
package/src/hub-bundle.mjs
CHANGED
|
@@ -330,22 +330,48 @@ export function hasInheritedOutputChild() {
|
|
|
330
330
|
return inheritedOutputChildren > 0
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
function runOnce(argv, { logger } = {}) {
|
|
333
|
+
export function runOnce(argv, { logger, redact = [] } = {}) {
|
|
334
|
+
const secrets = redact.filter((value) => value.length > 0).sort((a, b) => b.length - a.length)
|
|
335
|
+
const pattern = secrets.length
|
|
336
|
+
? new RegExp(secrets.map(_escapeRegExp).join("|"), "g")
|
|
337
|
+
: null
|
|
338
|
+
const sanitize = (value) => pattern ? value.replace(pattern, "***") : value
|
|
339
|
+
const safeError = (err) => pattern ? new Error(sanitize(err.message)) : err
|
|
334
340
|
return new Promise((resolve, reject) => {
|
|
335
341
|
const [bin, ...args] = argv
|
|
336
|
-
|
|
342
|
+
const cmd = sanitize(argv.join(" "))
|
|
343
|
+
if (!pattern) inheritedOutputChildren++
|
|
337
344
|
let released = false
|
|
338
345
|
const release = () => {
|
|
339
346
|
if (released) return
|
|
340
347
|
released = true
|
|
341
|
-
inheritedOutputChildren--
|
|
348
|
+
if (!pattern) inheritedOutputChildren--
|
|
342
349
|
}
|
|
343
350
|
let child
|
|
344
351
|
try {
|
|
345
|
-
child = spawn(bin, args, { stdio: "inherit" })
|
|
352
|
+
child = spawn(bin, args, { stdio: pattern ? ["inherit", "pipe", "pipe"] : "inherit" })
|
|
346
353
|
} catch (err) {
|
|
347
354
|
release()
|
|
348
|
-
throw err
|
|
355
|
+
throw safeError(err)
|
|
356
|
+
}
|
|
357
|
+
// close を待って全バイトを結合し、チャンク境界(UTF-8 の途中を含む)での漏出を防ぐ。
|
|
358
|
+
// 上限を超えたら収集済みも捨てて読み捨てに切り替える(途中で切ると資格の断片が残りうるため、
|
|
359
|
+
// 部分表示はしない)。pipe は close まで drain し続け、子を書き込み待ちで止めない。
|
|
360
|
+
const captured = { stdout: { chunks: [], bytes: 0, overflow: false }, stderr: { chunks: [], bytes: 0, overflow: false } }
|
|
361
|
+
if (pattern) {
|
|
362
|
+
for (const name of ["stdout", "stderr"]) {
|
|
363
|
+
const buf = captured[name]
|
|
364
|
+
child[name].on("data", (chunk) => {
|
|
365
|
+
if (buf.overflow) return
|
|
366
|
+
buf.bytes += chunk.length
|
|
367
|
+
if (buf.bytes > REDACTED_OUTPUT_MAX_BYTES) {
|
|
368
|
+
buf.overflow = true
|
|
369
|
+
buf.chunks = []
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
buf.chunks.push(chunk)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
349
375
|
}
|
|
350
376
|
// spawn 失敗 (ENOENT 等) は exit が来ないことがある。error でも必ず settle させ、
|
|
351
377
|
// bundle 同期を未完了のまま残さない (resolve/reject は最初の 1 回だけ有効)。
|
|
@@ -355,22 +381,54 @@ function runOnce(argv, { logger } = {}) {
|
|
|
355
381
|
}
|
|
356
382
|
child.once("error", (err) => {
|
|
357
383
|
release()
|
|
358
|
-
reject(err)
|
|
359
|
-
safeLog("warn", { cmd
|
|
384
|
+
reject(safeError(err))
|
|
385
|
+
safeLog("warn", { cmd, error: sanitize(err.message) }, "hub-bundle post_install failed")
|
|
360
386
|
})
|
|
361
|
-
child.on("exit", (code) => {
|
|
387
|
+
child.on(pattern ? "close" : "exit", (code) => {
|
|
362
388
|
release()
|
|
389
|
+
// 先に settle する。転送先の障害(ENOSPC / EPIPE 等)でコマンドの成否を変えない。
|
|
363
390
|
if (code === 0) {
|
|
364
391
|
resolve(code)
|
|
365
|
-
safeLog("info", { cmd: argv.join(" ") }, "hub-bundle post_install ok")
|
|
366
392
|
} else {
|
|
367
|
-
reject(new Error(`post_install command failed (exit ${code}): ${
|
|
368
|
-
|
|
393
|
+
reject(new Error(`post_install command failed (exit ${code}): ${cmd}`))
|
|
394
|
+
}
|
|
395
|
+
if (pattern) {
|
|
396
|
+
for (const name of ["stdout", "stderr"]) {
|
|
397
|
+
const buf = captured[name]
|
|
398
|
+
const text = buf.overflow
|
|
399
|
+
? `[hub-agent] 出力が ${REDACTED_OUTPUT_MAX_BYTES} バイトを超えたため省略しました\n`
|
|
400
|
+
: sanitize(Buffer.concat(buf.chunks).toString("utf8"))
|
|
401
|
+
if (text) forwardRedactedOutput(process[name], text)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (code === 0) {
|
|
405
|
+
safeLog("info", { cmd }, "hub-bundle post_install ok")
|
|
406
|
+
} else {
|
|
407
|
+
safeLog("warn", { cmd, code }, "hub-bundle post_install failed")
|
|
369
408
|
}
|
|
370
409
|
})
|
|
371
410
|
})
|
|
372
411
|
}
|
|
373
412
|
|
|
413
|
+
// 伏せ字済み出力の 1 ストリームあたりの収集上限。claude mcp add の通常出力は数百バイト。
|
|
414
|
+
const REDACTED_OUTPUT_MAX_BYTES = 1024 * 1024
|
|
415
|
+
|
|
416
|
+
// 伏せ字済み出力を親の stdout / stderr へ書く。書き込み失敗は診断出力の欠落として扱い、
|
|
417
|
+
// 同期例外も非同期の error も外へ出さない。error listener はこの書き込みの間だけ付け、
|
|
418
|
+
// 共有ストリームの他のエラーを恒久的に握り潰さない。
|
|
419
|
+
function forwardRedactedOutput(stream, text) {
|
|
420
|
+
const onError = () => {}
|
|
421
|
+
stream.on("error", onError)
|
|
422
|
+
try {
|
|
423
|
+
stream.write(text, () => {
|
|
424
|
+
// callback 後に error が emit される実装があるため、1 tick 置いてから外す。
|
|
425
|
+
setImmediate(() => stream.removeListener("error", onError))
|
|
426
|
+
})
|
|
427
|
+
} catch {
|
|
428
|
+
stream.removeListener("error", onError)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
374
432
|
/**
|
|
375
433
|
* Bundle response の `mcp_servers` を Claude Code CLI に登録する。
|
|
376
434
|
*
|
|
@@ -474,7 +532,7 @@ export async function applyMcpServers(
|
|
|
474
532
|
// 未登録 → remove は失敗するが正常系。
|
|
475
533
|
}
|
|
476
534
|
}
|
|
477
|
-
await run(argv, { logger })
|
|
535
|
+
await run(argv, wantHeader ? { logger, redact: [agentToken] } : { logger })
|
|
478
536
|
added.push(s.name)
|
|
479
537
|
} catch (err) {
|
|
480
538
|
// ヘッダ不要の外部 MCP は既存なら "already exists" で失敗する。これは「既に正しく
|
package/src/main.mjs
CHANGED
|
@@ -2159,10 +2159,15 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
2159
2159
|
} catch (err) {
|
|
2160
2160
|
logger?.warn({ err: err?.message }, "gcDeadTerminals failed")
|
|
2161
2161
|
}
|
|
2162
|
+
try {
|
|
2163
|
+
claudeBridge?.foldInactiveSessions?.()
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
logger?.warn({ err: err?.message }, "claude foldInactiveSessions failed")
|
|
2166
|
+
}
|
|
2162
2167
|
// 子⑤ (T06544): claude 側 warm 境界(per-cwd=K / N-cwd LRU)。K は現行の既定 3
|
|
2163
2168
|
// (**旧仕様**は per-cwd=1 で「各 cwd 1 本」だった。1 cwd で複数会話を同時に生かすため
|
|
2164
2169
|
// 一般化し、総量は下の総 live 上限 M で押さえる)。従来 claude は per-session の softDetach
|
|
2165
|
-
//
|
|
2170
|
+
// タイマーだけで件数上限が無かったため、多数の cwd を渡り歩くと warm な
|
|
2166
2171
|
// SDK 常駐 query が蓄積し得た。codex の reconcileSessions と対で cwd 単位に有界化する。
|
|
2167
2172
|
try {
|
|
2168
2173
|
claudeBridge?.reconcileWarmBounds?.()
|