@cocorograph/hub-agent 0.7.69 → 0.7.70

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.69",
3
+ "version": "0.7.70",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -11,6 +11,7 @@
11
11
  * claude.input → input() (turn/start、ターン中は turn/steer)
12
12
  * claude.interrupt → interrupt() (turn/interrupt)
13
13
  * claude.detach → detach() (TTL 付き graceful close)
14
+ * (codex 固有) → keepalive() (閲覧中の生存シグナル。pty.resize 相当)
14
15
  * claude.permission.reply → permissionReply() (client.respond/respondError)
15
16
  * claude.event (生 SDK イベント素通し) → 'event' (生 notification 素通し、同じ思想)
16
17
  * claude.permission.request → 'permission' (承認/入力要求の生 method+params 素通し)
@@ -509,6 +510,26 @@ export class CodexStreamBridge extends EventEmitter {
509
510
  session.client.respond(request_id, result)
510
511
  }
511
512
 
513
+ /**
514
+ * ブラウザ閲覧中の生存シグナル (codex.keepalive)。frontend が Codex チャットを開いて
515
+ * いる間だけ定期送信するため、これが届く = 「人が見ている」の観測可能な証跡になる。
516
+ * gcAbandonedSessions は lastActivityMs しか見ないので、touch するだけで
517
+ * 「開いたまま無入力でも 1h で殺される」ギャップが閉じる (TTL の数字を増やす必要は無い)。
518
+ *
519
+ * ⚠️ 意図的に `_cancelIdleReap` を **呼ばない**。detach 済みセッションの 5 分 TTL は
520
+ * 「ブラウザが去った」という別事実に基づく予約であり、detach と in-flight keepalive が
521
+ * 競合しても予約を解除しない (= 明示 detach は必ず 5 分で回収される) 方が意味論が明快。
522
+ * 閲覧を再開したブラウザは必ず attach を送り直すので、再養子はそちらの経路で足りる。
523
+ *
524
+ * @returns {boolean} 生きているセッションを touch できたら true (未知 stream は false)。
525
+ */
526
+ keepalive({ stream_id }) {
527
+ const session = this.sessions.get(stream_id)
528
+ if (!session || session.dead) return false
529
+ this._touch(session)
530
+ return true
531
+ }
532
+
512
533
  /**
513
534
  * グレースフル切断。ターン実行中は完走を待ち、非 busy ならアイドル TTL 後に
514
535
  * プロセスを reap する (claude-stream-bridge.mjs の softDetach と同じ思想。
@@ -744,17 +765,17 @@ export class CodexStreamBridge extends EventEmitter {
744
765
  * 進行中の正常なターンは app-server 通知で都度 touch されるので回収されない。
745
766
  * main.mjs の state loop から claudeBridge.gcDeadTerminals と並べて定期実行する想定。
746
767
  *
747
- * ⚠️ 既知のギャップ (2026-07-17、次段の keepalive PR で解消予定): この reaper は
748
- * `detachRequested` を参照せず、`_touch()` は input() と app-server 通知でしか呼ばれない。
749
- * そのため **ブラウザで Codex チャットを開いたままでも、1 時間まったく入力も生成も
750
- * 無ければ回収される**。アイドル TTL(5 分) の誤爆を直しても、この 1h が次の上限になる。
751
- * TTL の数字を大きくするのではなく「見ている」を観測可能にするのが正しい解:
752
- * frontend/components/cockpit/CockpitTerminal.tsx の PTY keepalive (閲覧中は定期的に
753
- * pty.resize で lastSeenAt を触り、ブラウザが離れれば keepalive も止まるので orphan GC が
754
- * 正しく効く) と同じ構造を codex チャットへ移植する (codex.keepalive を新設し、backend
755
- * consumers.py の中継テーブルと main.mjs の dispatch をセットで変更する)。そうすれば
756
- * 「ブラウザ開 = 無期限 / 明示 detach = 5 分 / ブラウザ落ち = 1h reaper」になり、
757
- * 「N 時間放置しても生きていてほしい」という数字合わせ自体が不要になる。
768
+ * この reaper は `detachRequested` を **意図的に参照しない**。detach を送れずに消えた
769
+ * ブラウザ (タブごとクラッシュ / 端末スリープ) は detachRequested が立たないまま残るため、
770
+ * ここでフラグを見ると回収不能になりリークする。「無通信が続いた事実」だけで回収する。
771
+ *
772
+ * 閲覧中の誤爆は keepalive で防ぐ (2026-07-17 に解消。旧: 開いたままでも 1h 無入力で
773
+ * 回収されていた)。frontend は Codex チャットを開いている間 codex.keepalive を定期送信し、
774
+ * keepalive() が lastActivityMs を touch する。ブラウザが閉じれば keepalive も止まるので
775
+ * この reaper は本来の orphan GC として正しく効く。CockpitTerminal.tsx の PTY keepalive
776
+ * (pty.resize で lastSeenAt を触る) と同じ構造。結果として TTL の意味は
777
+ * 「ブラウザ開 = 無期限 / 明示 detach = 5 分 / ブラウザ落ち = 1h reaper」に整理され、
778
+ * 「N 時間放置しても生きていてほしい」という数字合わせ自体が不要になった。
758
779
  *
759
780
  * @param {number} [ttlMs] 無通信とみなす閾値 (テスト用に上書き可)。
760
781
  * @returns {number} 回収したセッション数。
package/src/main.mjs CHANGED
@@ -4335,6 +4335,13 @@ async function dispatch(msg, ctx) {
4335
4335
  if (!ctx.codexBridge) return
4336
4336
  ctx.codexBridge.detach({ stream_id: msg.stream_id })
4337
4337
  return
4338
+ case "codex.keepalive":
4339
+ // ブラウザが Codex チャットを開いている間の生存シグナル (CockpitTerminal.tsx の
4340
+ // PTY keepalive と同じ構造)。閲覧中に gcAbandonedSessions (既定 1h 無通信) が
4341
+ // 誤爆するのを防ぐ。未知 stream_id は false が返るだけ (無害な no-op)。
4342
+ if (!ctx.codexBridge) return
4343
+ ctx.codexBridge.keepalive({ stream_id: msg.stream_id })
4344
+ return
4338
4345
  case "codex.sessions.request": {
4339
4346
  // cwd 配下の Codex thread 一覧を返す (claude.sessions.request と対称。
4340
4347
  // Cockpit のセッションピッカー/最新セッション自動選択用)。