@cocorograph/hub-agent 0.7.90 → 0.7.91

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/main.mjs +43 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.7.90",
3
+ "version": "0.7.91",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -1146,6 +1146,11 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
1146
1146
  // ready-for-input 判定: claude プロセスの子プロセスを内省し「Stop フック/ツール実行が
1147
1147
  // 終わって真に入力可能になった」エッジを検出する (proc-introspect.mjs)。capture-pane は
1148
1148
  // Stop フック実行中を観測できないため、これが「stop の早期消灯/誤フラッシュ」の根治。
1149
+ // TUI の tmux session_name → Claude session_id。state loop 起動より**前**に作る。
1150
+ // 後で作ると state loop には undefined が渡り、バックグラウンド時だけ cwd 最新 JSONL
1151
+ // へフォールバックして別会話の context% を表示する。
1152
+ ctx.tuiReboundSessions = new Map()
1153
+
1149
1154
  const readinessTracker = new ReadinessTracker()
1150
1155
  // claude.tui.bind ハンドラが「生成中か (isArmed)」を参照して、生成中 claude を rebind の
1151
1156
  // respawn-pane -k で kill する「謎停止」を防ぐ (生成中ガード)。
@@ -1360,11 +1365,6 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
1360
1365
  )
1361
1366
  }
1362
1367
 
1363
- // T04784 会話継続バインディングの冪等性ガード: tmux セッション名 → 最後に --resume で
1364
- // 載せ替えた Claude session_id。TUI ビューが remount のたびに claude.tui.bind を送っても、
1365
- // 同じ id への再 respawn (= claude 再起動) を抑止する。
1366
- ctx.tuiReboundSessions = new Map()
1367
-
1368
1368
  // session 回転通知 (T04786 後続) の多重送信ガード: 回転キー (tmux 名 or cwd) →
1369
1369
  // 最後に push した new_session_id。閲覧ハートビートは 5s 間隔で来るため、フロントが
1370
1370
  // 切替を完了し new id でハートビートを送り直すまでの間に来る余分なハートビートで
@@ -2295,6 +2295,9 @@ export async function pushImmediateChatState({
2295
2295
  stalled: false,
2296
2296
  turn_active: false,
2297
2297
  cli_kind: cliKind,
2298
+ // context_pct がどの会話の値かを明示する。frontend は ID が変わったときだけ
2299
+ // 0% を採用し、同一会話の一時的な 0/null は直近の確定値を保持する。
2300
+ context_session_id: sessionId ?? null,
2298
2301
  chat_session_id: sessionId ?? null,
2299
2302
  })
2300
2303
  }
@@ -2610,10 +2613,13 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2610
2613
  // (cwd, cliKind) キー化により「チャット信号の cliKind == 行の cliKind」が恒真に
2611
2614
  // なったので、常に非 null の構造値を送る方が真に厳しい (= 情報量が多い) 上位互換。
2612
2615
  //
2613
- // chat_session_id は「その行のチャットが今どのセッション/スレッドか」。チャット信号が
2614
- // 無ければ null (TUI 専用行)。
2616
+ // context_session_id は「この context_pct がどの会話の値か」。チャットの session/thread を
2617
+ // 優先し、Claude TUI 専用行では bind 済みの session_id(fresh は fresh:<request_id>)を使う。
2618
+ // frontend はこれを境界として、新規会話の 0% と同一会話の一時的な 0/null を区別する。
2615
2619
  const chatCliKind = sessionCliKind
2616
- const chatSessionId = chat?.sessionId ?? null
2620
+ const contextSessionId =
2621
+ chat?.sessionId ??
2622
+ (sessionCliKind === "claude" ? boundSessions?.get(s.session_name) ?? null : codexThreadId)
2617
2623
 
2618
2624
  const prev = lastByName.get(s.session_name)
