@cocorograph/hub-agent 0.7.86 → 0.7.87

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.7.86",
3
+ "version": "0.7.87",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -2401,31 +2401,76 @@ export class ClaudeStreamBridge extends EventEmitter {
2401
2401
  * @param {string} cwd
2402
2402
  * @returns {string|null}
2403
2403
  */
2404
- liveSessionIdForCwd(cwd) {
2404
+ /**
2405
+ * cwd 内で「最終アクティビティが最も新しいライブセッション」を返す共通スキャン。
2406
+ *
2407
+ * ⚠️ this.sessions **と** _liveBySession の union を走査する (P5 / T06570 症状3): detach で
2408
+ * セッションは this.sessions から外れるが _liveBySession に warm 保持される (_softDetachSession)。
2409
+ * this.sessions だけを見ると「browser を離れた稼働中セッション」を取りこぼし、bound id /
2410
+ * turn_active が落ちて state loop が jsonl を mtime 最新で誤バインドし、非選択行のドットが
2411
+ * 稼働中でも赤へ誤落ちする。reconcileWarmBounds / shutdown と同じ union 走査に揃える。
2412
+ *
2413
+ * @param {string} cwd
2414
+ * @param {boolean} [requireSessionId=true] sessionId 未確定の fresh を除外するか (jsonl バインド用途は true)。
2415
+ * @returns {ClaudeStreamSession|null}
2416
+ */
2417
+ _liveSessionForCwd(cwd, requireSessionId = true) {
2405
2418
  if (!cwd) return null
2406
2419
  let best = null
2407
2420
  let bestAt = -1
2408
2421
  const seen = new Set()
2409
- for (const session of this.sessions.values()) {
2410
- if (seen.has(session) || session.cwd !== cwd || !session.sessionId) continue
2411
- seen.add(session)
2412
- let at = 0
2413
- for (const t of session.lastActivityByStream.values()) {
2414
- if (t > at) at = t
2415
- }
2416
- // `>=` (2026-07-03): Date.now() はミリ秒精度のため、短時間に複数セッションへ
2417
- // attach+input すると同一ミリ秒でタイが起き得る (テストや高速な連続操作で実測)。
2418
- // `this.sessions` は Map で挿入順を保持するため、タイ時は後から attach された
2419
- // (= イテレーション順で後に来る) セッションを採用するのが「最終アクティビティが
2420
- // 最も新しい」の意図に合う。厳密な `>` だと先勝ちになり意図と逆転する。
2421
- if (at >= bestAt) {
2422
- bestAt = at
2423
- best = session.sessionId
2422
+ // this.sessions (attached) を先に走査してから _liveBySession (soft-detached の warm) を走査し
2423
+ // union にする。同一セッションは両 Map に載り得るため seen で識別重複を排除する。
2424
+ const scan = (iter) => {
2425
+ for (const session of iter) {
2426
+ if (seen.has(session) || session.cwd !== cwd) continue
2427
+ if (requireSessionId && !session.sessionId) continue
2428
+ seen.add(session)
2429
+ let at = 0
2430
+ for (const t of session.lastActivityByStream.values()) {
2431
+ if (t > at) at = t
2432
+ }
2433
+ // `>=` (2026-07-03): Date.now() はミリ秒精度のため、短時間に複数セッションへ
2434
+ // attach+input すると同一ミリ秒でタイが起き得る。挿入順/走査順で後に来るものを採用する
2435
+ // のが「最終アクティビティが最も新しい」の意図に合う (attached を先に走査するため、
2436
+ // attached と warm がタイなら warm を採る=どちらも同一 cwd の生存 session で無害)。
2437
+ if (at >= bestAt) {
2438
+ bestAt = at
2439
+ best = session
2440
+ }
2424
2441
  }
2425
2442
  }
2443
+ scan(this.sessions.values())
2444
+ scan(this._liveBySession.values())
2426
2445
  return best
2427
2446
  }
2428
2447
 
2448
+ /**
2449
+ * main.mjs (startStateLoop / tmux.list_sessions ハンドラ) が使うチャット版 bound session 解決。
2450
+ * cwd 内で最終活性が最も新しいライブセッションの sessionId を返す。背景は _liveSessionForCwd 参照。
2451
+ * @param {string} cwd
2452
+ * @returns {string|null}
2453
+ */
2454
+ liveSessionIdForCwd(cwd) {
2455
+ return this._liveSessionForCwd(cwd, true)?.sessionId ?? null
2456
+ }
2457
+
2458
+ /**
2459
+ * P5 (T06570 症状3): cwd のライブチャットセッションの **agent 側ターン進行フラグ** (_busy) を返す。
2460
+ * _busy は SDK query ライフサイクル (ターン開始で true / result・idle・exit で同期 false) 由来で、
2461
+ * browser の attach/detach やストリーム tick に依存しない権威信号。state loop の turn_active に
2462
+ * これを採ることで、browser を離れた稼働中セッションでもドットが青のまま追従し、ターン終了時は
2463
+ * 次 tick で即座に赤へ落ちる (誤青を生まない)。Codex の codexTurnActiveForThread と対称。
2464
+ * fresh (sessionId 未確定) の初回ターンも稼働中として拾うため requireSessionId=false。
2465
+ * @param {string} cwd
2466
+ * @returns {boolean|null} true=ターン進行中 / false=idle / null=このcwdにライブチャットセッション無し (純TUI等)
2467
+ */
2468
+ chatTurnActiveForCwd(cwd) {
2469
+ const s = this._liveSessionForCwd(cwd, false)
2470
+ if (!s) return null
2471
+ return s._busy === true
2472
+ }
2473
+
2429
2474
  /**
2430
2475
  * 指定 cwd でライブなセッションへメッセージを直接注入する (sibling-inject 用、
2431
2476
  * 2026-07-04)。同一マシン内の別プロセス (ask-sibling スキル経由の cockpit_orch.py)
@@ -2445,21 +2490,10 @@ export class ClaudeStreamBridge extends EventEmitter {
2445
2490
  */
2446
2491
  sendToCwd(cwd, text) {
2447
2492
  if (!cwd || !text) return false
2448
- let best = null
2449
- let bestAt = -1
2450
- const seen = new Set()
2451
- for (const session of this.sessions.values()) {
2452
- if (seen.has(session) || session.cwd !== cwd) continue
2453
- seen.add(session)
2454
- let at = 0
2455
- for (const t of session.lastActivityByStream.values()) {
2456
- if (t > at) at = t
2457
- }
2458
- if (at >= bestAt) {
2459
- bestAt = at
2460
- best = session
2461
- }
2462
- }
2493
+ // P5 (T06570): warm (soft-detached) セッションも対象にするため union 走査へ委譲する
2494
+ // (従来は this.sessions のみで、detach 済みだが生きているセッションへ sibling-inject できず
2495
+ // no_live_session_for_cwd になっていた)。sessionId 未確定の fresh も対象にするため false。
2496
+ const best = this._liveSessionForCwd(cwd, false)
2463
2497
  if (!best) return false
2464
2498
  best.sendMessage({ role: "user", content: text }).catch((err) => {
2465
2499
  this.logger?.error(
package/src/main.mjs CHANGED
@@ -2579,14 +2579,27 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2579
2579
  case "codex":
2580
2580
  turnActive = await codexTurnActiveForThread(codexThreadId)
2581
2581
  break
2582
- case "claude":
2583
- turnActive = await turnActiveForCwd(
2584
- s.cwd,
2585
- resolveStateLoopBoundSessionId(boundSessions, s.session_name, s.cwd, claudeBridge),
2586
- s.session_name,
2587
- { paneAliveProbe: paneClaudeAliveOrUnknown },
2588
- )
2582
+ case "claude": {
2583
+ // P5 (T06570 症状3): チャットセッションは、そのセッションの SDK query ライフサイクル由来の
2584
+ // 権威フラグ _busy を最優先する (Codex の rollout flag 優先と対称)。これにより browser を
2585
+ // 離れて detach され this.sessions から外れた warm セッションでも、稼働中ターンの turn_active
2586
+ // が維持され、非選択行のドットが赤へ誤落ちしない。加えて bare-shell pane を dead 判定して
2587
+ // 赤へ倒す paneAliveProbe 経路 (チャットは実体が SDK 子プロセスで pane は素のシェル) も回避する。
2588
+ // chatTurnActiveForCwd が null (= この cwd にライブチャットセッション無し = 純 TUI / bare pane)
2589
+ // のときだけ、従来の jsonl 末尾 + pane probe 経路にフォールバックする (TUI 挙動は不変)。
2590
+ const chatActive = claudeBridge?.chatTurnActiveForCwd?.(s.cwd)
2591
+ if (chatActive === true || chatActive === false) {
2592
+ turnActive = chatActive
2593
+ } else {
2594
+ turnActive = await turnActiveForCwd(
2595
+ s.cwd,
2596
+ resolveStateLoopBoundSessionId(boundSessions, s.session_name, s.cwd, claudeBridge),
2597
+ s.session_name,
2598
+ { paneAliveProbe: paneClaudeAliveOrUnknown },
2599
+ )
2600
+ }
2589
2601
  break
2602
+ }
2590
2603
  default:
2591
2604
  turnActive = null
2592
2605
  }