@cocorograph/hub-agent 0.7.3 → 0.7.4

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.3",
3
+ "version": "0.7.4",
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
  listSessions as listTmuxSessions,
69
69
  listWorktreeNameHistory,
70
70
  listWorktreeStubs,
71
+ isPaneRunningClaude,
71
72
  rebindClaudeSession,
72
73
  shouldSkipRebindRespawn,
73
74
  recoverTuiInput,
@@ -2080,13 +2081,23 @@ async function dispatch(msg, ctx) {
2080
2081
  // resume は targetId をキー、fresh は session_id 不在のため bind の request_id を
2081
2082
  // キーにする (同一マウントの再送は同 request_id で skip、新規要求は再マウントで
2082
2083
  // 新 request_id になり 1 回だけ起動する)。
2084
+ // 実プロセス由来の「claude 実行中か」。respawn-pane -k による生成中 claude の誤 kill
2085
+ // (症状D 再発 D2: armed 140s cap / capture gap で generating=false に倒れ、--resume bind
2086
+ // でも実行中 claude を kill していた)を、capture/armed に依存しない pane_current_command で防ぐ。
2087
+ const paneRunningClaude = await isPaneRunningClaude(sessionName, { logger })
2083
2088
  let rebind = { ok: false, skipped: false }
2084
2089
  const bindKey = fresh ? `fresh:${request_id || ""}` : targetId
2085
2090
  if (bindKey && sessionName && (fresh || targetId)) {
2086
2091
  if (ctx.tuiReboundSessions.get(sessionName) === bindKey) {
2087
2092
  rebind = { ok: true, skipped: true }
2088
2093
  } else if (
2089
- shouldSkipRebindRespawn({ generating, fresh, targetId, newestId })
2094
+ shouldSkipRebindRespawn({
2095
+ generating,
2096
+ fresh,
2097
+ targetId,
2098
+ newestId,
2099
+ paneRunningClaude,
2100
+ })
2090
2101
  ) {
2091
2102
  // 生成中 claude を再接続/remount の bind で kill しない (謎停止対策)。respawn を抑止して
2092
2103
  // 既存の生成中 claude を温存する。bindKey は記録して以降の remount を冪等化する
package/src/tmux.mjs CHANGED
@@ -1237,8 +1237,49 @@ export async function recoverTuiInput(name, expectText, opts = {}) {
1237
1237
  * @param {{generating?: boolean, fresh?: boolean, targetId?: string|null, newestId?: string|null}} a
1238
1238
  * @returns {boolean}
1239
1239
  */
1240
- export function shouldSkipRebindRespawn({ generating, fresh, targetId, newestId } = {}) {
1240
+ /**
1241
+ * ペインの前景プロセスが claude を実行中か(= respawn-pane -k で kill すると in-flight 応答を失う)
1242
+ * を pane_current_command から判定する。capture-pane スクレイプや armed(140s cap) の脆い推論に
1243
+ * 依存しない実プロセス由来の判定で、hub-agent 再起動・armed cap・capture gap を跨いで有効。
1244
+ * シェル(fish/bash/zsh/sh/dash/tmux/login)なら false。判定不能時は安全側 false(= respawn 許可)。
1245
+ */
1246
+ export async function isPaneRunningClaude(name, opts = {}) {
1247
+ if (!name) return false
1248
+ try {
1249
+ const { stdout } = await execFileP(tmuxBin(opts), [
1250
+ "display-message",
1251
+ "-p",
1252
+ "-t",
1253
+ `${name}:`,
1254
+ "-F",
1255
+ "#{pane_current_command}",
1256
+ ])
1257
+ const cmd = (stdout || "").trim().toLowerCase()
1258
+ if (!cmd) return false
1259
+ const SHELLS = new Set(["fish", "bash", "zsh", "sh", "dash", "tmux", "login"])
1260
+ // TUI チャットのペインで走るのは claude(claude / claude.exe / node 等)かシェルのいずれか。
1261
+ // シェルでない前景プロセス = claude 実行中とみなす(命名差 claude.exe/node に強い)。
1262
+ return !SHELLS.has(cmd)
1263
+ } catch {
1264
+ return false
1265
+ }
1266
+ }
1267
+
1268
+ export function shouldSkipRebindRespawn({
1269
+ generating,
1270
+ fresh,
1271
+ targetId,
1272
+ newestId,
1273
+ paneRunningClaude,
1274
+ } = {}) {
1241
1275
  if (fresh) return false
1276
+ // 実行中 claude を kill しない最優先ガード(症状D / 再発 D2 の根治): ペインが現に claude を
1277
+ // 実行中で、最新セッション(= 今動いている会話)への再 bind(remount/reconnect)なら、respawn
1278
+ // (respawn-pane -k) は破壊的なだけで不要。generating の脆い検出(isArmed 140s cap / capture gap)
1279
+ // で false に倒れても、実プロセス由来の paneRunningClaude が守る。targetId 未指定(= newest 解決前)
1280
+ // も「最新への再 bind」とみなす。明示的な別会話への切替(targetId !== newestId)と +新規(fresh)は
1281
+ // 従来どおり respawn する。
1282
+ if (paneRunningClaude && (!targetId || targetId === newestId)) return true
1242
1283
  if (!generating) return false
1243
1284
  return !!targetId && targetId === newestId
1244
1285
  }