@cocorograph/hub-agent 0.6.99 → 0.7.0

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.6.99",
3
+ "version": "0.7.0",
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
@@ -68,6 +68,7 @@ import {
68
68
  listWorktreeNameHistory,
69
69
  listWorktreeStubs,
70
70
  rebindClaudeSession,
71
+ shouldSkipRebindRespawn,
71
72
  recoverTuiInput,
72
73
  removeWorktree as removeWorktreeDir,
73
74
  resumeWithMessage,
@@ -593,6 +594,9 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
593
594
  // 終わって真に入力可能になった」エッジを検出する (proc-introspect.mjs)。capture-pane は
594
595
  // Stop フック実行中を観測できないため、これが「stop の早期消灯/誤フラッシュ」の根治。
595
596
  const readinessTracker = new ReadinessTracker()
597
+ // claude.tui.bind ハンドラが「生成中か (isArmed)」を参照して、生成中 claude を rebind の
598
+ // respawn-pane -k で kill する「謎停止」を防ぐ (生成中ガード)。
599
+ ctx.readinessTracker = readinessTracker
596
600
  const stateLoop = startStateLoop({
597
601
  client,
598
602
  plugins,
@@ -1962,12 +1966,16 @@ async function dispatch(msg, ctx) {
1962
1966
  // resume せず resume 無しの claude を起動する。session_id は起動後の初回送信で
1963
1967
  // 生成され、frontend は回転検知 / sessions 応答で拾う。
1964
1968
  const fresh = msg.fresh === true
1965
- // 1) 載せ替え先 session_id を確定 (fresh は解決しない = 新規起動)
1969
+ // 1) 載せ替え先 session_id を確定 (fresh は解決しない = 新規起動)。あわせて
1970
+ // 現在動いているセッション (= cwd の最新 jsonl = newestId) も解決する。生成中ガードで
1971
+ // 「再接続/remount による同一セッションへの再 bind」か「別 session への明示 switch」かを
1972
+ // 区別するのに使う (前者は respawn を抑止して生成中 claude を温存する)。
1966
1973
  let targetId =
1967
1974
  typeof msg.session_id === "string" && msg.session_id
1968
1975
  ? msg.session_id
1969
1976
  : null
1970
- if (!fresh && !targetId && cwd) {
1977
+ let newestId = null
1978
+ if (!fresh && cwd) {
1971
1979
  const projectsRoot = await getActiveProjectsRoot()
1972
1980
  const { sessions } = await listSessions({
1973
1981
  cwd,
@@ -1975,7 +1983,20 @@ async function dispatch(msg, ctx) {
1975
1983
  limit: 1,
1976
1984
  logger,
1977
1985
  })
1978
- targetId = sessions?.[0]?.session_id || null
1986
+ newestId = sessions?.[0]?.session_id || null
1987
+ if (!targetId) targetId = newestId
1988
+ }
1989
+ // 生成中ガード (謎停止対策): respawn-pane -k は生成中 claude を強制 kill し in-flight 応答を失う。
1990
+ // hub-agent 再起動直後は tuiReboundSessions が空で冪等ガードが効かず、ブラウザ再接続の bind が
1991
+ // 生成中 claude を kill する。armed (ターン進行中) か pane=processing なら生成中とみなす。
1992
+ let generating = ctx.readinessTracker?.isArmed?.(sessionName) === true
1993
+ if (!generating && sessionName) {
1994
+ try {
1995
+ const snap = await detectSessionState(sessionName, {})
1996
+ if (snap?.status === "processing") generating = true
1997
+ } catch {
1998
+ // capture 失敗時は据え置き (生成中とみなさない = 従来挙動)
1999
+ }
1979
2000
  }
1980
2001
  // 2) SDK を停止 (cwd 全体 + 対象 id)。
1981
2002
  let stoppedSdk = 0
@@ -2002,6 +2023,18 @@ async function dispatch(msg, ctx) {
2002
2023
  if (bindKey && sessionName && (fresh || targetId)) {
2003
2024
  if (ctx.tuiReboundSessions.get(sessionName) === bindKey) {
2004
2025
  rebind = { ok: true, skipped: true }
2026
+ } else if (
2027
+ shouldSkipRebindRespawn({ generating, fresh, targetId, newestId })
2028
+ ) {
2029
+ // 生成中 claude を再接続/remount の bind で kill しない (謎停止対策)。respawn を抑止して
2030
+ // 既存の生成中 claude を温存する。bindKey は記録して以降の remount を冪等化する
2031
+ // (targetId === newestId = 動いているセッション本人なので記録は正しい)。
2032
+ rebind = { ok: true, skipped: true }
2033
+ ctx.tuiReboundSessions.set(sessionName, bindKey)
2034
+ logger?.info(
2035
+ { session: sessionName, session_id: targetId },
2036
+ "tui rebind: skipped respawn (turn in progress on the running session)",
2037
+ )
2005
2038
  } else {
2006
2039
  rebind = await rebindClaudeSession(sessionName, targetId, {
2007
2040
  cwd,
package/src/tmux.mjs CHANGED
@@ -1220,6 +1220,29 @@ export async function recoverTuiInput(name, expectText, opts = {}) {
1220
1220
  * @param {{cwd?:string,model?:string,permissionMode?:string,fresh?:boolean,logger?:object,tmuxBin?:string}} [opts]
1221
1221
  * @returns {Promise<{ok:boolean, cmd?:string, error?:string}>}
1222
1222
  */
1223
+ /**
1224
+ * rebind (respawn-pane -k + claude --resume) を抑止すべきか判定する純ロジック。
1225
+ *
1226
+ * 背景 (謎停止対策): claude.tui.bind は TUI ビューの mount / 再接続のたびに来る。respawn-pane -k は
1227
+ * 現 claude を強制 kill するため、生成中に走ると in-flight の応答が失われる (jsonl 未確定)。冪等ガード
1228
+ * (tuiReboundSessions) は hub-agent 再起動直後は空で効かず、ブラウザ再接続の bind が生成中 claude を
1229
+ * kill する事故 (謎停止) を起こす。
1230
+ *
1231
+ * 抑止条件: 生成中 (generating) かつ「今動いているセッション (= 最新 jsonl = newestId) への再 bind」。
1232
+ * これは再接続/remount であり respawn は純粋に破壊的。逆に:
1233
+ * - fresh (+新規セッション要求) は明示操作なので respawn する。
1234
+ * - targetId !== newestId (別 session への明示 switch) は respawn する (生成中セッションは別ペイン文脈で温存)。
1235
+ * - 非生成中は従来どおり respawn する (idle claude の --resume 載せ替えは非破壊)。
1236
+ *
1237
+ * @param {{generating?: boolean, fresh?: boolean, targetId?: string|null, newestId?: string|null}} a
1238
+ * @returns {boolean}
1239
+ */
1240
+ export function shouldSkipRebindRespawn({ generating, fresh, targetId, newestId } = {}) {
1241
+ if (fresh) return false
1242
+ if (!generating) return false
1243
+ return !!targetId && targetId === newestId
1244
+ }
1245
+
1223
1246
  export async function rebindClaudeSession(name, sessionId, opts = {}) {
1224
1247
  let cmd
1225
1248
  try {