2619
2625
  if (
@@ -2627,7 +2633,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2627
2633
  prev.stalled !== stalled ||
2628
2634
  prev.turn_active !== turnActive ||
2629
2635
  prev.cli_kind !== chatCliKind ||
2630
- prev.chat_session_id !== chatSessionId
2636
+ prev.context_session_id !== contextSessionId
2631
2637
  ) {
2632
2638
  lastByName.set(s.session_name, {
2633
2639
  status,
@@ -2639,7 +2645,9 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2639
2645
  stalled,
2640
2646
  turn_active: turnActive,
2641
2647
  cli_kind: chatCliKind,
2642
- chat_session_id: chatSessionId,
2648
+ context_session_id: contextSessionId,
2649
+ // 旧クライアント互換。新規 consumer は context_session_id を使う。
2650
+ chat_session_id: contextSessionId,
2643
2651
  })
2644
2652
  // 計装 (2026-06-19, 既定 OFF=2026-06-20): 状態変化時に値を記録する。常時 info はログ肥大の
2645
2653
  // 主因のため HUB_AGENT_STATE_TRACE=1 のときだけ出す。childBusy/outputActive を分けて出し、
@@ -2658,7 +2666,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2658
2666
  context_pct: contextPct,
2659
2667
  turn_active: turnActive,
2660
2668
  cli_kind: chatCliKind,
2661
- chat_session_id: chatSessionId,
2669
+ context_session_id: contextSessionId,
2662
2670
  },
2663
2671
  "session.state push",
2664
2672
  )
@@ -2680,9 +2688,11 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2680
2688
  turn_active: turnActive,
2681
2689
  // cli_kind (T05945): セッション名由来の構造値 ('claude' | 'codex')。frontend の
2682
2690
  // 単一セレクタ (useCockpitSessionDisplay) が「この push はどの CLI 行のものか」を
2683
- // 判定する唯一の軸。chat_session_id はチャット信号が無ければ null。
2691
+ // 判定する唯一の軸。context_session_id は context% の会話 ID。
2684
2692
  cli_kind: chatCliKind,
2685
- chat_session_id: chatSessionId,
2693
+ context_session_id: contextSessionId,
2694
+ // 旧クライアント互換。新規 consumer は context_session_id を使う。
2695
+ chat_session_id: contextSessionId,
2686
2696
  })
2687
2697
  }
2688
2698
  // 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
@@ -4172,7 +4182,11 @@ async function dispatch(msg, ctx) {
4172
4182
  // 非 fresh (resume/auto) の bind = ユーザーが既存会話を選んだ → 保留中の +新規 defer は破棄。
4173
4183
  if (!fresh) ctx.pendingFreshRespawn.delete(sessionName)
4174
4184
  let rebind = { ok: false, skipped: false }
4175
- const bindKey = fresh ? `fresh:${request_id || ""}` : targetId
4185
+ // 新規会話の 0% は会話切替として扱うため、request_id を送れない旧 client
4186
+ // 経由でも bind ごとに固有IDを作る。空の `fresh:` を再利用すると、2回目以降の
4187
+ // 新規会話が同一会話扱いになり、直前値が残る。
4188
+ const freshBindId = request_id || randomUUID()
4189
+ const bindKey = fresh ? `fresh:${freshBindId}` : targetId
4176
4190
  const recordDeferredFresh = () =>
4177
4191
  ctx.pendingFreshRespawn.set(sessionName, {
4178
4192
  cwd,
@@ -4281,6 +4295,19 @@ async function dispatch(msg, ctx) {
4281
4295
  noCache: true,
4282
4296
  logger,
4283
4297
  })
4298
+ // bind が確定している復帰は、capture-pane の見た目や cwd 最新ファイルでなく
4299
+ // **指定 session_id の JSONL** から context% を即時解決する。fresh はまだ実 ID
4300
+ // が無いため 0% と fresh:<request_id> を対にして送る。
4301
+ const contextSessionId = fresh ? bindKey : targetId
4302
+ const boundContext =
4303
+ targetId && cwd
4304
+ ? await jsonlContextForCwd(cwd, targetId).catch(() => null)
4305
+ : null
4306
+ const contextPct = fresh
4307
+ ? 0
4308
+ : typeof boundContext?.percent === "number"
4309
+ ? boundContext.percent
4310
+ : snap.context_pct
4284
4311
  // 【問題1 根治 2026-06-22】配る status はプロセス実体の busy を権威に解決する
4285
4312
  // (resolveBindSnapshotStatus)。capture(SPINNER_LINE_RE)の偽陽性に勝たせる。
4286
4313
  // - busy=true (ターン進行中): capture gap で snap.status が waiting に化けても
@@ -4296,7 +4323,8 @@ async function dispatch(msg, ctx) {
4296
4323
  type: "session.state",
4297
4324
  session_name: sessionName,
4298
4325
  status: snapStatus,
4299
- context_pct: snap.context_pct,
4326
+ context_pct: contextPct,
4327
+ context_session_id: contextSessionId ?? null,
4300
4328
  permission_mode: snap.permission_mode,
4301
4329
  stable:
4302
4330
  !busy && snapStatus !== "processing" && snap.stable === true,