@cocorograph/hub-agent 0.7.54 → 0.7.56
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 +201 -21
- package/src/hub-bundle.mjs +11 -4
- package/src/main.mjs +6 -0
package/package.json
CHANGED
|
@@ -271,6 +271,32 @@ function isSessionIdleEvent(msg) {
|
|
|
271
271
|
)
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* SDK-P0-4 (2026-07-13): SDK の権威的な「ターン開始」シグナル。
|
|
276
|
+
* sdk.d.ts (SDKSessionStateChangedMessage) の state は 'idle' | 'running' | 'requires_action'。
|
|
277
|
+
* `running` は SDK が新しい input の処理を実際に開始したときにだけ流れるため、
|
|
278
|
+
* **前ターンの遅延イベントでは絶対に立たない**。これがターン帰属の唯一の確実な境界になる。
|
|
279
|
+
*
|
|
280
|
+
* なぜ必要か: 旧ガード `_turnProgressed` は「現ターンが 1 件でもイベントを出したか」だったが、
|
|
281
|
+
* これは「**現ターンの**イベントが来た」ことを保証しない。abort (interrupt) 直後に drain で
|
|
282
|
+
* 次ターンを開始すると、中断されたターンの in-flight イベント (stream_event の残り delta /
|
|
283
|
+
* 実行中だった tool の user メッセージ等) が 1 件届くだけで _turnProgressed=true になり、
|
|
284
|
+
* その直後に届く**前ターンの idle** が新ターンを終端扱いして撃ち抜いていた (二重 drain →
|
|
285
|
+
* 実行中ターンの InputQueue へ追加 push → FIFO 崩れ・応答取り違え)。
|
|
286
|
+
*/
|
|
287
|
+
function isSessionRunningEvent(msg) {
|
|
288
|
+
return (
|
|
289
|
+
msg?.type === "system" &&
|
|
290
|
+
msg?.subtype === "session_state_changed" &&
|
|
291
|
+
msg?.state === "running"
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** SDK-P0-4: 「前ターンの遅延 idle」を待つ猶予 (ms)。generator のイベント順序は保証されるので
|
|
296
|
+
* 遅延 idle は通常ミリ秒で届く。ここを過ぎても届かなければガードを解除し、現ターンの idle を
|
|
297
|
+
* 正しく終端として扱う (ガードが固着して _busy が永久 true になる = T06052 の再発を防ぐ安全弁)。 */
|
|
298
|
+
const STALE_IDLE_GUARD_MS = 5000
|
|
299
|
+
|
|
274
300
|
/**
|
|
275
301
|
* 1 stream に対応する Claude セッション (per-message query モデル)。
|
|
276
302
|
*/
|
|
@@ -404,18 +430,40 @@ class ClaudeStreamSession {
|
|
|
404
430
|
this._turnSettled = false
|
|
405
431
|
/** SDK-P0-3: 現ターンが 1 件でもストリームイベントを出したか。
|
|
406
432
|
* 「前ターンの遅延 idle が、drain で始まったばかりの次ターンを誤終了させる」レースの
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* =
|
|
433
|
+
* 一次ガード。generator のイベント順序保証から導けるのは **否定方向だけ** である点に注意:
|
|
434
|
+
* 「まだ 1 件も出ていない (=false) → この idle は前ターンのもの」は正しいが、逆の
|
|
435
|
+
* 「1 件出ている (=true) → この idle は現ターンのもの」は導けない (前ターンの遅延イベントでも
|
|
436
|
+
* true になるため)。その穴は SDK-P0-4 の _staleIdleExpected / _turnRunningSeen で塞ぐ。
|
|
437
|
+
* ターン開始時に false へ戻す。 */
|
|
410
438
|
this._turnProgressed = false
|
|
439
|
+
/** SDK-P0-4 (2026-07-13): ターン連番。ログと帰属デバッグ用 (_beginResidentTurn で ++)。 */
|
|
440
|
+
this._turnSeq = 0
|
|
441
|
+
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。result で settle したとき、
|
|
442
|
+
* および abort (interrupt) でターンを畳んだときに arm する (SDK は result → idle の順で
|
|
443
|
+
* 流し、interrupt 後にも中断ターンの idle を流すため)。arm 中に届いた idle は
|
|
444
|
+
* **前ターンのもの**として 1 件だけ握り潰し、現ターンを終端しない。
|
|
445
|
+
* 解除条件: (a) idle を 1 件消費した (b) SDK の running が届いた (= 前ターンの全イベントは
|
|
446
|
+
* 順序保証により既に通過済み) (c) 現ターンの result が届いた (d) STALE_IDLE_GUARD_MS 経過。 */
|
|
447
|
+
this._staleIdleExpected = false
|
|
448
|
+
/** SDK-P0-4: _staleIdleExpected の安全弁タイマー (固着による _busy 永久 true を防ぐ)。 */
|
|
449
|
+
this._staleIdleTimer = null
|
|
450
|
+
/** SDK-P0-4: 現ターンで SDK の session_state_changed:running を観測したか。
|
|
451
|
+
* 観測済みなら「以降に届く idle は確実に現ターンのもの」と断定できる。 */
|
|
452
|
+
this._turnRunningSeen = false
|
|
411
453
|
/** detach 済みフラグ (新規ターン受付停止) */
|
|
412
454
|
this._closed = false
|
|
413
455
|
/** 実行中ターンの完走を待ってからクローズする予約フラグ (graceful detach 用)。
|
|
414
456
|
* 端末スリープ / ネット断で browser が切れても、明示的な中断 (interrupt) が
|
|
415
457
|
* 無い限りターンを落とさず最後まで走らせるために使う。 */
|
|
416
458
|
this._reapAfterTurn = false
|
|
417
|
-
/** 現在ターンの AbortController (interrupt 用) */
|
|
459
|
+
/** 現在ターンの AbortController (per-message の interrupt 用) */
|
|
418
460
|
this._abortController = null
|
|
461
|
+
/** SDK-P0-4 (2026-07-13): 常駐 query の AbortController (最終手段の強制終了用)。
|
|
462
|
+
* 常駐は interrupt() で「現ターンだけ」を中断するのが正規経路だが、control request が
|
|
463
|
+
* 失敗/無応答のとき (CLI ハング・control channel 断) に generator を畳む手段が無いと
|
|
464
|
+
* close()/shutdown() で子プロセスが残留する。per-message と同じく AbortController を
|
|
465
|
+
* options へ渡し、interrupt が reject したときと close() のときに最終手段として abort する。 */
|
|
466
|
+
this._residentAbortController = null
|
|
419
467
|
|
|
420
468
|
/** jsonl ライブ追従 watcher (0.6.5)。外部 (tmux 等) の追記を拾う。 */
|
|
421
469
|
this._watcher = null
|
|
@@ -477,8 +525,20 @@ class ClaudeStreamSession {
|
|
|
477
525
|
)
|
|
478
526
|
}
|
|
479
527
|
|
|
480
|
-
/** 現在の sessionId の jsonl を watch する (既に同じものを watch 中なら何もしない)。
|
|
528
|
+
/** 現在の sessionId の jsonl を watch する (既に同じものを watch 中なら何もしない)。
|
|
529
|
+
*
|
|
530
|
+
* SDK-P0-4 (2026-07-13): **常駐 (resident) セッションでは watcher を張らない**。
|
|
531
|
+
* _runResidentQuery の docstring が「常駐では query stream が唯一のイベント源のため
|
|
532
|
+
* watcher は張らない」と宣言しているのに、コンストラクタの `if (this.sessionId) _ensureWatch()`
|
|
533
|
+
* が resume セッション (= sessionId 非 null) で無条件に watcher を張っており、
|
|
534
|
+
* HUB_AGENT_CHAT_RESIDENT_RESUME=1 のとき **SDK ストリームと jsonl watcher の二重ソース**に
|
|
535
|
+
* なっていた。per-message は finally で `_busy=true` のまま skipToEnd() を待ってから
|
|
536
|
+
* busy を落とすことで二重配信を防いでいるが (下の _runPerMessage 参照)、常駐の終端
|
|
537
|
+
* (_settleResidentTurn) は同期で _busy=false にするだけで skipToEnd を持たないため、
|
|
538
|
+
* ターン末尾の書き込みを watcher が読み直して二重配信しうる。宣言した不変条件をここで
|
|
539
|
+
* 機械的に強制する (将来 CHAT_RESIDENT_RESUME を既定 ON にする際のブロッカーだった)。 */
|
|
481
540
|
_ensureWatch() {
|
|
541
|
+
if (this._residentEligible) return
|
|
482
542
|
if (this._closed || !this.sessionId || !this.cwd) return
|
|
483
543
|
if (this._watchedSessionId === this.sessionId && this._watcher) return
|
|
484
544
|
// 旧 watcher を畳む
|
|
@@ -1203,6 +1263,34 @@ class ClaudeStreamSession {
|
|
|
1203
1263
|
this._busy = true
|
|
1204
1264
|
this._turnSettled = false
|
|
1205
1265
|
this._turnProgressed = false
|
|
1266
|
+
this._turnRunningSeen = false
|
|
1267
|
+
this._turnSeq += 1
|
|
1268
|
+
// ⚠️ _staleIdleExpected はここでリセットしない。ターン開始は「前ターンを settle した
|
|
1269
|
+
// 直後の同期 drain」から呼ばれるため、ここでクリアすると前ターンの遅延 idle を握り潰す
|
|
1270
|
+
// ガードが自ら消えてしまう (それが SDK-P0-3 の穴だった)。解除は _clearStaleIdleGuard()
|
|
1271
|
+
// (idle 消費 / running 到着 / 現ターン result / タイムアウト) でのみ行う。
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** SDK-P0-4: 「直前ターンの遅延 idle を 1 件だけ握り潰す」ガードを張る。
|
|
1275
|
+
* 安全弁として STALE_IDLE_GUARD_MS で自動解除する (ガード固着による _busy 永久 true を防ぐ)。 */
|
|
1276
|
+
_armStaleIdleGuard() {
|
|
1277
|
+
this._staleIdleExpected = true
|
|
1278
|
+
if (this._staleIdleTimer) clearTimeout(this._staleIdleTimer)
|
|
1279
|
+
this._staleIdleTimer = setTimeout(() => {
|
|
1280
|
+
this._staleIdleTimer = null
|
|
1281
|
+
this._staleIdleExpected = false
|
|
1282
|
+
}, STALE_IDLE_GUARD_MS)
|
|
1283
|
+
// Node の event loop を引き止めない (プロセス終了をブロックしない)。
|
|
1284
|
+
this._staleIdleTimer?.unref?.()
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** SDK-P0-4: 遅延 idle ガードを解除する。 */
|
|
1288
|
+
_clearStaleIdleGuard() {
|
|
1289
|
+
this._staleIdleExpected = false
|
|
1290
|
+
if (this._staleIdleTimer) {
|
|
1291
|
+
clearTimeout(this._staleIdleTimer)
|
|
1292
|
+
this._staleIdleTimer = null
|
|
1293
|
+
}
|
|
1206
1294
|
}
|
|
1207
1295
|
|
|
1208
1296
|
/** SDK-P0-3 (2026-07-10): resident の「ターン終端」を 1 箇所に集約する (result / idle 共通)。
|
|
@@ -1216,6 +1304,11 @@ class ClaudeStreamSession {
|
|
|
1216
1304
|
this._turnSettled = true
|
|
1217
1305
|
denyPending?.("turn ended")
|
|
1218
1306
|
this._busy = false
|
|
1307
|
+
// SDK-P0-4: result で畳んだターンは、この後に自分の idle を必ず 1 件流す
|
|
1308
|
+
// (sdk.d.ts: "'idle' fires after heldBackResult flushes … authoritative turn-over signal")。
|
|
1309
|
+
// drain が同期で次ターンを開始するため、その idle が新ターンに誤帰属しないようガードを張る。
|
|
1310
|
+
// idle 自身で畳んだ場合は既にその idle を消費済みなので張らない。
|
|
1311
|
+
if (reason === "result") this._armStaleIdleGuard()
|
|
1219
1312
|
// 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
|
|
1220
1313
|
// ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
|
|
1221
1314
|
// async。for await ループ内からは fire-and-forget で呼ぶ (最初の await より前に
|
|
@@ -1271,11 +1364,16 @@ class ClaudeStreamSession {
|
|
|
1271
1364
|
* 常駐では query stream が唯一のイベント源のため watcher は張らない (過去確定分は browser が
|
|
1272
1365
|
* history.request で hydrate 済み。resume で query stream が過去行を replay しない前提)。 */
|
|
1273
1366
|
async _runResidentQuery() {
|
|
1367
|
+
// SDK-P0-4 (2026-07-13): 常駐にも AbortController を渡す (per-message には元からある)。
|
|
1368
|
+
// 正規の中断は interrupt() だが、control request が失敗/無応答のときに generator を畳む
|
|
1369
|
+
// 最終手段が無いと、close()/shutdown() で CLI 子プロセスが残留する。
|
|
1370
|
+
this._residentAbortController = new AbortController()
|
|
1274
1371
|
const options = {
|
|
1275
1372
|
cwd: this.cwd,
|
|
1276
1373
|
env: siblingEnvFor(this.cwd),
|
|
1277
1374
|
canUseTool: (toolName, input) => this._canUseTool(toolName, input),
|
|
1278
1375
|
includePartialMessages: true,
|
|
1376
|
+
abortController: this._residentAbortController,
|
|
1279
1377
|
stderr: (data) => this._stderrHandler(data),
|
|
1280
1378
|
}
|
|
1281
1379
|
if (this.model) options.model = this.model
|
|
@@ -1307,6 +1405,10 @@ class ClaudeStreamSession {
|
|
|
1307
1405
|
}
|
|
1308
1406
|
if (msg?.type === "result") {
|
|
1309
1407
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1408
|
+
// SDK-P0-4: 現ターンの result が届いた = 前ターンの遅延 idle は順序保証により
|
|
1409
|
+
// 既に通過済み (届いたなら消費済み / 届かないなら二度と来ない)。ガードを解除してから
|
|
1410
|
+
// settle する (settle が result 用のガードを張り直す)。
|
|
1411
|
+
this._clearStaleIdleGuard()
|
|
1310
1412
|
// ターン完了: 未解決 permission を閉じ、次 input 受付へ (query は継続)。
|
|
1311
1413
|
this._settleResidentTurn("result", denyPending)
|
|
1312
1414
|
} else if (isSessionIdleEvent(msg)) {
|
|
@@ -1315,13 +1417,18 @@ class ClaudeStreamSession {
|
|
|
1315
1417
|
// _pendingMessages に退避されたまま (drain 契機の result が永久に来ないので)
|
|
1316
1418
|
// 永久滞留していた ("resident busy, message queued" が出続ける / リロードで消える)。
|
|
1317
1419
|
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
|
|
1324
|
-
|
|
1420
|
+
// SDK-P0-4 (2026-07-13) 帰属ガード: この idle は「現ターンのもの」か「前ターン
|
|
1421
|
+
// (result で畳んだ / interrupt で中断した) の遅延分」か。
|
|
1422
|
+
// 1) _staleIdleExpected=true → 前ターンの遅延 idle。1 件だけ握り潰して現ターンは
|
|
1423
|
+
// 継続させる (旧実装はここで新ターンを撃ち抜いていた)。
|
|
1424
|
+
// 2) それ以外で _busy かつ (running を観測済み or 何かイベントを出した) → 現ターンの終端。
|
|
1425
|
+
if (this._staleIdleExpected) {
|
|
1426
|
+
this._clearStaleIdleGuard()
|
|
1427
|
+
this.logger?.debug?.(
|
|
1428
|
+
{ stream_id: this.stream_id, turn: this._turnSeq },
|
|
1429
|
+
"stale idle from previous turn ignored",
|
|
1430
|
+
)
|
|
1431
|
+
} else if (this._busy && (this._turnRunningSeen || this._turnProgressed)) {
|
|
1325
1432
|
this._settleResidentTurn("idle", denyPending)
|
|
1326
1433
|
// per-message の RC-4 と対称に、チャット信号 (サイドバードット) を
|
|
1327
1434
|
// processing→waiting へ落とす。result 経由なら main.mjs が result を見て
|
|
@@ -1333,8 +1440,15 @@ class ClaudeStreamSession {
|
|
|
1333
1440
|
}
|
|
1334
1441
|
}
|
|
1335
1442
|
} else {
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1443
|
+
// SDK-P0-4: running は「SDK が新しい input の処理を実際に開始した」ことの権威的な
|
|
1444
|
+
// 証跡で、前ターンの遅延イベントでは絶対に立たない。これを観測した時点で、
|
|
1445
|
+
// 前ターン由来の遅延 idle はもう届かない (順序保証) のでガードを解除できる。
|
|
1446
|
+
if (isSessionRunningEvent(msg)) {
|
|
1447
|
+
this._turnRunningSeen = true
|
|
1448
|
+
this._clearStaleIdleGuard()
|
|
1449
|
+
}
|
|
1450
|
+
// 現ターンが「進行した」証跡 (一次ガード)。init / assistant / stream_event / user、
|
|
1451
|
+
// および session_state_changed の running / requires_action もここに含む。
|
|
1338
1452
|
this._turnProgressed = true
|
|
1339
1453
|
}
|
|
1340
1454
|
try {
|
|
@@ -1404,17 +1518,42 @@ class ClaudeStreamSession {
|
|
|
1404
1518
|
// 常駐 query は interrupt() で現ターンのみ中断する (query は継続、次 input 受付継続)。
|
|
1405
1519
|
if (this._residentQuery && typeof this._residentQuery.interrupt === "function") {
|
|
1406
1520
|
const wasBusy = this._busy
|
|
1521
|
+
// SDK-P0-4 (2026-07-13): Query.interrupt() は control request の往復を伴う **async
|
|
1522
|
+
// メソッド** (Promise を返す)。同期 try/catch では reject を捕まえられず unhandled
|
|
1523
|
+
// rejection になっていた (同ファイルの setModel / setPermissionMode / setMaxThinkingTokens は
|
|
1524
|
+
// すべて .catch() 済みで、interrupt だけが非対称だった)。
|
|
1525
|
+
// さらに重要なのは「interrupt が届いたか確認する前に _busy=false にして drain する」点で、
|
|
1526
|
+
// control request が失敗すると SDK 側のターンは走り続けているのにこちらは「終わった」と
|
|
1527
|
+
// 判断し、次の pending を同じ InputQueue へ push してしまう (実行中ターンへの割り込み)。
|
|
1528
|
+
// reject 時は AbortController で generator ごと畳む (finally が pending を resume 再起動で
|
|
1529
|
+
// 拾い直すため、取りこぼしにはならない) 最終手段へフォールバックする。
|
|
1407
1530
|
try {
|
|
1408
|
-
this._residentQuery.interrupt()
|
|
1409
|
-
|
|
1410
|
-
|
|
1531
|
+
const p = this._residentQuery.interrupt()
|
|
1532
|
+
if (p && typeof p.catch === "function") {
|
|
1533
|
+
p.catch((err) => {
|
|
1534
|
+
this.logger?.warn(
|
|
1535
|
+
{ stream_id: this.stream_id, err: err?.message },
|
|
1536
|
+
"resident interrupt() rejected; falling back to hard abort",
|
|
1537
|
+
)
|
|
1538
|
+
this._hardAbortResident()
|
|
1539
|
+
})
|
|
1540
|
+
}
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
this.logger?.warn(
|
|
1543
|
+
{ stream_id: this.stream_id, err: err?.message },
|
|
1544
|
+
"resident interrupt() threw; falling back to hard abort",
|
|
1545
|
+
)
|
|
1546
|
+
this._hardAbortResident()
|
|
1411
1547
|
}
|
|
1412
1548
|
// SDK-P0-3 (2026-07-10): 中断も「このターンの終端」なので settled を立てる。
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1415
|
-
// (
|
|
1549
|
+
// SDK-P0-4: 中断されたターンも session_state_changed:idle を流しうる。drain が同期で
|
|
1550
|
+
// 次ターンを開始するため、その遅延 idle が新ターンを撃ち抜かないようガードを張る
|
|
1551
|
+
// (旧コメントは「_turnSettled か _turnProgressed のどちらかで必ず無視される」と断言して
|
|
1552
|
+
// いたが、中断ターンの in-flight イベントが _turnProgressed を立ててしまうため成立して
|
|
1553
|
+
// いなかった)。
|
|
1416
1554
|
this._turnSettled = true
|
|
1417
1555
|
this._busy = false
|
|
1556
|
+
this._armStaleIdleGuard()
|
|
1418
1557
|
// 孤児カード防止 (2026-07-10 監査): 中断したターンに紐づく未応答 permission は
|
|
1419
1558
|
// もう誰も応答しない。deny で canUseTool の Promise を解放し、browser のカードも
|
|
1420
1559
|
// 撤去する。これが無いと (a) 画面に承認カードが残り続け (b) _permissionResolvers に
|
|
@@ -1483,6 +1622,19 @@ class ClaudeStreamSession {
|
|
|
1483
1622
|
}
|
|
1484
1623
|
}
|
|
1485
1624
|
|
|
1625
|
+
/** SDK-P0-4 (2026-07-13): 常駐 query の最終手段の強制終了。interrupt() (control request) が
|
|
1626
|
+
* 失敗・無応答のときと close() から呼ぶ。AbortController を落とすと generator が終了し、
|
|
1627
|
+
* _runResidentQuery の finally が pending を resume 付きで再起動して取りこぼしを防ぐ。 */
|
|
1628
|
+
_hardAbortResident() {
|
|
1629
|
+
const ac = this._residentAbortController
|
|
1630
|
+
if (!ac || ac.signal?.aborted) return
|
|
1631
|
+
try {
|
|
1632
|
+
ac.abort()
|
|
1633
|
+
} catch {
|
|
1634
|
+
/* ignore */
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1486
1638
|
/** このセッションの常駐 query 内で MCP サーバーを再接続する (動作中チャットへ波及)。
|
|
1487
1639
|
* 常駐 query が無い (per-message セッション) 場合は false。per-message は毎ターン
|
|
1488
1640
|
* 新プロセスで MCP を張り直すため、次メッセージで自動的に再接続される。
|
|
@@ -1516,6 +1668,15 @@ class ClaudeStreamSession {
|
|
|
1516
1668
|
// 常駐 query は input キューを閉じて generator を終わらせ query を完走させる。
|
|
1517
1669
|
if (this._inputQueue) this._inputQueue.close()
|
|
1518
1670
|
this.abortTurn()
|
|
1671
|
+
// SDK-P0-4 (2026-07-13): close は「もう使わない」宣言なので、常駐 query は最終手段で
|
|
1672
|
+
// 確実に畳む。inputQueue.close() + interrupt() だけだと、CLI が control request に
|
|
1673
|
+
// 応答しない/ハングしたときに generator が終わらず、SIGTERM 受信時 (shutdown) に子プロセスが
|
|
1674
|
+
// 残留する。_closed=true 済みなので finally の pending 再起動経路には入らない。
|
|
1675
|
+
this._hardAbortResident()
|
|
1676
|
+
if (this._staleIdleTimer) {
|
|
1677
|
+
clearTimeout(this._staleIdleTimer)
|
|
1678
|
+
this._staleIdleTimer = null
|
|
1679
|
+
}
|
|
1519
1680
|
if (this._idleTimer) {
|
|
1520
1681
|
clearTimeout(this._idleTimer)
|
|
1521
1682
|
this._idleTimer = null
|
|
@@ -1583,6 +1744,12 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
1583
1744
|
if (session.sessionId && this._liveBySession.get(session.sessionId) === session) {
|
|
1584
1745
|
this._liveBySession.delete(session.sessionId)
|
|
1585
1746
|
}
|
|
1747
|
+
// SDK-P0-4 (2026-07-13): session_id 回転前に登録した旧キーも外す (現 sessionId しか見ないと
|
|
1748
|
+
// 旧 id → 死んだ session のエントリが残り、attach が孤児を引き当てる)。
|
|
1749
|
+
const prevKey = session._liveKey
|
|
1750
|
+
if (prevKey && this._liveBySession.get(prevKey) === session) {
|
|
1751
|
+
this._liveBySession.delete(prevKey)
|
|
1752
|
+
}
|
|
1586
1753
|
}
|
|
1587
1754
|
|
|
1588
1755
|
/**
|
|
@@ -1722,7 +1889,20 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
1722
1889
|
event,
|
|
1723
1890
|
})
|
|
1724
1891
|
// session_id 確定後は索引に登録 (冪等)。再接続時の再アタッチに使う。
|
|
1725
|
-
|
|
1892
|
+
// SDK-P0-4 (2026-07-13): /clear や compaction で session_id が回転すると、旧 id →
|
|
1893
|
+
// 生存 session のエントリが Map に永久残留し、attach(resume_session_id=旧 id) が
|
|
1894
|
+
// 意図しない live session を引き当てる (+ Map リーク)。登録済みキーを覚えておき、
|
|
1895
|
+
// 回転を検知したら旧キーを外してから張り替える。
|
|
1896
|
+
if (session.sessionId) {
|
|
1897
|
+
const prevKey = session._liveKey
|
|
1898
|
+
if (prevKey && prevKey !== session.sessionId) {
|
|
1899
|
+
if (this._liveBySession.get(prevKey) === session) {
|
|
1900
|
+
this._liveBySession.delete(prevKey)
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
session._liveKey = session.sessionId
|
|
1904
|
+
this._liveBySession.set(session.sessionId, session)
|
|
1905
|
+
}
|
|
1726
1906
|
},
|
|
1727
1907
|
onPermission: ({ tool_name, input, request_id }) => {
|
|
1728
1908
|
// 多端末共有: session_id を載せて backend が session group へ broadcast できるように
|
package/src/hub-bundle.mjs
CHANGED
|
@@ -300,7 +300,7 @@ function runOnce(argv, { logger } = {}) {
|
|
|
300
300
|
* (Cockpit 用途では install.sh が事前に claude をインストールしている
|
|
301
301
|
* 想定だが、独自運用環境への配慮で fail-soft にする)
|
|
302
302
|
*/
|
|
303
|
-
export async function applyMcpServers(bundle, { logger } = {}) {
|
|
303
|
+
export async function applyMcpServers(bundle, { agentId, agentToken, logger } = {}) {
|
|
304
304
|
const servers = Array.isArray(bundle?.mcp_servers) ? bundle.mcp_servers : []
|
|
305
305
|
if (servers.length === 0) return { added: [], skipped: [] }
|
|
306
306
|
|
|
@@ -328,9 +328,16 @@ export async function applyMcpServers(bundle, { logger } = {}) {
|
|
|
328
328
|
transport,
|
|
329
329
|
"--scope",
|
|
330
330
|
scope,
|
|
331
|
-
s.name,
|
|
332
|
-
s.url,
|
|
333
331
|
]
|
|
332
|
+
// 越境制御 (2026-07): 一般向け `hub` MCP には、このホストの資格
|
|
333
|
+
// `X-Cockpit-Agent: <agentId>:<agentToken>` をヘッダで付与する。Hub backend は
|
|
334
|
+
// これを検証してホストのテナントを解決し、`hub` の acting tenant をそのテナントに
|
|
335
|
+
// **固定**する(クライアントホストは自テナントの外を触れない)。`hub_staff`(社内
|
|
336
|
+
// 横断)や外部 MCP には付けない。
|
|
337
|
+
if (s.name === "hub" && agentId && agentToken) {
|
|
338
|
+
argv.push("--header", `X-Cockpit-Agent: ${agentId}:${agentToken}`)
|
|
339
|
+
}
|
|
340
|
+
argv.push(s.name, s.url)
|
|
334
341
|
try {
|
|
335
342
|
await runOnce(argv, { logger })
|
|
336
343
|
added.push(s.name)
|
|
@@ -428,7 +435,7 @@ export async function syncBundle({ hubUrl, agentId, agentToken, logger, fetchImp
|
|
|
428
435
|
try {
|
|
429
436
|
mcp = cliKind === "codex"
|
|
430
437
|
? await applyCodexMcpServers(bundle, { logger })
|
|
431
|
-
: await applyMcpServers(bundle, { logger })
|
|
438
|
+
: await applyMcpServers(bundle, { agentId, agentToken, logger })
|
|
432
439
|
} catch (err) {
|
|
433
440
|
logger?.warn({ err: err.message }, "applyMcpServers failed (continuing)")
|
|
434
441
|
}
|
package/src/main.mjs
CHANGED
|
@@ -4076,6 +4076,12 @@ async function dispatch(msg, ctx) {
|
|
|
4076
4076
|
type: "claude.history.response",
|
|
4077
4077
|
stream_id,
|
|
4078
4078
|
session_id,
|
|
4079
|
+
// SDK-P0-4 (2026-07-13): 要求時の cwd をエコーバックする。claude.sessions.response は
|
|
4080
|
+
// 元から cwd を載せており frontend が cwd 照合で「別ワークスペース宛の遅延応答」を
|
|
4081
|
+
// 捨てられるのに、history.response だけ cwd が無く照合材料が存在しなかった。
|
|
4082
|
+
// その結果、無条件 hydrate → session_id 上書き → 購読スワップ → localStorage 汚染の
|
|
4083
|
+
// 経路が残っていた (sessionScope.ts が stream_id 主軸で守るのは relay イベントのみ)。
|
|
4084
|
+
cwd,
|
|
4079
4085
|
events: result.events,
|
|
4080
4086
|
total_lines: result.total_lines,
|
|
4081
4087
|
truncated: result.truncated,
|